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