View Javadoc

1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one or more
3    *  contributor license agreements.  See the NOTICE file distributed with
4    *  this work for additional information regarding copyright ownership.
5    *  The ASF licenses this file to You under the Apache License, Version 2.0
6    *  (the "License"); you may not use this file except in compliance with
7    *  the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  package org.apache.commons.collections;
18  
19  import java.beans.BeanInfo;
20  import java.beans.IntrospectionException;
21  import java.beans.Introspector;
22  import java.beans.PropertyDescriptor;
23  import java.lang.reflect.Constructor;
24  import java.lang.reflect.InvocationTargetException;
25  import java.lang.reflect.Method;
26  import java.util.AbstractMap;
27  import java.util.AbstractSet;
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.HashMap;
31  import java.util.Iterator;
32  import java.util.Set;
33  
34  import org.apache.commons.collections.list.UnmodifiableList;
35  import org.apache.commons.collections.keyvalue.AbstractMapEntry;
36  import org.apache.commons.collections.set.UnmodifiableSet;
37  
38  /** 
39   * An implementation of Map for JavaBeans which uses introspection to
40   * get and put properties in the bean.
41   * <p>
42   * If an exception occurs during attempts to get or set a property then the
43   * property is considered non existent in the Map
44   *
45   * @since Commons Collections 1.0
46   * @version $Revision: 646777 $ $Date: 2008-04-10 13:33:15 +0100 (Thu, 10 Apr 2008) $
47   * 
48   * @author James Strachan
49   * @author Stephen Colebourne
50   * @author Dimiter Dimitrov
51   * 
52   * @deprecated Identical class now available in commons-beanutils (full jar version).
53   * This version is due to be removed in collections v4.0.
54   */
55  public class BeanMap extends AbstractMap implements Cloneable {
56  
57      private transient Object bean;
58  
59      private transient HashMap readMethods = new HashMap();
60      private transient HashMap writeMethods = new HashMap();
61      private transient HashMap types = new HashMap();
62  
63      /**
64       * An empty array.  Used to invoke accessors via reflection.
65       */
66      public static final Object[] NULL_ARGUMENTS = {};
67  
68      /**
69       * Maps primitive Class types to transformers.  The transformer
70       * transform strings into the appropriate primitive wrapper.
71       */
72      public static HashMap defaultTransformers = new HashMap();
73      
74      static {
75          defaultTransformers.put( 
76              Boolean.TYPE, 
77              new Transformer() {
78                  public Object transform( Object input ) {
79                      return Boolean.valueOf( input.toString() );
80                  }
81              }
82          );
83          defaultTransformers.put( 
84              Character.TYPE, 
85              new Transformer() {
86                  public Object transform( Object input ) {
87                      return new Character( input.toString().charAt( 0 ) );
88                  }
89              }
90          );
91          defaultTransformers.put( 
92              Byte.TYPE, 
93              new Transformer() {
94                  public Object transform( Object input ) {
95                      return Byte.valueOf( input.toString() );
96                  }
97              }
98          );
99          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 }