    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    }