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 */
017package org.apache.commons.beanutils;
018
019import java.beans.BeanInfo;
020import java.beans.IntrospectionException;
021import java.beans.Introspector;
022import java.beans.PropertyDescriptor;
023import java.lang.reflect.Constructor;
024import java.lang.reflect.InvocationTargetException;
025import java.lang.reflect.Method;
026import java.util.AbstractMap;
027import java.util.AbstractSet;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.Iterator;
033import java.util.Map;
034import java.util.Set;
035
036import org.apache.commons.collections.list.UnmodifiableList;
037import org.apache.commons.collections.keyvalue.AbstractMapEntry;
038import org.apache.commons.collections.set.UnmodifiableSet;
039import org.apache.commons.collections.Transformer;
040
041/** 
042 * An implementation of Map for JavaBeans which uses introspection to
043 * get and put properties in the bean.
044 * <p>
045 * If an exception occurs during attempts to get or set a property then the
046 * property is considered non existent in the Map
047 *
048 * @version $Revision: 812176 $ $Date: 2009-09-07 15:59:25 +0100 (Mon, 07 Sep 2009) $
049 * 
050 * @author James Strachan
051 * @author Stephen Colebourne
052 */
053public class BeanMap extends AbstractMap implements Cloneable {
054
055    private transient Object bean;
056
057    private transient HashMap readMethods = new HashMap();
058    private transient HashMap writeMethods = new HashMap();
059    private transient HashMap types = new HashMap();
060
061    /**
062     * An empty array.  Used to invoke accessors via reflection.
063     */
064    public static final Object[] NULL_ARGUMENTS = {};
065
066    /**
067     * Maps primitive Class types to transformers.  The transformer
068     * transform strings into the appropriate primitive wrapper.
069     *
070     * N.B. private & unmodifiable replacement for the (public & static) defaultTransformers instance.
071     */
072    private static final Map typeTransformers = Collections.unmodifiableMap(createTypeTransformers());
073
074    /**
075     * This HashMap has been made unmodifiable to prevent issues when
076     * loaded in a shared ClassLoader enviroment.
077     *
078     * @see "http://issues.apache.org/jira/browse/BEANUTILS-112"
079     * @deprecated Use {@link BeanMap#getTypeTransformer(Class)} method
080     */
081    public static HashMap defaultTransformers = new HashMap() {
082        public void clear() {
083            throw new UnsupportedOperationException();
084        }
085        public boolean containsKey(Object key) {
086            return typeTransformers.containsKey(key);
087        }
088        public boolean containsValue(Object value) {
089            return typeTransformers.containsValue(value);
090        }
091        public Set entrySet() {
092            return typeTransformers.entrySet();
093        }
094        public Object get(Object key) {
095            return typeTransformers.get(key);
096        }
097        public boolean isEmpty() {
098            return false;
099        }
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    // Constructors
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    // Map interface
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            // no bean, just an empty bean map at the moment.  return a newly
254            // cloned and empty bean map.
255            return newMap;
256        }
257
258        Object newBean = null;            
259        Class beanClass = bean.getClass(); // Cannot throw Exception
260        try {
261            newBean = beanClass.newInstance();
262        } catch (Exception e) {
263            // unable to instantiate
264            throw new CloneNotSupportedException
265                ("Unable to instantiate the underlying bean \"" +
266                 beanClass.getName() + "\": " + e);
267        }
268            
269        try {
270            newMap.setBean(newBean);
271        } catch (Exception exception) {
272            throw new CloneNotSupportedException
273                ("Unable to set bean in the cloned bean map: " + 
274                 exception);
275        }
276            
277        try {
278            // copy only properties that are readable and writable.  If its
279            // not readable, we can't get the value from the old map.  If
280            // its not writable, we can't write a value into the new map.
281            Iterator readableKeys = readMethods.keySet().iterator();
282            while(readableKeys.hasNext()) {
283                Object key = readableKeys.next();
284                if(getWriteMethod(key) != null) {
285                    newMap.put(key, get(key));
286                }
287            }
288        } catch (Exception exception) {
289            throw new CloneNotSupportedException
290                ("Unable to copy bean values to cloned bean map: " +
291                 exception);
292        }
293
294        return newMap;
295    }
296
297    /**
298     * Puts all of the writable properties from the given BeanMap into this
299     * BeanMap. Read-only and Write-only properties will be ignored.
300     *
301     * @param map  the BeanMap whose properties to put
302     */
303    public void putAllWriteable(BeanMap map) {
304        Iterator readableKeys = map.readMethods.keySet().iterator();
305        while (readableKeys.hasNext()) {
306            Object key = readableKeys.next();
307            if (getWriteMethod(key) != null) {
308                this.put(key, map.get(key));
309            }
310        }
311    }
312
313
314    /**
315     * This method reinitializes the bean map to have default values for the
316     * bean's properties.  This is accomplished by constructing a new instance
317     * of the bean which the map uses as its underlying data source.  This
318     * behavior for <code>clear()</code> differs from the Map contract in that
319     * the mappings are not actually removed from the map (the mappings for a
320     * BeanMap are fixed).
321     */
322    public void clear() {
323        if(bean == null) {
324            return;
325        }
326
327        Class beanClass = null;
328        try {
329            beanClass = bean.getClass();
330            bean = beanClass.newInstance();
331        }
332        catch (Exception e) {
333            throw new UnsupportedOperationException( "Could not create new instance of class: " + beanClass );
334        }
335    }
336
337    /**
338     * Returns true if the bean defines a property with the given name.
339     * <p>
340     * The given name must be a <code>String</code>; if not, this method
341     * returns false. This method will also return false if the bean
342     * does not define a property with that name.
343     * <p>
344     * Write-only properties will not be matched as the test operates against
345     * property read methods.
346     *
347     * @param name  the name of the property to check
348     * @return false if the given name is null or is not a <code>String</code>;
349     *   false if the bean does not define a property with that name; or
350     *   true if the bean does define a property with that name
351     */
352    public boolean containsKey(Object name) {
353        Method method = getReadMethod(name);
354        return method != null;
355    }
356
357    /**
358     * Returns true if the bean defines a property whose current value is
359     * the given object.
360     *
361     * @param value  the value to check
362     * @return false  true if the bean has at least one property whose 
363     *   current value is that object, false otherwise
364     */
365    public boolean containsValue(Object value) {
366        // use default implementation
367        return super.containsValue(value);
368    }
369
370    /**
371     * Returns the value of the bean's property with the given name.
372     * <p>
373     * The given name must be a {@link String} and must not be 
374     * null; otherwise, this method returns <code>null</code>.
375     * If the bean defines a property with the given name, the value of
376     * that property is returned.  Otherwise, <code>null</code> is 
377     * returned.
378     * <p>
379     * Write-only properties will not be matched as the test operates against
380     * property read methods.
381     *
382     * @param name  the name of the property whose value to return
383     * @return  the value of the property with that name
384     */
385    public Object get(Object name) {
386        if ( bean != null ) {
387            Method method = getReadMethod( name );
388            if ( method != null ) {
389                try {
390                    return method.invoke( bean, NULL_ARGUMENTS );
391                }
392                catch (  IllegalAccessException e ) {
393                    logWarn( e );
394                }
395                catch ( IllegalArgumentException e ) {
396                    logWarn(  e );
397                }
398                catch ( InvocationTargetException e ) {
399                    logWarn(  e );
400                }
401                catch ( NullPointerException e ) {
402                    logWarn(  e );
403                }
404            }
405        }
406        return null;
407    }
408
409    /**
410     * Sets the bean property with the given name to the given value.
411     *
412     * @param name  the name of the property to set
413     * @param value  the value to set that property to
414     * @return  the previous value of that property
415     * @throws IllegalArgumentException  if the given name is null;
416     *   if the given name is not a {@link String}; if the bean doesn't
417     *   define a property with that name; or if the bean property with
418     *   that name is read-only
419     * @throws ClassCastException if an error occurs creating the method args
420     */
421    public Object put(Object name, Object value) throws IllegalArgumentException, ClassCastException {
422        if ( bean != null ) {
423            Object oldValue = get( name );
424            Method method = getWriteMethod( name );
425            if ( method == null ) {
426                throw new IllegalArgumentException( "The bean of type: "+ 
427                        bean.getClass().getName() + " has no property called: " + name );
428            }
429            try {
430                Object[] arguments = createWriteMethodArguments( method, value );
431                method.invoke( bean, arguments );
432
433                Object newValue = get( name );
434                firePropertyChange( name, oldValue, newValue );
435            }
436            catch ( InvocationTargetException e ) {
437                logInfo( e );
438                throw new IllegalArgumentException( e.getMessage() );
439            }
440            catch ( IllegalAccessException e ) {
441                logInfo( e );
442                throw new IllegalArgumentException( e.getMessage() );
443            }
444            return oldValue;
445        }
446        return null;
447    }
448                    
449    /**
450     * Returns the number of properties defined by the bean.
451     *
452     * @return  the number of properties defined by the bean
453     */
454    public int size() {
455        return readMethods.size();
456    }
457
458    
459    /**
460     * Get the keys for this BeanMap.
461     * <p>
462     * Write-only properties are <b>not</b> included in the returned set of
463     * property names, although it is possible to set their value and to get 
464     * their type.
465     * 
466     * @return BeanMap keys.  The Set returned by this method is not
467     *        modifiable.
468     */
469    public Set keySet() {
470        return UnmodifiableSet.decorate(readMethods.keySet());
471    }
472
473    /**
474     * Gets a Set of MapEntry objects that are the mappings for this BeanMap.
475     * <p>
476     * Each MapEntry can be set but not removed.
477     * 
478     * @return the unmodifiable set of mappings
479     */
480    public Set entrySet() {
481        return UnmodifiableSet.decorate(new AbstractSet() {
482            public Iterator iterator() {
483                return entryIterator();
484            }
485            public int size() {
486              return BeanMap.this.readMethods.size();
487            }
488        });
489    }
490
491    /**
492     * Returns the values for the BeanMap.
493     * 
494     * @return values for the BeanMap.  The returned collection is not
495     *        modifiable.
496     */
497    public Collection values() {
498        ArrayList answer = new ArrayList( readMethods.size() );
499        for ( Iterator iter = valueIterator(); iter.hasNext(); ) {
500            answer.add( iter.next() );
501        }
502        return UnmodifiableList.decorate(answer);
503    }
504
505
506    // Helper methods
507    //-------------------------------------------------------------------------
508
509    /**
510     * Returns the type of the property with the given name.
511     *
512     * @param name  the name of the property
513     * @return  the type of the property, or <code>null</code> if no such
514     *  property exists
515     */
516    public Class getType(String name) {
517        return (Class) types.get( name );
518    }
519
520    /**
521     * Convenience method for getting an iterator over the keys.
522     * <p>
523     * Write-only properties will not be returned in the iterator.
524     *
525     * @return an iterator over the keys
526     */
527    public Iterator keyIterator() {
528        return readMethods.keySet().iterator();
529    }
530
531    /**
532     * Convenience method for getting an iterator over the values.
533     *
534     * @return an iterator over the values
535     */
536    public Iterator valueIterator() {
537        final Iterator iter = keyIterator();
538        return new Iterator() {            
539            public boolean hasNext() {
540                return iter.hasNext();
541            }
542            public Object next() {
543                Object key = iter.next();
544                return get(key);
545            }
546            public void remove() {
547                throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
548            }
549        };
550    }
551
552    /**
553     * Convenience method for getting an iterator over the entries.
554     *
555     * @return an iterator over the entries
556     */
557    public Iterator entryIterator() {
558        final Iterator iter = keyIterator();
559        return new Iterator() {            
560            public boolean hasNext() {
561                return iter.hasNext();
562            }            
563            public Object next() {
564                Object key = iter.next();
565                Object value = get(key);
566                return new Entry( BeanMap.this, key, value );
567            }            
568            public void remove() {
569                throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
570            }
571        };
572    }
573
574
575    // Properties
576    //-------------------------------------------------------------------------
577
578    /**
579     * Returns the bean currently being operated on.  The return value may
580     * be null if this map is empty.
581     *
582     * @return the bean being operated on by this map
583     */
584    public Object getBean() {
585        return bean;
586    }
587
588    /**
589     * Sets the bean to be operated on by this map.  The given value may
590     * be null, in which case this map will be empty.
591     *
592     * @param newBean  the new bean to operate on
593     */
594    public void setBean( Object newBean ) {
595        bean = newBean;
596        reinitialise();
597    }
598
599    /**
600     * Returns the accessor for the property with the given name.
601     *
602     * @param name  the name of the property 
603     * @return the accessor method for the property, or null
604     */
605    public Method getReadMethod(String name) {
606        return (Method) readMethods.get(name);
607    }
608
609    /**
610     * Returns the mutator for the property with the given name.
611     *
612     * @param name  the name of the property
613     * @return the mutator method for the property, or null
614     */
615    public Method getWriteMethod(String name) {
616        return (Method) writeMethods.get(name);
617    }
618
619
620    // Implementation methods
621    //-------------------------------------------------------------------------
622
623    /**
624     * Returns the accessor for the property with the given name.
625     *
626     * @param name  the name of the property 
627     * @return null if the name is null; null if the name is not a 
628     * {@link String}; null if no such property exists; or the accessor
629     *  method for that property
630     */
631    protected Method getReadMethod( Object name ) {
632        return (Method) readMethods.get( name );
633    }
634
635    /**
636     * Returns the mutator for the property with the given name.
637     *
638     * @param name  the name of the 
639     * @return null if the name is null; null if the name is not a 
640     * {@link String}; null if no such property exists; null if the 
641     * property is read-only; or the mutator method for that property
642     */
643    protected Method getWriteMethod( Object name ) {
644        return (Method) writeMethods.get( name );
645    }
646
647    /**
648     * Reinitializes this bean.  Called during {@link #setBean(Object)}.
649     * Does introspection to find properties.
650     */
651    protected void reinitialise() {
652        readMethods.clear();
653        writeMethods.clear();
654        types.clear();
655        initialise();
656    }
657
658    private void initialise() {
659        if(getBean() == null) {
660            return;
661        }
662
663        Class  beanClass = getBean().getClass();
664        try {
665            //BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
666            BeanInfo beanInfo = Introspector.getBeanInfo( beanClass );
667            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
668            if ( propertyDescriptors != null ) {
669                for ( int i = 0; i < propertyDescriptors.length; i++ ) {
670                    PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
671                    if ( propertyDescriptor != null ) {
672                        String name = propertyDescriptor.getName();
673                        Method readMethod = propertyDescriptor.getReadMethod();
674                        Method writeMethod = propertyDescriptor.getWriteMethod();
675                        Class aType = propertyDescriptor.getPropertyType();
676
677                        if ( readMethod != null ) {
678                            readMethods.put( name, readMethod );
679                        }
680                        if ( writeMethod != null ) {
681                            writeMethods.put( name, writeMethod );
682                        }
683                        types.put( name, aType );
684                    }
685                }
686            }
687        }
688        catch ( IntrospectionException e ) {
689            logWarn(  e );
690        }
691    }
692
693    /**
694     * Called during a successful {@link #put(Object,Object)} operation.
695     * Default implementation does nothing.  Override to be notified of
696     * property changes in the bean caused by this map.
697     *
698     * @param key  the name of the property that changed
699     * @param oldValue  the old value for that property
700     * @param newValue  the new value for that property
701     */
702    protected void firePropertyChange( Object key, Object oldValue, Object newValue ) {
703    }
704
705    // Implementation classes
706    //-------------------------------------------------------------------------
707
708    /**
709     * Map entry used by {@link BeanMap}.
710     */
711    protected static class Entry extends AbstractMapEntry {        
712        private BeanMap owner;
713        
714        /**
715         * Constructs a new <code>Entry</code>.
716         *
717         * @param owner  the BeanMap this entry belongs to
718         * @param key  the key for this entry
719         * @param value  the value for this entry
720         */
721        protected Entry( BeanMap owner, Object key, Object value ) {
722            super( key, value );
723            this.owner = owner;
724        }
725
726        /**
727         * Sets the value.
728         *
729         * @param value  the new value for the entry
730         * @return the old value for the entry
731         */
732        public Object setValue(Object value) {
733            Object key = getKey();
734            Object oldValue = owner.get( key );
735
736            owner.put( key, value );
737            Object newValue = owner.get( key );
738            super.setValue( newValue );
739            return oldValue;
740        }
741    }
742
743    /**
744     * Creates an array of parameters to pass to the given mutator method.
745     * If the given object is not the right type to pass to the method 
746     * directly, it will be converted using {@link #convertType(Class,Object)}.
747     *
748     * @param method  the mutator method
749     * @param value  the value to pass to the mutator method
750     * @return an array containing one object that is either the given value
751     *   or a transformed value
752     * @throws IllegalAccessException if {@link #convertType(Class,Object)}
753     *   raises it
754     * @throws IllegalArgumentException if any other exception is raised
755     *   by {@link #convertType(Class,Object)}
756     * @throws ClassCastException if an error occurs creating the method args
757     */
758    protected Object[] createWriteMethodArguments( Method method, Object value ) 
759        throws IllegalAccessException, ClassCastException {            
760        try {
761            if ( value != null ) {
762                Class[] types = method.getParameterTypes();
763                if ( types != null && types.length > 0 ) {
764                    Class paramType = types[0];
765                    if ( ! paramType.isAssignableFrom( value.getClass() ) ) {
766                        value = convertType( paramType, value );
767                    }
768                }
769            }
770            Object[] answer = { value };
771            return answer;
772        }
773        catch ( InvocationTargetException e ) {
774            logInfo( e );
775            throw new IllegalArgumentException( e.getMessage() );
776        }
777        catch ( InstantiationException e ) {
778            logInfo( e );
779            throw new IllegalArgumentException( e.getMessage() );
780        }
781    }
782
783    /**
784     * Converts the given value to the given type.  First, reflection is
785     * is used to find a public constructor declared by the given class 
786     * that takes one argument, which must be the precise type of the 
787     * given value.  If such a constructor is found, a new object is
788     * created by passing the given value to that constructor, and the
789     * newly constructed object is returned.<P>
790     *
791     * If no such constructor exists, and the given type is a primitive
792     * type, then the given value is converted to a string using its 
793     * {@link Object#toString() toString()} method, and that string is
794     * parsed into the correct primitive type using, for instance, 
795     * {@link Integer#valueOf(String)} to convert the string into an
796     * <code>int</code>.<P>
797     *
798     * If no special constructor exists and the given type is not a 
799     * primitive type, this method returns the original value.
800     *
801     * @param newType  the type to convert the value to
802     * @param value  the value to convert
803     * @return the converted value
804     * @throws NumberFormatException if newType is a primitive type, and 
805     *  the string representation of the given value cannot be converted
806     *  to that type
807     * @throws InstantiationException  if the constructor found with 
808     *  reflection raises it
809     * @throws InvocationTargetException  if the constructor found with
810     *  reflection raises it
811     * @throws IllegalAccessException  never
812     * @throws IllegalArgumentException  never
813     */
814    protected Object convertType( Class newType, Object value ) 
815        throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
816        
817        // try call constructor
818        Class[] types = { value.getClass() };
819        try {
820            Constructor constructor = newType.getConstructor( types );        
821            Object[] arguments = { value };
822            return constructor.newInstance( arguments );
823        }
824        catch ( NoSuchMethodException e ) {
825            // try using the transformers
826            Transformer transformer = getTypeTransformer( newType );
827            if ( transformer != null ) {
828                return transformer.transform( value );
829            }
830            return value;
831        }
832    }
833
834    /**
835     * Returns a transformer for the given primitive type.
836     *
837     * @param aType  the primitive type whose transformer to return
838     * @return a transformer that will convert strings into that type,
839     *  or null if the given type is not a primitive type
840     */
841    protected Transformer getTypeTransformer( Class aType ) {
842        return (Transformer) typeTransformers.get( aType );
843    }
844
845    /**
846     * Logs the given exception to <code>System.out</code>.  Used to display
847     * warnings while accessing/mutating the bean.
848     *
849     * @param ex  the exception to log
850     */
851    protected void logInfo(Exception ex) {
852        // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
853        System.out.println( "INFO: Exception: " + ex );
854    }
855
856    /**
857     * Logs the given exception to <code>System.err</code>.  Used to display
858     * errors while accessing/mutating the bean.
859     *
860     * @param ex  the exception to log
861     */
862    protected void logWarn(Exception ex) {
863        // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
864        System.out.println( "WARN: Exception: " + ex );
865        ex.printStackTrace();
866    }
867}