    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 *      https://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 */
    017package org.apache.commons.beanutils2;
    018
    019import java.util.Map;
    020import java.util.Objects;
    021
    022/**
    023 * <p>
    024 * Provides a <em>light weight</em> {@code DynaBean</code> facade to a <code>Map}
    025 *  with <em>lazy</em> map/list processing.</p>
    026 *
    027 * <p>Its a <em>light weight</em> {@code DynaBean} implementation because there is no
    028 *    actual {@code DynaClass</code> associated with this <code>DynaBean} - in fact
    029 *    it implements the {@code DynaClass} interface itself providing <em>pseudo</em> DynaClass
    030 *    behavior from the actual values stored in the {@code Map}.</p>
    031 *
    032 * <p>As well providing rhe standard {@code DynaBean</code> access to the <code>Map}'s properties
    033 *    this class also provides the usual <em>Lazy</em> behavior:</p>
    034 *    <ul>
    035 *       <li>Properties don't need to be pre-defined in a {@code DynaClass}</li>
    036 *       <li>Indexed properties ({@code Lists</code> or <code>Arrays}) are automatically instantiated
    037 *           and <em>grown</em> so that they are large enough to cater for the index being set.</li>
    038 *       <li>Mapped properties are automatically instantiated.</li>
    039 *    </ul>
    040 *
    041 * <p><strong><u><em>Restricted</em> DynaClass</u></strong></p>
    042 *    <p>This class implements the {@code MutableDynaClass} interface.
    043 *       {@code MutableDynaClass</code> have a facility to <em>restrict</em> the <code>DynaClass} so that its properties cannot be modified. If the
    044 * {@code MutableDynaClass} is restricted then calling any of the {@code set()} methods for a property which doesn't exist will result in a
    045 * {@code IllegalArgumentException} being thrown.
    046 * </p>
    047 */
    048public class LazyDynaMap extends LazyDynaBean implements MutableDynaClass {
    049
    050    private static final long serialVersionUID = 1L;
    051
    052    /**
    053     * The name of this DynaClass (analogous to the {@code getName()</code> method of <code>java.lang.Class}).
    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>
    064     * Controls whether the {@code getDynaProperty()} method returns null if a property doesn't exist - or creates a new one.
    065     * </p>
    066     *
    067     * <p>
    068     * Default is {@code false}.
    069     */
    070    protected boolean returnNull;
    071
    072    /**
    073     * Constructs a new instance.
    074     */
    075    public LazyDynaMap() {
    076        this(null, (Map<String, Object>) null);
    077    }
    078
    079    /**
    080     * Constructs a new {@code LazyDynaMap} based on an exisiting DynaClass
    081     *
    082     * @param dynaClass DynaClass to copy the name and properties from
    083     */
    084    public LazyDynaMap(final DynaClass dynaClass) {
    085        this(dynaClass.getName(), dynaClass.getDynaProperties());
    086    }
    087
    088    /**
    089     * Constructs a new {@code LazyDynaMap} with the specified properties.
    090     *
    091     * @param properties Property descriptors for the supported properties
    092     */
    093    public LazyDynaMap(final DynaProperty[] properties) {
    094        this(null, properties);
    095    }
    096
    097    /**
    098     * Constructs a new {@code LazyDynaMap</code> with the specified <code>Map}.
    099     *
    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}