    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.util.ArrayList;
    020import java.util.Collection;
    021import java.util.Collections;
    022import java.util.HashSet;
    023import java.util.List;
    024import java.util.Map;
    025import java.util.Set;
    026
    027/**
    028 * <p>A base class for decorators providing <code>Map</code> behavior on
    029 * {@link DynaBean}s.</p>
    030 *
    031 * <p>The motivation for this implementation is to provide access to {@link DynaBean}
    032 *    properties in technologies that are unaware of BeanUtils and {@link DynaBean}s -
    033 *    such as the expression languages of JSTL and JSF.</p>
    034 *
    035 * <p>This rather technical base class implements the methods of the
    036 *    {@code Map} interface on top of a {@code DynaBean}. It was introduced
    037 *    to handle generic parameters in a meaningful way without breaking
    038 *    backwards compatibility of the {@link DynaBeanMapDecorator} class: A
    039 *    map wrapping a {@code DynaBean} should be of type {@code Map<String, Object>}.
    040 *    However, when using these generic parameters in {@code DynaBeanMapDecorator}
    041 *    this would be an incompatible change (as method signatures would have to
    042 *    be adapted). To solve this problem, this generic base class is added
    043 *    which allows specifying the key type as parameter. This makes it easy to
    044 *    have a new subclass using the correct generic parameters while
    045 *    {@code DynaBeanMapDecorator} could still remain with compatible
    046 *    parameters.</p>
    047 *
    048 * @param <K> the type of the keys in the decorated map
    049 * @since BeanUtils 1.9.0
    050 */
    051public abstract class BaseDynaBeanMapDecorator<K> implements Map<K, Object> {
    052
    053    /**
    054     * Map.Entry implementation.
    055     */
    056    private static class MapEntry<K> implements Map.Entry<K, Object> {
    057        private final K key;
    058        private final Object value;
    059        MapEntry(final K key, final Object value) {
    060            this.key = key;
    061            this.value = value;
    062        }
    063        @Override
    064        public boolean equals(final Object o) {
    065            if (!(o instanceof Map.Entry)) {
    066                return false;
    067            }
    068            final Map.Entry<?, ?> e = (Map.Entry<?, ?>)o;
    069            return key.equals(e.getKey()) &&
    070                    (value == null ? e.getValue() == null
    071                                   : value.equals(e.getValue()));
    072        }
    073        @Override
    074        public K getKey() {
    075            return key;
    076        }
    077        @Override
    078        public Object getValue() {
    079            return value;
    080        }
    081        @Override
    082        public int hashCode() {
    083            return key.hashCode() + (value == null ? 0 : value.hashCode());
    084        }
    085        @Override
    086        public Object setValue(final Object value) {
    087            throw new UnsupportedOperationException();
    088        }
    089    }
    090    private final DynaBean dynaBean;
    091    private final boolean readOnly;
    092
    093    private transient Set<K> keySet;
    094
    095    /**
    096     * Constructs a read only Map for the specified
    097     * {@link DynaBean}.
    098     *
    099     * @param dynaBean The dyna bean being decorated
    100     * @throws IllegalArgumentException if the {@link DynaBean} is null.
    101     */
    102    public BaseDynaBeanMapDecorator(final DynaBean dynaBean) {
    103        this(dynaBean, true);
    104    }
    105
    106    /**
    107     * Construct a Map for the specified {@link DynaBean}.
    108     *
    109     * @param dynaBean The dyna bean being decorated
    110     * @param readOnly <code>true</code> if the Map is read only
    111     * otherwise <code>false</code>
    112     * @throws IllegalArgumentException if the {@link DynaBean} is null.
    113     */
    114    public BaseDynaBeanMapDecorator(final DynaBean dynaBean, final boolean readOnly) {
    115        if (dynaBean == null) {
    116            throw new IllegalArgumentException("DynaBean is null");
    117        }
    118        this.dynaBean = dynaBean;
    119        this.readOnly = readOnly;
    120    }
    121
    122    /**
    123     * Always throws UnsupportedOperationException because this operation is unsupported.
    124     *
    125     * @throws UnsupportedOperationException Always thrown.
    126     */
    127    @Override
    128    public void clear() {
    129        throw new UnsupportedOperationException();
    130    }
    131
    132    /**
    133     * Indicate whether the {@link DynaBean} contains a specified
    134     * value for one (or more) of its properties.
    135     *
    136     * @param key The {@link DynaBean}'s property name
    137     * @return <code>true</code> if one of the {@link DynaBean}'s
    138     * properties contains a specified value.
    139     */
    140    @Override
    141    public boolean containsKey(final Object key) {
    142        final DynaClass dynaClass = getDynaBean().getDynaClass();
    143        final DynaProperty dynaProperty = dynaClass.getDynaProperty(toString(key));
    144        return dynaProperty == null ? false : true;
    145    }
    146
    147    /**
    148     * Indicates whether the decorated {@link DynaBean} contains
    149     * a specified value.
    150     *
    151     * @param value The value to check for.
    152     * @return <code>true</code> if one of the the {@link DynaBean}'s
    153     * properties contains the specified value, otherwise
    154     * <code>false</code>.
    155     */
    156    @Override
    157    public boolean containsValue(final Object value) {
    158        final DynaProperty[] properties = getDynaProperties();
    159        for (final DynaProperty propertie : properties) {
    160            final String key = propertie.getName();
    161            final Object prop = getDynaBean().get(key);
    162            if (value == null) {
    163                if (prop == null) {
    164                    return true;
    165                }
    166            } else if (value.equals(prop)) {
    167                return true;
    168            }
    169        }
    170        return false;
    171    }
    172
    173    /**
    174     * Converts the name of a property to the key type of this decorator.
    175     *
    176     * @param propertyName the name of a property
    177     * @return the converted key to be used in the decorated map
    178     */
    179    protected abstract K convertKey(String propertyName);
    180
    181    /**
    182     * <p>Returns the Set of the property/value mappings
    183     * in the decorated {@link DynaBean}.</p>
    184     *
    185     * <p>Each element in the Set is a <code>Map.Entry</code>
    186     * type.</p>
    187     *
    188     * @return An unmodifiable set of the DynaBean
    189     * property name/value pairs
    190     */
    191    @Override
    192    public Set<Map.Entry<K, Object>> entrySet() {
    193        final DynaProperty[] properties = getDynaProperties();
    194        final Set<Map.Entry<K, Object>> set = new HashSet<>(properties.length);
    195        for (final DynaProperty propertie : properties) {
    196            final K key = convertKey(propertie.getName());
    197            final Object value = getDynaBean().get(propertie.getName());
    198            set.add(new MapEntry<>(key, value));
    199        }
    200        return Collections.unmodifiableSet(set);
    201    }
    202
    203    /**
    204     * Return the value for the specified key from
    205     * the decorated {@link DynaBean}.
    206     *
    207     * @param key The {@link DynaBean}'s property name
    208     * @return The value for the specified property.
    209     */
    210    @Override
    211    public Object get(final Object key) {
    212        return getDynaBean().get(toString(key));
    213    }
    214
    215    /**
    216     * Provide access to the underlying {@link DynaBean}
    217     * this Map decorates.
    218     *
    219     * @return the decorated {@link DynaBean}.
    220     */
    221    public DynaBean getDynaBean() {
    222        return dynaBean;
    223    }
    224
    225    /**
    226     * Convenience method to retrieve the {@link DynaProperty}s
    227     * for this {@link DynaClass}.
    228     *
    229     * @return The an array of the {@link DynaProperty}s.
    230     */
    231    private DynaProperty[] getDynaProperties() {
    232        return getDynaBean().getDynaClass().getDynaProperties();
    233    }
    234
    235    /**
    236     * Indicate whether the decorated {@link DynaBean} has
    237     * any properties.
    238     *
    239     * @return <code>true</code> if the {@link DynaBean} has
    240     * no properties, otherwise <code>false</code>.
    241     */
    242    @Override
    243    public boolean isEmpty() {
    244        return getDynaProperties().length == 0;
    245    }
    246
    247    /**
    248     * Indicate whether the Map is read only.
    249     *
    250     * @return <code>true</code> if the Map is read only,
    251     * otherwise <code>false</code>.
    252     */
    253    public boolean isReadOnly() {
    254        return readOnly;
    255    }
    256
    257    /**
    258     * <p>Returns the Set of the property
    259     * names in the decorated {@link DynaBean}.</p>
    260     *
    261     * <p><strong>N.B.</strong>For {@link DynaBean}s whose associated {@link DynaClass}
    262     * is a {@link MutableDynaClass} a new Set is created every
    263     * time, otherwise the Set is created only once and cached.</p>
    264     *
    265     * @return An unmodifiable set of the {@link DynaBean}s
    266     * property names.
    267     */
    268    @Override
    269    public Set<K> keySet() {
    270        if (keySet != null) {
    271            return keySet;
    272        }
    273
    274        // Create a Set of the keys
    275        final DynaProperty[] properties = getDynaProperties();
    276        Set<K> set = new HashSet<>(properties.length);
    277        for (final DynaProperty propertie : properties) {
    278            set.add(convertKey(propertie.getName()));
    279        }
    280        set = Collections.unmodifiableSet(set);
    281
    282        // Cache the keySet if Not a MutableDynaClass
    283        final DynaClass dynaClass = getDynaBean().getDynaClass();
    284        if (!(dynaClass instanceof MutableDynaClass)) {
    285            keySet = set;
    286        }
    287
    288        return set;
    289
    290    }
    291
    292    /**
    293     * Set the value for the specified property in
    294     * the decorated {@link DynaBean}.
    295     *
    296     * @param key The {@link DynaBean}'s property name
    297     * @param value The value for the specified property.
    298     * @return The previous property's value.
    299     * @throws UnsupportedOperationException if
    300     * <code>isReadOnly()</code> is true.
    301     */
    302    @Override
    303    public Object put(final K key, final Object value) {
    304        if (isReadOnly()) {
    305            throw new UnsupportedOperationException("Map is read only");
    306        }
    307        final String property = toString(key);
    308        final Object previous = getDynaBean().get(property);
    309        getDynaBean().set(property, value);
    310        return previous;
    311    }
    312
    313    /**
    314     * Copy the contents of a Map to the decorated {@link DynaBean}.
    315     *
    316     * @param map The Map of values to copy.
    317     * @throws UnsupportedOperationException if
    318     * <code>isReadOnly()</code> is true.
    319     */
    320    @Override
    321    public void putAll(final Map<? extends K, ? extends Object> map) {
    322        if (isReadOnly()) {
    323            throw new UnsupportedOperationException("Map is read only");
    324        }
    325        for (final Map.Entry<? extends K, ?> e : map.entrySet()) {
    326            put(e.getKey(), e.getValue());
    327        }
    328    }
    329
    330    /**
    331     * Always throws UnsupportedOperationException because this operation is unsupported.
    332     *
    333     * @param key The {@link DynaBean}'s property name.
    334     * @return the value removed.
    335     * @throws UnsupportedOperationException Always thrown.
    336     */
    337    @Override
    338    public Object remove(final Object key) {
    339        throw new UnsupportedOperationException();
    340    }
    341
    342    /**
    343     * Returns the number properties in the decorated
    344     * {@link DynaBean}.
    345     * @return The number of properties.
    346     */
    347    @Override
    348    public int size() {
    349        return getDynaProperties().length;
    350    }
    351
    352    /**
    353     * Convenience method to convert an Object
    354     * to a String.
    355     *
    356     * @param obj The Object to convert
    357     * @return String representation of the object
    358     */
    359    private String toString(final Object obj) {
    360        return obj == null ? null : obj.toString();
    361    }
    362
    363    /**
    364     * Returns the set of property values in the
    365     * decorated {@link DynaBean}.
    366     *
    367     * @return Unmodifiable collection of values.
    368     */
    369    @Override
    370    public Collection<Object> values() {
    371        final DynaProperty[] properties = getDynaProperties();
    372        final List<Object> values = new ArrayList<>(properties.length);
    373        for (final DynaProperty propertie : properties) {
    374            final String key = propertie.getName();
    375            final Object value = getDynaBean().get(key);
    376            values.add(value);
    377        }
    378        return Collections.unmodifiableList(values);
    379    }
    380
    381}