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     */
017    package org.apache.commons.beanutils;
018    
019    import java.util.Map;
020    import java.util.Iterator;
021    
022    /**
023     * <p>Provides a <i>light weight</i> <code>DynaBean</code> facade to a <code>Map</code>
024     *  with <i>lazy</i> map/list processing.</p>
025     *
026     * <p>Its a <i>light weight</i> <code>DynaBean</code> implementation because there is no
027     *    actual <code>DynaClass</code> associated with this <code>DynaBean</code> - in fact
028     *    it implements the <code>DynaClass</code> interface itself providing <i>pseudo</i> DynaClass
029     *    behaviour from the actual values stored in the <code>Map</code>.</p>
030     *
031     * <p>As well providing rhe standard <code>DynaBean</code> access to the <code>Map</code>'s properties
032     *    this class also provides the usual <i>Lazy</i> behaviour:</p>
033     *    <ul>
034     *       <li>Properties don't need to be pre-defined in a <code>DynaClass</code></li>
035     *       <li>Indexed properties (<code>Lists</code> or <code>Arrays</code>) are automatically instantiated
036     *           and <i>grown</i> so that they are large enough to cater for the index being set.</li>
037     *       <li>Mapped properties are automatically instantiated.</li>
038     *    </ul>
039     *
040     * <p><b><u><i>Restricted</i> DynaClass</u></b></p>
041     *    <p>This class implements the <code>MutableDynaClass</code> interface.
042     *       <code>MutableDynaClass</code> have a facility to <i>restrict</i> the <code>DynaClass</code>
043     *       so that its properties cannot be modified. If the <code>MutableDynaClass</code> is
044     *       restricted then calling any of the <code>set()</code> methods for a property which
045     *       doesn't exist will result in a <code>IllegalArgumentException</code> being thrown.</p>
046     *
047     * @author Niall Pemberton
048     */
049    public class LazyDynaMap extends LazyDynaBean implements MutableDynaClass {
050    
051        /**
052         * The name of this DynaClass (analogous to the
053         * <code>getName()</code> method of <code>java.lang.Class</code>).
054         */
055        protected String name;
056    
057        /**
058         * Controls whether changes to this DynaClass's properties are allowed.
059         */
060        protected boolean restricted;
061    
062        /**
063         * <p>Controls whether the <code>getDynaProperty()</code> method returns
064         * null if a property doesn't exist - or creates a new one.</p>
065         *
066         * <p>Default is <code>false</code>.
067         */
068        protected boolean returnNull = false;
069    
070    
071        // ------------------- Constructors ----------------------------------
072    
073        /**
074         * Default Constructor.
075         */
076        public LazyDynaMap() {
077            this(null, (Map)null);
078        }
079    
080        /**
081         * Construct a new <code>LazyDynaMap</code> with the specified name.
082         *
083         * @param name Name of this DynaBean class
084         */
085        public LazyDynaMap(String name) {
086            this(name, (Map)null);
087        }
088    
089        /**
090         * Construct a new <code>LazyDynaMap</code> with the specified <code>Map</code>.
091         *
092         * @param values The Map backing this <code>LazyDynaMap</code>
093         */
094        public LazyDynaMap(Map values) {
095            this(null, values);
096        }
097    
098        /**
099         * Construct a new <code>LazyDynaMap</code> with the specified name and  <code>Map</code>.
100         *
101         * @param name Name of this DynaBean class
102         * @param values The Map backing this <code>LazyDynaMap</code>
103         */
104        public LazyDynaMap(String name, Map values) {
105            this.name      = name   == null ? "LazyDynaMap" : name;
106            this.values    = values == null ? newMap()      : values;
107            this.dynaClass = this;
108        }
109    
110        /**
111         * Construct a new <code>LazyDynaMap</code> with the specified properties.
112         *
113         * @param properties Property descriptors for the supported properties
114         */
115        public LazyDynaMap(DynaProperty[] properties) {
116            this(null, properties);
117        }
118    
119        /**
120         * Construct a new <code>LazyDynaMap</code> with the specified name and properties.
121         *
122         * @param name Name of this DynaBean class
123         * @param properties Property descriptors for the supported properties
124         */
125        public LazyDynaMap(String name, DynaProperty[] properties) {
126            this(name, (Map)null);
127            if (properties != null) {
128                for (int i = 0; i < properties.length; i++) {
129                    add(properties[i]);
130                }
131            }
132        }
133    
134        /**
135         * Construct a new <code>LazyDynaMap</code> based on an exisiting DynaClass
136         *
137         * @param dynaClass DynaClass to copy the name and properties from
138         */
139        public LazyDynaMap(DynaClass dynaClass) {
140            this(dynaClass.getName(), dynaClass.getDynaProperties());
141        }
142    
143        // ------------------- Public Methods ----------------------------------
144    
145        /**
146         * Set the Map backing this <code>DynaBean</code>
147         *
148         * @param values The new Map of values
149         */
150        public void setMap(Map values) {
151            this.values = values;
152        }
153    
154        /**
155         * Return the underlying Map backing this <code>DynaBean</code>
156         * @return the underlying Map
157         */
158        public Map getMap() {
159            return values;
160        }
161    
162        // ------------------- DynaBean Methods ----------------------------------
163    
164        /**
165         * Set the value of a simple property with the specified name.
166         *
167         * @param name Name of the property whose value is to be set
168         * @param value Value to which this property is to be set
169         */
170        public void set(String name, Object value) {
171    
172            if (isRestricted() && !values.containsKey(name)) {
173                throw new IllegalArgumentException
174                        ("Invalid property name '" + name + "' (DynaClass is restricted)");
175            }
176    
177            values.put(name, value);
178    
179        }
180    
181        // ------------------- DynaClass Methods ----------------------------------
182    
183        /**
184         * Return the name of this DynaClass (analogous to the
185         * <code>getName()</code> method of <code>java.lang.Class</code)
186         *
187         * @return the name of the DynaClass
188         */
189        public String getName() {
190            return this.name;
191        }
192    
193        /**
194         * <p>Return a property descriptor for the specified property.</p>
195         *
196         * <p>If the property is not found and the <code>returnNull</code> indicator is
197         *    <code>true</code>, this method always returns <code>null</code>.</p>
198         *
199         * <p>If the property is not found and the <code>returnNull</code> indicator is
200         *    <code>false</code> a new property descriptor is created and returned (although
201         *    its not actually added to the DynaClass's properties). This is the default
202         *    beahviour.</p>
203         *
204         * <p>The reason for not returning a <code>null</code> property descriptor is that
205         *    <code>BeanUtils</code> uses this method to check if a property exists
206         *    before trying to set it - since these <i>Map</i> implementations automatically
207         *    add any new properties when they are set, returning <code>null</code> from
208         *    this method would defeat their purpose.</p>
209         *
210         * @param name Name of the dynamic property for which a descriptor
211         *  is requested
212         * @return The descriptor for the specified property
213         *
214         * @exception IllegalArgumentException if no property name is specified
215         */
216        public DynaProperty getDynaProperty(String name) {
217    
218            if (name == null) {
219                throw new IllegalArgumentException("Property name is missing.");
220            }
221    
222            // If it doesn't exist and returnNull is false
223            // create a new DynaProperty
224            if (!values.containsKey(name) && isReturnNull()) {
225                return null;
226            }
227    
228            Object value = values.get(name);
229    
230            if (value == null) {
231                return new DynaProperty(name);
232            } else {
233                return new DynaProperty(name, value.getClass());
234            }
235    
236        }
237    
238        /**
239         * <p>Return an array of <code>ProperyDescriptors</code> for the properties
240         * currently defined in this DynaClass.  If no properties are defined, a
241         * zero-length array will be returned.</p>
242         *
243         * <p><strong>FIXME</strong> - Should we really be implementing
244         * <code>getBeanInfo()</code> instead, which returns property descriptors
245         * and a bunch of other stuff?</p>
246         * @return the set of properties for this DynaClass
247         */
248        public DynaProperty[] getDynaProperties() {
249    
250            int i = 0;
251            DynaProperty[] properties = new DynaProperty[values.size()];
252            Iterator iterator = values.keySet().iterator();
253    
254            while (iterator.hasNext()) {
255                String name = (String)iterator.next();
256                Object value = values.get(name);
257                properties[i++] = new DynaProperty(name, value == null ? null : value.getClass());
258            }
259    
260            return properties;
261    
262        }
263    
264        /**
265         * Instantiate and return a new DynaBean instance, associated
266         * with this DynaClass.
267         * @return A new <code>DynaBean</code> instance
268         */
269        public DynaBean newInstance()  {
270    
271            // Create a new instance of the Map
272            Map newMap = null;
273            try {
274                newMap = (Map)getMap().getClass().newInstance();
275            } catch(Exception ex) {
276                newMap = newMap();
277            }
278    
279            // Crate new LazyDynaMap and initialize properties
280            LazyDynaMap lazyMap = new LazyDynaMap(newMap);
281            DynaProperty[] properties = this.getDynaProperties();
282            if (properties != null) {
283                for (int i = 0; i < properties.length; i++) {
284                    lazyMap.add(properties[i]);
285                }
286            }
287            return lazyMap;
288        }
289    
290    
291        // ------------------- MutableDynaClass Methods ----------------------------------
292    
293        /**
294         * <p>Is this DynaClass currently restricted.</p>
295         * <p>If restricted, no changes to the existing registration of
296         *  property names, data types, readability, or writeability are allowed.</p>
297         *
298         * @return <code>true</code> if this Mutable {@link DynaClass} is restricted,
299         * otherwise <code>false</code>
300         */
301        public boolean isRestricted() {
302            return restricted;
303        }
304    
305        /**
306         * <p>Set whether this DynaClass is currently restricted.</p>
307         * <p>If restricted, no changes to the existing registration of
308         *  property names, data types, readability, or writeability are allowed.</p>
309         *
310         * @param restricted The new restricted state
311         */
312        public void setRestricted(boolean restricted) {
313            this.restricted = restricted;
314        }
315    
316        /**
317         * Add a new dynamic property with no restrictions on data type,
318         * readability, or writeability.
319         *
320         * @param name Name of the new dynamic property
321         *
322         * @exception IllegalArgumentException if name is null
323         */
324        public void add(String name) {
325            add(name, null);
326        }
327    
328        /**
329         * Add a new dynamic property with the specified data type, but with
330         * no restrictions on readability or writeability.
331         *
332         * @param name Name of the new dynamic property
333         * @param type Data type of the new dynamic property (null for no
334         *  restrictions)
335         *
336         * @exception IllegalArgumentException if name is null
337         * @exception IllegalStateException if this DynaClass is currently
338         *  restricted, so no new properties can be added
339         */
340        public void add(String name, Class type) {
341    
342            if (name == null) {
343                throw new IllegalArgumentException("Property name is missing.");
344            }
345    
346            if (isRestricted()) {
347                throw new IllegalStateException("DynaClass is currently restricted. No new properties can be added.");
348            }
349    
350            Object value = values.get(name);
351    
352            // Check if the property already exists
353            if (value == null) {
354                values.put(name, type == null ? null : createProperty(name, type));
355            }
356    
357        }
358    
359        /**
360         * <p>Add a new dynamic property with the specified data type, readability,
361         * and writeability.</p>
362         *
363         * <p><strong>N.B.</strong>Support for readable/writeable properties has not been implemented
364         *    and this method always throws a <code>UnsupportedOperationException</code>.</p>
365         *
366         * <p>I'm not sure the intention of the original authors for this method, but it seems to
367         *    me that readable/writable should be attributes of the <code>DynaProperty</code> class
368         *    (which they are not) and is the reason this method has not been implemented.</p>
369         *
370         * @param name Name of the new dynamic property
371         * @param type Data type of the new dynamic property (null for no
372         *  restrictions)
373         * @param readable Set to <code>true</code> if this property value
374         *  should be readable
375         * @param writeable Set to <code>true</code> if this property value
376         *  should be writeable
377         *
378         * @exception UnsupportedOperationException anytime this method is called
379         */
380        public void add(String name, Class type, boolean readable, boolean writeable) {
381            throw new java.lang.UnsupportedOperationException("readable/writable properties not supported");
382        }
383    
384        /**
385         * Add a new dynamic property.
386         *
387         * @param property Property the new dynamic property to add.
388         *
389         * @exception IllegalArgumentException if name is null
390         */
391        protected void add(DynaProperty property) {
392            add(property.getName(), property.getType());
393        }
394    
395        /**
396         * Remove the specified dynamic property, and any associated data type,
397         * readability, and writeability, from this dynamic class.
398         * <strong>NOTE</strong> - This does <strong>NOT</strong> cause any
399         * corresponding property values to be removed from DynaBean instances
400         * associated with this DynaClass.
401         *
402         * @param name Name of the dynamic property to remove
403         *
404         * @exception IllegalArgumentException if name is null
405         * @exception IllegalStateException if this DynaClass is currently
406         *  restricted, so no properties can be removed
407         */
408        public void remove(String name) {
409    
410            if (name == null) {
411                throw new IllegalArgumentException("Property name is missing.");
412            }
413    
414            if (isRestricted()) {
415                throw new IllegalStateException("DynaClass is currently restricted. No properties can be removed.");
416            }
417    
418            // Remove, if property doesn't exist
419            if (values.containsKey(name)) {
420                values.remove(name);
421            }
422    
423        }
424    
425    
426        // ------------------- Additional Public Methods ----------------------------------
427    
428        /**
429         * Should this DynaClass return a <code>null</code> from
430         * the <code>getDynaProperty(name)</code> method if the property
431         * doesn't exist.
432         *
433         * @return <code>true<code> if a <code>null</code> {@link DynaProperty}
434         * should be returned if the property doesn't exist, otherwise
435         * <code>false</code> if a new {@link DynaProperty} should be created.
436         */
437        public boolean isReturnNull() {
438            return returnNull;
439        }
440    
441        /**
442         * Set whether this DynaClass should return a <code>null</code> from
443         * the <code>getDynaProperty(name)</code> method if the property
444         * doesn't exist.
445         *
446         * @param returnNull <code>true<code> if a <code>null</code> {@link DynaProperty}
447         * should be returned if the property doesn't exist, otherwise
448         * <code>false</code> if a new {@link DynaProperty} should be created.
449         */
450        public void setReturnNull(boolean returnNull) {
451            this.returnNull = returnNull;
452        }
453    
454    
455        // ------------------- Protected Methods ----------------------------------
456    
457       /**
458         * <p>Indicate whether a property actually exists.</p>
459         *
460         * <p><strong>N.B.</strong> Using <code>getDynaProperty(name) == null</code>
461         * doesn't work in this implementation because that method might
462         * return a DynaProperty if it doesn't exist (depending on the
463         * <code>returnNull</code> indicator).</p>
464         *
465         * @param name Name of the dynamic property
466         * @return <code>true</code> if the property exists,
467         * otherwise <code>false</code>
468         * @exception IllegalArgumentException if no property name is specified
469         */
470        protected boolean isDynaProperty(String name) {
471    
472            if (name == null) {
473                throw new IllegalArgumentException("Property name is missing.");
474            }
475    
476            return values.containsKey(name);
477    
478        }
479    
480    }