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 * http://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.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Objects; 026import java.util.Set; 027 028/** 029 * <p> 030 * A base class for decorators providing {@code Map} behavior on {@link DynaBean}s. 031 * </p> 032 * 033 * <p> 034 * The motivation for this implementation is to provide access to {@link DynaBean} properties in technologies that are unaware of BeanUtils and 035 * {@link DynaBean}s - such as the expression languages of JSTL and JSF. 036 * </p> 037 * 038 * <p> 039 * 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 040 * parameters in a meaningful way without breaking backwards compatibility of the 1.x {@code DynaBeanMapDecorator} class: A map wrapping a {@code DynaBean} 041 * should be of type {@code Map<String, Object>}. However, when using these generic parameters in {@code DynaBeanMapDecorator} this would be an incompatible 042 * 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 043 * parameter. This makes it easy to have a new subclass using the correct generic parameters while {@code DynaBeanMapDecorator} could still remain with 044 * compatible parameters. 045 * </p> 046 * 047 * @param <K> the type of the keys in the decorated map 048 * @since 1.9.0 049 */ 050public abstract class BaseDynaBeanMapDecorator<K> implements Map<K, Object> { 051 052 /** 053 * Map.Entry implementation. 054 */ 055 private static final class MapEntry<K> implements Map.Entry<K, Object> { 056 057 private final K key; 058 private final Object value; 059 060 MapEntry(final K key, final Object value) { 061 this.key = key; 062 this.value = value; 063 } 064 065 @Override 066 public boolean equals(final Object obj) { 067 if (this == obj) { 068 return true; 069 } 070 if (!(obj instanceof Map.Entry)) { 071 return false; 072 } 073 final Map.Entry<?, ?> other = (Map.Entry<?, ?>) obj; 074 return Objects.equals(key, other.getKey()) && Objects.equals(value, other.getValue()); 075 } 076 077 @Override 078 public K getKey() { 079 return key; 080 } 081 082 @Override 083 public Object getValue() { 084 return value; 085 } 086 087 @Override 088 public int hashCode() { 089 return Objects.hash(key, value); 090 } 091 092 @Override 093 public Object setValue(final Object value) { 094 throw new UnsupportedOperationException(); 095 } 096 } 097 098 private final DynaBean dynaBean; 099 private final boolean readOnly; 100 101 private transient Set<K> keySet; 102 103 /** 104 * Constructs a read only Map for the specified {@link DynaBean}. 105 * 106 * @param dynaBean The dyna bean being decorated 107 * @throws IllegalArgumentException if the {@link DynaBean} is null. 108 */ 109 public BaseDynaBeanMapDecorator(final DynaBean dynaBean) { 110 this(dynaBean, true); 111 } 112 113 /** 114 * Constructs a Map for the specified {@link DynaBean}. 115 * 116 * @param dynaBean The dyna bean being decorated 117 * @param readOnly {@code true} if the Map is read only otherwise {@code false} 118 * @throws IllegalArgumentException if the {@link DynaBean} is null. 119 */ 120 public BaseDynaBeanMapDecorator(final DynaBean dynaBean, final boolean readOnly) { 121 this.dynaBean = Objects.requireNonNull(dynaBean, "dynaBean"); 122 this.readOnly = readOnly; 123 } 124 125 /** 126 * clear() operation is not supported. 127 * 128 * @throws UnsupportedOperationException This operation is not yet supported 129 */ 130 @Override 131 public void clear() { 132 throw new UnsupportedOperationException(); 133 } 134 135 /** 136 * Indicate whether the {@link DynaBean} contains a specified value for one (or more) of its properties. 137 * 138 * @param key The {@link DynaBean}'s property name 139 * @return {@code true} if one of the {@link DynaBean}'s properties contains a specified value. 140 */ 141 @Override 142 public boolean containsKey(final Object key) { 143 final DynaClass dynaClass = getDynaBean().getDynaClass(); 144 final DynaProperty dynaProperty = dynaClass.getDynaProperty(toString(key)); 145 return dynaProperty != null; 146 } 147 148 /** 149 * Indicates whether the decorated {@link DynaBean} contains a specified value. 150 * 151 * @param value The value to check for. 152 * @return {@code true} if one of the {@link DynaBean}'s properties contains the specified value, otherwise {@code false}. 153 */ 154 @Override 155 public boolean containsValue(final Object value) { 156 final DynaProperty[] properties = getDynaProperties(); 157 for (final DynaProperty property : properties) { 158 final String key = property.getName(); 159 final Object prop = getDynaBean().get(key); 160 if (value == null) { 161 if (prop == null) { 162 return true; 163 } 164 } else if (value.equals(prop)) { 165 return true; 166 } 167 } 168 return false; 169 } 170 171 /** 172 * Converts the name of a property to the key type of this decorator. 173 * 174 * @param propertyName the name of a property 175 * @return the converted key to be used in the decorated map 176 */ 177 protected abstract K convertKey(String propertyName); 178 179 /** 180 * <p> 181 * Returns the Set of the property/value mappings in the decorated {@link DynaBean}. 182 * </p> 183 * 184 * <p> 185 * Each element in the Set is a {@code Map.Entry} type. 186 * </p> 187 * 188 * @return An unmodifiable set of the DynaBean property name/value pairs 189 */ 190 @Override 191 public Set<Map.Entry<K, Object>> entrySet() { 192 final DynaProperty[] properties = getDynaProperties(); 193 final Set<Map.Entry<K, Object>> set = new HashSet<>(properties.length); 194 for (final DynaProperty property : properties) { 195 final K key = convertKey(property.getName()); 196 final Object value = getDynaBean().get(property.getName()); 197 set.add(new MapEntry<>(key, value)); 198 } 199 return Collections.unmodifiableSet(set); 200 } 201 202 /** 203 * Gets the value for the specified key from the decorated {@link DynaBean}. 204 * 205 * @param key The {@link DynaBean}'s property name 206 * @return The value for the specified property. 207 */ 208 @Override 209 public Object get(final Object key) { 210 return getDynaBean().get(toString(key)); 211 } 212 213 /** 214 * Provide access to the underlying {@link DynaBean} this Map decorates. 215 * 216 * @return the decorated {@link DynaBean}. 217 */ 218 public DynaBean getDynaBean() { 219 return dynaBean; 220 } 221 222 /** 223 * Convenience method to retrieve the {@link DynaProperty}s for this {@link DynaClass}. 224 * 225 * @return The an array of the {@link DynaProperty}s. 226 */ 227 private DynaProperty[] getDynaProperties() { 228 return getDynaBean().getDynaClass().getDynaProperties(); 229 } 230 231 /** 232 * Indicate whether the decorated {@link DynaBean} has any properties. 233 * 234 * @return {@code true} if the {@link DynaBean} has no properties, otherwise {@code false}. 235 */ 236 @Override 237 public boolean isEmpty() { 238 return getDynaProperties().length == 0; 239 } 240 241 /** 242 * Indicate whether the Map is read only. 243 * 244 * @return {@code true} if the Map is read only, otherwise {@code false}. 245 */ 246 public boolean isReadOnly() { 247 return readOnly; 248 } 249 250 /** 251 * <p> 252 * Returns the Set of the property names in the decorated {@link DynaBean}. 253 * </p> 254 * 255 * <p> 256 * <strong>N.B.</strong>For {@link DynaBean}s whose associated {@link DynaClass} is a {@link MutableDynaClass} a new Set is created every time, otherwise 257 * the Set is created only once and cached. 258 * </p> 259 * 260 * @return An unmodifiable set of the {@link DynaBean}s property names. 261 */ 262 @Override 263 public Set<K> keySet() { 264 if (keySet != null) { 265 return keySet; 266 } 267 268 // Create a Set of the keys 269 final DynaProperty[] properties = getDynaProperties(); 270 Set<K> set = new HashSet<>(properties.length); 271 for (final DynaProperty property : properties) { 272 set.add(convertKey(property.getName())); 273 } 274 set = Collections.unmodifiableSet(set); 275 276 // Cache the keySet if Not a MutableDynaClass 277 final DynaClass dynaClass = getDynaBean().getDynaClass(); 278 if (!(dynaClass instanceof MutableDynaClass)) { 279 keySet = set; 280 } 281 282 return set; 283 284 } 285 286 /** 287 * Puts the value for the specified property in the decorated {@link DynaBean}. 288 * 289 * @param key The {@link DynaBean}'s property name 290 * @param value The value for the specified property. 291 * @return The previous property's value. 292 * @throws UnsupportedOperationException if {@code isReadOnly()} is true. 293 */ 294 @Override 295 public Object put(final K key, final Object value) { 296 if (isReadOnly()) { 297 throw new UnsupportedOperationException("Map is read only"); 298 } 299 final String property = toString(key); 300 final Object previous = getDynaBean().get(property); 301 getDynaBean().set(property, value); 302 return previous; 303 } 304 305 /** 306 * Copy the contents of a Map to the decorated {@link DynaBean}. 307 * 308 * @param map The Map of values to copy. 309 * @throws UnsupportedOperationException if {@code isReadOnly()} is true. 310 */ 311 @Override 312 public void putAll(final Map<? extends K, ? extends Object> map) { 313 if (isReadOnly()) { 314 throw new UnsupportedOperationException("Map is read only"); 315 } 316 map.forEach(this::put); 317 } 318 319 /** 320 * remove() operation is not supported. 321 * 322 * @param key The {@link DynaBean}'s property name 323 * @return the value removed 324 * @throws UnsupportedOperationException This operation is not yet supported 325 */ 326 @Override 327 public Object remove(final Object key) { 328 throw new UnsupportedOperationException(); 329 } 330 331 /** 332 * Returns the number properties in the decorated {@link DynaBean}. 333 * 334 * @return The number of properties. 335 */ 336 @Override 337 public int size() { 338 return getDynaProperties().length; 339 } 340 341 /** 342 * Convenience method to convert an Object to a String. 343 * 344 * @param obj The Object to convert 345 * @return String representation of the object 346 */ 347 private String toString(final Object obj) { 348 return Objects.toString(obj, null); 349 } 350 351 /** 352 * Returns the set of property values in the decorated {@link DynaBean}. 353 * 354 * @return Unmodifiable collection of values. 355 */ 356 @Override 357 public Collection<Object> values() { 358 final DynaProperty[] properties = getDynaProperties(); 359 final List<Object> values = new ArrayList<>(properties.length); 360 for (final DynaProperty property : properties) { 361 final String key = property.getName(); 362 final Object value = getDynaBean().get(key); 363 values.add(value); 364 } 365 return Collections.unmodifiableList(values); 366 } 367 368}