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 *      https://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 */
017package org.apache.commons.beanutils;
018
019import java.beans.BeanInfo;
020import java.beans.IntrospectionException;
021import java.beans.Introspector;
022import java.beans.PropertyDescriptor;
023import java.lang.reflect.Constructor;
024import java.lang.reflect.InvocationTargetException;
025import java.lang.reflect.Method;
026import java.util.AbstractMap;
027import java.util.AbstractSet;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.Iterator;
033import java.util.Map;
034import java.util.Set;
035
036import org.apache.commons.collections.Transformer;
037import org.apache.commons.collections.keyvalue.AbstractMapEntry;
038
039/**
040 * An implementation of Map for JavaBeans which uses introspection to get and put properties in the bean.
041 * <p>
042 * If an exception occurs during attempts to get or set a property then the property is considered non existent in the Map
043 *
044 */
045public class BeanMap extends AbstractMap<Object, Object> implements Cloneable {
046
047    /**
048     * Map entry used by {@link BeanMap}.
049     */
050    protected static class Entry extends AbstractMapEntry {
051        private final BeanMap owner;
052
053        /**
054         * Constructs a new <code>Entry</code>.
055         *
056         * @param owner the BeanMap this entry belongs to
057         * @param key   the key for this entry
058         * @param value the value for this entry
059         */
060        protected Entry(final BeanMap owner, final Object key, final Object value) {
061            super(key, value);
062            this.owner = owner;
063        }
064
065        /**
066         * Sets the value.
067         *
068         * @param value the new value for the entry
069         * @return the old value for the entry
070         */
071        @Override
072        public Object setValue(final Object value) {
073            final Object key = getKey();
074            final Object oldValue = owner.get(key);
075
076            owner.put(key, value);
077            final Object newValue = owner.get(key);
078            super.setValue(newValue);
079            return oldValue;
080        }
081    }
082
083    /**
084     * An empty array. Used to invoke accessors via reflection.
085     */
086    public static final Object[] NULL_ARGUMENTS = {};
087
088    /**
089     * Maps primitive Class types to transformers. The transformer transform strings into the appropriate primitive wrapper.
090     *
091     * Private & unmodifiable replacement for the (public & static) defaultTransformers instance.
092     */
093    private static final Map<Class<? extends Object>, Transformer> typeTransformers = Collections.unmodifiableMap(createTypeTransformers());
094
095    /**
096     * This HashMap has been made unmodifiable to prevent issues when loaded in a shared ClassLoader enviroment.
097     *
098     * @see "http://issues.apache.org/jira/browse/BEANUTILS-112"
099     * @deprecated Use {@link BeanMap#getTypeTransformer(Class)} method
100     */
101    @Deprecated
102    public static HashMap defaultTransformers = new HashMap() {
103        private static final long serialVersionUID = 1L;
104
105        @Override
106        public void clear() {
107            throw new UnsupportedOperationException();
108        }
109
110        @Override
111        public boolean containsKey(final Object key) {
112            return typeTransformers.containsKey(key);
113        }
114
115        @Override
116        public boolean containsValue(final Object value) {
117            return typeTransformers.containsValue(value);
118        }
119
120        @Override
121        public Set entrySet() {
122            return typeTransformers.entrySet();
123        }
124
125        @Override
126        public Object get(final Object key) {
127            return typeTransformers.get(key);
128        }
129
130        @Override
131        public boolean isEmpty() {
132            return false;
133        }
134
135        @Override
136        public Set keySet() {
137            return typeTransformers.keySet();
138        }
139
140        @Override
141        public Object put(final Object key, final Object value) {
142            throw new UnsupportedOperationException();
143        }
144
145        @Override
146        public void putAll(final Map m) {
147            throw new UnsupportedOperationException();
148        }
149
150        @Override
151        public Object remove(final Object key) {
152            throw new UnsupportedOperationException();
153        }
154
155        @Override
156        public int size() {
157            return typeTransformers.size();
158        }
159
160        @Override
161        public Collection values() {
162            return typeTransformers.values();
163        }
164    };
165
166    private static Map<Class<? extends Object>, Transformer> createTypeTransformers() {
167        final Map<Class<? extends Object>, Transformer> defaultTransformers = new HashMap<>();
168        defaultTransformers.put(Boolean.TYPE, input -> Boolean.valueOf(input.toString()));
169        defaultTransformers.put(Character.TYPE, input -> Character.valueOf(input.toString().charAt(0)));
170        defaultTransformers.put(Byte.TYPE, input -> Byte.valueOf(input.toString()));
171        defaultTransformers.put(Short.TYPE, input -> Short.valueOf(input.toString()));
172        defaultTransformers.put(Integer.TYPE, input -> Integer.valueOf(input.toString()));
173        defaultTransformers.put(Long.TYPE, input -> Long.valueOf(input.toString()));
174        defaultTransformers.put(Float.TYPE, input -> Float.valueOf(input.toString()));
175        defaultTransformers.put(Double.TYPE, input -> Double.valueOf(input.toString()));
176        return defaultTransformers;
177    }
178
179    private transient Object bean;
180
181    private transient HashMap<String, Method> readMethods = new HashMap<>();
182
183    private transient HashMap<String, Method> writeMethods = new HashMap<>();
184
185    // Constructors
186
187    private transient HashMap<String, Class<? extends Object>> types = new HashMap<>();
188
189    /**
190     * Constructs a new empty <code>BeanMap</code>.
191     */
192    public BeanMap() {
193    }
194
195    // Map interface
196
197    /**
198     * Constructs a new <code>BeanMap</code> that operates on the specified bean. If the given bean is <code>null</code>, then this map will be empty.
199     *
200     * @param bean the bean for this map to operate on
201     */
202    public BeanMap(final Object bean) {
203        this.bean = bean;
204        initialise();
205    }
206
207    /**
208     * 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
209     * which the map uses as its underlying data source. This behavior for <code>clear()</code> differs from the Map contract in that the mappings are not
210     * actually removed from the map (the mappings for a BeanMap are fixed).
211     */
212    @Override
213    public void clear() {
214        if (bean == null) {
215            return;
216        }
217        Class<? extends Object> beanClass = null;
218        try {
219            beanClass = bean.getClass();
220            bean = beanClass.getConstructor().newInstance();
221        } catch (final Exception e) {
222            throw new UnsupportedOperationException("Could not create new instance of class: " + beanClass, e);
223        }
224    }
225
226    /**
227     * Clone this bean map using the following process:
228     *
229     * <ul>
230     * <li>If there is no underlying bean, return a cloned BeanMap without a bean.</li>
231     * <li>Since there is an underlying bean, try to instantiate a new bean of the same type using Class.newInstance().</li>
232     * <li>If the instantiation fails, throw a CloneNotSupportedException</li>
233     * <li>Clone the bean map and set the newly instantiated bean as the underlying bean for the bean map.</li>
234     * <li>Copy each property that is both readable and writable from the existing object to a cloned bean map.</li>
235     * <li>If anything fails along the way, throw a CloneNotSupportedException.</li>
236     * </ul>
237     *
238     * @return a cloned instance of this bean map
239     * @throws CloneNotSupportedException if the underlying bean cannot be cloned
240     */
241    @Override
242    public Object clone() throws CloneNotSupportedException {
243        final BeanMap newMap = (BeanMap) super.clone();
244        if (bean == null) {
245            // no bean, just an empty bean map at the moment. return a newly
246            // cloned and empty bean map.
247            return newMap;
248        }
249        Object newBean = null;
250        final Class<? extends Object> beanClass = bean.getClass(); // Cannot throw Exception
251        try {
252            newBean = beanClass.getConstructor().newInstance();
253        } catch (final Exception e) {
254            // unable to instantiate
255            final CloneNotSupportedException cnse = new CloneNotSupportedException(
256                    "Unable to instantiate the underlying bean \"" + beanClass.getName() + "\": " + e);
257            cnse.initCause(e);
258            throw cnse;
259        }
260        try {
261            newMap.setBean(newBean);
262        } catch (final Exception exception) {
263            final CloneNotSupportedException cnse = new CloneNotSupportedException("Unable to set bean in the cloned bean map: " + exception);
264            cnse.initCause(exception);
265            throw cnse;
266        }
267        try {
268            for (final Object key : readMethods.keySet()) {
269                if (getWriteMethod(key) != null) {
270                    newMap.put(key, get(key));
271                }
272            }
273        } catch (final Exception exception) {
274            final CloneNotSupportedException cnse = new CloneNotSupportedException("Unable to copy bean values to cloned bean map: " + exception);
275            cnse.initCause(exception);
276            throw cnse;
277        }
278
279        return newMap;
280    }
281
282    /**
283     * Returns true if the bean defines a property with the given name.
284     * <p>
285     * The given name must be a <code>String</code>; if not, this method returns false. This method will also return false if the bean does not define a
286     * property with that name.
287     * <p>
288     * Write-only properties will not be matched as the test operates against property read methods.
289     *
290     * @param name the name of the property to check
291     * @return false if the given name is null or is not a <code>String</code>; false if the bean does not define a property with that name; or true if the bean
292     *         does define a property with that name
293     */
294    @Override
295    public boolean containsKey(final Object name) {
296        final Method method = getReadMethod(name);
297        return method != null;
298    }
299
300    /**
301     * Returns true if the bean defines a property whose current value is the given object.
302     *
303     * @param value the value to check
304     * @return false true if the bean has at least one property whose current value is that object, false otherwise
305     */
306    @Override
307    public boolean containsValue(final Object value) {
308        // use default implementation
309        return super.containsValue(value);
310    }
311
312    /**
313     * Converts the given value to the given type. First, reflection is is used to find a public constructor declared by the given class that takes one
314     * argument, 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
315     * constructor, and the newly constructed object is returned.
316     * <P>
317     *
318     * 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()
319     * toString()} method, and that string is parsed into the correct primitive type using, for instance, {@link Integer#valueOf(String)} to convert the string
320     * into an <code>int</code>.
321     * <P>
322     *
323     * If no special constructor exists and the given type is not a primitive type, this method returns the original value.
324     *
325     * @param newType the type to convert the value to
326     * @param value   the value to convert
327     * @return the converted value
328     * @throws NumberFormatException     if newType is a primitive type, and the string representation of the given value cannot be converted to that type
329     * @throws InstantiationException    if the constructor found with reflection raises it
330     * @throws InvocationTargetException if the constructor found with reflection raises it
331     * @throws IllegalAccessException    never
332     * @throws IllegalArgumentException  never
333     */
334    protected Object convertType(final Class<?> newType, final Object value)
335            throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
336
337        // try call constructor
338        final Class<?>[] types = { value.getClass() };
339        try {
340            final Constructor<?> constructor = newType.getConstructor(types);
341            final Object[] arguments = { value };
342            return constructor.newInstance(arguments);
343        } catch (final NoSuchMethodException e) {
344            // try using the transformers
345            final Transformer transformer = getTypeTransformer(newType);
346            if (transformer != null) {
347                return transformer.transform(value);
348            }
349            return value;
350        }
351    }
352
353    /**
354     * 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
355     * converted using {@link #convertType(Class,Object)}.
356     *
357     * @param method the mutator method
358     * @param value  the value to pass to the mutator method
359     * @return an array containing one object that is either the given value or a transformed value
360     * @throws IllegalAccessException   if {@link #convertType(Class,Object)} raises it
361     * @throws IllegalArgumentException if any other exception is raised by {@link #convertType(Class,Object)}
362     * @throws ClassCastException       if an error occurs creating the method args
363     */
364    protected Object[] createWriteMethodArguments(final Method method, Object value) throws IllegalAccessException, ClassCastException {
365        try {
366            if (value != null) {
367                final Class<? extends Object>[] types = method.getParameterTypes();
368                if (types != null && types.length > 0) {
369                    final Class<? extends Object> paramType = types[0];
370                    if (!paramType.isAssignableFrom(value.getClass())) {
371                        value = convertType(paramType, value);
372                    }
373                }
374            }
375            return new Object[] { value };
376        } catch (final InvocationTargetException | InstantiationException e) {
377            throw new IllegalArgumentException(e.getMessage(), e);
378        }
379    }
380
381    /**
382     * Convenience method for getting an iterator over the entries.
383     *
384     * @return an iterator over the entries
385     */
386    public Iterator<Map.Entry<Object, Object>> entryIterator() {
387        final Iterator<String> iter = keyIterator();
388        return new Iterator<Map.Entry<Object, Object>>() {
389            @Override
390            public boolean hasNext() {
391                return iter.hasNext();
392            }
393
394            @Override
395            public Map.Entry<Object, Object> next() {
396                final Object key = iter.next();
397                final Object value = get(key);
398                @SuppressWarnings("unchecked")
399                final
400                // This should not cause any problems; the key is actually a
401                // string, but it does no harm to expose it as Object
402                Map.Entry<Object, Object> tmpEntry = new Entry(BeanMap.this, key, value);
403                return tmpEntry;
404            }
405
406            @Override
407            public void remove() {
408                throw new UnsupportedOperationException("remove() not supported for BeanMap");
409            }
410        };
411    }
412
413    /**
414     * Gets a Set of MapEntry objects that are the mappings for this BeanMap.
415     * <p>
416     * Each MapEntry can be set but not removed.
417     *
418     * @return the unmodifiable set of mappings
419     */
420    @Override
421    public Set<Map.Entry<Object, Object>> entrySet() {
422        return Collections.unmodifiableSet(new AbstractSet<Map.Entry<Object, Object>>() {
423            @Override
424            public Iterator<Map.Entry<Object, Object>> iterator() {
425                return entryIterator();
426            }
427
428            @Override
429            public int size() {
430                return BeanMap.this.readMethods.size();
431            }
432        });
433    }
434
435    /**
436     * Called during a successful {@link #put(Object,Object)} operation. Default implementation does nothing. Override to be notified of property changes in the
437     * bean caused by this map.
438     *
439     * @param key      the name of the property that changed
440     * @param oldValue the old value for that property
441     * @param newValue the new value for that property
442     */
443    protected void firePropertyChange(final Object key, final Object oldValue, final Object newValue) {
444    }
445
446    /**
447     * Returns the value of the bean's property with the given name.
448     * <p>
449     * The given name must be a {@link String} and must not be null; otherwise, this method returns <code>null</code>. If the bean defines a property with the
450     * given name, the value of that property is returned. Otherwise, <code>null</code> is returned.
451     * <p>
452     * Write-only properties will not be matched as the test operates against property read methods.
453     *
454     * @param name the name of the property whose value to return
455     * @return the value of the property with that name
456     */
457    @Override
458    public Object get(final Object name) {
459        if (bean != null) {
460            final Method method = getReadMethod(name);
461            if (method != null) {
462                try {
463                    return method.invoke(bean, NULL_ARGUMENTS);
464                } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException | NullPointerException e) {
465                    logWarn(e);
466                }
467            }
468        }
469        return null;
470    }
471
472    /**
473     * Returns the bean currently being operated on. The return value may be null if this map is empty.
474     *
475     * @return the bean being operated on by this map
476     */
477    public Object getBean() {
478        return bean;
479    }
480
481    // Helper methods
482
483    /**
484     * Returns the accessor for the property with the given name.
485     *
486     * @param name the name of the property
487     * @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
488     */
489    protected Method getReadMethod(final Object name) {
490        return readMethods.get(name);
491    }
492
493    /**
494     * Returns the accessor for the property with the given name.
495     *
496     * @param name the name of the property
497     * @return the accessor method for the property, or null
498     */
499    public Method getReadMethod(final String name) {
500        return readMethods.get(name);
501    }
502
503    /**
504     * Returns the type of the property with the given name.
505     *
506     * @param name the name of the property
507     * @return the type of the property, or <code>null</code> if no such property exists
508     */
509    public Class<?> getType(final String name) {
510        return types.get(name);
511    }
512
513    /**
514     * Returns a transformer for the given primitive type.
515     *
516     * @param aType the primitive type whose transformer to return
517     * @return a transformer that will convert strings into that type, or null if the given type is not a primitive type
518     */
519    protected Transformer getTypeTransformer(final Class<?> aType) {
520        return typeTransformers.get(aType);
521    }
522
523    // Properties
524
525    /**
526     * Returns the mutator for the property with the given name.
527     *
528     * @param name the name of the
529     * @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
530     *         mutator method for that property
531     */
532    protected Method getWriteMethod(final Object name) {
533        return writeMethods.get(name);
534    }
535
536    /**
537     * Returns the mutator for the property with the given name.
538     *
539     * @param name the name of the property
540     * @return the mutator method for the property, or null
541     */
542    public Method getWriteMethod(final String name) {
543        return writeMethods.get(name);
544    }
545
546    private void initialise() {
547        if (getBean() == null) {
548            return;
549        }
550
551        final Class<? extends Object> beanClass = getBean().getClass();
552        try {
553            // BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
554            final BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
555            final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
556            if (propertyDescriptors != null) {
557                for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {
558                    if (propertyDescriptor != null) {
559                        final String name = propertyDescriptor.getName();
560                        final Method readMethod = propertyDescriptor.getReadMethod();
561                        final Method writeMethod = propertyDescriptor.getWriteMethod();
562                        final Class<? extends Object> aType = propertyDescriptor.getPropertyType();
563
564                        if (readMethod != null) {
565                            readMethods.put(name, readMethod);
566                        }
567                        if (writeMethod != null) {
568                            writeMethods.put(name, writeMethod);
569                        }
570                        types.put(name, aType);
571                    }
572                }
573            }
574        } catch (final IntrospectionException e) {
575            logWarn(e);
576        }
577    }
578
579    /**
580     * Convenience method for getting an iterator over the keys.
581     * <p>
582     * Write-only properties will not be returned in the iterator.
583     *
584     * @return an iterator over the keys
585     */
586    public Iterator<String> keyIterator() {
587        return readMethods.keySet().iterator();
588    }
589
590    // Implementation methods
591
592    /**
593     * Get the keys for this BeanMap.
594     * <p>
595     * 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
596     * their type.
597     *
598     * @return BeanMap keys. The Set returned by this method is not modifiable.
599     */
600    @SuppressWarnings({ "unchecked", "rawtypes" })
601    // The set actually contains strings; however, because it cannot be
602    // modified there is no danger in selling it as Set<Object>
603    @Override
604    public Set<Object> keySet() {
605        return Collections.unmodifiableSet((Set) readMethods.keySet());
606    }
607
608    /**
609     * Logs the given exception to <code>System.out</code>. Used to display warnings while accessing/mutating the bean.
610     *
611     * @param ex the exception to log
612     */
613    protected void logInfo(final Exception ex) {
614        // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
615        System.out.println("INFO: Exception: " + ex);
616    }
617
618    /**
619     * Logs the given exception to <code>System.err</code>. Used to display errors while accessing/mutating the bean.
620     *
621     * @param ex the exception to log
622     */
623    protected void logWarn(final Exception ex) {
624        // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
625        System.out.println("WARN: Exception: " + ex);
626        ex.printStackTrace();
627    }
628
629    /**
630     * Sets the bean property with the given name to the given value.
631     *
632     * @param name  the name of the property to set
633     * @param value the value to set that property to
634     * @return the previous value of that property
635     * @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
636     *                                  name; or if the bean property with that name is read-only
637     * @throws ClassCastException       if an error occurs creating the method args
638     */
639    @Override
640    public Object put(final Object name, final Object value) throws IllegalArgumentException, ClassCastException {
641        if (bean != null) {
642            final Object oldValue = get(name);
643            final Method method = getWriteMethod(name);
644            if (method == null) {
645                throw new IllegalArgumentException("The bean of type: " + bean.getClass().getName() + " has no property called: " + name);
646            }
647            try {
648                final Object[] arguments = createWriteMethodArguments(method, value);
649                method.invoke(bean, arguments);
650
651                final Object newValue = get(name);
652                firePropertyChange(name, oldValue, newValue);
653            } catch (final InvocationTargetException | IllegalAccessException e) {
654                throw new IllegalArgumentException(e.getMessage(), e);
655            }
656            return oldValue;
657        }
658        return null;
659    }
660
661    /**
662     * Puts all of the writable properties from the given BeanMap into this BeanMap. Read-only and Write-only properties will be ignored.
663     *
664     * @param map the BeanMap whose properties to put
665     */
666    public void putAllWriteable(final BeanMap map) {
667        for (final Object key : map.readMethods.keySet()) {
668            if (getWriteMethod(key) != null) {
669                put(key, map.get(key));
670            }
671        }
672    }
673
674    // Implementation classes
675
676    /**
677     * Reinitializes this bean. Called during {@link #setBean(Object)}. Does introspection to find properties.
678     */
679    protected void reinitialise() {
680        readMethods.clear();
681        writeMethods.clear();
682        types.clear();
683        initialise();
684    }
685
686    /**
687     * Sets the bean to be operated on by this map. The given value may be null, in which case this map will be empty.
688     *
689     * @param newBean the new bean to operate on
690     */
691    public void setBean(final Object newBean) {
692        bean = newBean;
693        reinitialise();
694    }
695
696    /**
697     * Returns the number of properties defined by the bean.
698     *
699     * @return the number of properties defined by the bean
700     */
701    @Override
702    public int size() {
703        return readMethods.size();
704    }
705
706    /**
707     * Renders a string representation of this object.
708     *
709     * @return a <code>String</code> representation of this object
710     */
711    @Override
712    public String toString() {
713        return "BeanMap<" + String.valueOf(bean) + ">";
714    }
715
716    /**
717     * Convenience method for getting an iterator over the values.
718     *
719     * @return an iterator over the values
720     */
721    public Iterator<Object> valueIterator() {
722        final Iterator<?> iter = keyIterator();
723        return new Iterator<Object>() {
724            @Override
725            public boolean hasNext() {
726                return iter.hasNext();
727            }
728
729            @Override
730            public Object next() {
731                final Object key = iter.next();
732                return get(key);
733            }
734
735            @Override
736            public void remove() {
737                throw new UnsupportedOperationException("remove() not supported for BeanMap");
738            }
739        };
740    }
741
742    /**
743     * Returns the values for the BeanMap.
744     *
745     * @return values for the BeanMap. The returned collection is not modifiable.
746     */
747    @Override
748    public Collection<Object> values() {
749        final ArrayList<Object> answer = new ArrayList<>(readMethods.size());
750        for (final Iterator<Object> iter = valueIterator(); iter.hasNext();) {
751            answer.add(iter.next());
752        }
753        return Collections.unmodifiableList(answer);
754    }
755}