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