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