LazyDynaClass.java

  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.beanutils2;

  18. import java.util.Arrays;
  19. import java.util.Objects;

  20. /**
  21.  * <p>
  22.  * DynaClass which implements the {@code MutableDynaClass} interface.
  23.  * </p>
  24.  *
  25.  * <p>
  26.  * A {@code MutableDynaClass</code> is a specialized extension to <code>DynaClass}
  27.  *    that allows properties to be added or removed dynamically.</p>
  28.  *
  29.  * <p>This implementation has one slightly unusual default behavior - calling
  30.  *    the {@code getDynaProperty(name)} method for a property which doesn't
  31.  *    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
  32.  * 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
  33.  * {@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
  34.  * property actually exists. If the more <em>normal</em> behavior of returning {@code null} is required, then this can be achieved by calling the
  35.  * {@code setReturnNull(true)}.
  36.  * </p>
  37.  *
  38.  * <p>
  39.  * The {@code add(name, type, readable, writable)} method is not implemented and always throws an {@code UnsupportedOperationException}. I believe this
  40.  * attributes need to be added to the {@code DynaProperty} class in order to control read/write facilities.
  41.  * </p>
  42.  *
  43.  * @see LazyDynaBean
  44.  */
  45. public class LazyDynaClass extends BasicDynaClass implements MutableDynaClass {

  46.     private static final long serialVersionUID = 1L;

  47.     /**
  48.      * Controls whether changes to this DynaClass's properties are allowed.
  49.      */
  50.     protected boolean restricted;

  51.     /**
  52.      * <p>
  53.      * Controls whether the {@code getDynaProperty()} method returns null if a property doesn't exist - or creates a new one.
  54.      * </p>
  55.      *
  56.      * <p>
  57.      * Default is {@code false}.
  58.      */
  59.     protected boolean returnNull;

  60.     /**
  61.      * Constructs a new LazyDynaClass with default parameters.
  62.      */
  63.     public LazyDynaClass() {
  64.         this(null, (DynaProperty[]) null);
  65.     }

  66.     /**
  67.      * Constructs a new LazyDynaClass with the specified name.
  68.      *
  69.      * @param name Name of this DynaBean class
  70.      */
  71.     public LazyDynaClass(final String name) {
  72.         this(name, (DynaProperty[]) null);
  73.     }

  74.     /**
  75.      * Constructs a new LazyDynaClass with the specified name and DynaBean class.
  76.      *
  77.      * @param name          Name of this DynaBean class
  78.      * @param dynaBeanClass The implementation class for new instances
  79.      */
  80.     public LazyDynaClass(final String name, final Class<?> dynaBeanClass) {
  81.         this(name, dynaBeanClass, null);
  82.     }

  83.     /**
  84.      * Constructs a new LazyDynaClass with the specified name, DynaBean class and properties.
  85.      *
  86.      * @param name          Name of this DynaBean class
  87.      * @param dynaBeanClass The implementation class for new instances
  88.      * @param properties    Property descriptors for the supported properties
  89.      */
  90.     public LazyDynaClass(final String name, final Class<?> dynaBeanClass, final DynaProperty[] properties) {
  91.         super(name, dynaBeanClass, properties);
  92.     }

  93.     /**
  94.      * Constructs a new LazyDynaClass with the specified name and properties.
  95.      *
  96.      * @param name       Name of this DynaBean class
  97.      * @param properties Property descriptors for the supported properties
  98.      */
  99.     public LazyDynaClass(final String name, final DynaProperty[] properties) {
  100.         this(name, LazyDynaBean.class, properties);
  101.     }

  102.     /**
  103.      * Add a new dynamic property.
  104.      *
  105.      * @param property Property the new dynamic property to add.
  106.      * @throws IllegalArgumentException if name is null
  107.      * @throws IllegalStateException    if this DynaClass is currently restricted, so no new properties can be added
  108.      */
  109.     protected void add(final DynaProperty property) {
  110.         Objects.requireNonNull(property, "property");
  111.         Objects.requireNonNull(property.getName(), "property.getName()");
  112.         if (isRestricted()) {
  113.             throw new IllegalStateException("DynaClass is currently restricted. No new properties can be added.");
  114.         }
  115.         // Check if property already exists
  116.         if (propertiesMap.get(property.getName()) != null) {
  117.             return;
  118.         }
  119.         // Create a new property array with the specified property
  120.         final DynaProperty[] oldProperties = getDynaProperties();
  121.         final DynaProperty[] newProperties = Arrays.copyOf(oldProperties, oldProperties.length + 1);
  122.         newProperties[oldProperties.length] = property;
  123.         // Update the properties
  124.         setProperties(newProperties);
  125.     }

  126.     /**
  127.      * Add a new dynamic property with no restrictions on data type, readability, or writeability.
  128.      *
  129.      * @param name Name of the new dynamic property
  130.      * @throws IllegalArgumentException if name is null
  131.      * @throws IllegalStateException    if this DynaClass is currently restricted, so no new properties can be added
  132.      */
  133.     @Override
  134.     public void add(final String name) {
  135.         add(new DynaProperty(name));
  136.     }

  137.     /**
  138.      * Add a new dynamic property with the specified data type, but with no restrictions on readability or writeability.
  139.      *
  140.      * @param name Name of the new dynamic property
  141.      * @param type Data type of the new dynamic property (null for no restrictions)
  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, final Class<?> type) {
  147.         if (type == null) {
  148.             add(name);
  149.         } else {
  150.             add(new DynaProperty(name, type));
  151.         }
  152.     }

  153.     /**
  154.      * <p>
  155.      * Add a new dynamic property with the specified data type, readability, and writeability.
  156.      * </p>
  157.      *
  158.      * <p>
  159.      * <strong>N.B.</strong>Support for readable/writable properties has not been implemented and this method always throws a
  160.      * {@code UnsupportedOperationException}.
  161.      * </p>
  162.      *
  163.      * <p>
  164.      * 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
  165.      * {@code DynaProperty} class (which they are not) and is the reason this method has not been implemented.
  166.      * </p>
  167.      *
  168.      * @param name     Name of the new dynamic property
  169.      * @param type     Data type of the new dynamic property (null for no restrictions)
  170.      * @param readable Set to {@code true} if this property value should be readable
  171.      * @param writable Set to {@code true} if this property value should be writable
  172.      * @throws UnsupportedOperationException anytime this method is called
  173.      */
  174.     @Override
  175.     public void add(final String name, final Class<?> type, final boolean readable, final boolean writable) {
  176.         throw new java.lang.UnsupportedOperationException("readable/writable properties not supported");
  177.     }

  178.     /**
  179.      * <p>
  180.      * Return a property descriptor for the specified property.
  181.      * </p>
  182.      *
  183.      * <p>
  184.      * If the property is not found and the {@code returnNull} indicator is {@code true</code>, this method always returns <code>null}.
  185.      * </p>
  186.      *
  187.      * <p>
  188.      * 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
  189.      * actually added to the DynaClass's properties). This is the default behavior.
  190.      * </p>
  191.      *
  192.      * <p>
  193.      * 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
  194.      * set it - since these <em>Lazy</em> implementations automatically add any new properties when they are set, returning {@code null} from this method would
  195.      * defeat their purpose.
  196.      * </p>
  197.      *
  198.      * @param name Name of the dynamic property for which a descriptor is requested
  199.      * @return The dyna property for the specified name
  200.      * @throws IllegalArgumentException if no property name is specified
  201.      */
  202.     @Override
  203.     public DynaProperty getDynaProperty(final String name) {
  204.         Objects.requireNonNull(name, "name");
  205.         DynaProperty dynaProperty = propertiesMap.get(name);
  206.         // If it doesn't exist and returnNull is false
  207.         // create a new DynaProperty
  208.         if (dynaProperty == null && !isReturnNull() && !isRestricted()) {
  209.             dynaProperty = new DynaProperty(name);
  210.         }
  211.         return dynaProperty;
  212.     }

  213.     /**
  214.      * <p>
  215.      * Indicate whether a property actually exists.
  216.      * </p>
  217.      *
  218.      * <p>
  219.      * <strong>N.B.</strong> Using {@code getDynaProperty(name) == null} doesn't work in this implementation because that method might return a DynaProperty if
  220.      * it doesn't exist (depending on the {@code returnNull} indicator).
  221.      * </p>
  222.      *
  223.      * @param name The name of the property to check
  224.      * @return {@code true} if there is a property of the specified name, otherwise {@code false}
  225.      * @throws IllegalArgumentException if no property name is specified
  226.      */
  227.     public boolean isDynaProperty(final String name) {
  228.         return propertiesMap.get(Objects.requireNonNull(name, "name")) != null;
  229.     }

  230.     /**
  231.      * <p>
  232.      * Is this DynaClass currently restricted.
  233.      * </p>
  234.      * <p>
  235.      * If restricted, no changes to the existing registration of property names, data types, readability, or writeability are allowed.
  236.      * </p>
  237.      *
  238.      * @return {@code true} if this {@link MutableDynaClass} cannot be changed otherwise {@code false}
  239.      */
  240.     @Override
  241.     public boolean isRestricted() {
  242.         return restricted;
  243.     }

  244.     /**
  245.      * Should this DynaClass return a {@code null} from the {@code getDynaProperty(name)} method if the property doesn't exist.
  246.      *
  247.      * @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
  248.      *         {@link DynaProperty} should be created.
  249.      */
  250.     public boolean isReturnNull() {
  251.         return returnNull;
  252.     }

  253.     /**
  254.      * Remove the specified dynamic property, and any associated data type, readability, and writeability, from this dynamic class. <strong>NOTE</strong> - This
  255.      * does <strong>NOT</strong> cause any corresponding property values to be removed from DynaBean instances associated with this DynaClass.
  256.      *
  257.      * @param name Name of the dynamic property to remove
  258.      * @throws IllegalArgumentException if name is null
  259.      * @throws IllegalStateException    if this DynaClass is currently restricted, so no properties can be removed
  260.      */
  261.     @Override
  262.     public void remove(final String name) {
  263.         Objects.requireNonNull(name, "name");
  264.         if (isRestricted()) {
  265.             throw new IllegalStateException("DynaClass is currently restricted. No properties can be removed.");
  266.         }

  267.         // Ignore if property doesn't exist
  268.         if (propertiesMap.get(name) == null) {
  269.             return;
  270.         }

  271.         // Create a new property array of without the specified property
  272.         final DynaProperty[] oldProperties = getDynaProperties();
  273.         final DynaProperty[] newProperties = new DynaProperty[oldProperties.length - 1];
  274.         int j = 0;
  275.         for (final DynaProperty oldProperty : oldProperties) {
  276.             if (!name.equals(oldProperty.getName())) {
  277.                 newProperties[j] = oldProperty;
  278.                 j++;
  279.             }
  280.         }

  281.         // Update the properties
  282.         setProperties(newProperties);
  283.     }

  284.     /**
  285.      * <p>
  286.      * Set whether this DynaClass is currently restricted.
  287.      * </p>
  288.      * <p>
  289.      * If restricted, no changes to the existing registration of property names, data types, readability, or writeability are allowed.
  290.      * </p>
  291.      *
  292.      * @param restricted {@code true} if this {@link MutableDynaClass} cannot be changed otherwise {@code false}
  293.      */
  294.     @Override
  295.     public void setRestricted(final boolean restricted) {
  296.         this.restricted = restricted;
  297.     }

  298.     /**
  299.      * Sets whether this DynaClass should return a {@code null} from the {@code getDynaProperty(name)} method if the property doesn't exist.
  300.      *
  301.      * @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
  302.      *                   new {@link DynaProperty} should be created.
  303.      */
  304.     public void setReturnNull(final boolean returnNull) {
  305.         this.returnNull = returnNull;
  306.     }

  307. }