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.Transformer;
037import org.apache.commons.collections.keyvalue.AbstractMapEntry;
038
039/**
040 * An implementation of Map for JavaBeans which uses introspection to
041 * get and put properties in the bean.
042 * <p>
043 * If an exception occurs during attempts to get or set a property then the
044 * property is considered non existent in the Map
045 *
046 * @version $Id: BeanMap.java 1540518 2013-11-10 19:04:04Z oheger $
047 */
048public class BeanMap extends AbstractMap<Object, Object> implements Cloneable {
049
050    private transient Object bean;
051
052    private transient HashMap<String, Method> readMethods = new HashMap<String, Method>();
053    private transient HashMap<String, Method> writeMethods = new HashMap<String, Method>();
054    private transient HashMap<String, Class<? extends Object>> types = new HashMap<String, Class<? extends Object>>();
055
056    /**
057     * An empty array.  Used to invoke accessors via reflection.
058     */
059    public static final Object[] NULL_ARGUMENTS = {};
060
061    /**
062     * Maps primitive Class types to transformers.  The transformer
063     * transform strings into the appropriate primitive wrapper.
064     *
065     * N.B. private & unmodifiable replacement for the (public & static) defaultTransformers instance.
066     */
067    private static final Map<Class<? extends Object>, Transformer> typeTransformers =
068            Collections.unmodifiableMap(createTypeTransformers());
069
070    /**
071     * This HashMap has been made unmodifiable to prevent issues when
072     * loaded in a shared ClassLoader enviroment.
073     *
074     * @see "http://issues.apache.org/jira/browse/BEANUTILS-112"
075     * @deprecated Use {@link BeanMap#getTypeTransformer(Class)} method
076     */
077    @Deprecated
078    public static HashMap defaultTransformers = new HashMap() {
079        @Override
080        public void clear() {
081            throw new UnsupportedOperationException();
082        }
083        @Override
084        public boolean containsKey(Object key) {
085            return typeTransformers.containsKey(key);
086        }
087        @Override
088        public boolean containsValue(Object value) {
089            return typeTransformers.containsValue(value);
090        }
091        @Override
092        public Set entrySet() {
093            return typeTransformers.entrySet();
094        }
095        @Override
096        public Object get(Object key) {
097            return typeTransformers.get(key);
098        }
099        @Override
100        public boolean isEmpty() {
101            return false;
102        }
103        @Override
104        public Set keySet() {
105            return typeTransformers.keySet();
106        }
107        @Override
108        public Object put(Object key, Object value) {
109            throw new UnsupportedOperationException();
110        }
111        @Override
112        public void putAll(Map m) {
113            throw new UnsupportedOperationException();
114        }
115        @Override
116        public Object remove(Object key) {
117            throw new UnsupportedOperationException();
118        }
119        @Override
120        public int size() {
121            return typeTransformers.size();
122        }
123        @Override
124        public Collection values() {
125            return typeTransformers.values();
126        }
127    };
128
129    private static Map<Class<? extends Object>, Transformer> createTypeTransformers() {
130        Map<Class<? extends Object>, Transformer> defaultTransformers =
131                new HashMap<Class<? extends Object>, Transformer>();
132        defaultTransformers.put(
133            Boolean.TYPE,
134            new Transformer() {
135                public Object transform( Object input ) {
136                    return Boolean.valueOf( input.toString() );
137                }
138            }
139        );
140        defaultTransformers.put(
141            Character.TYPE,
142            new Transformer() {
143                public Object transform( Object input ) {
144                    return new Character( input.toString().charAt( 0 ) );
145                }
146            }
147        );
148        defaultTransformers.put(
149            Byte.TYPE,
150            new Transformer() {
151                public Object transform( Object input ) {
152                    return Byte.valueOf( input.toString() );
153                }
154            }
155        );
156        defaultTransformers.put(
157            Short.TYPE,
158            new Transformer() {
159                public Object transform( Object input ) {
160                    return Short.valueOf( input.toString() );
161                }
162            }
163        );
164        defaultTransformers.put(
165            Integer.TYPE,
166            new Transformer() {
167                public Object transform( Object input ) {
168                    return Integer.valueOf( input.toString() );
169                }
170            }
171        );
172        defaultTransformers.put(
173            Long.TYPE,
174            new Transformer() {
175                public Object transform( Object input ) {
176                    return Long.valueOf( input.toString() );
177                }
178            }
179        );
180        defaultTransformers.put(
181            Float.TYPE,
182            new Transformer() {
183                public Object transform( Object input ) {
184                    return Float.valueOf( input.toString() );
185                }
186            }
187        );
188        defaultTransformers.put(
189            Double.TYPE,
190            new Transformer() {
191                public Object transform( Object input ) {
192                    return Double.valueOf( input.toString() );
193                }
194            }
195        );
196        return defaultTransformers;
197    }
198
199
200    // Constructors
201    //-------------------------------------------------------------------------
202
203    /**
204     * Constructs a new empty <code>BeanMap</code>.
205     */
206    public BeanMap() {
207    }
208
209    /**
210     * Constructs a new <code>BeanMap</code> that operates on the
211     * specified bean.  If the given bean is <code>null</code>, then
212     * this map will be empty.
213     *
214     * @param bean  the bean for this map to operate on
215     */
216    public BeanMap(Object bean) {
217        this.bean = bean;
218        initialise();
219    }
220
221    // Map interface
222    //-------------------------------------------------------------------------
223
224    /**
225     * Renders a string representation of this object.
226     * @return a <code>String</code> representation of this object
227     */
228    @Override
229    public String toString() {
230        return "BeanMap<" + String.valueOf(bean) + ">";
231    }
232
233    /**
234     * Clone this bean map using the following process:
235     *
236     * <ul>
237     * <li>If there is no underlying bean, return a cloned BeanMap without a
238     * bean.
239     *
240     * <li>Since there is an underlying bean, try to instantiate a new bean of
241     * the same type using Class.newInstance().
242     *
243     * <li>If the instantiation fails, throw a CloneNotSupportedException
244     *
245     * <li>Clone the bean map and set the newly instantiated bean as the
246     * underlying bean for the bean map.
247     *
248     * <li>Copy each property that is both readable and writable from the
249     * existing object to a cloned bean map.
250     *
251     * <li>If anything fails along the way, throw a
252     * CloneNotSupportedException.
253     *
254     * <ul>
255     *
256     * @return a cloned instance of this bean map
257     * @throws CloneNotSupportedException if the underlying bean
258     * cannot be cloned
259     */
260    @Override
261    public Object clone() throws CloneNotSupportedException {
262        BeanMap newMap = (BeanMap)super.clone();
263
264        if(bean == null) {
265            // no bean, just an empty bean map at the moment.  return a newly
266            // cloned and empty bean map.
267            return newMap;
268        }
269
270        Object newBean = null;
271        Class<? extends Object> beanClass = bean.getClass(); // Cannot throw Exception
272        try {
273            newBean = beanClass.newInstance();
274        } catch (Exception e) {
275            // unable to instantiate
276            CloneNotSupportedException cnse = new CloneNotSupportedException
277                ("Unable to instantiate the underlying bean \"" +
278                 beanClass.getName() + "\": " + e);
279            BeanUtils.initCause(cnse, e);
280            throw cnse;
281        }
282
283        try {
284            newMap.setBean(newBean);
285        } catch (Exception exception) {
286            CloneNotSupportedException cnse = new CloneNotSupportedException
287                ("Unable to set bean in the cloned bean map: " +
288                 exception);
289            BeanUtils.initCause(cnse, exception);
290            throw cnse;
291        }
292
293        try {
294            // copy only properties that are readable and writable.  If its
295            // not readable, we can't get the value from the old map.  If
296            // its not writable, we can't write a value into the new map.
297            Iterator<?> readableKeys = readMethods.keySet().iterator();
298            while(readableKeys.hasNext()) {
299                Object key = readableKeys.next();
300                if(getWriteMethod(key) != null) {
301                    newMap.put(key, get(key));
302                }
303            }
304        } catch (Exception exception) {
305            CloneNotSupportedException cnse = new CloneNotSupportedException
306                ("Unable to copy bean values to cloned bean map: " +
307                 exception);
308            BeanUtils.initCause(cnse, exception);
309            throw cnse;
310        }
311
312        return newMap;
313    }
314
315    /**
316     * Puts all of the writable properties from the given BeanMap into this
317     * BeanMap. Read-only and Write-only properties will be ignored.
318     *
319     * @param map  the BeanMap whose properties to put
320     */
321    public void putAllWriteable(BeanMap map) {
322        Iterator<?> readableKeys = map.readMethods.keySet().iterator();
323        while (readableKeys.hasNext()) {
324            Object key = readableKeys.next();
325            if (getWriteMethod(key) != null) {
326                this.put(key, map.get(key));
327            }
328        }
329    }
330
331
332    /**
333     * This method reinitializes the bean map to have default values for the
334     * bean's properties.  This is accomplished by constructing a new instance
335     * of the bean which the map uses as its underlying data source.  This
336     * behavior for <code>clear()</code> differs from the Map contract in that
337     * the mappings are not actually removed from the map (the mappings for a
338     * BeanMap are fixed).
339     */
340    @Override
341    public void clear() {
342        if(bean == null) {
343            return;
344        }
345
346        Class<? extends Object> beanClass = null;
347        try {
348            beanClass = bean.getClass();
349            bean = beanClass.newInstance();
350        }
351        catch (Exception e) {
352            UnsupportedOperationException uoe =
353                new UnsupportedOperationException("Could not create new instance of class: " + beanClass);
354            BeanUtils.initCause(uoe, e);
355            throw uoe;
356        }
357    }
358
359    /**
360     * Returns true if the bean defines a property with the given name.
361     * <p>
362     * The given name must be a <code>String</code>; if not, this method
363     * returns false. This method will also return false if the bean
364     * does not define a property with that name.
365     * <p>
366     * Write-only properties will not be matched as the test operates against
367     * property read methods.
368     *
369     * @param name  the name of the property to check
370     * @return false if the given name is null or is not a <code>String</code>;
371     *   false if the bean does not define a property with that name; or
372     *   true if the bean does define a property with that name
373     */
374    @Override
375    public boolean containsKey(Object name) {
376        Method method = getReadMethod(name);
377        return method != null;
378    }
379
380    /**
381     * Returns true if the bean defines a property whose current value is
382     * the given object.
383     *
384     * @param value  the value to check
385     * @return false  true if the bean has at least one property whose
386     *   current value is that object, false otherwise
387     */
388    @Override
389    public boolean containsValue(Object value) {
390        // use default implementation
391        return super.containsValue(value);
392    }
393
394    /**
395     * Returns the value of the bean's property with the given name.
396     * <p>
397     * The given name must be a {@link String} and must not be
398     * null; otherwise, this method returns <code>null</code>.
399     * If the bean defines a property with the given name, the value of
400     * that property is returned.  Otherwise, <code>null</code> is
401     * returned.
402     * <p>
403     * Write-only properties will not be matched as the test operates against
404     * property read methods.
405     *
406     * @param name  the name of the property whose value to return
407     * @return  the value of the property with that name
408     */
409    @Override
410    public Object get(Object name) {
411        if ( bean != null ) {
412            Method method = getReadMethod( name );
413            if ( method != null ) {
414                try {
415                    return method.invoke( bean, NULL_ARGUMENTS );
416                }
417                catch (  IllegalAccessException e ) {
418                    logWarn( e );
419                }
420                catch ( IllegalArgumentException e ) {
421                    logWarn(  e );
422                }
423                catch ( InvocationTargetException e ) {
424                    logWarn(  e );
425                }
426                catch ( NullPointerException e ) {
427                    logWarn(  e );
428                }
429            }
430        }
431        return null;
432    }
433
434    /**
435     * Sets the bean property with the given name to the given value.
436     *
437     * @param name  the name of the property to set
438     * @param value  the value to set that property to
439     * @return  the previous value of that property
440     * @throws IllegalArgumentException  if the given name is null;
441     *   if the given name is not a {@link String}; if the bean doesn't
442     *   define a property with that name; or if the bean property with
443     *   that name is read-only
444     * @throws ClassCastException if an error occurs creating the method args
445     */
446    @Override
447    public Object put(Object name, Object value) throws IllegalArgumentException, ClassCastException {
448        if ( bean != null ) {
449            Object oldValue = get( name );
450            Method method = getWriteMethod( name );
451            if ( method == null ) {
452                throw new IllegalArgumentException( "The bean of type: "+
453                        bean.getClass().getName() + " has no property called: " + name );
454            }
455            try {
456                Object[] arguments = createWriteMethodArguments( method, value );
457                method.invoke( bean, arguments );
458
459                Object newValue = get( name );
460                firePropertyChange( name, oldValue, newValue );
461            }
462            catch ( InvocationTargetException e ) {
463                IllegalArgumentException iae = new IllegalArgumentException(e.getMessage());
464                if (BeanUtils.initCause(iae, e) == false) {
465                    logInfo(e);
466                }
467                throw iae;
468            }
469            catch ( IllegalAccessException e ) {
470                IllegalArgumentException iae = new IllegalArgumentException(e.getMessage());
471                if (BeanUtils.initCause(iae, e) == false) {
472                    logInfo(e);
473                }
474                throw iae;
475            }
476            return oldValue;
477        }
478        return null;
479    }
480
481    /**
482     * Returns the number of properties defined by the bean.
483     *
484     * @return  the number of properties defined by the bean
485     */
486    @Override
487    public int size() {
488        return readMethods.size();
489    }
490
491
492    /**
493     * Get the keys for this BeanMap.
494     * <p>
495     * Write-only properties are <b>not</b> included in the returned set of
496     * property names, although it is possible to set their value and to get
497     * their type.
498     *
499     * @return BeanMap keys.  The Set returned by this method is not
500     *        modifiable.
501     */
502    @SuppressWarnings({ "unchecked", "rawtypes" })
503    // The set actually contains strings; however, because it cannot be
504    // modified there is no danger in selling it as Set<Object>
505    @Override
506    public Set<Object> keySet() {
507        return Collections.unmodifiableSet((Set) readMethods.keySet());
508    }
509
510    /**
511     * Gets a Set of MapEntry objects that are the mappings for this BeanMap.
512     * <p>
513     * Each MapEntry can be set but not removed.
514     *
515     * @return the unmodifiable set of mappings
516     */
517    @Override
518    public Set<Map.Entry<Object, Object>> entrySet() {
519        return Collections.unmodifiableSet(new AbstractSet<Map.Entry<Object, Object>>() {
520            @Override
521            public Iterator<Map.Entry<Object, Object>> iterator() {
522                return entryIterator();
523            }
524            @Override
525            public int size() {
526              return BeanMap.this.readMethods.size();
527            }
528        });
529    }
530
531    /**
532     * Returns the values for the BeanMap.
533     *
534     * @return values for the BeanMap.  The returned collection is not
535     *        modifiable.
536     */
537    @Override
538    public Collection<Object> values() {
539        ArrayList<Object> answer = new ArrayList<Object>( readMethods.size() );
540        for ( Iterator<Object> iter = valueIterator(); iter.hasNext(); ) {
541            answer.add( iter.next() );
542        }
543        return Collections.unmodifiableList(answer);
544    }
545
546
547    // Helper methods
548    //-------------------------------------------------------------------------
549
550    /**
551     * Returns the type of the property with the given name.
552     *
553     * @param name  the name of the property
554     * @return  the type of the property, or <code>null</code> if no such
555     *  property exists
556     */
557    public Class<?> getType(String name) {
558        return types.get( name );
559    }
560
561    /**
562     * Convenience method for getting an iterator over the keys.
563     * <p>
564     * Write-only properties will not be returned in the iterator.
565     *
566     * @return an iterator over the keys
567     */
568    public Iterator<String> keyIterator() {
569        return readMethods.keySet().iterator();
570    }
571
572    /**
573     * Convenience method for getting an iterator over the values.
574     *
575     * @return an iterator over the values
576     */
577    public Iterator<Object> valueIterator() {
578        final Iterator<?> iter = keyIterator();
579        return new Iterator<Object>() {
580            public boolean hasNext() {
581                return iter.hasNext();
582            }
583            public Object next() {
584                Object key = iter.next();
585                return get(key);
586            }
587            public void remove() {
588                throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
589            }
590        };
591    }
592
593    /**
594     * Convenience method for getting an iterator over the entries.
595     *
596     * @return an iterator over the entries
597     */
598    public Iterator<Map.Entry<Object, Object>> entryIterator() {
599        final Iterator<String> iter = keyIterator();
600        return new Iterator<Map.Entry<Object, Object>>() {
601            public boolean hasNext() {
602                return iter.hasNext();
603            }
604            public Map.Entry<Object, Object> next() {
605                Object key = iter.next();
606                Object value = get(key);
607                @SuppressWarnings("unchecked")
608                // This should not cause any problems; the key is actually a
609                // string, but it does no harm to expose it as Object
610                Map.Entry<Object, Object> tmpEntry = new Entry( BeanMap.this, key, value );
611                return tmpEntry;
612            }
613            public void remove() {
614                throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
615            }
616        };
617    }
618
619
620    // Properties
621    //-------------------------------------------------------------------------
622
623    /**
624     * Returns the bean currently being operated on.  The return value may
625     * be null if this map is empty.
626     *
627     * @return the bean being operated on by this map
628     */
629    public Object getBean() {
630        return bean;
631    }
632
633    /**
634     * Sets the bean to be operated on by this map.  The given value may
635     * be null, in which case this map will be empty.
636     *
637     * @param newBean  the new bean to operate on
638     */
639    public void setBean( Object newBean ) {
640        bean = newBean;
641        reinitialise();
642    }
643
644    /**
645     * Returns the accessor for the property with the given name.
646     *
647     * @param name  the name of the property
648     * @return the accessor method for the property, or null
649     */
650    public Method getReadMethod(String name) {
651        return readMethods.get(name);
652    }
653
654    /**
655     * Returns the mutator for the property with the given name.
656     *
657     * @param name  the name of the property
658     * @return the mutator method for the property, or null
659     */
660    public Method getWriteMethod(String name) {
661        return writeMethods.get(name);
662    }
663
664
665    // Implementation methods
666    //-------------------------------------------------------------------------
667
668    /**
669     * Returns the accessor for the property with the given name.
670     *
671     * @param name  the name of the property
672     * @return null if the name is null; null if the name is not a
673     * {@link String}; null if no such property exists; or the accessor
674     *  method for that property
675     */
676    protected Method getReadMethod( Object name ) {
677        return readMethods.get( name );
678    }
679
680    /**
681     * Returns the mutator for the property with the given name.
682     *
683     * @param name  the name of the
684     * @return null if the name is null; null if the name is not a
685     * {@link String}; null if no such property exists; null if the
686     * property is read-only; or the mutator method for that property
687     */
688    protected Method getWriteMethod( Object name ) {
689        return writeMethods.get( name );
690    }
691
692    /**
693     * Reinitializes this bean.  Called during {@link #setBean(Object)}.
694     * Does introspection to find properties.
695     */
696    protected void reinitialise() {
697        readMethods.clear();
698        writeMethods.clear();
699        types.clear();
700        initialise();
701    }
702
703    private void initialise() {
704        if(getBean() == null) {
705            return;
706        }
707
708        Class<? extends Object>  beanClass = getBean().getClass();
709        try {
710            //BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
711            BeanInfo beanInfo = Introspector.getBeanInfo( beanClass );
712            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
713            if ( propertyDescriptors != null ) {
714                for ( int i = 0; i < propertyDescriptors.length; i++ ) {
715                    PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
716                    if ( propertyDescriptor != null ) {
717                        String name = propertyDescriptor.getName();
718                        Method readMethod = propertyDescriptor.getReadMethod();
719                        Method writeMethod = propertyDescriptor.getWriteMethod();
720                        Class<? extends Object> aType = propertyDescriptor.getPropertyType();
721
722                        if ( readMethod != null ) {
723                            readMethods.put( name, readMethod );
724                        }
725                        if ( writeMethod != null ) {
726                            writeMethods.put( name, writeMethod );
727                        }
728                        types.put( name, aType );
729                    }
730                }
731            }
732        }
733        catch ( IntrospectionException e ) {
734            logWarn(  e );
735        }
736    }
737
738    /**
739     * Called during a successful {@link #put(Object,Object)} operation.
740     * Default implementation does nothing.  Override to be notified of
741     * property changes in the bean caused by this map.
742     *
743     * @param key  the name of the property that changed
744     * @param oldValue  the old value for that property
745     * @param newValue  the new value for that property
746     */
747    protected void firePropertyChange( Object key, Object oldValue, Object newValue ) {
748    }
749
750    // Implementation classes
751    //-------------------------------------------------------------------------
752
753    /**
754     * Map entry used by {@link BeanMap}.
755     */
756    protected static class Entry extends AbstractMapEntry {
757        private final BeanMap owner;
758
759        /**
760         * Constructs a new <code>Entry</code>.
761         *
762         * @param owner  the BeanMap this entry belongs to
763         * @param key  the key for this entry
764         * @param value  the value for this entry
765         */
766        protected Entry( BeanMap owner, Object key, Object value ) {
767            super( key, value );
768            this.owner = owner;
769        }
770
771        /**
772         * Sets the value.
773         *
774         * @param value  the new value for the entry
775         * @return the old value for the entry
776         */
777        @Override
778        public Object setValue(Object value) {
779            Object key = getKey();
780            Object oldValue = owner.get( key );
781
782            owner.put( key, value );
783            Object newValue = owner.get( key );
784            super.setValue( newValue );
785            return oldValue;
786        }
787    }
788
789    /**
790     * Creates an array of parameters to pass to the given mutator method.
791     * If the given object is not the right type to pass to the method
792     * directly, it will be converted using {@link #convertType(Class,Object)}.
793     *
794     * @param method  the mutator method
795     * @param value  the value to pass to the mutator method
796     * @return an array containing one object that is either the given value
797     *   or a transformed value
798     * @throws IllegalAccessException if {@link #convertType(Class,Object)}
799     *   raises it
800     * @throws IllegalArgumentException if any other exception is raised
801     *   by {@link #convertType(Class,Object)}
802     * @throws ClassCastException if an error occurs creating the method args
803     */
804    protected Object[] createWriteMethodArguments( Method method, Object value )
805        throws IllegalAccessException, ClassCastException {
806        try {
807            if ( value != null ) {
808                Class<? extends Object>[] types = method.getParameterTypes();
809                if ( types != null && types.length > 0 ) {
810                    Class<? extends Object> paramType = types[0];
811                    if ( ! paramType.isAssignableFrom( value.getClass() ) ) {
812                        value = convertType( paramType, value );
813                    }
814                }
815            }
816            Object[] answer = { value };
817            return answer;
818        }
819        catch ( InvocationTargetException e ) {
820            IllegalArgumentException iae = new IllegalArgumentException(e.getMessage());
821            if (BeanUtils.initCause(iae, e) == false) {
822                logInfo(e);
823            }
824            throw iae;
825        }
826        catch ( InstantiationException e ) {
827            IllegalArgumentException iae = new IllegalArgumentException(e.getMessage());
828            if (BeanUtils.initCause(iae, e) == false) {
829                logInfo(e);
830            }
831            BeanUtils.initCause(iae, e);
832            throw iae;
833        }
834    }
835
836    /**
837     * Converts the given value to the given type.  First, reflection is
838     * is used to find a public constructor declared by the given class
839     * that takes one argument, which must be the precise type of the
840     * given value.  If such a constructor is found, a new object is
841     * created by passing the given value to that constructor, and the
842     * newly constructed object is returned.<P>
843     *
844     * If no such constructor exists, and the given type is a primitive
845     * type, then the given value is converted to a string using its
846     * {@link Object#toString() toString()} method, and that string is
847     * parsed into the correct primitive type using, for instance,
848     * {@link Integer#valueOf(String)} to convert the string into an
849     * <code>int</code>.<P>
850     *
851     * If no special constructor exists and the given type is not a
852     * primitive type, this method returns the original value.
853     *
854     * @param newType  the type to convert the value to
855     * @param value  the value to convert
856     * @return the converted value
857     * @throws NumberFormatException if newType is a primitive type, and
858     *  the string representation of the given value cannot be converted
859     *  to that type
860     * @throws InstantiationException  if the constructor found with
861     *  reflection raises it
862     * @throws InvocationTargetException  if the constructor found with
863     *  reflection raises it
864     * @throws IllegalAccessException  never
865     * @throws IllegalArgumentException  never
866     */
867    protected Object convertType( Class<?> newType, Object value )
868        throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
869
870        // try call constructor
871        Class<?>[] types = { value.getClass() };
872        try {
873            Constructor<?> constructor = newType.getConstructor( types );
874            Object[] arguments = { value };
875            return constructor.newInstance( arguments );
876        }
877        catch ( NoSuchMethodException e ) {
878            // try using the transformers
879            Transformer transformer = getTypeTransformer( newType );
880            if ( transformer != null ) {
881                return transformer.transform( value );
882            }
883            return value;
884        }
885    }
886
887    /**
888     * Returns a transformer for the given primitive type.
889     *
890     * @param aType  the primitive type whose transformer to return
891     * @return a transformer that will convert strings into that type,
892     *  or null if the given type is not a primitive type
893     */
894    protected Transformer getTypeTransformer( Class<?> aType ) {
895        return typeTransformers.get( aType );
896    }
897
898    /**
899     * Logs the given exception to <code>System.out</code>.  Used to display
900     * warnings while accessing/mutating the bean.
901     *
902     * @param ex  the exception to log
903     */
904    protected void logInfo(Exception ex) {
905        // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
906        System.out.println( "INFO: Exception: " + ex );
907    }
908
909    /**
910     * Logs the given exception to <code>System.err</code>.  Used to display
911     * errors while accessing/mutating the bean.
912     *
913     * @param ex  the exception to log
914     */
915    protected void logWarn(Exception ex) {
916        // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
917        System.out.println( "WARN: Exception: " + ex );
918        ex.printStackTrace();
919    }
920}