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 }