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.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}