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.Map;
20  import java.util.Objects;
21  
22  /**
23   * <p>
24   * Provides a <em>light weight</em> {@code DynaBean</code> facade to a <code>Map}
25   *  with <em>lazy</em> map/list processing.</p>
26   *
27   * <p>Its a <em>light weight</em> {@code DynaBean} implementation because there is no
28   *    actual {@code DynaClass</code> associated with this <code>DynaBean} - in fact
29   *    it implements the {@code DynaClass} interface itself providing <em>pseudo</em> DynaClass
30   *    behavior from the actual values stored in the {@code Map}.</p>
31   *
32   * <p>As well providing rhe standard {@code DynaBean</code> access to the <code>Map}'s properties
33   *    this class also provides the usual <em>Lazy</em> behavior:</p>
34   *    <ul>
35   *       <li>Properties don't need to be pre-defined in a {@code DynaClass}</li>
36   *       <li>Indexed properties ({@code Lists</code> or <code>Arrays}) are automatically instantiated
37   *           and <em>grown</em> so that they are large enough to cater for the index being set.</li>
38   *       <li>Mapped properties are automatically instantiated.</li>
39   *    </ul>
40   *
41   * <p><strong><u><em>Restricted</em> DynaClass</u></strong></p>
42   *    <p>This class implements the {@code MutableDynaClass} interface.
43   *       {@code MutableDynaClass</code> have a facility to <em>restrict</em> the <code>DynaClass} so that its properties cannot be modified. If the
44   * {@code MutableDynaClass} is restricted then calling any of the {@code set()} methods for a property which doesn't exist will result in a
45   * {@code IllegalArgumentException} being thrown.
46   * </p>
47   */
48  public class LazyDynaMap extends LazyDynaBean implements MutableDynaClass {
49  
50      private static final long serialVersionUID = 1L;
51  
52      /**
53       * The name of this DynaClass (analogous to the {@code getName()</code> method of <code>java.lang.Class}).
54       */
55      protected String name;
56  
57      /**
58       * Controls whether changes to this DynaClass's properties are allowed.
59       */
60      protected boolean restricted;
61  
62      /**
63       * <p>
64       * Controls whether the {@code getDynaProperty()} method returns null if a property doesn't exist - or creates a new one.
65       * </p>
66       *
67       * <p>
68       * Default is {@code false}.
69       */
70      protected boolean returnNull;
71  
72      /**
73       * Constructs a new instance.
74       */
75      public LazyDynaMap() {
76          this(null, (Map<String, Object>) null);
77      }
78  
79      /**
80       * Constructs a new {@code LazyDynaMap} based on an exisiting DynaClass
81       *
82       * @param dynaClass DynaClass to copy the name and properties from
83       */
84      public LazyDynaMap(final DynaClass dynaClass) {
85          this(dynaClass.getName(), dynaClass.getDynaProperties());
86      }
87  
88      /**
89       * Constructs a new {@code LazyDynaMap} with the specified properties.
90       *
91       * @param properties Property descriptors for the supported properties
92       */
93      public LazyDynaMap(final DynaProperty[] properties) {
94          this(null, properties);
95      }
96  
97      /**
98       * Constructs a new {@code LazyDynaMap</code> with the specified <code>Map}.
99       *
100      * @param values The Map backing this {@code LazyDynaMap}
101      */
102     public LazyDynaMap(final Map<String, Object> values) {
103         this(null, values);
104     }
105 
106     /**
107      * Constructs a new {@code LazyDynaMap} with the specified name.
108      *
109      * @param name Name of this DynaBean class
110      */
111     public LazyDynaMap(final String name) {
112         this(name, (Map<String, Object>) null);
113     }
114 
115     /**
116      * Constructs a new {@code LazyDynaMap} with the specified name and properties.
117      *
118      * @param name       Name of this DynaBean class
119      * @param properties Property descriptors for the supported properties
120      */
121     public LazyDynaMap(final String name, final DynaProperty[] properties) {
122         this(name, (Map<String, Object>) null);
123         if (properties != null) {
124             for (final DynaProperty property : properties) {
125                 add(property);
126             }
127         }
128     }
129 
130     /**
131      * Constructs a new {@code LazyDynaMap</code> with the specified name and  <code>Map}.
132      *
133      * @param name   Name of this DynaBean class
134      * @param values The Map backing this {@code LazyDynaMap}
135      */
136     public LazyDynaMap(final String name, final Map<String, Object> values) {
137         this.name = name == null ? "LazyDynaMap" : name;
138         this.values = values == null ? newMap() : values;
139         this.dynaClass = this;
140     }
141 
142     /**
143      * Add a new dynamic property.
144      *
145      * @param property Property the new dynamic property to add.
146      * @throws IllegalArgumentException if name is null
147      */
148     protected void add(final DynaProperty property) {
149         add(property.getName(), property.getType());
150     }
151 
152     /**
153      * Add a new dynamic property with no restrictions on data type, readability, or writeability.
154      *
155      * @param name Name of the new dynamic property
156      * @throws IllegalArgumentException if name is null
157      */
158     @Override
159     public void add(final String name) {
160         add(name, null);
161     }
162 
163     /**
164      * Add a new dynamic property with the specified data type, but with no restrictions on readability or writeability.
165      *
166      * @param name Name of the new dynamic property
167      * @param type Data type of the new dynamic property (null for no restrictions)
168      * @throws IllegalArgumentException if name is null
169      * @throws IllegalStateException    if this DynaClass is currently restricted, so no new properties can be added
170      */
171     @Override
172     public void add(final String name, final Class<?> type) {
173         Objects.requireNonNull(name, "name");
174         if (isRestricted()) {
175             throw new IllegalStateException("DynaClass is currently restricted. No new properties can be added.");
176         }
177         // Check if the property already exists
178         values.computeIfAbsent(name, k -> type == null ? null : createProperty(name, type));
179     }
180 
181     /**
182      * <p>
183      * Add a new dynamic property with the specified data type, readability, and writeability.
184      * </p>
185      *
186      * <p>
187      * <strong>N.B.</strong>Support for readable/writable properties has not been implemented and this method always throws a
188      * {@code UnsupportedOperationException}.
189      * </p>
190      *
191      * <p>
192      * I'm not sure the intention of the original authors for this method, but it seems to me that readable/writable should be attributes of the
193      * {@code DynaProperty} class (which they are not) and is the reason this method has not been implemented.
194      * </p>
195      *
196      * @param name     Name of the new dynamic property
197      * @param type     Data type of the new dynamic property (null for no restrictions)
198      * @param readable Set to {@code true} if this property value should be readable
199      * @param writable Set to {@code true} if this property value should be writable
200      * @throws UnsupportedOperationException anytime this method is called
201      */
202     @Override
203     public void add(final String name, final Class<?> type, final boolean readable, final boolean writable) {
204         throw new java.lang.UnsupportedOperationException("readable/writable properties not supported");
205     }
206 
207     /**
208      * <p>
209      * Return an array of {@code PropertyDescriptor} for the properties currently defined in this DynaClass. If no properties are defined, a zero-length array
210      * will be returned.
211      * </p>
212      *
213      * <p>
214      * <strong>FIXME</strong> - Should we really be implementing {@code getBeanInfo()} instead, which returns property descriptors and a bunch of other stuff?
215      * </p>
216      *
217      * @return the set of properties for this DynaClass
218      */
219     @Override
220     public DynaProperty[] getDynaProperties() {
221         int i = 0;
222         final DynaProperty[] properties = new DynaProperty[values.size()];
223         for (final Map.Entry<String, Object> e : values.entrySet()) {
224             final String name = e.getKey();
225             final Object value = values.get(name);
226             properties[i++] = new DynaProperty(name, value == null ? null : value.getClass());
227         }
228 
229         return properties;
230     }
231 
232     /**
233      * <p>
234      * Return a property descriptor for the specified property.
235      * </p>
236      *
237      * <p>
238      * If the property is not found and the {@code returnNull} indicator is {@code true</code>, this method always returns <code>null}.
239      * </p>
240      *
241      * <p>
242      * If the property is not found and the {@code returnNull} indicator is {@code false} a new property descriptor is created and returned (although its not
243      * actually added to the DynaClass's properties). This is the default behavior.
244      * </p>
245      *
246      * <p>
247      * The reason for not returning a {@code null} property descriptor is that {@code BeanUtils} uses this method to check if a property exists before trying to
248      * set it - since these <em>Map</em> implementations automatically add any new properties when they are set, returning {@code null} from this method would
249      * defeat their purpose.
250      * </p>
251      *
252      * @param name Name of the dynamic property for which a descriptor is requested
253      * @return The descriptor for the specified property
254      * @throws IllegalArgumentException if no property name is specified
255      */
256     @Override
257     public DynaProperty getDynaProperty(final String name) {
258         Objects.requireNonNull(name, "name");
259         final Object value = values.get(name);
260         // If it doesn't exist and returnNull is false
261         // create a new DynaProperty
262         if (value == null && isReturnNull()) {
263             return null;
264         }
265         if (value == null) {
266             return new DynaProperty(name);
267         }
268         return new DynaProperty(name, value.getClass());
269     }
270 
271     /**
272      * Gets the underlying Map backing this {@code DynaBean}
273      *
274      * @return the underlying Map
275      * @since 1.8.0
276      */
277     @Override
278     public Map<String, Object> getMap() {
279         return values;
280     }
281 
282     /**
283      * Gets the name of this DynaClass (analogous to the {@code getName()</code> method of <code>java.lang.Class})
284      *
285      * @return the name of the DynaClass
286      */
287     @Override
288     public String getName() {
289         return this.name;
290     }
291 
292     /**
293      * <p>
294      * Indicate whether a property actually exists.
295      * </p>
296      *
297      * <p>
298      * <strong>N.B.</strong> Using {@code getDynaProperty(name) == null} doesn't work in this implementation because that method might return a DynaProperty if
299      * it doesn't exist (depending on the {@code returnNull} indicator).
300      * </p>
301      *
302      * @param name Name of the dynamic property
303      * @return {@code true} if the property exists, otherwise {@code false}
304      * @throws IllegalArgumentException if no property name is specified
305      */
306     @Override
307     protected boolean isDynaProperty(final String name) {
308         return values.containsKey(Objects.requireNonNull(name, "name"));
309     }
310 
311     /**
312      * <p>
313      * Is this DynaClass currently restricted.
314      * </p>
315      * <p>
316      * If restricted, no changes to the existing registration of property names, data types, readability, or writeability are allowed.
317      * </p>
318      *
319      * @return {@code true} if this Mutable {@link DynaClass} is restricted, otherwise {@code false}
320      */
321     @Override
322     public boolean isRestricted() {
323         return restricted;
324     }
325 
326     /**
327      * Should this DynaClass return a {@code null} from the {@code getDynaProperty(name)} method if the property doesn't exist.
328      *
329      * @return {@code true</code> if a <code>null} {@link DynaProperty} should be returned if the property doesn't exist, otherwise {@code false} if a new
330      *         {@link DynaProperty} should be created.
331      */
332     public boolean isReturnNull() {
333         return returnNull;
334     }
335 
336     /**
337      * Instantiate and return a new DynaBean instance, associated with this DynaClass.
338      *
339      * @return A new {@code DynaBean} instance
340      */
341     @Override
342     public DynaBean newInstance() {
343         // Create a new instance of the Map
344         Map<String, Object> newMap = null;
345         try {
346             final
347             // The new map is used as properties map
348             Map<String, Object> temp = getMap().getClass().newInstance();
349             newMap = temp;
350         } catch (final Exception ex) {
351             newMap = newMap();
352         }
353 
354         // Crate new LazyDynaMap and initialize properties
355         final LazyDynaMap lazyMap = new LazyDynaMap(newMap);
356         final DynaProperty[] properties = getDynaProperties();
357         if (properties != null) {
358             for (final DynaProperty property : properties) {
359                 lazyMap.add(property);
360             }
361         }
362         return lazyMap;
363     }
364 
365     /**
366      * Remove the specified dynamic property, and any associated data type, readability, and writeability, from this dynamic class. <strong>NOTE</strong> - This
367      * does <strong>NOT</strong> cause any corresponding property values to be removed from DynaBean instances associated with this DynaClass.
368      *
369      * @param name Name of the dynamic property to remove
370      * @throws IllegalArgumentException if name is null
371      * @throws IllegalStateException    if this DynaClass is currently restricted, so no properties can be removed
372      */
373     @Override
374     public void remove(final String name) {
375         Objects.requireNonNull(name, "name");
376         if (isRestricted()) {
377             throw new IllegalStateException("DynaClass is currently restricted. No properties can be removed.");
378         }
379         values.remove(name);
380     }
381 
382     /**
383      * Sets the value of a simple property with the specified name.
384      *
385      * @param name  Name of the property whose value is to be set
386      * @param value Value to which this property is to be set
387      */
388     @Override
389     public void set(final String name, final Object value) {
390         if (isRestricted() && !values.containsKey(name)) {
391             throw new IllegalArgumentException("Invalid property name '" + name + "' (DynaClass is restricted)");
392         }
393         values.put(name, value);
394     }
395 
396     /**
397      * Sets the Map backing this {@code DynaBean}
398      *
399      * @param values The new Map of values
400      */
401     public void setMap(final Map<String, Object> values) {
402         this.values = values;
403     }
404 
405     /**
406      * <p>
407      * Set whether this DynaClass is currently restricted.
408      * </p>
409      * <p>
410      * If restricted, no changes to the existing registration of property names, data types, readability, or writeability are allowed.
411      * </p>
412      *
413      * @param restricted The new restricted state
414      */
415     @Override
416     public void setRestricted(final boolean restricted) {
417         this.restricted = restricted;
418     }
419 
420     /**
421      * Sets whether this DynaClass should return a {@code null} from the {@code getDynaProperty(name)} method if the property doesn't exist.
422      *
423      * @param returnNull {@code true</code> if a <code>null} {@link DynaProperty} should be returned if the property doesn't exist, otherwise {@code false} if a
424      *                   new {@link DynaProperty} should be created.
425      */
426     public void setReturnNull(final boolean returnNull) {
427         this.returnNull = returnNull;
428     }
429 
430 }