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