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