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