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