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 * https://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 }