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