View Javadoc

1   /*
2    * Copyright 2006-2016 The JGUIraffe Team.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License")
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.jguiraffe.di;
17  
18  import java.util.LinkedList;
19  
20  import org.apache.commons.beanutils.ConversionException;
21  import org.apache.commons.beanutils.ConvertUtilsBean;
22  import org.apache.commons.beanutils.Converter;
23  import org.apache.commons.lang.ClassUtils;
24  
25  /**
26   * <p>
27   * A helper class providing functionality related to data type conversion and
28   * registration of custom converters.
29   * </p>
30   * <p>
31   * The conversion of data types - e.g. for properties or method parameters - is
32   * an important feature: Because beans to be managed by the dependency injection
33   * framework are typically defined in XML builder scripts all property values or
34   * method parameters are initially strings. The framework has then to find
35   * appropriate methods compatible with the specified parameters. If necessary, a
36   * type conversion of the values involved has to be performed.
37   * </p>
38   * <p>
39   * This class uses <a href="http://commons.apache.org/beanutils">Commons
40   * BeanUtils</a> for implementing type conversion facilities. The BeanUtils
41   * library allows defining custom type converters. Such converters can be
42   * registered at an instance of this class to enhance the type conversion
43   * capabilities.
44   * </p>
45   * <p>
46   * Objects of this class can be connected in a hierarchical way: an instance can
47   * have a parent. If a required type converter is not found in this instance,
48   * the parent's converters are searched. This allows for instance to define base
49   * converters on a top-level instance. Child instances can define specialized
50   * converters and even override converters of their parents.
51   * </p>
52   * <p>
53   * Implementation note: This class is thread-safe.
54   * </p>
55   *
56   * @author Oliver Heger
57   * @version $Id: ConversionHelper.java 205 2012-01-29 18:29:57Z oheger $
58   */
59  public class ConversionHelper
60  {
61      /** Stores a reference to the parent instance. */
62      private final ConversionHelper parent;
63  
64      /** The helper object for performing data conversions. */
65      private final ConvertUtilsBean convertBean;
66  
67      /** A list for the registered base class converters. */
68      private final LinkedList<ConverterData> baseClassConverters;
69  
70      /**
71       * Creates a new instance of {@code ConversionHelper} which does not have a
72       * parent.
73       */
74      public ConversionHelper()
75      {
76          this(null);
77      }
78  
79      /**
80       * Creates a new instance of {@code ConversionHelper} and initializes it
81       * with the given parent instance. If the parent is defined, it may be
82       * queried for type converters if this instance cannot resolve specific data
83       * types.
84       *
85       * @param parent the parent instance (may be <b>null</b>)
86       */
87      public ConversionHelper(ConversionHelper parent)
88      {
89          baseClassConverters = new LinkedList<ConverterData>();
90          convertBean = new CustomConverterBean();
91          convertBean.register(true, false, 0);
92          this.parent = parent;
93  
94          if (parent == null)
95          {
96              registerDefaultConverters();
97          }
98      }
99  
100     /**
101      * Returns the parent of this instance. This may be <b>null</b> if no parent
102      * has been set.
103      *
104      * @return the parent
105      */
106     public ConversionHelper getParent()
107     {
108         return parent;
109     }
110 
111     /**
112      * Registers a converter for the specified target class. This converter is
113      * used by the {@link #convert(Class, Object)} method if a conversion to the
114      * specified target class is needed.
115      *
116      * @param converter the converter to be registered (must not be <b>null</b>)
117      * @param targetClass the target class of the converter (must not be
118      *        <b>null</b>)
119      * @throws IllegalArgumentException if a required parameter is missing
120      */
121     public final void registerConverter(Converter converter,
122             Class<?> targetClass)
123     {
124         checkRegisterConverterArgs(converter, targetClass);
125         getConvertBean().register(converter, targetClass);
126     }
127 
128     /**
129      * Registers a converter for the given base class and all derived classes.
130      * If a data conversion is to be performed, it is checked first whether a
131      * specific converter for the target class has been registered (using the
132      * {@link #registerConverter(Converter, Class)} method). If this is the
133      * case, this converter is used. Otherwise, it is checked whether a
134      * converter has been registered using this method whose target class is a
135      * super class of the desired target class. If this is the case, this
136      * converter is invoked. With this method converters for whole class
137      * hierarchies can be registered. This can be useful if all members of the
138      * hierarchy require a similar conversion. An example are enumeration
139      * classes. Base class converters are checked in reverse order they have
140      * been registered. So if they form a hierarchy themselves, the least
141      * specific converter should be registered first, followed by more specific
142      * ones. For instance, consider some base class converters dealing with
143      * collection classes. There may be one converter that handles
144      * {@code java.util.List} objects and one for generic
145      * {@code java.util.Collection} objects. In this scenario the collection
146      * converter has to be registered first followed by the specific one for
147      * lists. Otherwise, the generic collection converter will also be used for
148      * lists. This reverse order approach makes it possible for applications to
149      * override default base class converters with their own implementations: if
150      * custom converters are added later, they take precedence over the already
151      * registered converters.
152      *
153      * @param converter the converter to be registered (must not be <b>null</b>)
154      * @param targetClass the target class of the converter (must not be
155      *        <b>null</b>)
156      * @throws IllegalArgumentException if a required parameter is missing
157      */
158     public final void registerBaseClassConverter(Converter converter,
159             Class<?> targetClass)
160     {
161         checkRegisterConverterArgs(converter, targetClass);
162         synchronized (baseClassConverters)
163         {
164             baseClassConverters.add(0,
165                     new ConverterData(converter, targetClass));
166         }
167     }
168 
169     /**
170      * Performs a type conversion. This method tries to convert the specified
171      * value to the given target class. Under the hood converters of the
172      * <em>Commons BeanUtils</em> library are used to actually perform the
173      * conversion. If custom data types are involved, specialized converters can
174      * be registered. If no conversion is possible, an
175      * {@code InjectionException} exception is thrown.
176      *
177      * @param <T> the type of the target class
178      * @param targetClass the target class (must not be <b>null</b>)
179      * @param value the value to be converted
180      * @return the converted value
181      * @throws InjectionException if no conversion is possible
182      * @throws IllegalArgumentException if the target class is <b>null</b>
183      */
184     public <T> T convert(Class<T> targetClass, Object value)
185     {
186         checkTargetClass(targetClass);
187         if (value == null)
188         {
189             return null;
190         }
191 
192         if (!targetClass.isInstance(value))
193         {
194             return convertObject(targetClass, value);
195         }
196         else
197         {
198             @SuppressWarnings("unchecked")
199             // because of instance check
200             T result = (T) value;
201             return result;
202         }
203     }
204 
205     /**
206      * Returns the helper object for type conversions.
207      *
208      * @return the {@code ConvertUtilsBean} object responsible for conversions
209      */
210     protected ConvertUtilsBean getConvertBean()
211     {
212         return convertBean;
213     }
214 
215     /**
216      * Performs a type conversion to the given target class if possible. This
217      * method is called by {@link #convert(Class, Object)} if a conversion is
218      * actually required. The arguments have already been checked for
219      * <b>null</b>. This implementation uses the conversion helper object
220      * returned by {@link #getConvertBean()} to do the conversion.
221      *
222      * @param <T> the type of the target class
223      * @param targetClass the target class of the conversion
224      * @param value the value to be converted
225      * @return the converted value
226      * @throws InjectionException if conversion fails
227      */
228     protected <T> T convertObject(Class<T> targetClass, Object value)
229     {
230         Class<?> destinationClass =
231                 targetClass.isPrimitive() ? ClassUtils
232                         .primitiveToWrapper(targetClass) : targetClass;
233         Object result;
234 
235         try
236         {
237             result = getConvertBean().convert(value, destinationClass);
238         }
239         catch (ConversionException cex)
240         {
241             throw new InjectionException("Error when converting '" + value
242                     + "' to class " + targetClass.getName(), cex);
243         }
244 
245         if (!destinationClass.isInstance(result))
246         {
247             throw new InjectionException("Cannot convert value '" + value
248                     + "' to class " + targetClass.getName());
249         }
250 
251         // because of previous checks result is of type targetClass or of a
252         // corresponding primitive wrapper class
253         @SuppressWarnings("unchecked")
254         T castResult = (T) result;
255         return castResult;
256     }
257 
258     /**
259      * Helper method for checking whether the target class is specified. Throws
260      * an exception if not.
261      *
262      * @param targetClass the target class to be checked
263      */
264     static void checkTargetClass(Class<?> targetClass)
265     {
266         if (targetClass == null)
267         {
268             throw new IllegalArgumentException("Target class must not be null!");
269         }
270     }
271 
272     /**
273      * Registers default converters. This method is called by the constructor.
274      */
275     private void registerDefaultConverters()
276     {
277         registerBaseClassConverter(EnumConverter.getInstance(), Enum.class);
278     }
279 
280     /**
281      * Helper method for verifying parameters for registering converters. This
282      * implementation throws an exception if the parameters are invalid.
283      *
284      * @param conv the converter to be registered
285      * @param targetClass the target class
286      * @throws IllegalArgumentException if the arguments are invalid
287      */
288     private static void checkRegisterConverterArgs(Converter conv,
289             Class<?> targetClass)
290     {
291         if (conv == null)
292         {
293             throw new IllegalArgumentException("Converter must not be null!");
294         }
295         checkTargetClass(targetClass);
296     }
297 
298     /**
299      * A specialized implementation of {@code ConvertUtilsBean}. This
300      * implementation handles base class converters, i.e. converters that can
301      * deal with a whole class hierarchy rather than a specific target class.
302      */
303     private class CustomConverterBean extends ConvertUtilsBean
304     {
305         /**
306          * Searches for a converter that can handle the specified class. This
307          * implementation supports base class converters. It first delegates to
308          * the inherited method. If a suitable converter is found, it is
309          * returned. Otherwise, the registered base class converters are
310          * checked. The first one found whose base class is a super class of the
311          * specified class is returned.
312          *
313          * @param clazz the target class of the conversion
314          * @return a converter that can handle this class or <b>null</b> if none
315          *         is found
316          */
317         @Override
318         public Converter lookup(@SuppressWarnings("rawtypes") Class clazz)
319         {
320             Converter conv = super.lookup(clazz);
321 
322             if (conv == null)
323             {
324                 synchronized (baseClassConverters)
325                 {
326                     for (ConverterData cd : baseClassConverters)
327                     {
328                         if (cd.canHandleConverter(clazz))
329                         {
330                             conv = cd.getConverter();
331                             break;
332                         }
333                     }
334                 }
335             }
336 
337             if (conv == null && getParent() != null)
338             {
339                 conv = getParent().getConvertBean().lookup(clazz);
340             }
341 
342             return conv;
343         }
344     }
345 
346     /**
347      * A simple data class for storing information about a converter and its
348      * target class.
349      */
350     private static class ConverterData
351     {
352         /** The converter. */
353         private final Converter converter;
354 
355         /** The target class. */
356         private Class<?> targetClass;
357 
358         /**
359          * Creates a new instance of {@code ConverterData} and initializes it.
360          *
361          * @param conv the converter
362          * @param cls the target class of the converter
363          */
364         public ConverterData(Converter conv, Class<?> cls)
365         {
366             converter = conv;
367             targetClass = cls;
368         }
369 
370         /**
371          * Returns the converter.
372          *
373          * @return the converter
374          */
375         public Converter getConverter()
376         {
377             return converter;
378         }
379 
380         /**
381          * Tests whether this converter can handle a conversion to the given
382          * target class.
383          *
384          * @param clazz the desired target class of the conversion
385          * @return a flag whether this conversion is supported
386          */
387         public boolean canHandleConverter(Class<?> clazz)
388         {
389             return ClassUtils.isAssignable(clazz, targetClass);
390         }
391     }
392 }