View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.beanutils2;
18  
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Objects;
26  import java.util.Set;
27  
28  /**
29   * <p>
30   * A base class for decorators providing {@code Map} behavior on {@link DynaBean}s.
31   * </p>
32   *
33   * <p>
34   * The motivation for this implementation is to provide access to {@link DynaBean} properties in technologies that are unaware of BeanUtils and
35   * {@link DynaBean}s - such as the expression languages of JSTL and JSF.
36   * </p>
37   *
38   * <p>
39   * 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
40   * parameters in a meaningful way without breaking backwards compatibility of the 1.x {@code DynaBeanMapDecorator} class: A map wrapping a {@code DynaBean}
41   * should be of type {@code Map<String, Object>}. However, when using these generic parameters in {@code DynaBeanMapDecorator} this would be an incompatible
42   * 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
43   * parameter. This makes it easy to have a new subclass using the correct generic parameters while {@code DynaBeanMapDecorator} could still remain with
44   * compatible parameters.
45   * </p>
46   *
47   * @param <K> the type of the keys in the decorated map
48   * @since 1.9.0
49   */
50  public abstract class BaseDynaBeanMapDecorator<K> implements Map<K, Object> {
51  
52      /**
53       * Map.Entry implementation.
54       */
55      private static final class MapEntry<K> implements Map.Entry<K, Object> {
56  
57          private final K key;
58          private final Object value;
59  
60          MapEntry(final K key, final Object value) {
61              this.key = key;
62              this.value = value;
63          }
64  
65          @Override
66          public boolean equals(final Object obj) {
67              if (this == obj) {
68                  return true;
69              }
70              if (!(obj instanceof Map.Entry)) {
71                  return false;
72              }
73              final Map.Entry<?, ?> other = (Map.Entry<?, ?>) obj;
74              return Objects.equals(key, other.getKey()) && Objects.equals(value, other.getValue());
75          }
76  
77          @Override
78          public K getKey() {
79              return key;
80          }
81  
82          @Override
83          public Object getValue() {
84              return value;
85          }
86  
87          @Override
88          public int hashCode() {
89              return Objects.hash(key, value);
90          }
91  
92          @Override
93          public Object setValue(final Object value) {
94              throw new UnsupportedOperationException();
95          }
96      }
97  
98      private final DynaBean dynaBean;
99      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 }