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.beans.BeanInfo; 020import java.beans.IntrospectionException; 021import java.beans.Introspector; 022import java.beans.PropertyDescriptor; 023import java.lang.reflect.Constructor; 024import java.lang.reflect.InvocationTargetException; 025import java.lang.reflect.Method; 026import java.util.AbstractMap; 027import java.util.AbstractSet; 028import java.util.ArrayList; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.Iterator; 033import java.util.Map; 034import java.util.Set; 035import java.util.function.Function; 036 037/** 038 * An implementation of Map for JavaBeans which uses introspection to get and put properties in the bean. 039 * <p> 040 * If an exception occurs during attempts to get or set a property then the property is considered non existent in the Map 041 * </p> 042 */ 043public class BeanMap extends AbstractMap<String, Object> implements Cloneable { 044 045 /** 046 * Map entry used by {@link BeanMap}. 047 */ 048 protected static class Entry extends AbstractMap.SimpleEntry<String, Object> { 049 050 private static final long serialVersionUID = 1L; 051 052 /** 053 * The owner. 054 */ 055 private final BeanMap owner; 056 057 /** 058 * Constructs a new {@code Entry}. 059 * 060 * @param owner the BeanMap this entry belongs to 061 * @param key the key for this entry 062 * @param value the value for this entry 063 */ 064 protected Entry(final BeanMap owner, final String key, final Object value) { 065 super(key, value); 066 this.owner = owner; 067 } 068 069 /** 070 * Sets the value. 071 * 072 * @param value the new value for the entry 073 * @return the old value for the entry 074 */ 075 @Override 076 public Object setValue(final Object value) { 077 final String key = getKey(); 078 final Object oldValue = owner.get(key); 079 080 owner.put(key, value); 081 final Object newValue = owner.get(key); 082 super.setValue(newValue); 083 return oldValue; 084 } 085 } 086 087 /** 088 * An empty array. Used to invoke accessors via reflection. 089 */ 090 public static final Object[] NULL_ARGUMENTS = {}; 091 092 /** 093 * Maps primitive Class types to transformers. The transformer transform strings into the appropriate primitive wrapper. 094 * 095 * N.B. private & unmodifiable replacement for the (public & static) defaultTransformers instance. 096 */ 097 private static final Map<Class<? extends Object>, Function<?, ?>> typeTransformers = Collections.unmodifiableMap(createTypeTransformers()); 098 099 private static Map<Class<? extends Object>, Function<?, ?>> createTypeTransformers() { 100 final Map<Class<? extends Object>, Function<?, ?>> defTransformers = new HashMap<>(); 101 defTransformers.put(Boolean.TYPE, input -> Boolean.valueOf(input.toString())); 102 defTransformers.put(Character.TYPE, input -> Character.valueOf(input.toString().charAt(0))); 103 defTransformers.put(Byte.TYPE, input -> Byte.valueOf(input.toString())); 104 defTransformers.put(Short.TYPE, input -> Short.valueOf(input.toString())); 105 defTransformers.put(Integer.TYPE, input -> Integer.valueOf(input.toString())); 106 defTransformers.put(Long.TYPE, input -> Long.valueOf(input.toString())); 107 defTransformers.put(Float.TYPE, input -> Float.valueOf(input.toString())); 108 defTransformers.put(Double.TYPE, input -> Double.valueOf(input.toString())); 109 return defTransformers; 110 } 111 112 private transient Object bean; 113 114 private final transient HashMap<String, Method> readMethods = new HashMap<>(); 115 116 private final transient HashMap<String, Method> writeMethods = new HashMap<>(); 117 118 private final transient HashMap<String, Class<? extends Object>> types = new HashMap<>(); 119 120 /** 121 * Constructs a new empty {@code BeanMap}. 122 */ 123 public BeanMap() { 124 } 125 126 // Map interface 127 128 /** 129 * Constructs a new {@code BeanMap} that operates on the specified bean. If the given bean is {@code null}, then this map will be empty. 130 * 131 * @param bean the bean for this map to operate on 132 */ 133 public BeanMap(final Object bean) { 134 this.bean = bean; 135 initialize(); 136 } 137 138 /** 139 * This method reinitializes the bean map to have default values for the bean's properties. This is accomplished by constructing a new instance of the bean 140 * which the map uses as its underlying data source. This behavior for {@code clear()} differs from the Map contract in that the mappings are not actually 141 * removed from the map (the mappings for a BeanMap are fixed). 142 */ 143 @Override 144 public void clear() { 145 if (bean == null) { 146 return; 147 } 148 Class<? extends Object> beanClass = null; 149 try { 150 beanClass = bean.getClass(); 151 bean = beanClass.newInstance(); 152 } catch (final Exception e) { 153 throw new UnsupportedOperationException("Could not create new instance of class: " + beanClass, e); 154 } 155 } 156 157 /** 158 * Clone this bean map using the following process: 159 * 160 * <ul> 161 * <li>If there is no underlying bean, return a cloned BeanMap without a bean. 162 * <li>Since there is an underlying bean, try to instantiate a new bean of the same type using Class.newInstance(). 163 * <li>If the instantiation fails, throw a CloneNotSupportedException 164 * <li>Clone the bean map and set the newly instantiated bean as the underlying bean for the bean map. 165 * <li>Copy each property that is both readable and writable from the existing object to a cloned bean map. 166 * <li>If anything fails along the way, throw a CloneNotSupportedException. 167 * </ul> 168 * 169 * @return a cloned instance of this bean map 170 * @throws CloneNotSupportedException if the underlying bean cannot be cloned 171 */ 172 @Override 173 public Object clone() throws CloneNotSupportedException { 174 final BeanMap newMap = (BeanMap) super.clone(); 175 if (bean == null) { 176 // no bean, just an empty bean map at the moment. return a newly 177 // cloned and empty bean map. 178 return newMap; 179 } 180 Object newBean = null; 181 final Class<? extends Object> beanClass = bean.getClass(); // Cannot throw Exception 182 try { 183 newBean = beanClass.newInstance(); 184 } catch (final Exception e) { 185 // unable to instantiate 186 final CloneNotSupportedException cnse = new CloneNotSupportedException( 187 "Unable to instantiate the underlying bean \"" + beanClass.getName() + "\": " + e); 188 cnse.initCause(e); 189 throw cnse; 190 } 191 try { 192 newMap.setBean(newBean); 193 } catch (final Exception e) { 194 final CloneNotSupportedException cnse = new CloneNotSupportedException("Unable to set bean in the cloned bean map: " + e); 195 cnse.initCause(e); 196 throw cnse; 197 } 198 try { 199 // copy only properties that are readable and writable. If its 200 // not readable, we can't get the value from the old map. If 201 // its not writable, we can't write a value into the new map. 202 readMethods.keySet().forEach(key -> { 203 if (getWriteMethod(key) != null) { 204 newMap.put(key, get(key)); 205 } 206 }); 207 } catch (final Exception e) { 208 final CloneNotSupportedException cnse = new CloneNotSupportedException("Unable to copy bean values to cloned bean map: " + e); 209 cnse.initCause(e); 210 throw cnse; 211 } 212 return newMap; 213 } 214 215 /** 216 * Returns true if the bean defines a property with the given name. 217 * <p> 218 * The given name must be a {@code String}; if not, this method returns false. This method will also return false if the bean does not define a property 219 * with that name. 220 * </p> 221 * <p> 222 * Write-only properties will not be matched as the test operates against property read methods. 223 * </p> 224 * 225 * @param name the name of the property to check 226 * @return false if the given name is null or is not a {@code String}; false if the bean does not define a property with that name; or true if the bean does 227 * define a property with that name 228 */ 229 @Override 230 public boolean containsKey(final Object name) { 231 return getReadMethod(name) != null; 232 } 233 234 /** 235 * Converts the given value to the given type. First, reflection is used to find a public constructor declared by the given class that takes one argument, 236 * which must be the precise type of the given value. If such a constructor is found, a new object is created by passing the given value to that 237 * constructor, and the newly constructed object is returned. 238 * <p> 239 * If no such constructor exists, and the given type is a primitive type, then the given value is converted to a string using its {@link Object#toString() 240 * toString()} method, and that string is parsed into the correct primitive type using, for instance, {@link Integer#valueOf(String)} to convert the string 241 * into an {@code int}. 242 * </p> 243 * <p> 244 * If no special constructor exists and the given type is not a primitive type, this method returns the original value. 245 * </p> 246 * 247 * @param <R> The return type. 248 * @param newType the type to convert the value to 249 * @param value the value to convert 250 * @return the converted value 251 * @throws NumberFormatException if newType is a primitive type, and the string representation of the given value cannot be converted to that type 252 * @throws InstantiationException if the constructor found with reflection raises it 253 * @throws InvocationTargetException if the constructor found with reflection raises it 254 * @throws IllegalAccessException never 255 * @throws IllegalArgumentException never 256 */ 257 protected <R> Object convertType(final Class<R> newType, final Object value) 258 throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { 259 260 // try call constructor 261 try { 262 final Constructor<R> constructor = newType.getConstructor(value.getClass()); 263 return constructor.newInstance(value); 264 } catch (final NoSuchMethodException e) { 265 // try using the transformers 266 final Function<Object, R> transformer = getTypeTransformer(newType); 267 if (transformer != null) { 268 return transformer.apply(value); 269 } 270 return value; 271 } 272 } 273 274 /** 275 * Creates an array of parameters to pass to the given mutator method. If the given object is not the right type to pass to the method directly, it will be 276 * converted using {@link #convertType(Class,Object)}. 277 * 278 * @param method the mutator method 279 * @param value the value to pass to the mutator method 280 * @return an array containing one object that is either the given value or a transformed value 281 * @throws IllegalAccessException if {@link #convertType(Class,Object)} raises it 282 * @throws IllegalArgumentException if any other exception is raised by {@link #convertType(Class,Object)} 283 * @throws ClassCastException if an error occurs creating the method args 284 */ 285 protected Object[] createWriteMethodArguments(final Method method, Object value) throws IllegalAccessException, ClassCastException { 286 try { 287 if (value != null) { 288 final Class<? extends Object>[] paramTypes = method.getParameterTypes(); 289 if (paramTypes != null && paramTypes.length > 0) { 290 final Class<? extends Object> paramType = paramTypes[0]; 291 if (!paramType.isAssignableFrom(value.getClass())) { 292 value = convertType(paramType, value); 293 } 294 } 295 } 296 297 return new Object[] { value }; 298 } catch (final InvocationTargetException | InstantiationException e) { 299 throw new IllegalArgumentException(e.getMessage(), e); 300 } 301 } 302 303 /** 304 * Convenience method for getting an iterator over the entries. 305 * 306 * @return an iterator over the entries 307 */ 308 public Iterator<Map.Entry<String, Object>> entryIterator() { 309 final Iterator<String> iter = keyIterator(); 310 return new Iterator<Map.Entry<String, Object>>() { 311 @Override 312 public boolean hasNext() { 313 return iter.hasNext(); 314 } 315 316 @Override 317 public Map.Entry<String, Object> next() { 318 final String key = iter.next(); 319 final Object value = get(key); 320 // This should not cause any problems; the key is actually a 321 // string, but it does no harm to expose it as Object 322 return new Entry(BeanMap.this, key, value); 323 } 324 325 @Override 326 public void remove() { 327 throw new UnsupportedOperationException("remove() not supported for BeanMap"); 328 } 329 }; 330 } 331 332 /** 333 * Gets a Set of MapEntry objects that are the mappings for this BeanMap. 334 * <p> 335 * Each MapEntry can be set but not removed. 336 * </p> 337 * 338 * @return the unmodifiable set of mappings 339 */ 340 @Override 341 public Set<Map.Entry<String, Object>> entrySet() { 342 return Collections.unmodifiableSet(new AbstractSet<Map.Entry<String, Object>>() { 343 @Override 344 public Iterator<Map.Entry<String, Object>> iterator() { 345 return entryIterator(); 346 } 347 348 @Override 349 public int size() { 350 return BeanMap.this.readMethods.size(); 351 } 352 }); 353 } 354 355 /** 356 * Called during a successful {@link #put(String,Object)} operation. Default implementation does nothing. Override to be notified of property changes in the 357 * bean caused by this map. 358 * 359 * @param key the name of the property that changed 360 * @param oldValue the old value for that property 361 * @param newValue the new value for that property 362 */ 363 protected void firePropertyChange(final Object key, final Object oldValue, final Object newValue) { 364 // noop 365 } 366 367 /** 368 * Gets the value of the bean's property with the given name. 369 * <p> 370 * The given name must be a {@link String} and must not be null; otherwise, this method returns {@code null}. If the bean defines a property with the given 371 * name, the value of that property is returned. Otherwise, {@code null} is returned. 372 * </p> 373 * <p> 374 * Write-only properties will not be matched as the test operates against property read methods. 375 * </p> 376 * 377 * @param name the name of the property whose value to return 378 * @return the value of the property with that name 379 */ 380 @Override 381 public Object get(final Object name) { 382 if (bean != null) { 383 final Method method = getReadMethod(name); 384 if (method != null) { 385 try { 386 return method.invoke(bean, NULL_ARGUMENTS); 387 } catch (final IllegalAccessException | NullPointerException | InvocationTargetException | IllegalArgumentException e) { 388 logWarn(e); 389 } 390 } 391 } 392 return null; 393 } 394 395 /** 396 * Gets the bean currently being operated on. The return value may be null if this map is empty. 397 * 398 * @return the bean being operated on by this map 399 */ 400 public Object getBean() { 401 return bean; 402 } 403 404 // Helper methods 405 406 /** 407 * Gets the accessor for the property with the given name. 408 * 409 * @param name the name of the property 410 * @return null if the name is null; null if the name is not a {@link String}; null if no such property exists; or the accessor method for that property 411 */ 412 protected Method getReadMethod(final Object name) { 413 return readMethods.get(name); 414 } 415 416 /** 417 * Gets the accessor for the property with the given name. 418 * 419 * @param name the name of the property 420 * @return the accessor method for the property, or null 421 */ 422 public Method getReadMethod(final String name) { 423 return readMethods.get(name); 424 } 425 426 /** 427 * Gets the type of the property with the given name. 428 * 429 * @param name the name of the property 430 * @return the type of the property, or {@code null} if no such property exists 431 */ 432 public Class<?> getType(final String name) { 433 return types.get(name); 434 } 435 436 /** 437 * Gets a transformer for the given primitive type. 438 * 439 * @param <R> The transformer result type. 440 * @param type the primitive type whose transformer to return 441 * @return a transformer that will convert strings into that type, or null if the given type is not a primitive type 442 */ 443 protected <R> Function<Object, R> getTypeTransformer(final Class<R> type) { 444 return (Function<Object, R>) typeTransformers.get(type); 445 } 446 447 /** 448 * Gets the mutator for the property with the given name. 449 * 450 * @param name the name of the 451 * @return null if the name is null; null if the name is not a {@link String}; null if no such property exists; null if the property is read-only; or the 452 * mutator method for that property 453 */ 454 protected Method getWriteMethod(final Object name) { 455 return writeMethods.get(name); 456 } 457 458 /** 459 * Gets the mutator for the property with the given name. 460 * 461 * @param name the name of the property 462 * @return the mutator method for the property, or null 463 */ 464 public Method getWriteMethod(final String name) { 465 return writeMethods.get(name); 466 } 467 468 private void initialize() { 469 if (getBean() == null) { 470 return; 471 } 472 473 final Class<? extends Object> beanClass = getBean().getClass(); 474 try { 475 // BeanInfo beanInfo = Introspector.getBeanInfo( bean, null ); 476 final BeanInfo beanInfo = Introspector.getBeanInfo(beanClass); 477 final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); 478 if (propertyDescriptors != null) { 479 for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) { 480 if (propertyDescriptor != null) { 481 final String name = propertyDescriptor.getName(); 482 final Method readMethod = propertyDescriptor.getReadMethod(); 483 final Method writeMethod = propertyDescriptor.getWriteMethod(); 484 final Class<? extends Object> aType = propertyDescriptor.getPropertyType(); 485 486 if (readMethod != null) { 487 readMethods.put(name, readMethod); 488 } 489 if (writeMethod != null) { 490 writeMethods.put(name, writeMethod); 491 } 492 types.put(name, aType); 493 } 494 } 495 } 496 } catch (final IntrospectionException e) { 497 logWarn(e); 498 } 499 } 500 501 /** 502 * Convenience method for getting an iterator over the keys. 503 * <p> 504 * Write-only properties will not be returned in the iterator. 505 * </p> 506 * 507 * @return an iterator over the keys 508 */ 509 public Iterator<String> keyIterator() { 510 return readMethods.keySet().iterator(); 511 } 512 513 // Implementation methods 514 515 /** 516 * Gets the keys for this BeanMap. 517 * <p> 518 * Write-only properties are <strong>not</strong> included in the returned set of property names, although it is possible to set their value and to get 519 * their type. 520 * </p> 521 * 522 * @return BeanMap keys. The Set returned by this method is not modifiable. 523 */ 524 @SuppressWarnings({ "unchecked", "rawtypes" }) 525 // The set actually contains strings; however, because it cannot be 526 // modified there is no danger in selling it as Set<Object> 527 @Override 528 public Set<String> keySet() { 529 return Collections.unmodifiableSet((Set) readMethods.keySet()); 530 } 531 532 /** 533 * Logs the given exception to {@code System.out}. Used to display warnings while accessing/mutating the bean. 534 * 535 * @param ex the exception to log 536 */ 537 protected void logInfo(final Exception ex) { 538 // Deliberately do not use LOG4J or Commons Logging to avoid dependencies 539 System.out.println("INFO: Exception: " + ex); 540 } 541 542 /** 543 * Logs the given exception to {@code System.err}. Used to display errors while accessing/mutating the bean. 544 * 545 * @param ex the exception to log 546 */ 547 protected void logWarn(final Exception ex) { 548 // Deliberately do not use LOG4J or Commons Logging to avoid dependencies 549 System.out.println("WARN: Exception: " + ex); 550 ex.printStackTrace(); 551 } 552 553 /** 554 * Sets the bean property with the given name to the given value. 555 * 556 * @param name the name of the property to set 557 * @param value the value to set that property to 558 * @return the previous value of that property 559 * @throws IllegalArgumentException if the given name is null; if the given name is not a {@link String}; if the bean doesn't define a property with that 560 * name; or if the bean property with that name is read-only 561 * @throws ClassCastException if an error occurs creating the method args 562 */ 563 @Override 564 public Object put(final String name, final Object value) throws IllegalArgumentException, ClassCastException { 565 if (bean != null) { 566 final Object oldValue = get(name); 567 final Method method = getWriteMethod(name); 568 if (method == null) { 569 throw new IllegalArgumentException("The bean of type: " + bean.getClass().getName() + " has no property called: " + name); 570 } 571 try { 572 final Object[] arguments = createWriteMethodArguments(method, value); 573 method.invoke(bean, arguments); 574 575 final Object newValue = get(name); 576 firePropertyChange(name, oldValue, newValue); 577 } catch (final InvocationTargetException | IllegalAccessException e) { 578 throw new IllegalArgumentException(e.getMessage(), e); 579 } 580 return oldValue; 581 } 582 return null; 583 } 584 585 /** 586 * Puts all of the writable properties from the given BeanMap into this BeanMap. Read-only and Write-only properties will be ignored. 587 * 588 * @param map the BeanMap whose properties to put 589 */ 590 public void putAllWriteable(final BeanMap map) { 591 map.readMethods.keySet().forEach(key -> { 592 if (getWriteMethod(key) != null) { 593 put(key, map.get(key)); 594 } 595 }); 596 } 597 598 // Implementation classes 599 600 /** 601 * Reinitializes this bean. Called during {@link #setBean(Object)}. Does introspection to find properties. 602 */ 603 protected void reinitialise() { 604 readMethods.clear(); 605 writeMethods.clear(); 606 types.clear(); 607 initialize(); 608 } 609 610 /** 611 * Sets the bean to be operated on by this map. The given value may be null, in which case this map will be empty. 612 * 613 * @param newBean the new bean to operate on 614 */ 615 public void setBean(final Object newBean) { 616 bean = newBean; 617 reinitialise(); 618 } 619 620 /** 621 * Returns the number of properties defined by the bean. 622 * 623 * @return the number of properties defined by the bean 624 */ 625 @Override 626 public int size() { 627 return readMethods.size(); 628 } 629 630 /** 631 * Renders a string representation of this object. 632 * 633 * @return a {@code String} representation of this object 634 */ 635 @Override 636 public String toString() { 637 return "BeanMap<" + bean + ">"; 638 } 639 640 /** 641 * Convenience method for getting an iterator over the values. 642 * 643 * @return an iterator over the values 644 */ 645 public Iterator<Object> valueIterator() { 646 final Iterator<?> iter = keyIterator(); 647 return new Iterator<Object>() { 648 @Override 649 public boolean hasNext() { 650 return iter.hasNext(); 651 } 652 653 @Override 654 public Object next() { 655 final Object key = iter.next(); 656 return get(key); 657 } 658 659 @Override 660 public void remove() { 661 throw new UnsupportedOperationException("remove() not supported for BeanMap"); 662 } 663 }; 664 } 665 666 /** 667 * Gets the values for the BeanMap. 668 * 669 * @return values for the BeanMap. The returned collection is not modifiable. 670 */ 671 @Override 672 public Collection<Object> values() { 673 final ArrayList<Object> answer = new ArrayList<>(readMethods.size()); 674 valueIterator().forEachRemaining(answer::add); 675 return Collections.unmodifiableList(answer); 676 } 677}