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 package org.apache.commons.beanutils2;
18
19 import java.beans.BeanInfo;
20 import java.beans.IntrospectionException;
21 import java.beans.Introspector;
22 import java.beans.PropertyDescriptor;
23 import java.lang.reflect.Constructor;
24 import java.lang.reflect.InvocationTargetException;
25 import java.lang.reflect.Method;
26 import java.util.AbstractMap;
27 import java.util.AbstractSet;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.function.Function;
36
37 /**
38 * An implementation of Map for JavaBeans which uses introspection to get and put properties in the bean.
39 * <p>
40 * If an exception occurs during attempts to get or set a property then the property is considered non existent in the Map
41 * </p>
42 */
43 public class BeanMap extends AbstractMap<String, Object> implements Cloneable {
44
45 /**
46 * Map entry used by {@link BeanMap}.
47 */
48 protected static class Entry extends AbstractMap.SimpleEntry<String, Object> {
49
50 private static final long serialVersionUID = 1L;
51
52 /**
53 * The owner.
54 */
55 private final BeanMap owner;
56
57 /**
58 * Constructs a new {@code Entry}.
59 *
60 * @param owner the BeanMap this entry belongs to
61 * @param key the key for this entry
62 * @param value the value for this entry
63 */
64 protected Entry(final BeanMap owner, final String key, final Object value) {
65 super(key, value);
66 this.owner = owner;
67 }
68
69 /**
70 * Sets the value.
71 *
72 * @param value the new value for the entry
73 * @return the old value for the entry
74 */
75 @Override
76 public Object setValue(final Object value) {
77 final String key = getKey();
78 final Object oldValue = owner.get(key);
79
80 owner.put(key, value);
81 final Object newValue = owner.get(key);
82 super.setValue(newValue);
83 return oldValue;
84 }
85 }
86
87 /**
88 * An empty array. Used to invoke accessors via reflection.
89 */
90 public static final Object[] NULL_ARGUMENTS = {};
91
92 /**
93 * Maps primitive Class types to transformers. The transformer transform strings into the appropriate primitive wrapper.
94 *
95 * Private & unmodifiable replacement for the (public & static) defaultTransformers instance.
96 */
97 private static final Map<Class<? extends Object>, Function<?, ?>> typeTransformers = Collections.unmodifiableMap(createTypeTransformers());
98
99 private static Map<Class<? extends Object>, Function<?, ?>> createTypeTransformers() {
100 final Map<Class<? extends Object>, Function<?, ?>> defTransformers = new HashMap<>();
101 defTransformers.put(Boolean.TYPE, input -> Boolean.valueOf(input.toString()));
102 defTransformers.put(Character.TYPE, input -> Character.valueOf(input.toString().charAt(0)));
103 defTransformers.put(Byte.TYPE, input -> Byte.valueOf(input.toString()));
104 defTransformers.put(Short.TYPE, input -> Short.valueOf(input.toString()));
105 defTransformers.put(Integer.TYPE, input -> Integer.valueOf(input.toString()));
106 defTransformers.put(Long.TYPE, input -> Long.valueOf(input.toString()));
107 defTransformers.put(Float.TYPE, input -> Float.valueOf(input.toString()));
108 defTransformers.put(Double.TYPE, input -> Double.valueOf(input.toString()));
109 return defTransformers;
110 }
111
112 private transient Object bean;
113
114 private final transient HashMap<String, Method> readMethods = new HashMap<>();
115
116 private final transient HashMap<String, Method> writeMethods = new HashMap<>();
117
118 private final transient HashMap<String, Class<? extends Object>> types = new HashMap<>();
119
120 /**
121 * Constructs a new empty {@code BeanMap}.
122 */
123 public BeanMap() {
124 }
125
126 // Map interface
127
128 /**
129 * Constructs a new {@code BeanMap} that operates on the specified bean. If the given bean is {@code null}, then this map will be empty.
130 *
131 * @param bean the bean for this map to operate on
132 */
133 public BeanMap(final Object bean) {
134 this.bean = bean;
135 initialize();
136 }
137
138 /**
139 * This method reinitializes the bean map to have default values for the bean's properties. This is accomplished by constructing a new instance of the bean
140 * which the map uses as its underlying data source. This behavior for {@code clear()} differs from the Map contract in that the mappings are not actually
141 * removed from the map (the mappings for a BeanMap are fixed).
142 */
143 @Override
144 public void clear() {
145 if (bean == null) {
146 return;
147 }
148 Class<? extends Object> beanClass = null;
149 try {
150 beanClass = bean.getClass();
151 bean = beanClass.newInstance();
152 } catch (final Exception e) {
153 throw new UnsupportedOperationException("Could not create new instance of class: " + beanClass, e);
154 }
155 }
156
157 /**
158 * Clone this bean map using the following process:
159 *
160 * <ul>
161 * <li>If there is no underlying bean, return a cloned BeanMap without a bean.
162 * <li>Since there is an underlying bean, try to instantiate a new bean of the same type using Class.newInstance().
163 * <li>If the instantiation fails, throw a CloneNotSupportedException
164 * <li>Clone the bean map and set the newly instantiated bean as the underlying bean for the bean map.
165 * <li>Copy each property that is both readable and writable from the existing object to a cloned bean map.
166 * <li>If anything fails along the way, throw a CloneNotSupportedException.
167 * </ul>
168 *
169 * @return a cloned instance of this bean map
170 * @throws CloneNotSupportedException if the underlying bean cannot be cloned
171 */
172 @Override
173 public Object clone() throws CloneNotSupportedException {
174 final BeanMap newMap = (BeanMap) super.clone();
175 if (bean == null) {
176 // no bean, just an empty bean map at the moment. return a newly
177 // cloned and empty bean map.
178 return newMap;
179 }
180 Object newBean = null;
181 final Class<? extends Object> beanClass = bean.getClass(); // Cannot throw Exception
182 try {
183 newBean = beanClass.newInstance();
184 } catch (final Exception e) {
185 // unable to instantiate
186 final CloneNotSupportedException cnse = new CloneNotSupportedException(
187 "Unable to instantiate the underlying bean \"" + beanClass.getName() + "\": " + e);
188 cnse.initCause(e);
189 throw cnse;
190 }
191 try {
192 newMap.setBean(newBean);
193 } catch (final Exception e) {
194 final CloneNotSupportedException cnse = new CloneNotSupportedException("Unable to set bean in the cloned bean map: " + e);
195 cnse.initCause(e);
196 throw cnse;
197 }
198 try {
199 // copy only properties that are readable and writable. If its
200 // not readable, we can't get the value from the old map. If
201 // its not writable, we can't write a value into the new map.
202 readMethods.keySet().forEach(key -> {
203 if (getWriteMethod(key) != null) {
204 newMap.put(key, get(key));
205 }
206 });
207 } catch (final Exception e) {
208 final CloneNotSupportedException cnse = new CloneNotSupportedException("Unable to copy bean values to cloned bean map: " + e);
209 cnse.initCause(e);
210 throw cnse;
211 }
212 return newMap;
213 }
214
215 /**
216 * Returns true if the bean defines a property with the given name.
217 * <p>
218 * The given name must be a {@code String}; if not, this method returns false. This method will also return false if the bean does not define a property
219 * with that name.
220 * </p>
221 * <p>
222 * Write-only properties will not be matched as the test operates against property read methods.
223 * </p>
224 *
225 * @param name the name of the property to check
226 * @return false if the given name is null or is not a {@code String}; false if the bean does not define a property with that name; or true if the bean does
227 * define a property with that name
228 */
229 @Override
230 public boolean containsKey(final Object name) {
231 return getReadMethod(name) != null;
232 }
233
234 /**
235 * Converts the given value to the given type. First, reflection is used to find a public constructor declared by the given class that takes one argument,
236 * which must be the precise type of the given value. If such a constructor is found, a new object is created by passing the given value to that
237 * constructor, and the newly constructed object is returned.
238 * <p>
239 * If no such constructor exists, and the given type is a primitive type, then the given value is converted to a string using its {@link Object#toString()
240 * toString()} method, and that string is parsed into the correct primitive type using, for instance, {@link Integer#valueOf(String)} to convert the string
241 * into an {@code int}.
242 * </p>
243 * <p>
244 * If no special constructor exists and the given type is not a primitive type, this method returns the original value.
245 * </p>
246 *
247 * @param <R> The return type.
248 * @param newType the type to convert the value to
249 * @param value the value to convert
250 * @return the converted value
251 * @throws NumberFormatException if newType is a primitive type, and the string representation of the given value cannot be converted to that type
252 * @throws InstantiationException if the constructor found with reflection raises it
253 * @throws InvocationTargetException if the constructor found with reflection raises it
254 * @throws IllegalAccessException never
255 * @throws IllegalArgumentException never
256 */
257 protected <R> Object convertType(final Class<R> newType, final Object value)
258 throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
259
260 // try call constructor
261 try {
262 final Constructor<R> constructor = newType.getConstructor(value.getClass());
263 return constructor.newInstance(value);
264 } catch (final NoSuchMethodException e) {
265 // try using the transformers
266 final Function<Object, R> transformer = getTypeTransformer(newType);
267 if (transformer != null) {
268 return transformer.apply(value);
269 }
270 return value;
271 }
272 }
273
274 /**
275 * Creates an array of parameters to pass to the given mutator method. If the given object is not the right type to pass to the method directly, it will be
276 * converted using {@link #convertType(Class,Object)}.
277 *
278 * @param method the mutator method
279 * @param value the value to pass to the mutator method
280 * @return an array containing one object that is either the given value or a transformed value
281 * @throws IllegalAccessException if {@link #convertType(Class,Object)} raises it
282 * @throws IllegalArgumentException if any other exception is raised by {@link #convertType(Class,Object)}
283 * @throws ClassCastException if an error occurs creating the method args
284 */
285 protected Object[] createWriteMethodArguments(final Method method, Object value) throws IllegalAccessException, ClassCastException {
286 try {
287 if (value != null) {
288 final Class<? extends Object>[] paramTypes = method.getParameterTypes();
289 if (paramTypes != null && paramTypes.length > 0) {
290 final Class<? extends Object> paramType = paramTypes[0];
291 if (!paramType.isAssignableFrom(value.getClass())) {
292 value = convertType(paramType, value);
293 }
294 }
295 }
296
297 return new Object[] { value };
298 } catch (final InvocationTargetException | InstantiationException e) {
299 throw new IllegalArgumentException(e.getMessage(), e);
300 }
301 }
302
303 /**
304 * Convenience method for getting an iterator over the entries.
305 *
306 * @return an iterator over the entries
307 */
308 public Iterator<Map.Entry<String, Object>> entryIterator() {
309 final Iterator<String> iter = keyIterator();
310 return new Iterator<Map.Entry<String, Object>>() {
311 @Override
312 public boolean hasNext() {
313 return iter.hasNext();
314 }
315
316 @Override
317 public Map.Entry<String, Object> next() {
318 final String key = iter.next();
319 final Object value = get(key);
320 // This should not cause any problems; the key is actually a
321 // string, but it does no harm to expose it as Object
322 return new Entry(BeanMap.this, key, value);
323 }
324
325 @Override
326 public void remove() {
327 throw new UnsupportedOperationException("remove() not supported for BeanMap");
328 }
329 };
330 }
331
332 /**
333 * Gets a Set of MapEntry objects that are the mappings for this BeanMap.
334 * <p>
335 * Each MapEntry can be set but not removed.
336 * </p>
337 *
338 * @return the unmodifiable set of mappings
339 */
340 @Override
341 public Set<Map.Entry<String, Object>> entrySet() {
342 return Collections.unmodifiableSet(new AbstractSet<Map.Entry<String, Object>>() {
343 @Override
344 public Iterator<Map.Entry<String, Object>> iterator() {
345 return entryIterator();
346 }
347
348 @Override
349 public int size() {
350 return BeanMap.this.readMethods.size();
351 }
352 });
353 }
354
355 /**
356 * Called during a successful {@link #put(String,Object)} operation. Default implementation does nothing. Override to be notified of property changes in the
357 * bean caused by this map.
358 *
359 * @param key the name of the property that changed
360 * @param oldValue the old value for that property
361 * @param newValue the new value for that property
362 */
363 protected void firePropertyChange(final Object key, final Object oldValue, final Object newValue) {
364 // noop
365 }
366
367 /**
368 * Gets the value of the bean's property with the given name.
369 * <p>
370 * The given name must be a {@link String} and must not be null; otherwise, this method returns {@code null}. If the bean defines a property with the given
371 * name, the value of that property is returned. Otherwise, {@code null} is returned.
372 * </p>
373 * <p>
374 * Write-only properties will not be matched as the test operates against property read methods.
375 * </p>
376 *
377 * @param name the name of the property whose value to return
378 * @return the value of the property with that name
379 */
380 @Override
381 public Object get(final Object name) {
382 if (bean != null) {
383 final Method method = getReadMethod(name);
384 if (method != null) {
385 try {
386 return method.invoke(bean, NULL_ARGUMENTS);
387 } catch (final IllegalAccessException | NullPointerException | InvocationTargetException | IllegalArgumentException e) {
388 logWarn(e);
389 }
390 }
391 }
392 return null;
393 }
394
395 /**
396 * Gets the bean currently being operated on. The return value may be null if this map is empty.
397 *
398 * @return the bean being operated on by this map
399 */
400 public Object getBean() {
401 return bean;
402 }
403
404 // Helper methods
405
406 /**
407 * Gets the accessor for the property with the given name.
408 *
409 * @param name the name of the property
410 * @return null if the name is null; null if the name is not a {@link String}; null if no such property exists; or the accessor method for that property
411 */
412 protected Method getReadMethod(final Object name) {
413 return readMethods.get(name);
414 }
415
416 /**
417 * Gets the accessor for the property with the given name.
418 *
419 * @param name the name of the property
420 * @return the accessor method for the property, or null
421 */
422 public Method getReadMethod(final String name) {
423 return readMethods.get(name);
424 }
425
426 /**
427 * Gets the type of the property with the given name.
428 *
429 * @param name the name of the property
430 * @return the type of the property, or {@code null} if no such property exists
431 */
432 public Class<?> getType(final String name) {
433 return types.get(name);
434 }
435
436 /**
437 * Gets a transformer for the given primitive type.
438 *
439 * @param <R> The transformer result type.
440 * @param type the primitive type whose transformer to return
441 * @return a transformer that will convert strings into that type, or null if the given type is not a primitive type
442 */
443 protected <R> Function<Object, R> getTypeTransformer(final Class<R> type) {
444 return (Function<Object, R>) typeTransformers.get(type);
445 }
446
447 /**
448 * Gets the mutator for the property with the given name.
449 *
450 * @param name the name of the
451 * @return null if the name is null; null if the name is not a {@link String}; null if no such property exists; null if the property is read-only; or the
452 * mutator method for that property
453 */
454 protected Method getWriteMethod(final Object name) {
455 return writeMethods.get(name);
456 }
457
458 /**
459 * Gets the mutator for the property with the given name.
460 *
461 * @param name the name of the property
462 * @return the mutator method for the property, or null
463 */
464 public Method getWriteMethod(final String name) {
465 return writeMethods.get(name);
466 }
467
468 private void initialize() {
469 if (getBean() == null) {
470 return;
471 }
472
473 final Class<? extends Object> beanClass = getBean().getClass();
474 try {
475 // BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
476 final BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
477 final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
478 if (propertyDescriptors != null) {
479 for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {
480 if (propertyDescriptor != null) {
481 final String name = propertyDescriptor.getName();
482 final Method readMethod = propertyDescriptor.getReadMethod();
483 final Method writeMethod = propertyDescriptor.getWriteMethod();
484 final Class<? extends Object> aType = propertyDescriptor.getPropertyType();
485
486 if (readMethod != null) {
487 readMethods.put(name, readMethod);
488 }
489 if (writeMethod != null) {
490 writeMethods.put(name, writeMethod);
491 }
492 types.put(name, aType);
493 }
494 }
495 }
496 } catch (final IntrospectionException e) {
497 logWarn(e);
498 }
499 }
500
501 /**
502 * Convenience method for getting an iterator over the keys.
503 * <p>
504 * Write-only properties will not be returned in the iterator.
505 * </p>
506 *
507 * @return an iterator over the keys
508 */
509 public Iterator<String> keyIterator() {
510 return readMethods.keySet().iterator();
511 }
512
513 // Implementation methods
514
515 /**
516 * Gets the keys for this BeanMap.
517 * <p>
518 * Write-only properties are <strong>not</strong> included in the returned set of property names, although it is possible to set their value and to get
519 * their type.
520 * </p>
521 *
522 * @return BeanMap keys. The Set returned by this method is not modifiable.
523 */
524 @SuppressWarnings({ "unchecked", "rawtypes" })
525 // The set actually contains strings; however, because it cannot be
526 // modified there is no danger in selling it as Set<Object>
527 @Override
528 public Set<String> keySet() {
529 return Collections.unmodifiableSet((Set) readMethods.keySet());
530 }
531
532 /**
533 * Logs the given exception to {@code System.out}. Used to display warnings while accessing/mutating the bean.
534 *
535 * @param ex the exception to log
536 */
537 protected void logInfo(final Exception ex) {
538 // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
539 System.out.println("INFO: Exception: " + ex);
540 }
541
542 /**
543 * Logs the given exception to {@code System.err}. Used to display errors while accessing/mutating the bean.
544 *
545 * @param ex the exception to log
546 */
547 protected void logWarn(final Exception ex) {
548 // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
549 System.out.println("WARN: Exception: " + ex);
550 ex.printStackTrace();
551 }
552
553 /**
554 * Sets the bean property with the given name to the given value.
555 *
556 * @param name the name of the property to set
557 * @param value the value to set that property to
558 * @return the previous value of that property
559 * @throws IllegalArgumentException if the given name is null; if the given name is not a {@link String}; if the bean doesn't define a property with that
560 * name; or if the bean property with that name is read-only
561 * @throws ClassCastException if an error occurs creating the method args
562 */
563 @Override
564 public Object put(final String name, final Object value) throws IllegalArgumentException, ClassCastException {
565 if (bean != null) {
566 final Object oldValue = get(name);
567 final Method method = getWriteMethod(name);
568 if (method == null) {
569 throw new IllegalArgumentException("The bean of type: " + bean.getClass().getName() + " has no property called: " + name);
570 }
571 try {
572 final Object[] arguments = createWriteMethodArguments(method, value);
573 method.invoke(bean, arguments);
574
575 final Object newValue = get(name);
576 firePropertyChange(name, oldValue, newValue);
577 } catch (final InvocationTargetException | IllegalAccessException e) {
578 throw new IllegalArgumentException(e.getMessage(), e);
579 }
580 return oldValue;
581 }
582 return null;
583 }
584
585 /**
586 * Puts all of the writable properties from the given BeanMap into this BeanMap. Read-only and Write-only properties will be ignored.
587 *
588 * @param map the BeanMap whose properties to put
589 */
590 public void putAllWriteable(final BeanMap map) {
591 map.readMethods.keySet().forEach(key -> {
592 if (getWriteMethod(key) != null) {
593 put(key, map.get(key));
594 }
595 });
596 }
597
598 // Implementation classes
599
600 /**
601 * Reinitializes this bean. Called during {@link #setBean(Object)}. Does introspection to find properties.
602 */
603 protected void reinitialise() {
604 readMethods.clear();
605 writeMethods.clear();
606 types.clear();
607 initialize();
608 }
609
610 /**
611 * Sets the bean to be operated on by this map. The given value may be null, in which case this map will be empty.
612 *
613 * @param newBean the new bean to operate on
614 */
615 public void setBean(final Object newBean) {
616 bean = newBean;
617 reinitialise();
618 }
619
620 /**
621 * Returns the number of properties defined by the bean.
622 *
623 * @return the number of properties defined by the bean
624 */
625 @Override
626 public int size() {
627 return readMethods.size();
628 }
629
630 /**
631 * Renders a string representation of this object.
632 *
633 * @return a {@code String} representation of this object
634 */
635 @Override
636 public String toString() {
637 return "BeanMap<" + bean + ">";
638 }
639
640 /**
641 * Convenience method for getting an iterator over the values.
642 *
643 * @return an iterator over the values
644 */
645 public Iterator<Object> valueIterator() {
646 final Iterator<?> iter = keyIterator();
647 return new Iterator<Object>() {
648 @Override
649 public boolean hasNext() {
650 return iter.hasNext();
651 }
652
653 @Override
654 public Object next() {
655 final Object key = iter.next();
656 return get(key);
657 }
658
659 @Override
660 public void remove() {
661 throw new UnsupportedOperationException("remove() not supported for BeanMap");
662 }
663 };
664 }
665
666 /**
667 * Gets the values for the BeanMap.
668 *
669 * @return values for the BeanMap. The returned collection is not modifiable.
670 */
671 @Override
672 public Collection<Object> values() {
673 final ArrayList<Object> answer = new ArrayList<>(readMethods.size());
674 valueIterator().forEachRemaining(answer::add);
675 return Collections.unmodifiableList(answer);
676 }
677 }