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    }