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