    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: 812176 $ $Date: 2009-09-07 15:59:25 +0100 (Mon, 07 Sep 2009) $
    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 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    }