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 */ 017 018package org.apache.commons.beanutils; 019 020import java.io.Serializable; 021import java.lang.reflect.Array; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Objects; 026 027/** 028 * <p>Minimal implementation of the <code>DynaBean</code> interface. Can be 029 * used as a convenience base class for more sophisticated implementations.</p> 030 * 031 * <p><strong>IMPLEMENTATION NOTE</strong> - Instances of this class that are 032 * accessed from multiple threads simultaneously need to be synchronized.</p> 033 * 034 * <p><strong>IMPLEMENTATION NOTE</strong> - Instances of this class can be 035 * successfully serialized and deserialized <strong>ONLY</strong> if all 036 * property values are <code>Serializable</code>.</p> 037 * 038 */ 039 040public class BasicDynaBean implements DynaBean, Serializable { 041 042 private static final long serialVersionUID = 1L; 043 044 /** 045 * The <code>DynaClass</code> "base class" that this DynaBean 046 * is associated with. 047 */ 048 protected DynaClass dynaClass; 049 050 /** 051 * The set of property values for this DynaBean, keyed by property name. 052 */ 053 protected HashMap<String, Object> values = new HashMap<>(); 054 055 /** Map decorator for this DynaBean */ 056 private transient Map<String, Object> mapDecorator; 057 058 /** 059 * Construct a new <code>DynaBean</code> associated with the specified 060 * <code>DynaClass</code> instance. 061 * 062 * @param dynaClass The DynaClass we are associated with 063 */ 064 public BasicDynaBean(final DynaClass dynaClass) { 065 066 this.dynaClass = dynaClass; 067 068 } 069 070 /** 071 * Does the specified mapped property contain a value for the specified 072 * key value? 073 * 074 * @param name Name of the property to check 075 * @param key Name of the key to check 076 * @return <code>true</code> if the mapped property contains a value for 077 * the specified key, otherwise <code>false</code> 078 * 079 * @throws IllegalArgumentException if there is no property 080 * of the specified name 081 */ 082 @Override 083 public boolean contains(final String name, final String key) { 084 final Object value = values.get(name); 085 Objects.requireNonNull(value, () -> "No mapped value for '" + name + "(" + key + ")'"); 086 if (value instanceof Map) { 087 return ((Map<?, ?>) value).containsKey(key); 088 } 089 throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'"); 090 } 091 092 /** 093 * Return the value of a simple property with the specified name. 094 * 095 * @param name Name of the property whose value is to be retrieved 096 * @return The property's value 097 * @throws IllegalArgumentException if there is no property 098 * of the specified name 099 */ 100 @Override 101 public Object get(final String name) { 102 103 // Return any non-null value for the specified property 104 final Object value = values.get(name); 105 if (value != null) { 106 return value; 107 } 108 109 // Return a null value for a non-primitive property 110 final Class<?> type = getDynaProperty(name).getType(); 111 if (!type.isPrimitive()) { 112 return value; 113 } 114 115 // Manufacture default values for primitive properties 116 if (type == Boolean.TYPE) { 117 return Boolean.FALSE; 118 } 119 if (type == Byte.TYPE) { 120 return Byte.valueOf((byte) 0); 121 } 122 if (type == Character.TYPE) { 123 return Character.valueOf((char) 0); 124 } 125 if (type == Double.TYPE) { 126 return Double.valueOf(0.0); 127 } 128 if (type == Float.TYPE) { 129 return Float.valueOf((float) 0.0); 130 } 131 if (type == Integer.TYPE) { 132 return Integer.valueOf(0); 133 } 134 if (type == Long.TYPE) { 135 return Long.valueOf(0); 136 } 137 if (type == Short.TYPE) { 138 return Short.valueOf((short) 0); 139 } 140 return null; 141 } 142 143 /** 144 * Return the value of an indexed property with the specified name. 145 * 146 * @param name Name of the property whose value is to be retrieved 147 * @param index Index of the value to be retrieved 148 * @return The indexed property's value 149 * @throws IllegalArgumentException if there is no property 150 * of the specified name 151 * @throws IllegalArgumentException if the specified property 152 * exists, but is not indexed 153 * @throws IndexOutOfBoundsException if the specified index 154 * is outside the range of the underlying property 155 * @throws NullPointerException if no array or List has been 156 * initialized for this property 157 */ 158 @Override 159 public Object get(final String name, final int index) { 160 final Object value = values.get(name); 161 Objects.requireNonNull(value, () -> "No indexed value for '" + name + "[" + index + "]'"); 162 if (value.getClass().isArray()) { 163 return Array.get(value, index); 164 } 165 if (value instanceof List) { 166 return ((List<?>) value).get(index); 167 } 168 throw new IllegalArgumentException("Non-indexed property for '" + name + "[" + index + "]'"); 169 } 170 171 /** 172 * Return the value of a mapped property with the specified name, 173 * or <code>null</code> if there is no value for the specified key. 174 * 175 * @param name Name of the property whose value is to be retrieved 176 * @param key Key of the value to be retrieved 177 * @return The mapped property's value 178 * @throws IllegalArgumentException if there is no property 179 * of the specified name 180 * @throws IllegalArgumentException if the specified property 181 * exists, but is not mapped 182 */ 183 @Override 184 public Object get(final String name, final String key) { 185 final Object value = values.get(name); 186 Objects.requireNonNull(value, () -> "No mapped value for '" + name + "(" + key + ")'"); 187 if (value instanceof Map) { 188 return ((Map<?, ?>) value).get(key); 189 } 190 throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'"); 191 } 192 193 /** 194 * Return the <code>DynaClass</code> instance that describes the set of 195 * properties available for this DynaBean. 196 * 197 * @return The associated DynaClass 198 */ 199 @Override 200 public DynaClass getDynaClass() { 201 202 return this.dynaClass; 203 204 } 205 206 /** 207 * Return the property descriptor for the specified property name. 208 * 209 * @param name Name of the property for which to retrieve the descriptor 210 * @return The property descriptor 211 * @throws IllegalArgumentException if this is not a valid property 212 * name for our DynaClass 213 */ 214 protected DynaProperty getDynaProperty(final String name) { 215 216 final DynaProperty descriptor = getDynaClass().getDynaProperty(name); 217 if (descriptor == null) { 218 throw new IllegalArgumentException 219 ("Invalid property name '" + name + "'"); 220 } 221 return descriptor; 222 223 } 224 225 /** 226 * Return a Map representation of this DynaBean. 227 * <p> 228 * This, for example, could be used in JSTL in the following way to access 229 * a DynaBean's <code>fooProperty</code>: 230 * </p> 231 * <ul><li><code>${myDynaBean.<strong>map</strong>.fooProperty}</code></li></ul> 232 * 233 * @return a Map representation of this DynaBean 234 * @since 1.8.0 235 */ 236 public Map<String, Object> getMap() { 237 238 // cache the Map 239 if (mapDecorator == null) { 240 mapDecorator = new DynaBeanPropertyMapDecorator(this); 241 } 242 return mapDecorator; 243 244 } 245 246 /** 247 * Is an object of the source class assignable to the destination class? 248 * 249 * @param dest Destination class 250 * @param source Source class 251 * @return <code>true</code> if the source class is assignable to the 252 * destination class, otherwise <code>false</code> 253 */ 254 protected boolean isAssignable(final Class<?> dest, final Class<?> source) { 255 256 if (dest.isAssignableFrom(source) || 257 dest == Boolean.TYPE && source == Boolean.class || 258 dest == Byte.TYPE && source == Byte.class || 259 dest == Character.TYPE && source == Character.class || 260 dest == Double.TYPE && source == Double.class || 261 dest == Float.TYPE && source == Float.class || 262 dest == Integer.TYPE && source == Integer.class || 263 dest == Long.TYPE && source == Long.class || 264 dest == Short.TYPE && source == Short.class) { 265 return true; 266 } 267 return false; 268 269 } 270 271 /** 272 * Remove any existing value for the specified key on the 273 * specified mapped property. 274 * 275 * @param name Name of the property for which a value is to 276 * be removed 277 * @param key Key of the value to be removed 278 * @throws IllegalArgumentException if there is no property 279 * of the specified name 280 */ 281 @Override 282 public void remove(final String name, final String key) { 283 final Object value = values.get(name); 284 Objects.requireNonNull(value, () -> "No mapped value for '" + name + "(" + key + ")'"); 285 if (!(value instanceof Map)) { 286 throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'"); 287 } 288 ((Map<?, ?>) value).remove(key); 289 } 290 291 /** 292 * Set the value of an indexed property with the specified name. 293 * 294 * @param name Name of the property whose value is to be set 295 * @param index Index of the property to be set 296 * @param value Value to which this property is to be set 297 * @throws ConversionException if the specified value cannot be 298 * converted to the type required for this property 299 * @throws IllegalArgumentException if there is no property 300 * of the specified name 301 * @throws IllegalArgumentException if the specified property 302 * exists, but is not indexed 303 * @throws IndexOutOfBoundsException if the specified index 304 * is outside the range of the underlying property 305 */ 306 @Override 307 public void set(final String name, final int index, final Object value) { 308 309 final Object prop = values.get(name); 310 Objects.requireNonNull(prop, () -> "No indexed value for '" + name + "[" + index + "]'"); 311 if (prop.getClass().isArray()) { 312 Array.set(prop, index, value); 313 } else if (prop instanceof List) { 314 try { 315 @SuppressWarnings("unchecked") 316 final 317 // This is safe to cast because list properties are always 318 // of type Object 319 List<Object> list = (List<Object>) prop; 320 list.set(index, value); 321 } catch (final ClassCastException e) { 322 throw new ConversionException(e.getMessage()); 323 } 324 } else { 325 throw new IllegalArgumentException 326 ("Non-indexed property for '" + name + "[" + index + "]'"); 327 } 328 329 } 330 331 /** 332 * Set the value of a simple property with the specified name. 333 * 334 * @param name Name of the property whose value is to be set 335 * @param value Value to which this property is to be set 336 * @throws ConversionException if the specified value cannot be 337 * converted to the type required for this property 338 * @throws IllegalArgumentException if there is no property 339 * of the specified name 340 * @throws NullPointerException if an attempt is made to set a 341 * primitive property to null 342 */ 343 @Override 344 public void set(final String name, final Object value) { 345 346 final DynaProperty descriptor = getDynaProperty(name); 347 if (value == null) { 348 if (descriptor.getType().isPrimitive()) { 349 throw new NullPointerException 350 ("Primitive value for '" + name + "'"); 351 } 352 } else if (!isAssignable(descriptor.getType(), value.getClass())) { 353 throw new ConversionException 354 ("Cannot assign value of type '" + 355 value.getClass().getName() + 356 "' to property '" + name + "' of type '" + 357 descriptor.getType().getName() + "'"); 358 } 359 values.put(name, value); 360 361 } 362 363 /** 364 * Set the value of a mapped property with the specified name. 365 * 366 * @param name Name of the property whose value is to be set 367 * @param key Key of the property to be set 368 * @param value Value to which this property is to be set 369 * @throws ConversionException if the specified value cannot be 370 * converted to the type required for this property 371 * @throws IllegalArgumentException if there is no property 372 * of the specified name 373 * @throws IllegalArgumentException if the specified property 374 * exists, but is not mapped 375 */ 376 @Override 377 public void set(final String name, final String key, final Object value) { 378 final Object prop = values.get(name); 379 Objects.requireNonNull(prop, () -> "No mapped value for '" + name + "(" + key + ")'"); 380 if (!(prop instanceof Map)) { 381 throw new IllegalArgumentException 382 ("Non-mapped property for '" + name + "(" + key + ")'"); 383 } 384 // This is safe to cast because mapped properties are always 385 // maps of types String -> Object 386 @SuppressWarnings("unchecked") 387 final Map<String, Object> map = (Map<String, Object>) prop; 388 map.put(key, value); 389 } 390 391} 392