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