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.beanutils;
    18
    19  import java.util.Map;
    20  import java.util.Iterator;
    21
    22  /***
    23   * <p>Provides a <i>light weight</i> <code>DynaBean</code> facade to a <code>Map</code>
    24   *  with <i>lazy</i> map/list processing.</p>
    25   *
    26   * <p>Its a <i>light weight</i> <code>DynaBean</code> implementation because there is no
    27   *    actual <code>DynaClass</code> associated with this <code>DynaBean</code> - in fact
    28   *    it implements the <code>DynaClass</code> interface itself providing <i>pseudo</i> DynaClass
    29   *    behaviour from the actual values stored in the <code>Map</code>.</p>
    30   *
    31   * <p>As well providing rhe standard <code>DynaBean</code> access to the <code>Map</code>'s properties
    32   *    this class also provides the usual <i>Lazy</i> behaviour:</p>
    33   *    <ul>
    34   *       <li>Properties don't need to be pre-defined in a <code>DynaClass</code></li>
    35   *       <li>Indexed properties (<code>Lists</code> or <code>Arrays</code>) are automatically instantiated
    36   *           and <i>grown</i> so that they are large enough to cater for the index being set.</li>
    37   *       <li>Mapped properties are automatically instantiated.</li>
    38   *    </ul>
    39   *
    40   * <p><b><u><i>Restricted</i> DynaClass</u></b></p>
    41   *    <p>This class implements the <code>MutableDynaClass</code> interface.
    42   *       <code>MutableDynaClass</code> have a facility to <i>restrict</i> the <code>DynaClass</code>
    43   *       so that its properties cannot be modified. If the <code>MutableDynaClass</code> is
    44   *       restricted then calling any of the <code>set()</code> methods for a property which
    45   *       doesn't exist will result in a <code>IllegalArgumentException</code> being thrown.</p>
    46   *
    47   * @author Niall Pemberton
    48   */
    49  public class LazyDynaMap extends LazyDynaBean implements MutableDynaClass {
    50
    51      /***
    52       * The name of this DynaClass (analogous to the
    53       * <code>getName()</code> method of <code>java.lang.Class</code>).
    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>Controls whether the <code>getDynaProperty()</code> method returns
    64       * null if a property doesn't exist - or creates a new one.</p>
    65       *
    66       * <p>Default is <code>false</code>.
    67       */
    68      protected boolean returnNull = false;
    69
    70
    71      // ------------------- Constructors ----------------------------------
    72
    73      /***
    74       * Default Constructor.
    75       */
    76      public LazyDynaMap() {
    77          this(null, (Map)null);
    78      }
    79
    80      /***
    81       * Construct a new <code>LazyDynaMap</code> with the specified name.
    82       *
    83       * @param name Name of this DynaBean class
    84       */
    85      public LazyDynaMap(String name) {
    86          this(name, (Map)null);
    87      }
    88
    89      /***
    90       * Construct a new <code>LazyDynaMap</code> with the specified <code>Map</code>.
    91       *
    92       * @param values The Map backing this <code>LazyDynaMap</code>
    93       */
    94      public LazyDynaMap(Map values) {
    95          this(null, values);
    96      }
    97
    98      /***
    99       * 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 }