001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.beanutils2; 018 019import java.util.Map; 020import java.util.Objects; 021 022/** 023 * <p> 024 * Provides a <em>light weight</em> {@code DynaBean</code> facade to a <code>Map} 025 * with <em>lazy</em> map/list processing.</p> 026 * 027 * <p>Its a <em>light weight</em> {@code DynaBean} implementation because there is no 028 * actual {@code DynaClass</code> associated with this <code>DynaBean} - in fact 029 * it implements the {@code DynaClass} interface itself providing <em>pseudo</em> DynaClass 030 * behavior from the actual values stored in the {@code Map}.</p> 031 * 032 * <p>As well providing rhe standard {@code DynaBean</code> access to the <code>Map}'s properties 033 * this class also provides the usual <em>Lazy</em> behavior:</p> 034 * <ul> 035 * <li>Properties don't need to be pre-defined in a {@code DynaClass}</li> 036 * <li>Indexed properties ({@code Lists</code> or <code>Arrays}) are automatically instantiated 037 * and <em>grown</em> so that they are large enough to cater for the index being set.</li> 038 * <li>Mapped properties are automatically instantiated.</li> 039 * </ul> 040 * 041 * <p><strong><u><em>Restricted</em> DynaClass</u></strong></p> 042 * <p>This class implements the {@code MutableDynaClass} interface. 043 * {@code MutableDynaClass</code> have a facility to <em>restrict</em> the <code>DynaClass} so that its properties cannot be modified. If the 044 * {@code MutableDynaClass} is restricted then calling any of the {@code set()} methods for a property which doesn't exist will result in a 045 * {@code IllegalArgumentException} being thrown. 046 * </p> 047 */ 048public class LazyDynaMap extends LazyDynaBean implements MutableDynaClass { 049 050 private static final long serialVersionUID = 1L; 051 052 /** 053 * The name of this DynaClass (analogous to the {@code getName()</code> method of <code>java.lang.Class}). 054 */ 055 protected String name; 056 057 /** 058 * Controls whether changes to this DynaClass's properties are allowed. 059 */ 060 protected boolean restricted; 061 062 /** 063 * <p> 064 * Controls whether the {@code getDynaProperty()} method returns null if a property doesn't exist - or creates a new one. 065 * </p> 066 * 067 * <p> 068 * Default is {@code false}. 069 */ 070 protected boolean returnNull; 071 072 /** 073 * Constructs a new instance. 074 */ 075 public LazyDynaMap() { 076 this(null, (Map<String, Object>) null); 077 } 078 079 /** 080 * Constructs a new {@code LazyDynaMap} based on an exisiting DynaClass 081 * 082 * @param dynaClass DynaClass to copy the name and properties from 083 */ 084 public LazyDynaMap(final DynaClass dynaClass) { 085 this(dynaClass.getName(), dynaClass.getDynaProperties()); 086 } 087 088 /** 089 * Constructs a new {@code LazyDynaMap} with the specified properties. 090 * 091 * @param properties Property descriptors for the supported properties 092 */ 093 public LazyDynaMap(final DynaProperty[] properties) { 094 this(null, properties); 095 } 096 097 /** 098 * Constructs a new {@code LazyDynaMap</code> with the specified <code>Map}. 099 * 100 * @param values The Map backing this {@code LazyDynaMap} 101 */ 102 public LazyDynaMap(final Map<String, Object> values) { 103 this(null, values); 104 } 105 106 /** 107 * Constructs a new {@code LazyDynaMap} with the specified name. 108 * 109 * @param name Name of this DynaBean class 110 */ 111 public LazyDynaMap(final String name) { 112 this(name, (Map<String, Object>) null); 113 } 114 115 /** 116 * Constructs a new {@code LazyDynaMap} with the specified name and properties. 117 * 118 * @param name Name of this DynaBean class 119 * @param properties Property descriptors for the supported properties 120 */ 121 public LazyDynaMap(final String name, final DynaProperty[] properties) { 122 this(name, (Map<String, Object>) null); 123 if (properties != null) { 124 for (final DynaProperty property : properties) { 125 add(property); 126 } 127 } 128 } 129 130 /** 131 * Constructs a new {@code LazyDynaMap</code> with the specified name and <code>Map}. 132 * 133 * @param name Name of this DynaBean class 134 * @param values The Map backing this {@code LazyDynaMap} 135 */ 136 public LazyDynaMap(final String name, final Map<String, Object> values) { 137 this.name = name == null ? "LazyDynaMap" : name; 138 this.values = values == null ? newMap() : values; 139 this.dynaClass = this; 140 } 141 142 /** 143 * Add a new dynamic property. 144 * 145 * @param property Property the new dynamic property to add. 146 * @throws IllegalArgumentException if name is null 147 */ 148 protected void add(final DynaProperty property) { 149 add(property.getName(), property.getType()); 150 } 151 152 /** 153 * Add a new dynamic property with no restrictions on data type, readability, or writeability. 154 * 155 * @param name Name of the new dynamic property 156 * @throws IllegalArgumentException if name is null 157 */ 158 @Override 159 public void add(final String name) { 160 add(name, null); 161 } 162 163 /** 164 * Add a new dynamic property with the specified data type, but with no restrictions on readability or writeability. 165 * 166 * @param name Name of the new dynamic property 167 * @param type Data type of the new dynamic property (null for no restrictions) 168 * @throws IllegalArgumentException if name is null 169 * @throws IllegalStateException if this DynaClass is currently restricted, so no new properties can be added 170 */ 171 @Override 172 public void add(final String name, final Class<?> type) { 173 Objects.requireNonNull(name, "name"); 174 if (isRestricted()) { 175 throw new IllegalStateException("DynaClass is currently restricted. No new properties can be added."); 176 } 177 // Check if the property already exists 178 values.computeIfAbsent(name, k -> type == null ? null : createProperty(name, type)); 179 } 180 181 /** 182 * <p> 183 * Add a new dynamic property with the specified data type, readability, and writeability. 184 * </p> 185 * 186 * <p> 187 * <strong>N.B.</strong>Support for readable/writable properties has not been implemented and this method always throws a 188 * {@code UnsupportedOperationException}. 189 * </p> 190 * 191 * <p> 192 * I'm not sure the intention of the original authors for this method, but it seems to me that readable/writable should be attributes of the 193 * {@code DynaProperty} class (which they are not) and is the reason this method has not been implemented. 194 * </p> 195 * 196 * @param name Name of the new dynamic property 197 * @param type Data type of the new dynamic property (null for no restrictions) 198 * @param readable Set to {@code true} if this property value should be readable 199 * @param writable Set to {@code true} if this property value should be writable 200 * @throws UnsupportedOperationException anytime this method is called 201 */ 202 @Override 203 public void add(final String name, final Class<?> type, final boolean readable, final boolean writable) { 204 throw new java.lang.UnsupportedOperationException("readable/writable properties not supported"); 205 } 206 207 /** 208 * <p> 209 * Return an array of {@code PropertyDescriptor} for the properties currently defined in this DynaClass. If no properties are defined, a zero-length array 210 * will be returned. 211 * </p> 212 * 213 * <p> 214 * <strong>FIXME</strong> - Should we really be implementing {@code getBeanInfo()} instead, which returns property descriptors and a bunch of other stuff? 215 * </p> 216 * 217 * @return the set of properties for this DynaClass 218 */ 219 @Override 220 public DynaProperty[] getDynaProperties() { 221 int i = 0; 222 final DynaProperty[] properties = new DynaProperty[values.size()]; 223 for (final Map.Entry<String, Object> e : values.entrySet()) { 224 final String name = e.getKey(); 225 final Object value = values.get(name); 226 properties[i++] = new DynaProperty(name, value == null ? null : value.getClass()); 227 } 228 229 return properties; 230 } 231 232 /** 233 * <p> 234 * Return a property descriptor for the specified property. 235 * </p> 236 * 237 * <p> 238 * If the property is not found and the {@code returnNull} indicator is {@code true</code>, this method always returns <code>null}. 239 * </p> 240 * 241 * <p> 242 * If the property is not found and the {@code returnNull} indicator is {@code false} a new property descriptor is created and returned (although its not 243 * actually added to the DynaClass's properties). This is the default behavior. 244 * </p> 245 * 246 * <p> 247 * The reason for not returning a {@code null} property descriptor is that {@code BeanUtils} uses this method to check if a property exists before trying to 248 * set it - since these <em>Map</em> implementations automatically add any new properties when they are set, returning {@code null} from this method would 249 * defeat their purpose. 250 * </p> 251 * 252 * @param name Name of the dynamic property for which a descriptor is requested 253 * @return The descriptor for the specified property 254 * @throws IllegalArgumentException if no property name is specified 255 */ 256 @Override 257 public DynaProperty getDynaProperty(final String name) { 258 Objects.requireNonNull(name, "name"); 259 final Object value = values.get(name); 260 // If it doesn't exist and returnNull is false 261 // create a new DynaProperty 262 if (value == null && isReturnNull()) { 263 return null; 264 } 265 if (value == null) { 266 return new DynaProperty(name); 267 } 268 return new DynaProperty(name, value.getClass()); 269 } 270 271 /** 272 * Gets the underlying Map backing this {@code DynaBean} 273 * 274 * @return the underlying Map 275 * @since 1.8.0 276 */ 277 @Override 278 public Map<String, Object> getMap() { 279 return values; 280 } 281 282 /** 283 * Gets the name of this DynaClass (analogous to the {@code getName()</code> method of <code>java.lang.Class}) 284 * 285 * @return the name of the DynaClass 286 */ 287 @Override 288 public String getName() { 289 return this.name; 290 } 291 292 /** 293 * <p> 294 * Indicate whether a property actually exists. 295 * </p> 296 * 297 * <p> 298 * <strong>N.B.</strong> Using {@code getDynaProperty(name) == null} doesn't work in this implementation because that method might return a DynaProperty if 299 * it doesn't exist (depending on the {@code returnNull} indicator). 300 * </p> 301 * 302 * @param name Name of the dynamic property 303 * @return {@code true} if the property exists, otherwise {@code false} 304 * @throws IllegalArgumentException if no property name is specified 305 */ 306 @Override 307 protected boolean isDynaProperty(final String name) { 308 return values.containsKey(Objects.requireNonNull(name, "name")); 309 } 310 311 /** 312 * <p> 313 * Is this DynaClass currently restricted. 314 * </p> 315 * <p> 316 * If restricted, no changes to the existing registration of property names, data types, readability, or writeability are allowed. 317 * </p> 318 * 319 * @return {@code true} if this Mutable {@link DynaClass} is restricted, otherwise {@code false} 320 */ 321 @Override 322 public boolean isRestricted() { 323 return restricted; 324 } 325 326 /** 327 * Should this DynaClass return a {@code null} from the {@code getDynaProperty(name)} method if the property doesn't exist. 328 * 329 * @return {@code true</code> if a <code>null} {@link DynaProperty} should be returned if the property doesn't exist, otherwise {@code false} if a new 330 * {@link DynaProperty} should be created. 331 */ 332 public boolean isReturnNull() { 333 return returnNull; 334 } 335 336 /** 337 * Instantiate and return a new DynaBean instance, associated with this DynaClass. 338 * 339 * @return A new {@code DynaBean} instance 340 */ 341 @Override 342 public DynaBean newInstance() { 343 // Create a new instance of the Map 344 Map<String, Object> newMap = null; 345 try { 346 final 347 // The new map is used as properties map 348 Map<String, Object> temp = getMap().getClass().newInstance(); 349 newMap = temp; 350 } catch (final Exception ex) { 351 newMap = newMap(); 352 } 353 354 // Crate new LazyDynaMap and initialize properties 355 final LazyDynaMap lazyMap = new LazyDynaMap(newMap); 356 final DynaProperty[] properties = getDynaProperties(); 357 if (properties != null) { 358 for (final DynaProperty property : properties) { 359 lazyMap.add(property); 360 } 361 } 362 return lazyMap; 363 } 364 365 /** 366 * Remove the specified dynamic property, and any associated data type, readability, and writeability, from this dynamic class. <strong>NOTE</strong> - This 367 * does <strong>NOT</strong> cause any corresponding property values to be removed from DynaBean instances associated with this DynaClass. 368 * 369 * @param name Name of the dynamic property to remove 370 * @throws IllegalArgumentException if name is null 371 * @throws IllegalStateException if this DynaClass is currently restricted, so no properties can be removed 372 */ 373 @Override 374 public void remove(final String name) { 375 Objects.requireNonNull(name, "name"); 376 if (isRestricted()) { 377 throw new IllegalStateException("DynaClass is currently restricted. No properties can be removed."); 378 } 379 values.remove(name); 380 } 381 382 /** 383 * Sets the value of a simple property with the specified name. 384 * 385 * @param name Name of the property whose value is to be set 386 * @param value Value to which this property is to be set 387 */ 388 @Override 389 public void set(final String name, final Object value) { 390 if (isRestricted() && !values.containsKey(name)) { 391 throw new IllegalArgumentException("Invalid property name '" + name + "' (DynaClass is restricted)"); 392 } 393 values.put(name, value); 394 } 395 396 /** 397 * Sets the Map backing this {@code DynaBean} 398 * 399 * @param values The new Map of values 400 */ 401 public void setMap(final Map<String, Object> values) { 402 this.values = values; 403 } 404 405 /** 406 * <p> 407 * Set whether this DynaClass is currently restricted. 408 * </p> 409 * <p> 410 * If restricted, no changes to the existing registration of property names, data types, readability, or writeability are allowed. 411 * </p> 412 * 413 * @param restricted The new restricted state 414 */ 415 @Override 416 public void setRestricted(final boolean restricted) { 417 this.restricted = restricted; 418 } 419 420 /** 421 * Sets whether this DynaClass should return a {@code null} from the {@code getDynaProperty(name)} method if the property doesn't exist. 422 * 423 * @param returnNull {@code true</code> if a <code>null} {@link DynaProperty} should be returned if the property doesn't exist, otherwise {@code false} if a 424 * new {@link DynaProperty} should be created. 425 */ 426 public void setReturnNull(final boolean returnNull) { 427 this.returnNull = returnNull; 428 } 429 430}