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         * @since 1.8.0
158         */
159        public Map getMap() {
160            return values;
161        }
162    
163        // ------------------- DynaBean Methods ----------------------------------
164    
165        /**
166         * Set the value of a simple property with the specified name.
167         *
168         * @param name Name of the property whose value is to be set
169         * @param value Value to which this property is to be set
170         */
171        public void set(String name, Object value) {
172    
173            if (isRestricted() && !values.containsKey(name)) {
174                throw new IllegalArgumentException
175                        ("Invalid property name '" + name + "' (DynaClass is restricted)");
176            }
177    
178            values.put(name, value);
179    
180        }
181    
182        // ------------------- DynaClass Methods ----------------------------------
183    
184        /**
185         * Return the name of this DynaClass (analogous to the
186         * <code>getName()</code> method of <code>java.lang.Class</code)
187         *
188         * @return the name of the DynaClass
189         */
190        public String getName() {
191            return this.name;
192        }
193    
194        /**
195         * <p>Return a property descriptor for the specified property.</p>
196         *
197         * <p>If the property is not found and the <code>returnNull</code> indicator is
198         *    <code>true</code>, this method always returns <code>null</code>.</p>
199         *
200         * <p>If the property is not found and the <code>returnNull</code> indicator is
201         *    <code>false</code> a new property descriptor is created and returned (although
202         *    its not actually added to the DynaClass's properties). This is the default
203         *    beahviour.</p>
204         *
205         * <p>The reason for not returning a <code>null</code> property descriptor is that
206         *    <code>BeanUtils</code> uses this method to check if a property exists
207         *    before trying to set it - since these <i>Map</i> implementations automatically
208         *    add any new properties when they are set, returning <code>null</code> from
209         *    this method would defeat their purpose.</p>
210         *
211         * @param name Name of the dynamic property for which a descriptor
212         *  is requested
213         * @return The descriptor for the specified property
214         *
215         * @exception IllegalArgumentException if no property name is specified
216         */
217        public DynaProperty getDynaProperty(String name) {
218    
219            if (name == null) {
220                throw new IllegalArgumentException("Property name is missing.");
221            }
222    
223            // If it doesn't exist and returnNull is false
224            // create a new DynaProperty
225            if (!values.containsKey(name) && isReturnNull()) {
226                return null;
227            }
228    
229            Object value = values.get(name);
230    
231            if (value == null) {
232                return new DynaProperty(name);
233            } else {
234                return new DynaProperty(name, value.getClass());
235            }
236    
237        }
238    
239        /**
240         * <p>Return an array of <code>ProperyDescriptors</code> for the properties
241         * currently defined in this DynaClass.  If no properties are defined, a
242         * zero-length array will be returned.</p>
243         *
244         * <p><strong>FIXME</strong> - Should we really be implementing
245         * <code>getBeanInfo()</code> instead, which returns property descriptors
246         * and a bunch of other stuff?</p>
247         * @return the set of properties for this DynaClass
248         */
249        public DynaProperty[] getDynaProperties() {
250    
251            int i = 0;
252            DynaProperty[] properties = new DynaProperty[values.size()];
253            Iterator iterator = values.keySet().iterator();
254    
255            while (iterator.hasNext()) {
256                String name = (String)iterator.next();
257                Object value = values.get(name);
258                properties[i++] = new DynaProperty(name, value == null ? null : value.getClass());
259            }
260    
261            return properties;
262    
263        }
264    
265        /**
266         * Instantiate and return a new DynaBean instance, associated
267         * with this DynaClass.
268         * @return A new <code>DynaBean</code> instance
269         */
270        public DynaBean newInstance()  {
271    
272            // Create a new instance of the Map
273            Map newMap = null;
274            try {
275                newMap = (Map)getMap().getClass().newInstance();
276            } catch(Exception ex) {
277                newMap = newMap();
278            }
279    
280            // Crate new LazyDynaMap and initialize properties
281            LazyDynaMap lazyMap = new LazyDynaMap(newMap);
282            DynaProperty[] properties = this.getDynaProperties();
283            if (properties != null) {
284                for (int i = 0; i < properties.length; i++) {
285                    lazyMap.add(properties[i]);
286                }
287            }
288            return lazyMap;
289        }
290    
291    
292        // ------------------- MutableDynaClass Methods ----------------------------------
293    
294        /**
295         * <p>Is this DynaClass currently restricted.</p>
296         * <p>If restricted, no changes to the existing registration of
297         *  property names, data types, readability, or writeability are allowed.</p>
298         *
299         * @return <code>true</code> if this Mutable {@link DynaClass} is restricted,
300         * otherwise <code>false</code>
301         */
302        public boolean isRestricted() {
303            return restricted;
304        }
305    
306        /**
307         * <p>Set whether this DynaClass is currently restricted.</p>
308         * <p>If restricted, no changes to the existing registration of
309         *  property names, data types, readability, or writeability are allowed.</p>
310         *
311         * @param restricted The new restricted state
312         */
313        public void setRestricted(boolean restricted) {
314            this.restricted = restricted;
315        }
316    
317        /**
318         * Add a new dynamic property with no restrictions on data type,
319         * readability, or writeability.
320         *
321         * @param name Name of the new dynamic property
322         *
323         * @exception IllegalArgumentException if name is null
324         */
325        public void add(String name) {
326            add(name, null);
327        }
328    
329        /**
330         * Add a new dynamic property with the specified data type, but with
331         * no restrictions on readability or writeability.
332         *
333         * @param name Name of the new dynamic property
334         * @param type Data type of the new dynamic property (null for no
335         *  restrictions)
336         *
337         * @exception IllegalArgumentException if name is null
338         * @exception IllegalStateException if this DynaClass is currently
339         *  restricted, so no new properties can be added
340         */
341        public void add(String name, Class type) {
342    
343            if (name == null) {
344                throw new IllegalArgumentException("Property name is missing.");
345            }
346    
347            if (isRestricted()) {
348                throw new IllegalStateException("DynaClass is currently restricted. No new properties can be added.");
349            }
350    
351            Object value = values.get(name);
352    
353            // Check if the property already exists
354            if (value == null) {
355                values.put(name, type == null ? null : createProperty(name, type));
356            }
357    
358        }
359    
360        /**
361         * <p>Add a new dynamic property with the specified data type, readability,
362         * and writeability.</p>
363         *
364         * <p><strong>N.B.</strong>Support for readable/writeable properties has not been implemented
365         *    and this method always throws a <code>UnsupportedOperationException</code>.</p>
366         *
367         * <p>I'm not sure the intention of the original authors for this method, but it seems to
368         *    me that readable/writable should be attributes of the <code>DynaProperty</code> class
369         *    (which they are not) and is the reason this method has not been implemented.</p>
370         *
371         * @param name Name of the new dynamic property
372         * @param type Data type of the new dynamic property (null for no
373         *  restrictions)
374         * @param readable Set to <code>true</code> if this property value
375         *  should be readable
376         * @param writeable Set to <code>true</code> if this property value
377         *  should be writeable
378         *
379         * @exception UnsupportedOperationException anytime this method is called
380         */
381        public void add(String name, Class type, boolean readable, boolean writeable) {
382            throw new java.lang.UnsupportedOperationException("readable/writable properties not supported");
383        }
384    
385        /**
386         * Add a new dynamic property.
387         *
388         * @param property Property the new dynamic property to add.
389         *
390         * @exception IllegalArgumentException if name is null
391         */
392        protected void add(DynaProperty property) {
393            add(property.getName(), property.getType());
394        }
395    
396        /**
397         * Remove the specified dynamic property, and any associated data type,
398         * readability, and writeability, from this dynamic class.
399         * <strong>NOTE</strong> - This does <strong>NOT</strong> cause any
400         * corresponding property values to be removed from DynaBean instances
401         * associated with this DynaClass.
402         *
403         * @param name Name of the dynamic property to remove
404         *
405         * @exception IllegalArgumentException if name is null
406         * @exception IllegalStateException if this DynaClass is currently
407         *  restricted, so no properties can be removed
408         */
409        public void remove(String name) {
410    
411            if (name == null) {
412                throw new IllegalArgumentException("Property name is missing.");
413            }
414    
415            if (isRestricted()) {
416                throw new IllegalStateException("DynaClass is currently restricted. No properties can be removed.");
417            }
418    
419            // Remove, if property doesn't exist
420            if (values.containsKey(name)) {
421                values.remove(name);
422            }
423    
424        }
425    
426    
427        // ------------------- Additional Public Methods ----------------------------------
428    
429        /**
430         * Should this DynaClass return a <code>null</code> from
431         * the <code>getDynaProperty(name)</code> method if the property
432         * doesn't exist.
433         *
434         * @return <code>true<code> if a <code>null</code> {@link DynaProperty}
435         * should be returned if the property doesn't exist, otherwise
436         * <code>false</code> if a new {@link DynaProperty} should be created.
437         */
438        public boolean isReturnNull() {
439            return returnNull;
440        }
441    
442        /**
443         * Set whether this DynaClass should return a <code>null</code> from
444         * the <code>getDynaProperty(name)</code> method if the property
445         * doesn't exist.
446         *
447         * @param returnNull <code>true<code> if a <code>null</code> {@link DynaProperty}
448         * should be returned if the property doesn't exist, otherwise
449         * <code>false</code> if a new {@link DynaProperty} should be created.
450         */
451        public void setReturnNull(boolean returnNull) {
452            this.returnNull = returnNull;
453        }
454    
455    
456        // ------------------- Protected Methods ----------------------------------
457    
458       /**
459         * <p>Indicate whether a property actually exists.</p>
460         *
461         * <p><strong>N.B.</strong> Using <code>getDynaProperty(name) == null</code>
462         * doesn't work in this implementation because that method might
463         * return a DynaProperty if it doesn't exist (depending on the
464         * <code>returnNull</code> indicator).</p>
465         *
466         * @param name Name of the dynamic property
467         * @return <code>true</code> if the property exists,
468         * otherwise <code>false</code>
469         * @exception IllegalArgumentException if no property name is specified
470         */
471        protected boolean isDynaProperty(String name) {
472    
473            if (name == null) {
474                throw new IllegalArgumentException("Property name is missing.");
475            }
476    
477            return values.containsKey(name);
478    
479        }
480    
481    }