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.beanutils2;
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;
035import java.util.function.Function;
036
037/**
038 * An implementation of Map for JavaBeans which uses introspection to get and put properties in the bean.
039 * <p>
040 * If an exception occurs during attempts to get or set a property then the property is considered non existent in the Map
041 * </p>
042 */
043public class BeanMap extends AbstractMap<String, Object> implements Cloneable {
044
045    /**
046     * Map entry used by {@link BeanMap}.
047     */
048    protected static class Entry extends AbstractMap.SimpleEntry<String, Object> {
049
050        private static final long serialVersionUID = 1L;
051
052        /**
053         * The owner.
054         */
055        private final BeanMap owner;
056
057        /**
058         * Constructs a new {@code Entry}.
059         *
060         * @param owner the BeanMap this entry belongs to
061         * @param key   the key for this entry
062         * @param value the value for this entry
063         */
064        protected Entry(final BeanMap owner, final String key, final Object value) {
065            super(key, value);
066            this.owner = owner;
067        }
068
069        /**
070         * Sets the value.
071         *
072         * @param value the new value for the entry
073         * @return the old value for the entry
074         */
075        @Override
076        public Object setValue(final Object value) {
077            final String key = getKey();
078            final Object oldValue = owner.get(key);
079
080            owner.put(key, value);
081            final Object newValue = owner.get(key);
082            super.setValue(newValue);
083            return oldValue;
084        }
085    }
086
087    /**
088     * An empty array. Used to invoke accessors via reflection.
089     */
090    public static final Object[] NULL_ARGUMENTS = {};
091
092    /**
093     * Maps primitive Class types to transformers. The transformer transform strings into the appropriate primitive wrapper.
094     *
095     * N.B. private & unmodifiable replacement for the (public & static) defaultTransformers instance.
096     */
097    private static final Map<Class<? extends Object>, Function<?, ?>> typeTransformers = Collections.unmodifiableMap(createTypeTransformers());
098
099    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}