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