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 * https://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 }