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 }