BaseDynaBeanMapDecorator.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.ArrayList;
  19. import java.util.Collection;
  20. import java.util.Collections;
  21. import java.util.HashSet;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.Objects;
  25. import java.util.Set;

  26. /**
  27.  * <p>
  28.  * A base class for decorators providing {@code Map} behavior on {@link DynaBean}s.
  29.  * </p>
  30.  *
  31.  * <p>
  32.  * The motivation for this implementation is to provide access to {@link DynaBean} properties in technologies that are unaware of BeanUtils and
  33.  * {@link DynaBean}s - such as the expression languages of JSTL and JSF.
  34.  * </p>
  35.  *
  36.  * <p>
  37.  * This rather technical base class implements the methods of the {@code Map} interface on top of a {@code DynaBean}. It was introduced to handle generic
  38.  * parameters in a meaningful way without breaking backwards compatibility of the 1.x {@code DynaBeanMapDecorator} class: A map wrapping a {@code DynaBean}
  39.  * should be of type {@code Map<String, Object>}. However, when using these generic parameters in {@code DynaBeanMapDecorator} this would be an incompatible
  40.  * change (as method signatures would have to be adapted). To solve this problem, this generic base class is added which allows specifying the key type as
  41.  * parameter. This makes it easy to have a new subclass using the correct generic parameters while {@code DynaBeanMapDecorator} could still remain with
  42.  * compatible parameters.
  43.  * </p>
  44.  *
  45.  * @param <K> the type of the keys in the decorated map
  46.  * @since 1.9.0
  47.  */
  48. public abstract class BaseDynaBeanMapDecorator<K> implements Map<K, Object> {

  49.     /**
  50.      * Map.Entry implementation.
  51.      */
  52.     private static final class MapEntry<K> implements Map.Entry<K, Object> {

  53.         private final K key;
  54.         private final Object value;

  55.         MapEntry(final K key, final Object value) {
  56.             this.key = key;
  57.             this.value = value;
  58.         }

  59.         @Override
  60.         public boolean equals(final Object obj) {
  61.             if (this == obj) {
  62.                 return true;
  63.             }
  64.             if (!(obj instanceof Map.Entry)) {
  65.                 return false;
  66.             }
  67.             final Map.Entry<?, ?> other = (Map.Entry<?, ?>) obj;
  68.             return Objects.equals(key, other.getKey()) && Objects.equals(value, other.getValue());
  69.         }

  70.         @Override
  71.         public K getKey() {
  72.             return key;
  73.         }

  74.         @Override
  75.         public Object getValue() {
  76.             return value;
  77.         }

  78.         @Override
  79.         public int hashCode() {
  80.             return Objects.hash(key, value);
  81.         }

  82.         @Override
  83.         public Object setValue(final Object value) {
  84.             throw new UnsupportedOperationException();
  85.         }
  86.     }

  87.     private final DynaBean dynaBean;
  88.     private final boolean readOnly;

  89.     private transient Set<K> keySet;

  90.     /**
  91.      * Constructs a read only Map for the specified {@link DynaBean}.
  92.      *
  93.      * @param dynaBean The dyna bean being decorated
  94.      * @throws IllegalArgumentException if the {@link DynaBean} is null.
  95.      */
  96.     public BaseDynaBeanMapDecorator(final DynaBean dynaBean) {
  97.         this(dynaBean, true);
  98.     }

  99.     /**
  100.      * Constructs a Map for the specified {@link DynaBean}.
  101.      *
  102.      * @param dynaBean The dyna bean being decorated
  103.      * @param readOnly {@code true} if the Map is read only otherwise {@code false}
  104.      * @throws IllegalArgumentException if the {@link DynaBean} is null.
  105.      */
  106.     public BaseDynaBeanMapDecorator(final DynaBean dynaBean, final boolean readOnly) {
  107.         this.dynaBean = Objects.requireNonNull(dynaBean, "dynaBean");
  108.         this.readOnly = readOnly;
  109.     }

  110.     /**
  111.      * clear() operation is not supported.
  112.      *
  113.      * @throws UnsupportedOperationException This operation is not yet supported
  114.      */
  115.     @Override
  116.     public void clear() {
  117.         throw new UnsupportedOperationException();
  118.     }

  119.     /**
  120.      * Indicate whether the {@link DynaBean} contains a specified value for one (or more) of its properties.
  121.      *
  122.      * @param key The {@link DynaBean}'s property name
  123.      * @return {@code true} if one of the {@link DynaBean}'s properties contains a specified value.
  124.      */
  125.     @Override
  126.     public boolean containsKey(final Object key) {
  127.         final DynaClass dynaClass = getDynaBean().getDynaClass();
  128.         final DynaProperty dynaProperty = dynaClass.getDynaProperty(toString(key));
  129.         return dynaProperty != null;
  130.     }

  131.     /**
  132.      * Indicates whether the decorated {@link DynaBean} contains a specified value.
  133.      *
  134.      * @param value The value to check for.
  135.      * @return {@code true} if one of the {@link DynaBean}'s properties contains the specified value, otherwise {@code false}.
  136.      */
  137.     @Override
  138.     public boolean containsValue(final Object value) {
  139.         final DynaProperty[] properties = getDynaProperties();
  140.         for (final DynaProperty property : properties) {
  141.             final String key = property.getName();
  142.             final Object prop = getDynaBean().get(key);
  143.             if (value == null) {
  144.                 if (prop == null) {
  145.                     return true;
  146.                 }
  147.             } else if (value.equals(prop)) {
  148.                 return true;
  149.             }
  150.         }
  151.         return false;
  152.     }

  153.     /**
  154.      * Converts the name of a property to the key type of this decorator.
  155.      *
  156.      * @param propertyName the name of a property
  157.      * @return the converted key to be used in the decorated map
  158.      */
  159.     protected abstract K convertKey(String propertyName);

  160.     /**
  161.      * <p>
  162.      * Returns the Set of the property/value mappings in the decorated {@link DynaBean}.
  163.      * </p>
  164.      *
  165.      * <p>
  166.      * Each element in the Set is a {@code Map.Entry} type.
  167.      * </p>
  168.      *
  169.      * @return An unmodifiable set of the DynaBean property name/value pairs
  170.      */
  171.     @Override
  172.     public Set<Map.Entry<K, Object>> entrySet() {
  173.         final DynaProperty[] properties = getDynaProperties();
  174.         final Set<Map.Entry<K, Object>> set = new HashSet<>(properties.length);
  175.         for (final DynaProperty property : properties) {
  176.             final K key = convertKey(property.getName());
  177.             final Object value = getDynaBean().get(property.getName());
  178.             set.add(new MapEntry<>(key, value));
  179.         }
  180.         return Collections.unmodifiableSet(set);
  181.     }

  182.     /**
  183.      * Gets the value for the specified key from the decorated {@link DynaBean}.
  184.      *
  185.      * @param key The {@link DynaBean}'s property name
  186.      * @return The value for the specified property.
  187.      */
  188.     @Override
  189.     public Object get(final Object key) {
  190.         return getDynaBean().get(toString(key));
  191.     }

  192.     /**
  193.      * Provide access to the underlying {@link DynaBean} this Map decorates.
  194.      *
  195.      * @return the decorated {@link DynaBean}.
  196.      */
  197.     public DynaBean getDynaBean() {
  198.         return dynaBean;
  199.     }

  200.     /**
  201.      * Convenience method to retrieve the {@link DynaProperty}s for this {@link DynaClass}.
  202.      *
  203.      * @return The an array of the {@link DynaProperty}s.
  204.      */
  205.     private DynaProperty[] getDynaProperties() {
  206.         return getDynaBean().getDynaClass().getDynaProperties();
  207.     }

  208.     /**
  209.      * Indicate whether the decorated {@link DynaBean} has any properties.
  210.      *
  211.      * @return {@code true} if the {@link DynaBean} has no properties, otherwise {@code false}.
  212.      */
  213.     @Override
  214.     public boolean isEmpty() {
  215.         return getDynaProperties().length == 0;
  216.     }

  217.     /**
  218.      * Indicate whether the Map is read only.
  219.      *
  220.      * @return {@code true} if the Map is read only, otherwise {@code false}.
  221.      */
  222.     public boolean isReadOnly() {
  223.         return readOnly;
  224.     }

  225.     /**
  226.      * <p>
  227.      * Returns the Set of the property names in the decorated {@link DynaBean}.
  228.      * </p>
  229.      *
  230.      * <p>
  231.      * <strong>N.B.</strong>For {@link DynaBean}s whose associated {@link DynaClass} is a {@link MutableDynaClass} a new Set is created every time, otherwise
  232.      * the Set is created only once and cached.
  233.      * </p>
  234.      *
  235.      * @return An unmodifiable set of the {@link DynaBean}s property names.
  236.      */
  237.     @Override
  238.     public Set<K> keySet() {
  239.         if (keySet != null) {
  240.             return keySet;
  241.         }

  242.         // Create a Set of the keys
  243.         final DynaProperty[] properties = getDynaProperties();
  244.         Set<K> set = new HashSet<>(properties.length);
  245.         for (final DynaProperty property : properties) {
  246.             set.add(convertKey(property.getName()));
  247.         }
  248.         set = Collections.unmodifiableSet(set);

  249.         // Cache the keySet if Not a MutableDynaClass
  250.         final DynaClass dynaClass = getDynaBean().getDynaClass();
  251.         if (!(dynaClass instanceof MutableDynaClass)) {
  252.             keySet = set;
  253.         }

  254.         return set;

  255.     }

  256.     /**
  257.      * Puts the value for the specified property in the decorated {@link DynaBean}.
  258.      *
  259.      * @param key   The {@link DynaBean}'s property name
  260.      * @param value The value for the specified property.
  261.      * @return The previous property's value.
  262.      * @throws UnsupportedOperationException if {@code isReadOnly()} is true.
  263.      */
  264.     @Override
  265.     public Object put(final K key, final Object value) {
  266.         if (isReadOnly()) {
  267.             throw new UnsupportedOperationException("Map is read only");
  268.         }
  269.         final String property = toString(key);
  270.         final Object previous = getDynaBean().get(property);
  271.         getDynaBean().set(property, value);
  272.         return previous;
  273.     }

  274.     /**
  275.      * Copy the contents of a Map to the decorated {@link DynaBean}.
  276.      *
  277.      * @param map The Map of values to copy.
  278.      * @throws UnsupportedOperationException if {@code isReadOnly()} is true.
  279.      */
  280.     @Override
  281.     public void putAll(final Map<? extends K, ? extends Object> map) {
  282.         if (isReadOnly()) {
  283.             throw new UnsupportedOperationException("Map is read only");
  284.         }
  285.         map.forEach(this::put);
  286.     }

  287.     /**
  288.      * remove() operation is not supported.
  289.      *
  290.      * @param key The {@link DynaBean}'s property name
  291.      * @return the value removed
  292.      * @throws UnsupportedOperationException This operation is not yet supported
  293.      */
  294.     @Override
  295.     public Object remove(final Object key) {
  296.         throw new UnsupportedOperationException();
  297.     }

  298.     /**
  299.      * Returns the number properties in the decorated {@link DynaBean}.
  300.      *
  301.      * @return The number of properties.
  302.      */
  303.     @Override
  304.     public int size() {
  305.         return getDynaProperties().length;
  306.     }

  307.     /**
  308.      * Convenience method to convert an Object to a String.
  309.      *
  310.      * @param obj The Object to convert
  311.      * @return String representation of the object
  312.      */
  313.     private String toString(final Object obj) {
  314.         return Objects.toString(obj, null);
  315.     }

  316.     /**
  317.      * Returns the set of property values in the decorated {@link DynaBean}.
  318.      *
  319.      * @return Unmodifiable collection of values.
  320.      */
  321.     @Override
  322.     public Collection<Object> values() {
  323.         final DynaProperty[] properties = getDynaProperties();
  324.         final List<Object> values = new ArrayList<>(properties.length);
  325.         for (final DynaProperty property : properties) {
  326.             final String key = property.getName();
  327.             final Object value = getDynaBean().get(key);
  328.             values.add(value);
  329.         }
  330.         return Collections.unmodifiableList(values);
  331.     }

  332. }