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}