1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.beanutils;
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.Collections;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.Map;
34 import java.util.Set;
35
36 import org.apache.commons.collections.list.UnmodifiableList;
37 import org.apache.commons.collections.keyvalue.AbstractMapEntry;
38 import org.apache.commons.collections.set.UnmodifiableSet;
39 import org.apache.commons.collections.Transformer;
40
41 /***
42 * An implementation of Map for JavaBeans which uses introspection to
43 * get and put properties in the bean.
44 * <p>
45 * If an exception occurs during attempts to get or set a property then the
46 * property is considered non existent in the Map
47 *
48 * @version $Revision: 557796 $ $Date: 2007-07-19 23:28:49 +0100 (Thu, 19 Jul 2007) $
49 *
50 * @author James Strachan
51 * @author Stephen Colebourne
52 */
53 public class BeanMap extends AbstractMap implements Cloneable {
54
55 private transient Object bean;
56
57 private transient HashMap readMethods = new HashMap();
58 private transient HashMap writeMethods = new HashMap();
59 private transient HashMap types = new HashMap();
60
61 /***
62 * An empty array. Used to invoke accessors via reflection.
63 */
64 public static final Object[] NULL_ARGUMENTS = {};
65
66 /***
67 * Maps primitive Class types to transformers. The transformer
68 * transform strings into the appropriate primitive wrapper.
69 *
70 * N.B. private & unmodifiable replacement for the (public & static) defaultTransformers instance.
71 */
72 private static Map typeTransformers = Collections.unmodifiableMap(createTypeTransformers());
73
74 /***
75 * This HashMap has been made unmodifiable to prevent issues when
76 * loaded in a shared ClassLoader enviroment.
77 *
78 * @see http://issues.apache.org/jira/browse/BEANUTILS-112
79 * @deprecated Use {@link BeanMap#getTypeTransformer(Class)} method
80 */
81 public static HashMap defaultTransformers = new HashMap() {
82 public void clear() {
83 throw new UnsupportedOperationException();
84 }
85 public boolean containsKey(Object key) {
86 return typeTransformers.containsKey(key);
87 }
88 public boolean containsValue(Object value) {
89 return typeTransformers.containsValue(value);
90 }
91 public Set entrySet() {
92 return typeTransformers.entrySet();
93 }
94 public Object get(Object key) {
95 return typeTransformers.get(key);
96 }
97 public boolean isEmpty() {
98 return false;
99 }
100 public Set keySet() {
101 return typeTransformers.keySet();
102 }
103 public Object put(Object key, Object value) {
104 throw new UnsupportedOperationException();
105 }
106 public void putAll(Map m) {
107 throw new UnsupportedOperationException();
108 }
109 public Object remove(Object key) {
110 throw new UnsupportedOperationException();
111 }
112 public int size() {
113 return typeTransformers.size();
114 }
115 public Collection values() {
116 return typeTransformers.values();
117 }
118 };
119
120 private static Map createTypeTransformers() {
121 Map defaultTransformers = new HashMap();
122 defaultTransformers.put(
123 Boolean.TYPE,
124 new Transformer() {
125 public Object transform( Object input ) {
126 return Boolean.valueOf( input.toString() );
127 }
128 }
129 );
130 defaultTransformers.put(
131 Character.TYPE,
132 new Transformer() {
133 public Object transform( Object input ) {
134 return new Character( input.toString().charAt( 0 ) );
135 }
136 }
137 );
138 defaultTransformers.put(
139 Byte.TYPE,
140 new Transformer() {
141 public Object transform( Object input ) {
142 return Byte.valueOf( input.toString() );
143 }
144 }
145 );
146 defaultTransformers.put(
147 Short.TYPE,
148 new Transformer() {
149 public Object transform( Object input ) {
150 return Short.valueOf( input.toString() );
151 }
152 }
153 );
154 defaultTransformers.put(
155 Integer.TYPE,
156 new Transformer() {
157 public Object transform( Object input ) {
158 return Integer.valueOf( input.toString() );
159 }
160 }
161 );
162 defaultTransformers.put(
163 Long.TYPE,
164 new Transformer() {
165 public Object transform( Object input ) {
166 return Long.valueOf( input.toString() );
167 }
168 }
169 );
170 defaultTransformers.put(
171 Float.TYPE,
172 new Transformer() {
173 public Object transform( Object input ) {
174 return Float.valueOf( input.toString() );
175 }
176 }
177 );
178 defaultTransformers.put(
179 Double.TYPE,
180 new Transformer() {
181 public Object transform( Object input ) {
182 return Double.valueOf( input.toString() );
183 }
184 }
185 );
186 return defaultTransformers;
187 }
188
189
190
191
192
193 /***
194 * Constructs a new empty <code>BeanMap</code>.
195 */
196 public BeanMap() {
197 }
198
199 /***
200 * Constructs a new <code>BeanMap</code> that operates on the
201 * specified bean. If the given bean is <code>null</code>, then
202 * this map will be empty.
203 *
204 * @param bean the bean for this map to operate on
205 */
206 public BeanMap(Object bean) {
207 this.bean = bean;
208 initialise();
209 }
210
211
212
213
214 /***
215 * Renders a string representation of this object.
216 * @return a <code>String</code> representation of this object
217 */
218 public String toString() {
219 return "BeanMap<" + String.valueOf(bean) + ">";
220 }
221
222 /***
223 * Clone this bean map using the following process:
224 *
225 * <ul>
226 * <li>If there is no underlying bean, return a cloned BeanMap without a
227 * bean.
228 *
229 * <li>Since there is an underlying bean, try to instantiate a new bean of
230 * the same type using Class.newInstance().
231 *
232 * <li>If the instantiation fails, throw a CloneNotSupportedException
233 *
234 * <li>Clone the bean map and set the newly instantiated bean as the
235 * underlying bean for the bean map.
236 *
237 * <li>Copy each property that is both readable and writable from the
238 * existing object to a cloned bean map.
239 *
240 * <li>If anything fails along the way, throw a
241 * CloneNotSupportedException.
242 *
243 * <ul>
244 *
245 * @return a cloned instance of this bean map
246 * @throws CloneNotSupportedException if the underlying bean
247 * cannot be cloned
248 */
249 public Object clone() throws CloneNotSupportedException {
250 BeanMap newMap = (BeanMap)super.clone();
251
252 if(bean == null) {
253
254
255 return newMap;
256 }
257
258 Object newBean = null;
259 Class beanClass = null;
260 try {
261 beanClass = bean.getClass();
262 newBean = beanClass.newInstance();
263 } catch (Exception e) {
264
265 throw new CloneNotSupportedException
266 ("Unable to instantiate the underlying bean \"" +
267 beanClass.getName() + "\": " + e);
268 }
269
270 try {
271 newMap.setBean(newBean);
272 } catch (Exception exception) {
273 throw new CloneNotSupportedException
274 ("Unable to set bean in the cloned bean map: " +
275 exception);
276 }
277
278 try {
279
280
281
282 Iterator readableKeys = readMethods.keySet().iterator();
283 while(readableKeys.hasNext()) {
284 Object key = readableKeys.next();
285 if(getWriteMethod(key) != null) {
286 newMap.put(key, get(key));
287 }
288 }
289 } catch (Exception exception) {
290 throw new CloneNotSupportedException
291 ("Unable to copy bean values to cloned bean map: " +
292 exception);
293 }
294
295 return newMap;
296 }
297
298 /***
299 * Puts all of the writable properties from the given BeanMap into this
300 * BeanMap. Read-only and Write-only properties will be ignored.
301 *
302 * @param map the BeanMap whose properties to put
303 */
304 public void putAllWriteable(BeanMap map) {
305 Iterator readableKeys = map.readMethods.keySet().iterator();
306 while (readableKeys.hasNext()) {
307 Object key = readableKeys.next();
308 if (getWriteMethod(key) != null) {
309 this.put(key, map.get(key));
310 }
311 }
312 }
313
314
315 /***
316 * This method reinitializes the bean map to have default values for the
317 * bean's properties. This is accomplished by constructing a new instance
318 * of the bean which the map uses as its underlying data source. This
319 * behavior for <code>clear()</code> differs from the Map contract in that
320 * the mappings are not actually removed from the map (the mappings for a
321 * BeanMap are fixed).
322 */
323 public void clear() {
324 if(bean == null) {
325 return;
326 }
327
328 Class beanClass = null;
329 try {
330 beanClass = bean.getClass();
331 bean = beanClass.newInstance();
332 }
333 catch (Exception e) {
334 throw new UnsupportedOperationException( "Could not create new instance of class: " + beanClass );
335 }
336 }
337
338 /***
339 * Returns true if the bean defines a property with the given name.
340 * <p>
341 * The given name must be a <code>String</code>; if not, this method
342 * returns false. This method will also return false if the bean
343 * does not define a property with that name.
344 * <p>
345 * Write-only properties will not be matched as the test operates against
346 * property read methods.
347 *
348 * @param name the name of the property to check
349 * @return false if the given name is null or is not a <code>String</code>;
350 * false if the bean does not define a property with that name; or
351 * true if the bean does define a property with that name
352 */
353 public boolean containsKey(Object name) {
354 Method method = getReadMethod(name);
355 return method != null;
356 }
357
358 /***
359 * Returns true if the bean defines a property whose current value is
360 * the given object.
361 *
362 * @param value the value to check
363 * @return false true if the bean has at least one property whose
364 * current value is that object, false otherwise
365 */
366 public boolean containsValue(Object value) {
367
368 return super.containsValue(value);
369 }
370
371 /***
372 * Returns the value of the bean's property with the given name.
373 * <p>
374 * The given name must be a {@link String} and must not be
375 * null; otherwise, this method returns <code>null</code>.
376 * If the bean defines a property with the given name, the value of
377 * that property is returned. Otherwise, <code>null</code> is
378 * returned.
379 * <p>
380 * Write-only properties will not be matched as the test operates against
381 * property read methods.
382 *
383 * @param name the name of the property whose value to return
384 * @return the value of the property with that name
385 */
386 public Object get(Object name) {
387 if ( bean != null ) {
388 Method method = getReadMethod( name );
389 if ( method != null ) {
390 try {
391 return method.invoke( bean, NULL_ARGUMENTS );
392 }
393 catch ( IllegalAccessException e ) {
394 logWarn( e );
395 }
396 catch ( IllegalArgumentException e ) {
397 logWarn( e );
398 }
399 catch ( InvocationTargetException e ) {
400 logWarn( e );
401 }
402 catch ( NullPointerException e ) {
403 logWarn( e );
404 }
405 }
406 }
407 return null;
408 }
409
410 /***
411 * Sets the bean property with the given name to the given value.
412 *
413 * @param name the name of the property to set
414 * @param value the value to set that property to
415 * @return the previous value of that property
416 * @throws IllegalArgumentException if the given name is null;
417 * if the given name is not a {@link String}; if the bean doesn't
418 * define a property with that name; or if the bean property with
419 * that name is read-only
420 * @throws ClassCastException if an error occurs creating the method args
421 */
422 public Object put(Object name, Object value) throws IllegalArgumentException, ClassCastException {
423 if ( bean != null ) {
424 Object oldValue = get( name );
425 Method method = getWriteMethod( name );
426 if ( method == null ) {
427 throw new IllegalArgumentException( "The bean of type: "+
428 bean.getClass().getName() + " has no property called: " + name );
429 }
430 try {
431 Object[] arguments = createWriteMethodArguments( method, value );
432 method.invoke( bean, arguments );
433
434 Object newValue = get( name );
435 firePropertyChange( name, oldValue, newValue );
436 }
437 catch ( InvocationTargetException e ) {
438 logInfo( e );
439 throw new IllegalArgumentException( e.getMessage() );
440 }
441 catch ( IllegalAccessException e ) {
442 logInfo( e );
443 throw new IllegalArgumentException( e.getMessage() );
444 }
445 return oldValue;
446 }
447 return null;
448 }
449
450 /***
451 * Returns the number of properties defined by the bean.
452 *
453 * @return the number of properties defined by the bean
454 */
455 public int size() {
456 return readMethods.size();
457 }
458
459
460 /***
461 * Get the keys for this BeanMap.
462 * <p>
463 * Write-only properties are <b>not</b> included in the returned set of
464 * property names, although it is possible to set their value and to get
465 * their type.
466 *
467 * @return BeanMap keys. The Set returned by this method is not
468 * modifiable.
469 */
470 public Set keySet() {
471 return UnmodifiableSet.decorate(readMethods.keySet());
472 }
473
474 /***
475 * Gets a Set of MapEntry objects that are the mappings for this BeanMap.
476 * <p>
477 * Each MapEntry can be set but not removed.
478 *
479 * @return the unmodifiable set of mappings
480 */
481 public Set entrySet() {
482 return UnmodifiableSet.decorate(new AbstractSet() {
483 public Iterator iterator() {
484 return entryIterator();
485 }
486 public int size() {
487 return BeanMap.this.readMethods.size();
488 }
489 });
490 }
491
492 /***
493 * Returns the values for the BeanMap.
494 *
495 * @return values for the BeanMap. The returned collection is not
496 * modifiable.
497 */
498 public Collection values() {
499 ArrayList answer = new ArrayList( readMethods.size() );
500 for ( Iterator iter = valueIterator(); iter.hasNext(); ) {
501 answer.add( iter.next() );
502 }
503 return UnmodifiableList.decorate(answer);
504 }
505
506
507
508
509
510 /***
511 * Returns the type of the property with the given name.
512 *
513 * @param name the name of the property
514 * @return the type of the property, or <code>null</code> if no such
515 * property exists
516 */
517 public Class getType(String name) {
518 return (Class) types.get( name );
519 }
520
521 /***
522 * Convenience method for getting an iterator over the keys.
523 * <p>
524 * Write-only properties will not be returned in the iterator.
525 *
526 * @return an iterator over the keys
527 */
528 public Iterator keyIterator() {
529 return readMethods.keySet().iterator();
530 }
531
532 /***
533 * Convenience method for getting an iterator over the values.
534 *
535 * @return an iterator over the values
536 */
537 public Iterator valueIterator() {
538 final Iterator iter = keyIterator();
539 return new Iterator() {
540 public boolean hasNext() {
541 return iter.hasNext();
542 }
543 public Object next() {
544 Object key = iter.next();
545 return get(key);
546 }
547 public void remove() {
548 throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
549 }
550 };
551 }
552
553 /***
554 * Convenience method for getting an iterator over the entries.
555 *
556 * @return an iterator over the entries
557 */
558 public Iterator entryIterator() {
559 final Iterator iter = keyIterator();
560 return new Iterator() {
561 public boolean hasNext() {
562 return iter.hasNext();
563 }
564 public Object next() {
565 Object key = iter.next();
566 Object value = get(key);
567 return new Entry( BeanMap.this, key, value );
568 }
569 public void remove() {
570 throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
571 }
572 };
573 }
574
575
576
577
578
579 /***
580 * Returns the bean currently being operated on. The return value may
581 * be null if this map is empty.
582 *
583 * @return the bean being operated on by this map
584 */
585 public Object getBean() {
586 return bean;
587 }
588
589 /***
590 * Sets the bean to be operated on by this map. The given value may
591 * be null, in which case this map will be empty.
592 *
593 * @param newBean the new bean to operate on
594 */
595 public void setBean( Object newBean ) {
596 bean = newBean;
597 reinitialise();
598 }
599
600 /***
601 * Returns the accessor for the property with the given name.
602 *
603 * @param name the name of the property
604 * @return the accessor method for the property, or null
605 */
606 public Method getReadMethod(String name) {
607 return (Method) readMethods.get(name);
608 }
609
610 /***
611 * Returns the mutator for the property with the given name.
612 *
613 * @param name the name of the property
614 * @return the mutator method for the property, or null
615 */
616 public Method getWriteMethod(String name) {
617 return (Method) writeMethods.get(name);
618 }
619
620
621
622
623
624 /***
625 * Returns the accessor for the property with the given name.
626 *
627 * @param name the name of the property
628 * @return null if the name is null; null if the name is not a
629 * {@link String}; null if no such property exists; or the accessor
630 * method for that property
631 */
632 protected Method getReadMethod( Object name ) {
633 return (Method) readMethods.get( name );
634 }
635
636 /***
637 * Returns the mutator for the property with the given name.
638 *
639 * @param name the name of the
640 * @return null if the name is null; null if the name is not a
641 * {@link String}; null if no such property exists; null if the
642 * property is read-only; or the mutator method for that property
643 */
644 protected Method getWriteMethod( Object name ) {
645 return (Method) writeMethods.get( name );
646 }
647
648 /***
649 * Reinitializes this bean. Called during {@link #setBean(Object)}.
650 * Does introspection to find properties.
651 */
652 protected void reinitialise() {
653 readMethods.clear();
654 writeMethods.clear();
655 types.clear();
656 initialise();
657 }
658
659 private void initialise() {
660 if(getBean() == null) {
661 return;
662 }
663
664 Class beanClass = getBean().getClass();
665 try {
666
667 BeanInfo beanInfo = Introspector.getBeanInfo( beanClass );
668 PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
669 if ( propertyDescriptors != null ) {
670 for ( int i = 0; i < propertyDescriptors.length; i++ ) {
671 PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
672 if ( propertyDescriptor != null ) {
673 String name = propertyDescriptor.getName();
674 Method readMethod = propertyDescriptor.getReadMethod();
675 Method writeMethod = propertyDescriptor.getWriteMethod();
676 Class aType = propertyDescriptor.getPropertyType();
677
678 if ( readMethod != null ) {
679 readMethods.put( name, readMethod );
680 }
681 if ( writeMethod != null ) {
682 writeMethods.put( name, writeMethod );
683 }
684 types.put( name, aType );
685 }
686 }
687 }
688 }
689 catch ( IntrospectionException e ) {
690 logWarn( e );
691 }
692 }
693
694 /***
695 * Called during a successful {@link #put(Object,Object)} operation.
696 * Default implementation does nothing. Override to be notified of
697 * property changes in the bean caused by this map.
698 *
699 * @param key the name of the property that changed
700 * @param oldValue the old value for that property
701 * @param newValue the new value for that property
702 */
703 protected void firePropertyChange( Object key, Object oldValue, Object newValue ) {
704 }
705
706
707
708
709 /***
710 * Map entry used by {@link BeanMap}.
711 */
712 protected static class Entry extends AbstractMapEntry {
713 private BeanMap owner;
714
715 /***
716 * Constructs a new <code>Entry</code>.
717 *
718 * @param owner the BeanMap this entry belongs to
719 * @param key the key for this entry
720 * @param value the value for this entry
721 */
722 protected Entry( BeanMap owner, Object key, Object value ) {
723 super( key, value );
724 this.owner = owner;
725 }
726
727 /***
728 * Sets the value.
729 *
730 * @param value the new value for the entry
731 * @return the old value for the entry
732 */
733 public Object setValue(Object value) {
734 Object key = getKey();
735 Object oldValue = owner.get( key );
736
737 owner.put( key, value );
738 Object newValue = owner.get( key );
739 super.setValue( newValue );
740 return oldValue;
741 }
742 }
743
744 /***
745 * Creates an array of parameters to pass to the given mutator method.
746 * If the given object is not the right type to pass to the method
747 * directly, it will be converted using {@link #convertType(Class,Object)}.
748 *
749 * @param method the mutator method
750 * @param value the value to pass to the mutator method
751 * @return an array containing one object that is either the given value
752 * or a transformed value
753 * @throws IllegalAccessException if {@link #convertType(Class,Object)}
754 * raises it
755 * @throws IllegalArgumentException if any other exception is raised
756 * by {@link #convertType(Class,Object)}
757 * @throws ClassCastException if an error occurs creating the method args
758 */
759 protected Object[] createWriteMethodArguments( Method method, Object value )
760 throws IllegalAccessException, ClassCastException {
761 try {
762 if ( value != null ) {
763 Class[] types = method.getParameterTypes();
764 if ( types != null && types.length > 0 ) {
765 Class paramType = types[0];
766 if ( ! paramType.isAssignableFrom( value.getClass() ) ) {
767 value = convertType( paramType, value );
768 }
769 }
770 }
771 Object[] answer = { value };
772 return answer;
773 }
774 catch ( InvocationTargetException e ) {
775 logInfo( e );
776 throw new IllegalArgumentException( e.getMessage() );
777 }
778 catch ( InstantiationException e ) {
779 logInfo( e );
780 throw new IllegalArgumentException( e.getMessage() );
781 }
782 }
783
784 /***
785 * Converts the given value to the given type. First, reflection is
786 * is used to find a public constructor declared by the given class
787 * that takes one argument, which must be the precise type of the
788 * given value. If such a constructor is found, a new object is
789 * created by passing the given value to that constructor, and the
790 * newly constructed object is returned.<P>
791 *
792 * If no such constructor exists, and the given type is a primitive
793 * type, then the given value is converted to a string using its
794 * {@link Object#toString() toString()} method, and that string is
795 * parsed into the correct primitive type using, for instance,
796 * {@link Integer#valueOf(String)} to convert the string into an
797 * <code>int</code>.<P>
798 *
799 * If no special constructor exists and the given type is not a
800 * primitive type, this method returns the original value.
801 *
802 * @param newType the type to convert the value to
803 * @param value the value to convert
804 * @return the converted value
805 * @throws NumberFormatException if newType is a primitive type, and
806 * the string representation of the given value cannot be converted
807 * to that type
808 * @throws InstantiationException if the constructor found with
809 * reflection raises it
810 * @throws InvocationTargetException if the constructor found with
811 * reflection raises it
812 * @throws IllegalAccessException never
813 * @throws IllegalArgumentException never
814 */
815 protected Object convertType( Class newType, Object value )
816 throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
817
818
819 Class[] types = { value.getClass() };
820 try {
821 Constructor constructor = newType.getConstructor( types );
822 Object[] arguments = { value };
823 return constructor.newInstance( arguments );
824 }
825 catch ( NoSuchMethodException e ) {
826
827 Transformer transformer = getTypeTransformer( newType );
828 if ( transformer != null ) {
829 return transformer.transform( value );
830 }
831 return value;
832 }
833 }
834
835 /***
836 * Returns a transformer for the given primitive type.
837 *
838 * @param aType the primitive type whose transformer to return
839 * @return a transformer that will convert strings into that type,
840 * or null if the given type is not a primitive type
841 */
842 protected Transformer getTypeTransformer( Class aType ) {
843 return (Transformer) typeTransformers.get( aType );
844 }
845
846 /***
847 * Logs the given exception to <code>System.out</code>. Used to display
848 * warnings while accessing/mutating the bean.
849 *
850 * @param ex the exception to log
851 */
852 protected void logInfo(Exception ex) {
853
854 System.out.println( "INFO: Exception: " + ex );
855 }
856
857 /***
858 * Logs the given exception to <code>System.err</code>. Used to display
859 * errors while accessing/mutating the bean.
860 *
861 * @param ex the exception to log
862 */
863 protected void logWarn(Exception ex) {
864
865 System.out.println( "WARN: Exception: " + ex );
866 ex.printStackTrace();
867 }
868 }