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