    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.Arrays;
    020import java.util.Objects;
    021
    022/**
    023 * <p>
    024 * DynaClass which implements the {@code MutableDynaClass} interface.
    025 * </p>
    026 *
    027 * <p>
    028 * A {@code MutableDynaClass</code> is a specialized extension to <code>DynaClass}
    029 *    that allows properties to be added or removed dynamically.</p>
    030 *
    031 * <p>This implementation has one slightly unusual default behavior - calling
    032 *    the {@code getDynaProperty(name)} method for a property which doesn't
    033 *    exist returns a {@code DynaProperty</code> rather than <code>null}. The reason for this is that {@code BeanUtils} calls this method to check if a property
    034 * exists before trying to set the value. This would defeat the object of the {@code LazyDynaBean} which automatically adds missing properties when any of its
    035 * {@code set()} methods are called. For this reason the {@code isDynaProperty(name)} method has been added to this implementation in order to determine if a
    036 * property actually exists. If the more <em>normal</em> behavior of returning {@code null} is required, then this can be achieved by calling the
    037 * {@code setReturnNull(true)}.
    038 * </p>
    039 *
    040 * <p>
    041 * The {@code add(name, type, readable, writable)} method is not implemented and always throws an {@code UnsupportedOperationException}. I believe this
    042 * attributes need to be added to the {@code DynaProperty} class in order to control read/write facilities.
    043 * </p>
    044 *
    045 * @see LazyDynaBean
    046 */
    047public class LazyDynaClass extends BasicDynaClass implements MutableDynaClass {
    048
    049    private static final long serialVersionUID = 1L;
    050
    051    /**
    052     * Controls whether changes to this DynaClass's properties are allowed.
    053     */
    054    protected boolean restricted;
    055
    056    /**
    057     * <p>
    058     * Controls whether the {@code getDynaProperty()} method returns null if a property doesn't exist - or creates a new one.
    059     * </p>
    060     *
    061     * <p>
    062     * Default is {@code false}.
    063     */
    064    protected boolean returnNull;
    065
    066    /**
    067     * Constructs a new LazyDynaClass with default parameters.
    068     */
    069    public LazyDynaClass() {
    070        this(null, (DynaProperty[]) null);
    071    }
    072
    073    /**
    074     * Constructs a new LazyDynaClass with the specified name.
    075     *
    076     * @param name Name of this DynaBean class
    077     */
    078    public LazyDynaClass(final String name) {
    079        this(name, (DynaProperty[]) null);
    080    }
    081
    082    /**
    083     * Constructs a new LazyDynaClass with the specified name and DynaBean class.
    084     *
    085     * @param name          Name of this DynaBean class
    086     * @param dynaBeanClass The implementation class for new instances
    087     */
    088    public LazyDynaClass(final String name, final Class<?> dynaBeanClass) {
    089        this(name, dynaBeanClass, null);
    090    }
    091
    092    /**
    093     * Constructs a new LazyDynaClass with the specified name, DynaBean class and properties.
    094     *
    095     * @param name          Name of this DynaBean class
    096     * @param dynaBeanClass The implementation class for new instances
    097     * @param properties    Property descriptors for the supported properties
    098     */
    099    public LazyDynaClass(final String name, final Class<?> dynaBeanClass, final DynaProperty[] properties) {
    100        super(name, dynaBeanClass, properties);
    101    }
    102
    103    /**
    104     * Constructs a new LazyDynaClass with the specified name and properties.
    105     *
    106     * @param name       Name of this DynaBean class
    107     * @param properties Property descriptors for the supported properties
    108     */
    109    public LazyDynaClass(final String name, final DynaProperty[] properties) {
    110        this(name, LazyDynaBean.class, properties);
    111    }
    112
    113    /**
    114     * Add a new dynamic property.
    115     *
    116     * @param property Property the new dynamic property to add.
    117     * @throws IllegalArgumentException if name is null
    118     * @throws IllegalStateException    if this DynaClass is currently restricted, so no new properties can be added
    119     */
    120    protected void add(final DynaProperty property) {
    121        Objects.requireNonNull(property, "property");
    122        Objects.requireNonNull(property.getName(), "property.getName()");
    123        if (isRestricted()) {
    124            throw new IllegalStateException("DynaClass is currently restricted. No new properties can be added.");
    125        }
    126        // Check if property already exists
    127        if (propertiesMap.get(property.getName()) != null) {
    128            return;
    129        }
    130        // Create a new property array with the specified property
    131        final DynaProperty[] oldProperties = getDynaProperties();
    132        final DynaProperty[] newProperties = Arrays.copyOf(oldProperties, oldProperties.length + 1);
    133        newProperties[oldProperties.length] = property;
    134        // Update the properties
    135        setProperties(newProperties);
    136    }
    137
    138    /**
    139     * Add a new dynamic property with no restrictions on data type, readability, or writeability.
    140     *
    141     * @param name Name of the new dynamic property
    142     * @throws IllegalArgumentException if name is null
    143     * @throws IllegalStateException    if this DynaClass is currently restricted, so no new properties can be added
    144     */
    145    @Override
    146    public void add(final String name) {
    147        add(new DynaProperty(name));
    148    }
    149
    150    /**
    151     * Add a new dynamic property with the specified data type, but with no restrictions on readability or writeability.
    152     *
    153     * @param name Name of the new dynamic property
    154     * @param type Data type of the new dynamic property (null for no restrictions)
    155     * @throws IllegalArgumentException if name is null
    156     * @throws IllegalStateException    if this DynaClass is currently restricted, so no new properties can be added
    157     */
    158    @Override
    159    public void add(final String name, final Class<?> type) {
    160        if (type == null) {
    161            add(name);
    162        } else {
    163            add(new DynaProperty(name, type));
    164        }
    165    }
    166
    167    /**
    168     * <p>
    169     * Add a new dynamic property with the specified data type, readability, and writeability.
    170     * </p>
    171     *
    172     * <p>
    173     * <strong>N.B.</strong>Support for readable/writable properties has not been implemented and this method always throws a
    174     * {@code UnsupportedOperationException}.
    175     * </p>
    176     *
    177     * <p>
    178     * 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
    179     * {@code DynaProperty} class (which they are not) and is the reason this method has not been implemented.
    180     * </p>
    181     *
    182     * @param name     Name of the new dynamic property
    183     * @param type     Data type of the new dynamic property (null for no restrictions)
    184     * @param readable Set to {@code true} if this property value should be readable
    185     * @param writable Set to {@code true} if this property value should be writable
    186     * @throws UnsupportedOperationException anytime this method is called
    187     */
    188    @Override
    189    public void add(final String name, final Class<?> type, final boolean readable, final boolean writable) {
    190        throw new java.lang.UnsupportedOperationException("readable/writable properties not supported");
    191    }
    192
    193    /**
    194     * <p>
    195     * Return a property descriptor for the specified property.
    196     * </p>
    197     *
    198     * <p>
    199     * If the property is not found and the {@code returnNull} indicator is {@code true</code>, this method always returns <code>null}.
    200     * </p>
    201     *
    202     * <p>
    203     * 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
    204     * actually added to the DynaClass's properties). This is the default behavior.
    205     * </p>
    206     *
    207     * <p>
    208     * 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
    209     * set it - since these <em>Lazy</em> implementations automatically add any new properties when they are set, returning {@code null} from this method would
    210     * defeat their purpose.
    211     * </p>
    212     *
    213     * @param name Name of the dynamic property for which a descriptor is requested
    214     * @return The dyna property for the specified name
    215     * @throws IllegalArgumentException if no property name is specified
    216     */
    217    @Override
    218    public DynaProperty getDynaProperty(final String name) {
    219        Objects.requireNonNull(name, "name");
    220        DynaProperty dynaProperty = propertiesMap.get(name);
    221        // If it doesn't exist and returnNull is false
    222        // create a new DynaProperty
    223        if (dynaProperty == null && !isReturnNull() && !isRestricted()) {
    224            dynaProperty = new DynaProperty(name);
    225        }
    226        return dynaProperty;
    227    }
    228
    229    /**
    230     * <p>
    231     * Indicate whether a property actually exists.
    232     * </p>
    233     *
    234     * <p>
    235     * <strong>N.B.</strong> Using {@code getDynaProperty(name) == null} doesn't work in this implementation because that method might return a DynaProperty if
    236     * it doesn't exist (depending on the {@code returnNull} indicator).
    237     * </p>
    238     *
    239     * @param name The name of the property to check
    240     * @return {@code true} if there is a property of the specified name, otherwise {@code false}
    241     * @throws IllegalArgumentException if no property name is specified
    242     */
    243    public boolean isDynaProperty(final String name) {
    244        return propertiesMap.get(Objects.requireNonNull(name, "name")) != null;
    245    }
    246
    247    /**
    248     * <p>
    249     * Is this DynaClass currently restricted.
    250     * </p>
    251     * <p>
    252     * If restricted, no changes to the existing registration of property names, data types, readability, or writeability are allowed.
    253     * </p>
    254     *
    255     * @return {@code true} if this {@link MutableDynaClass} cannot be changed otherwise {@code false}
    256     */
    257    @Override
    258    public boolean isRestricted() {
    259        return restricted;
    260    }
    261
    262    /**
    263     * Should this DynaClass return a {@code null} from the {@code getDynaProperty(name)} method if the property doesn't exist.
    264     *
    265     * @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
    266     *         {@link DynaProperty} should be created.
    267     */
    268    public boolean isReturnNull() {
    269        return returnNull;
    270    }
    271
    272    /**
    273     * Remove the specified dynamic property, and any associated data type, readability, and writeability, from this dynamic class. <strong>NOTE</strong> - This
    274     * does <strong>NOT</strong> cause any corresponding property values to be removed from DynaBean instances associated with this DynaClass.
    275     *
    276     * @param name Name of the dynamic property to remove
    277     * @throws IllegalArgumentException if name is null
    278     * @throws IllegalStateException    if this DynaClass is currently restricted, so no properties can be removed
    279     */
    280    @Override
    281    public void remove(final String name) {
    282        Objects.requireNonNull(name, "name");
    283        if (isRestricted()) {
    284            throw new IllegalStateException("DynaClass is currently restricted. No properties can be removed.");
    285        }
    286
    287        // Ignore if property doesn't exist
    288        if (propertiesMap.get(name) == null) {
    289            return;
    290        }
    291
    292        // Create a new property array of without the specified property
    293        final DynaProperty[] oldProperties = getDynaProperties();
    294        final DynaProperty[] newProperties = new DynaProperty[oldProperties.length - 1];
    295        int j = 0;
    296        for (final DynaProperty oldProperty : oldProperties) {
    297            if (!name.equals(oldProperty.getName())) {
    298                newProperties[j] = oldProperty;
    299                j++;
    300            }
    301        }
    302
    303        // Update the properties
    304        setProperties(newProperties);
    305    }
    306
    307    /**
    308     * <p>
    309     * Set whether this DynaClass is currently restricted.
    310     * </p>
    311     * <p>
    312     * If restricted, no changes to the existing registration of property names, data types, readability, or writeability are allowed.
    313     * </p>
    314     *
    315     * @param restricted {@code true} if this {@link MutableDynaClass} cannot be changed otherwise {@code false}
    316     */
    317    @Override
    318    public void setRestricted(final boolean restricted) {
    319        this.restricted = restricted;
    320    }
    321
    322    /**
    323     * Sets whether this DynaClass should return a {@code null} from the {@code getDynaProperty(name)} method if the property doesn't exist.
    324     *
    325     * @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
    326     *                   new {@link DynaProperty} should be created.
    327     */
    328    public void setReturnNull(final boolean returnNull) {
    329        this.returnNull = returnNull;
    330    }
    331
    332}