View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.beanutils.converters;
18  
19  import java.lang.reflect.Array;
20  import java.util.Collection;
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.commons.beanutils.BeanUtils;
24  import org.apache.commons.beanutils.ConversionException;
25  import org.apache.commons.beanutils.Converter;
26  
27  /**
28   * Base {@link Converter} implementation that provides the structure
29   * for handling conversion <b>to</b> and <b>from</b> a specified type.
30   * <p>
31   * This implementation provides the basic structure for
32   * converting to/from a specified type optionally using a default
33   * value or throwing a {@link ConversionException} if a
34   * conversion error occurs.
35   * <p>
36   * Implementations should provide conversion to the specified
37   * type and from the specified type to a <code>String</code> value
38   * by implementing the following methods:
39   * <ul>
40   *     <li><code>convertToString(value)</code> - convert to a String
41   *        (default implementation uses the objects <code>toString()</code>
42   *        method).</li>
43   *     <li><code>convertToType(Class, value)</code> - convert
44   *         to the specified type</li>
45   * </ul>
46   *
47   * @version $Revision: 640131 $ $Date: 2008-03-22 22:10:31 -0400 (Sat, 22 Mar 2008) $
48   * @since 1.8.0
49   */
50  public abstract class AbstractConverter implements Converter {
51  
52      /** Debug logging message to indicate default value configuration */
53      private static final String DEFAULT_CONFIG_MSG =
54          "(N.B. Converters can be configured to use default values to avoid throwing exceptions)";
55  
56      /** Current package name */
57      //    getPackage() below returns null on some platforms/jvm versions during the unit tests.
58  //    private static final String PACKAGE = AbstractConverter.class.getPackage().getName() + ".";
59      private static final String PACKAGE = "org.apache.commons.beanutils.converters.";
60  
61      /**
62       * Logging for this instance.
63       */
64      private transient Log log;
65  
66      /**
67       * Should we return the default value on conversion errors?
68       */
69      private boolean useDefault = false;
70  
71      /**
72       * The default value specified to our Constructor, if any.
73       */
74      private Object defaultValue = null;
75  
76      // ----------------------------------------------------------- Constructors
77  
78      /**
79       * Construct a <i>Converter</i> that throws a
80       * <code>ConversionException</code> if an error occurs.
81       */
82      public AbstractConverter() {
83      }
84  
85      /**
86       * Construct a <i>Converter</i> that returns a default
87       * value if an error occurs.
88       *
89       * @param defaultValue The default value to be returned
90       * if the value to be converted is missing or an error
91       * occurs converting the value.
92       */
93      public AbstractConverter(Object defaultValue) {
94          setDefaultValue(defaultValue);
95      }
96  
97      // --------------------------------------------------------- Public Methods
98  
99      /**
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 }