    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.converters;
    018
    019    import java.lang.reflect.Array;
    020    import java.util.Collection;
    021    import org.apache.commons.logging.Log;
    022    import org.apache.commons.logging.LogFactory;
    023    import org.apache.commons.beanutils.BeanUtils;
    024    import org.apache.commons.beanutils.ConversionException;
    025    import org.apache.commons.beanutils.Converter;
    026
    027    /**
    028     * Base {@link Converter} implementation that provides the structure
    029     * for handling conversion <b>to</b> and <b>from</b> a specified type.
    030     * <p>
    031     * This implementation provides the basic structure for
    032     * converting to/from a specified type optionally using a default
    033     * value or throwing a {@link ConversionException} if a
    034     * conversion error occurs.
    035     * <p>
    036     * Implementations should provide conversion to the specified
    037     * type and from the specified type to a <code>String</code> value
    038     * by implementing the following methods:
    039     * <ul>
    040     *     <li><code>convertToString(value)</code> - convert to a String
    041     *        (default implementation uses the objects <code>toString()</code>
    042     *        method).</li>
    043     *     <li><code>convertToType(Class, value)</code> - convert
    044     *         to the specified type</li>
    045     * </ul>
    046     *
    047     * @version $Revision: 640131 $ $Date: 2008-03-23 02:10:31 +0000 (Sun, 23 Mar 2008) $
    048     * @since 1.8.0
    049     */
    050    public abstract class AbstractConverter implements Converter {
    051
    052        /** Debug logging message to indicate default value configuration */
    053        private static final String DEFAULT_CONFIG_MSG =
    054            "(N.B. Converters can be configured to use default values to avoid throwing exceptions)";
    055
    056        /** Current package name */
    057        //    getPackage() below returns null on some platforms/jvm versions during the unit tests.
    058    //    private static final String PACKAGE = AbstractConverter.class.getPackage().getName() + ".";
    059        private static final String PACKAGE = "org.apache.commons.beanutils.converters.";
    060
    061        /**
    062         * Logging for this instance.
    063         */
    064        private transient Log log;
    065
    066        /**
    067         * Should we return the default value on conversion errors?
    068         */
    069        private boolean useDefault = false;
    070
    071        /**
    072         * The default value specified to our Constructor, if any.
    073         */
    074        private Object defaultValue = null;
    075
    076        // ----------------------------------------------------------- Constructors
    077
    078        /**
    079         * Construct a <i>Converter</i> that throws a
    080         * <code>ConversionException</code> if an error occurs.
    081         */
    082        public AbstractConverter() {
    083        }
    084
    085        /**
    086         * Construct a <i>Converter</i> that returns a default
    087         * value if an error occurs.
    088         *
    089         * @param defaultValue The default value to be returned
    090         * if the value to be converted is missing or an error
    091         * occurs converting the value.
    092         */
    093        public AbstractConverter(Object defaultValue) {
    094            setDefaultValue(defaultValue);
    095        }
    096
    097        // --------------------------------------------------------- Public Methods
    098
    099        /**
    100         * Indicates whether a default value will be returned or exception
    101         * thrown in the event of a conversion error.
    102         *
    103         * @return <code>true</code> if a default value will be returned for
    104         * conversion errors or <code>false</code> if a {@link ConversionException}
    105         * will be thrown.
    106         */
    107        public boolean isUseDefault() {
    108            return useDefault;
    109        }
    110
    111        /**
    112         * Convert the input object into an output object of the
    113         * specified type.
    114         *
    115         * @param type Data type to which this value should be converted
    116         * @param value The input value to be converted
    117         * @return The converted value.
    118         * @throws ConversionException if conversion cannot be performed
    119         * successfully and no default is specified.
    120         */
    121        public Object convert(Class type, Object value) {
    122
    123            Class sourceType  = value == null ? null : value.getClass();
    124            Class targetType  = primitive(type  == null ? getDefaultType() : type);
    125
    126            if (log().isDebugEnabled()) {
    127                log().debug("Converting"
    128                        + (value == null ? "" : " '" + toString(sourceType) + "'")
    129                        + " value '" + value + "' to type '" + toString(targetType) + "'");
    130            }
    131
    132            value = convertArray(value);
    133
    134            // Missing Value
    135            if (value == null) {
    136                return handleMissing(targetType);
    137            }
    138
    139            sourceType = value.getClass();
    140
    141            try {
    142                // Convert --> String
    143                if (targetType.equals(String.class)) {
    144                    return convertToString(value);
    145
    146                // No conversion necessary
    147                } else if (targetType.equals(sourceType)) {
    148                    if (log().isDebugEnabled()) {
    149                        log().debug("    No conversion required, value is already a "
    150                                        + toString(targetType));
    151                    }
    152                    return value;
    153
    154                // Convert --> Type
    155                } else {
    156                    Object result = convertToType(targetType, value);
    157                    if (log().isDebugEnabled()) {
    158                        log().debug("    Converted to " + toString(targetType) +
    159                                       " value '" + result + "'");
    160                    }
    161                    return result;
    162                }
    163            } catch (Throwable t) {
    164                return handleError(targetType, value, t);
    165            }
    166
    167        }
    168
    169        /**
    170         * Convert the input object into a String.
    171         * <p>
    172         * <b>N.B.</b>This implementation simply uses the value's
    173         * <code>toString()</code> method and should be overriden if a
    174         * more sophisticated mechanism for <i>conversion to a String</i>
    175         * is required.
    176         *
    177         * @param value The input value to be converted.
    178         * @return the converted String value.
    179         * @throws Throwable if an error occurs converting to a String
    180         */
    181        protected String convertToString(Object value) throws Throwable {
    182            return value.toString();
    183        }
    184
    185        /**
    186         * Convert the input object into an output object of the
    187         * specified type.
    188         * <p>
    189         * Typical implementations will provide a minimum of
    190         * <code>String --> type</code> conversion.
    191         *
    192         * @param type Data type to which this value should be converted.
    193         * @param value The input value to be converted.
    194         * @return The converted value.
    195         * @throws Throwable if an error occurs converting to the specified type
    196         */
    197        protected abstract Object convertToType(Class type, Object value) throws Throwable;
    198
    199        /**
    200         * Return the first element from an Array (or Collection)
    201         * or the value unchanged if not an Array (or Collection).
    202         *
    203         * N.B. This needs to be overriden for array/Collection converters.
    204         *
    205         * @param value The value to convert
    206         * @return The first element in an Array (or Collection)
    207         * or the value unchanged if not an Array (or Collection)
    208         */
    209        protected Object convertArray(Object value) {
    210            if (value == null) {
    211                return null;
    212            }
    213            if (value.getClass().isArray()) {
    214                if (Array.getLength(value) > 0) {
    215                    return Array.get(value, 0);
    216                } else {
    217                    return null;
    218                }
    219            }
    220            if (value instanceof Collection) {
    221                Collection collection = (Collection)value;
    222                if (collection.size() > 0) {
    223                    return collection.iterator().next();
    224                } else {
    225                    return null;
    226                }
    227            }
    228            return value;
    229        }
    230
    231        /**
    232         * Handle Conversion Errors.
    233         * <p>
    234         * If a default value has been specified then it is returned
    235         * otherwise a ConversionException is thrown.
    236         *
    237         * @param type Data type to which this value should be converted.
    238         * @param value The input value to be converted
    239         * @param cause The exception thrown by the <code>convert</code> method
    240         * @return The default value.
    241         * @throws ConversionException if no default value has been
    242         * specified for this {@link Converter}.
    243         */
    244        protected Object handleError(Class type, Object value, Throwable cause) {
    245            if (log().isDebugEnabled()) {
    246                if (cause instanceof ConversionException) {
    247                    log().debug("    Conversion threw ConversionException: " + cause.getMessage());
    248                } else {
    249                    log().debug("    Conversion threw " + cause);
    250                }
    251            }
    252
    253            if (useDefault) {
    254                return handleMissing(type);
    255            }
    256
    257            ConversionException cex = null;
    258            if (cause instanceof ConversionException) {
    259                cex = (ConversionException)cause;
    260                if (log().isDebugEnabled()) {
    261                    log().debug("    Re-throwing ConversionException: " + cex.getMessage());
    262                    log().debug("    " + DEFAULT_CONFIG_MSG);
    263                }
    264            } else {
    265                String msg = "Error converting from '" + toString(value.getClass()) +
    266                        "' to '" + toString(type) + "' " + cause.getMessage();
    267                cex = new ConversionException(msg, cause);
    268                if (log().isDebugEnabled()) {
    269                    log().debug("    Throwing ConversionException: " + msg);
    270                    log().debug("    " + DEFAULT_CONFIG_MSG);
    271                }
    272                BeanUtils.initCause(cex, cause);
    273            }
    274
    275            throw cex;
    276
    277        }
    278
    279        /**
    280         * Handle missing values.
    281         * <p>
    282         * If a default value has been specified then it is returned
    283         * otherwise a ConversionException is thrown.
    284         *
    285         * @param type Data type to which this value should be converted.
    286         * @return The default value.
    287         * @throws ConversionException if no default value has been
    288         * specified for this {@link Converter}.
    289         */
    290        protected Object handleMissing(Class type) {
    291
    292            if (useDefault || type.equals(String.class)) {
    293                Object value = getDefault(type);
    294                if (useDefault && value != null && !(type.equals(value.getClass()))) {
    295                    try {
    296                        value = convertToType(type, defaultValue);
    297                    } catch (Throwable t) {
    298                        log().error("    Default conversion to " + toString(type)
    299                                + "failed: " + t);
    300                    }
    301                }
    302                if (log().isDebugEnabled()) {
    303                    log().debug("    Using default "
    304                            + (value == null ? "" : toString(value.getClass()) + " ")
    305                            + "value '" + defaultValue + "'");
    306                }
    307                return value;
    308            }
    309
    310            ConversionException cex =  new ConversionException("No value specified for '" +
    311                    toString(type) + "'");
    312            if (log().isDebugEnabled()) {
    313                log().debug("    Throwing ConversionException: " + cex.getMessage());
    314                log().debug("    " + DEFAULT_CONFIG_MSG);
    315            }
    316            throw cex;
    317
    318        }
    319
    320        /**
    321         * Set the default value, converting as required.
    322         * <p>
    323         * If the default value is different from the type the
    324         * <code>Converter</code> handles, it will be converted
    325         * to the handled type.
    326         *
    327         * @param defaultValue The default value to be returned
    328         * if the value to be converted is missing or an error
    329         * occurs converting the value.
    330         * @throws ConversionException if an error occurs converting
    331         * the default value
    332         */
    333        protected void setDefaultValue(Object defaultValue) {
    334            useDefault = false;
    335            if (log().isDebugEnabled()) {
    336                log().debug("Setting default value: " + defaultValue);
    337            }
    338            if (defaultValue == null) {
    339               this.defaultValue  = null;
    340            } else {
    341               this.defaultValue  = convert(getDefaultType(), defaultValue);
    342            }
    343            useDefault = true;
    344        }
    345
    346        /**
    347         * Return the default type this <code>Converter</code> handles.
    348         *
    349         * @return The default type this <code>Converter</code> handles.
    350         */
    351        protected abstract Class getDefaultType();
    352
    353        /**
    354         * Return the default value for conversions to the specified
    355         * type.
    356         * @param type Data type to which this value should be converted.
    357         * @return The default value for the specified type.
    358         */
    359        protected Object getDefault(Class type) {
    360            if (type.equals(String.class)) {
    361                return null;
    362            } else {
    363                return defaultValue;
    364            }
    365        }
    366
    367        /**
    368         * Provide a String representation of this converter.
    369         *
    370         * @return A String representation of this converter
    371         */
    372        public String toString() {
    373            return toString(getClass()) + "[UseDefault=" + useDefault + "]";
    374        }
    375
    376        // ----------------------------------------------------------- Package Methods
    377
    378        /**
    379         * Accessor method for Log instance.
    380         * <p>
    381         * The Log instance variable is transient and
    382         * accessing it through this method ensures it
    383         * is re-initialized when this instance is
    384         * de-serialized.
    385         *
    386         * @return The Log instance.
    387         */
    388        Log log() {
    389            if (log == null) {
    390                log = LogFactory.getLog(getClass());
    391            }
    392            return log;
    393        }
    394
    395        /**
    396         * Change primitve Class types to the associated wrapper class.
    397         * @param type The class type to check.
    398         * @return The converted type.
    399         */
    400         Class primitive(Class type) {
    401            if (type == null || !type.isPrimitive()) {
    402                return type;
    403            }
    404
    405            if (type == Integer.TYPE) {
    406                return Integer.class;
    407            } else if (type == Double.TYPE) {
    408                return Double.class;
    409            } else if (type == Long.TYPE) {
    410                return Long.class;
    411            } else if (type == Boolean.TYPE) {
    412                return Boolean.class;
    413            } else if (type == Float.TYPE) {
    414                return Float.class;
    415            } else if (type == Short.TYPE) {
    416                return Short.class;
    417            } else if (type == Byte.TYPE) {
    418                return Byte.class;
    419            } else if (type == Character.TYPE) {
    420                return Character.class;
    421            } else {
    422                return type;
    423            }
    424        }
    425
    426        /**
    427         * Provide a String representation of a <code>java.lang.Class</code>.
    428         * @param type The <code>java.lang.Class</code>.
    429         * @return The String representation.
    430         */
    431        String toString(Class type) {
    432            String typeName = null;
    433            if (type == null) {
    434                typeName = "null";
    435            } else if (type.isArray()) {
    436                Class elementType = type.getComponentType();
    437                int count = 1;
    438                while (elementType.isArray()) {
    439                    elementType = elementType .getComponentType();
    440                    count++;
    441                }
    442                typeName = elementType.getName();
    443                for (int i = 0; i < count; i++) {
    444                    typeName += "[]";
    445                }
    446            } else {
    447                typeName = type.getName();
    448            }
    449            if (typeName.startsWith("java.lang.") ||
    450                typeName.startsWith("java.util.") ||
    451                typeName.startsWith("java.math.")) {
    452                typeName = typeName.substring("java.lang.".length());
    453            } else if (typeName.startsWith(PACKAGE)) {
    454                typeName = typeName.substring(PACKAGE.length());
    455            }
    456            return typeName;
    457        }
    458    }