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  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       * N.B. 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 }