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 }