View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.beanutils2;
19  
20  import java.lang.reflect.Array;
21  import java.util.HashMap;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Objects;
25  
26  /**
27   * <p>
28   * Minimal implementation of the {@code DynaBean} interface. Can be used as a convenience base class for more sophisticated implementations.
29   * </p>
30   *
31   * <p>
32   * <strong>IMPLEMENTATION NOTE</strong> - Instances of this class that are accessed from multiple threads simultaneously need to be synchronized.
33   * </p>
34   *
35   * <p>
36   * <strong>IMPLEMENTATION NOTE</strong> - Instances of this class can be successfully serialized and deserialized <strong>ONLY</strong> if all property values
37   * are {@code Serializable}.
38   * </p>
39   */
40  public class BasicDynaBean implements DynaBean {
41  
42      private static final Short SHORT_ZERO = Short.valueOf((short) 0);
43  
44      private static final Long LONG_ZERO = Long.valueOf(0);
45  
46      private static final Integer INTEGER_ZERO = Integer.valueOf(0);
47  
48      private static final Float FLOAT_ZERO = Float.valueOf((float) 0.0);
49  
50      private static final Double DOUBLE_ZERO = Double.valueOf(0.0);
51  
52      private static final Character CHARACTER_ZERO = Character.valueOf((char) 0);
53  
54      private static final Byte BYTE_ZERO = Byte.valueOf((byte) 0);
55  
56      private static final long serialVersionUID = 1L;
57  
58      /**
59       * The {@code DynaClass} "base class" that this DynaBean is associated with.
60       */
61      protected DynaClass dynaClass;
62  
63      /**
64       * The set of property values for this DynaBean, keyed by property name.
65       */
66      protected HashMap<String, Object> values = new HashMap<>();
67  
68      /** Map decorator for this DynaBean */
69      private transient Map<String, Object> mapDecorator;
70  
71      /**
72       * Constructs a new {@code DynaBean} associated with the specified {@code DynaClass} instance.
73       *
74       * @param dynaClass The DynaClass we are associated with
75       */
76      public BasicDynaBean(final DynaClass dynaClass) {
77          this.dynaClass = dynaClass;
78      }
79  
80      /**
81       * Does the specified mapped property contain a value for the specified key value?
82       *
83       * @param name Name of the property to check
84       * @param key  Name of the key to check
85       * @return {@code true} if the mapped property contains a value for the specified key, otherwise {@code false}
86       * @throws IllegalArgumentException if there is no property of the specified name
87       */
88      @Override
89      public boolean contains(final String name, final String key) {
90          final Object value = values.get(name);
91          requireMappedValue(name, key, value);
92          if (value instanceof Map) {
93              return ((Map<?, ?>) value).containsKey(key);
94          }
95          throw new IllegalArgumentException("Non-mapped property for '" + name + "(" + key + ")'");
96      }
97  
98      /**
99       * 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 }