1 /*
2 * Copyright (c) 2002-2006 by OpenSymphony
3 * All rights reserved.
4 */
5 package com.opensymphony.xwork2.conversion.impl;
6
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.lang.annotation.Annotation;
10 import java.lang.reflect.Member;
11 import java.lang.reflect.Method;
12 import java.net.URL;
13 import java.util.HashMap;
14 import java.util.HashSet;
15 import java.util.Iterator;
16 import java.util.Map;
17 import java.util.Properties;
18
19 import com.opensymphony.xwork2.ActionContext;
20 import com.opensymphony.xwork2.ObjectFactory;
21 import com.opensymphony.xwork2.XWorkMessages;
22 import com.opensymphony.xwork2.conversion.TypeConverter;
23 import com.opensymphony.xwork2.conversion.annotations.Conversion;
24 import com.opensymphony.xwork2.conversion.annotations.ConversionRule;
25 import com.opensymphony.xwork2.conversion.annotations.ConversionType;
26 import com.opensymphony.xwork2.conversion.annotations.TypeConversion;
27 import com.opensymphony.xwork2.inject.Inject;
28 import com.opensymphony.xwork2.ognl.XWorkTypeConverterWrapper;
29 import com.opensymphony.xwork2.util.AnnotationUtils;
30 import com.opensymphony.xwork2.util.ClassLoaderUtil;
31 import com.opensymphony.xwork2.util.CompoundRoot;
32 import com.opensymphony.xwork2.util.FileManager;
33 import com.opensymphony.xwork2.util.LocalizedTextUtil;
34 import com.opensymphony.xwork2.util.ValueStack;
35 import com.opensymphony.xwork2.util.logging.Logger;
36 import com.opensymphony.xwork2.util.logging.LoggerFactory;
37 import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
38
39
40 /**
41 * XWorkConverter is a singleton used by many of the Struts 2's Ognl extention points,
42 * such as InstantiatingNullHandler, XWorkListPropertyAccessor etc to do object
43 * conversion.
44 *
45 * <!-- START SNIPPET: javadoc -->
46 *
47 * Type conversion is great for situations where you need to turn a String in to a more complex object. Because the web
48 * is type-agnostic (everything is a string in HTTP), Struts 2's type conversion features are very useful. For instance,
49 * if you were prompting a user to enter in coordinates in the form of a string (such as "3, 22"), you could have
50 * Struts 2 do the conversion both from String to Point and from Point to String.
51 *
52 * <p/> Using this "point" example, if your action (or another compound object in which you are setting properties on)
53 * has a corresponding ClassName-conversion.properties file, Struts 2 will use the configured type converters for
54 * conversion to and from strings. So turning "3, 22" in to new Point(3, 22) is done by merely adding the following
55 * entry to <b>ClassName-conversion.properties</b> (Note that the PointConverter should impl the TypeConverter
56 * interface):
57 *
58 * <p/><b>point = com.acme.PointConverter</b>
59 *
60 * <p/> Your type converter should be sure to check what class type it is being requested to convert. Because it is used
61 * for both to and from strings, you will need to split the conversion method in to two parts: one that turns Strings in
62 * to Points, and one that turns Points in to Strings.
63 *
64 * <p/> After this is done, you can now reference your point (using <s:property value="point"/> in JSP or ${point}
65 * in FreeMarker) and it will be printed as "3, 22" again. As such, if you submit this back to an action, it will be
66 * converted back to a Point once again.
67 *
68 * <p/> In some situations you may wish to apply a type converter globally. This can be done by editing the file
69 * <b>xwork-conversion.properties</b> in the root of your class path (typically WEB-INF/classes) and providing a
70 * property in the form of the class name of the object you wish to convert on the left hand side and the class name of
71 * the type converter on the right hand side. For example, providing a type converter for all Point objects would mean
72 * adding the following entry:
73 *
74 * <p/><b>com.acme.Point = com.acme.PointConverter</b>
75 *
76 * <!-- END SNIPPET: javadoc -->
77 *
78 * <p/>
79 *
80 * <!-- START SNIPPET: i18n-note -->
81 *
82 * Type conversion should not be used as a substitute for i18n. It is not recommended to use this feature to print out
83 * properly formatted dates. Rather, you should use the i18n features of Struts 2 (and consult the JavaDocs for JDK's
84 * MessageFormat object) to see how a properly formatted date should be displayed.
85 *
86 * <!-- END SNIPPET: i18n-note -->
87 *
88 * <p/>
89 *
90 * <!-- START SNIPPET: error-reporting -->
91 *
92 * Any error that occurs during type conversion may or may not wish to be reported. For example, reporting that the
93 * input "abc" could not be converted to a number might be important. On the other hand, reporting that an empty string,
94 * "", cannot be converted to a number might not be important - especially in a web environment where it is hard to
95 * distinguish between a user not entering a value vs. entering a blank value.
96 *
97 * <p/> By default, all conversion errors are reported using the generic i18n key <b>xwork.default.invalid.fieldvalue</b>,
98 * which you can override (the default text is <i>Invalid field value for field "xxx"</i>, where xxx is the field name)
99 * in your global i18n resource bundle.
100 *
101 * <p/>However, sometimes you may wish to override this message on a per-field basis. You can do this by adding an i18n
102 * key associated with just your action (Action.properties) using the pattern <b>invalid.fieldvalue.xxx</b>, where xxx
103 * is the field name.
104 *
105 * <p/>It is important to know that none of these errors are actually reported directly. Rather, they are added to a map
106 * called <i>conversionErrors</i> in the ActionContext. There are several ways this map can then be accessed and the
107 * errors can be reported accordingly.
108 *
109 * <!-- END SNIPPET: error-reporting -->
110 *
111 * @author <a href="mailto:plightbo@gmail.com">Pat Lightbody</a>
112 * @author Rainer Hermanns
113 * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
114 * @author tm_jee
115 *
116 * @version $Date: 2008-03-28 19:48:23 +0100 (Fri, 28 Mar 2008) $ $Id: XWorkConverter.java 1770 2008-03-28 18:48:23Z rainerh $
117 *
118 * @see XWorkBasicConverter
119 */
120 public class XWorkConverter extends DefaultTypeConverter {
121
122 protected static final Logger LOG = LoggerFactory.getLogger(XWorkConverter.class);
123 public static final String REPORT_CONVERSION_ERRORS = "report.conversion.errors";
124 public static final String CONVERSION_PROPERTY_FULLNAME = "conversion.property.fullName";
125 public static final String CONVERSION_ERROR_PROPERTY_PREFIX = "invalid.fieldvalue.";
126 public static final String CONVERSION_COLLECTION_PREFIX = "Collection_";
127
128 public static final String LAST_BEAN_CLASS_ACCESSED = "last.bean.accessed";
129 public static final String LAST_BEAN_PROPERTY_ACCESSED = "last.property.accessed";
130
131 /**
132 * Target class conversion Mappings.
133 * <pre>
134 * Map<Class, Map<String, Object>>
135 * - Class -> convert to class
136 * - Map<String, Object>
137 * - String -> property name
138 * eg. Element_property, property etc.
139 * - Object -> String to represent properties
140 * eg. value part of
141 * KeyProperty_property=id
142 * -> TypeConverter to represent an Ognl TypeConverter
143 * eg. value part of
144 * property=foo.bar.MyConverter
145 * -> Class to represent a class
146 * eg. value part of
147 * Element_property=foo.bar.MyObject
148 * </pre>
149 */
150 protected HashMap<Class,Map<String,Object>> mappings = new HashMap<Class,Map<String,Object>>(); // action
151
152 /**
153 * Unavailable target class conversion mappings, serves as a simple cache.
154 */
155 protected HashSet<Class> noMapping = new HashSet<Class>(); // action
156
157 /**
158 * Record class and its type converter mapping.
159 * <pre>
160 * - String - classname as String
161 * - TypeConverter - instance of TypeConverter
162 * </pre>
163 */
164 protected HashMap<String, TypeConverter> defaultMappings = new HashMap<String, TypeConverter>(); // non-action (eg. returned value)
165
166 /**
167 * Record classes that doesn't have conversion mapping defined.
168 * <pre>
169 * - String -> classname as String
170 * </pre>
171 */
172 protected HashSet<String> unknownMappings = new HashSet<String>(); // non-action (eg. returned value)
173
174 private TypeConverter defaultTypeConverter;
175 private ObjectFactory objectFactory;
176
177
178 protected XWorkConverter() {
179 }
180
181 @Inject
182 public void setObjectFactory(ObjectFactory factory) {
183 this.objectFactory = factory;
184 try {
185 // note: this file is deprecated
186 loadConversionProperties("xwork-default-conversion.properties");
187 } catch (Exception e) {
188 }
189
190 try {
191 loadConversionProperties("xwork-conversion.properties");
192 } catch (Exception e) {
193 }
194 }
195
196 @Inject
197 public void setDefaultTypeConverter(XWorkBasicConverter conv) {
198 this.defaultTypeConverter = conv;
199 }
200
201 public static String getConversionErrorMessage(String propertyName, ValueStack stack) {
202 String defaultMessage = LocalizedTextUtil.findDefaultText(XWorkMessages.DEFAULT_INVALID_FIELDVALUE,
203 ActionContext.getContext().getLocale(),
204 new Object[]{
205 propertyName
206 });
207 String getTextExpression = "getText('" + CONVERSION_ERROR_PROPERTY_PREFIX + propertyName + "','" + defaultMessage + "')";
208 String message = (String) stack.findValue(getTextExpression);
209
210 if (message == null) {
211 message = defaultMessage;
212 }
213
214 return message;
215 }
216
217
218 public static String buildConverterFilename(Class clazz) {
219 String className = clazz.getName();
220 String resource = className.replace('.', '/') + "-conversion.properties";
221
222 return resource;
223 }
224
225 public Object convertValue(Map map, Object o, Class aClass) {
226 return convertValue(map, null, null, null, o, aClass);
227 }
228
229 /**
230 * Convert value from one form to another.
231 * Minimum requirement of arguments:
232 * <ul>
233 * <li>supplying context, toClass and value</li>
234 * <li>supplying context, target and value.</li>
235 * </ul>
236 *
237 * @see TypeConverter#convertValue(java.util.Map, java.lang.Object, java.lang.reflect.Member, java.lang.String, java.lang.Object, java.lang.Class)
238 */
239 public Object convertValue(Map context, Object target, Member member, String property, Object value, Class toClass) {
240 //
241 // Process the conversion using the default mappings, if one exists
242 //
243 TypeConverter tc = null;
244
245 if ((value != null) && (toClass == value.getClass())) {
246 return value;
247 }
248
249 // allow this method to be called without any context
250 // i.e. it can be called with as little as "Object value" and "Class toClass"
251 if (target != null) {
252 Class clazz = target.getClass();
253
254 Object[] classProp = null;
255
256 // this is to handle weird issues with setValue with a different type
257 if ((target instanceof CompoundRoot) && (context != null)) {
258 classProp = getClassProperty(context);
259 }
260
261 if (classProp != null) {
262 clazz = (Class) classProp[0];
263 property = (String) classProp[1];
264 }
265
266 tc = (TypeConverter) getConverter(clazz, property);
267
268 if (LOG.isDebugEnabled())
269 LOG.debug("field-level type converter for property ["+property+"] = "+(tc==null?"none found":tc));
270 }
271
272 if (tc == null && context != null) {
273 // ok, let's see if we can look it up by path as requested in XW-297
274 Object lastPropertyPath = context.get(ReflectionContextState.CURRENT_PROPERTY_PATH);
275 Class clazz = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
276 if (lastPropertyPath != null && clazz != null) {
277 String path = lastPropertyPath + "." + property;
278 tc = (TypeConverter) getConverter(clazz, path);
279 }
280 }
281
282 if (tc == null) {
283 if (toClass.equals(String.class) && (value != null) && !(value.getClass().equals(String.class) || value.getClass().equals(String[].class)))
284 {
285 // when converting to a string, use the source target's class's converter
286 tc = lookup(value.getClass());
287 } else {
288 // when converting from a string, use the toClass's converter
289 tc = lookup(toClass);
290 }
291
292 if (LOG.isDebugEnabled())
293 LOG.debug("global-level type converter for property ["+property+"] = "+(tc==null?"none found":tc));
294 }
295
296
297
298 if (tc != null) {
299 try {
300 return tc.convertValue(context, target, member, property, value, toClass);
301 } catch (Exception e) {
302 e.printStackTrace();
303 handleConversionException(context, property, value, target);
304
305 return TypeConverter.NO_CONVERSION_POSSIBLE;
306 }
307 }
308
309 if (defaultTypeConverter != null) {
310 try {
311 if (LOG.isDebugEnabled())
312 LOG.debug("falling back to default type converter ["+defaultTypeConverter+"]");
313 return defaultTypeConverter.convertValue(context, target, member, property, value, toClass);
314 } catch (Exception e) {
315 e.printStackTrace();
316 handleConversionException(context, property, value, target);
317
318 return TypeConverter.NO_CONVERSION_POSSIBLE;
319 }
320 } else {
321 try {
322 if (LOG.isDebugEnabled())
323 LOG.debug("falling back to Ognl's default type conversion");
324 return super.convertValue(value, toClass);
325 } catch (Exception e) {
326 e.printStackTrace();
327 handleConversionException(context, property, value, target);
328
329 return TypeConverter.NO_CONVERSION_POSSIBLE;
330 }
331 }
332 }
333
334 /**
335 * Looks for a TypeConverter in the default mappings.
336 *
337 * @param className name of the class the TypeConverter must handle
338 * @return a TypeConverter to handle the specified class or null if none can be found
339 */
340 public TypeConverter lookup(String className) {
341 if (unknownMappings.contains(className) && !defaultMappings.containsKey(className)) {
342 return null;
343 }
344
345 TypeConverter result = (TypeConverter) defaultMappings.get(className);
346
347 //Looks for super classes
348 if (result == null) {
349 Class clazz = null;
350
351 try {
352 clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
353 } catch (ClassNotFoundException cnfe) {
354 }
355
356 result = lookupSuper(clazz);
357
358 if (result != null) {
359 //Register now, the next lookup will be faster
360 registerConverter(className, result);
361 } else {
362 // if it isn't found, never look again (also faster)
363 registerConverterNotFound(className);
364 }
365 }
366
367 return result;
368 }
369
370 /**
371 * Looks for a TypeConverter in the default mappings.
372 *
373 * @param clazz the class the TypeConverter must handle
374 * @return a TypeConverter to handle the specified class or null if none can be found
375 */
376 public TypeConverter lookup(Class clazz) {
377 return lookup(clazz.getName());
378 }
379
380 protected Object getConverter(Class clazz, String property) {
381 if (LOG.isDebugEnabled()) {
382 LOG.debug("Property: " + property);
383 LOG.debug("Class: " + clazz.getName());
384 }
385 synchronized (clazz) {
386 if ((property != null) && !noMapping.contains(clazz)) {
387 try {
388 Map<String, Object> mapping = mappings.get(clazz);
389
390 if (mapping == null) {
391 mapping = buildConverterMapping(clazz);
392 } else {
393 mapping = conditionalReload(clazz, mapping);
394 }
395
396 Object converter = mapping.get(property);
397 if (LOG.isDebugEnabled() && converter == null) {
398 LOG.debug("converter is null for property " + property + ". Mapping size: " + mapping.size());
399 Iterator<String> iter = mapping.keySet().iterator();
400 while (iter.hasNext()) {
401 String next = iter.next();
402 LOG.debug(next + ":" + mapping.get(next));
403 }
404 }
405 return converter;
406 } catch (Throwable t) {
407 noMapping.add(clazz);
408 }
409 }
410 }
411
412 return null;
413 }
414
415 protected void handleConversionException(Map context, String property, Object value, Object object) {
416 if ((Boolean.TRUE.equals(context.get(REPORT_CONVERSION_ERRORS)))) {
417 String realProperty = property;
418 String fullName = (String) context.get(CONVERSION_PROPERTY_FULLNAME);
419
420 if (fullName != null) {
421 realProperty = fullName;
422 }
423
424 Map conversionErrors = (Map) context.get(ActionContext.CONVERSION_ERRORS);
425
426 if (conversionErrors == null) {
427 conversionErrors = new HashMap();
428 context.put(ActionContext.CONVERSION_ERRORS, conversionErrors);
429 }
430
431 conversionErrors.put(realProperty, value);
432 }
433 }
434
435 public synchronized void registerConverter(String className, TypeConverter converter) {
436 defaultMappings.put(className, converter);
437 if ( unknownMappings.contains(className)) {
438 unknownMappings.remove(className);
439 }
440 }
441
442 public synchronized void registerConverterNotFound(String className) {
443 unknownMappings.add(className);
444 }
445
446 private Object[] getClassProperty(Map context) {
447 return (Object[]) context.get("__link");
448 }
449
450 /**
451 * Looks for converter mappings for the specified class and adds it to an existing map. Only new converters are
452 * added. If a converter is defined on a key that already exists, the converter is ignored.
453 *
454 * @param mapping an existing map to add new converter mappings to
455 * @param clazz class to look for converter mappings for
456 */
457 void addConverterMapping(Map<String, Object> mapping, Class clazz) {
458 try {
459 String converterFilename = buildConverterFilename(clazz);
460 InputStream is = FileManager.loadFile(converterFilename, clazz);
461
462 if (is != null) {
463 if (LOG.isDebugEnabled()) {
464 LOG.debug("processing conversion file ["+converterFilename+"] [class="+clazz+"]");
465 }
466
467 Properties prop = new Properties();
468 prop.load(is);
469
470 Iterator it = prop.entrySet().iterator();
471
472 while (it.hasNext()) {
473 Map.Entry entry = (Map.Entry) it.next();
474 String key = (String) entry.getKey();
475
476 if (mapping.containsKey(key)) {
477 break;
478 }
479 // for keyProperty of Set
480 if (key.startsWith(DefaultObjectTypeDeterminer.KEY_PROPERTY_PREFIX)
481 || key.startsWith(DefaultObjectTypeDeterminer.CREATE_IF_NULL_PREFIX)) {
482 if (LOG.isDebugEnabled()) {
483 LOG.debug("\t"+key + ":" + entry.getValue()+"[treated as String]");
484 }
485 mapping.put(key, entry.getValue());
486 }
487 //for properties of classes
488 else if (!(key.startsWith(DefaultObjectTypeDeterminer.ELEMENT_PREFIX) ||
489 key.startsWith(DefaultObjectTypeDeterminer.KEY_PREFIX) ||
490 key.startsWith(DefaultObjectTypeDeterminer.DEPRECATED_ELEMENT_PREFIX))
491 ) {
492 TypeConverter _typeConverter = createTypeConverter((String) entry.getValue());
493 if (LOG.isDebugEnabled()) {
494 LOG.debug("\t"+key + ":" + entry.getValue()+"[treated as TypeConverter "+_typeConverter+"]");
495 }
496 mapping.put(key, _typeConverter);
497 }
498 //for keys of Maps
499 else if (key.startsWith(DefaultObjectTypeDeterminer.KEY_PREFIX)) {
500
501 Class converterClass = Thread.currentThread().getContextClassLoader().loadClass((String) entry.getValue());
502
503 //check if the converter is a type converter if it is one
504 //then just put it in the map as is. Otherwise
505 //put a value in for the type converter of the class
506 if (converterClass.isAssignableFrom(TypeConverter.class)) {
507 TypeConverter _typeConverter = createTypeConverter((String) entry.getValue());
508 if (LOG.isDebugEnabled()) {
509 LOG.debug("\t"+key + ":" + entry.getValue()+"[treated as TypeConverter "+_typeConverter+"]");
510 }
511 mapping.put(key, _typeConverter);
512 } else {
513 if (LOG.isDebugEnabled()) {
514 LOG.debug("\t"+key + ":" + entry.getValue()+"[treated as Class "+converterClass+"]");
515 }
516 mapping.put(key, converterClass);
517 }
518 }
519 //elements(values) of maps / lists
520 else {
521 Class _c = Thread.currentThread().getContextClassLoader().loadClass((String) entry.getValue());
522 if (LOG.isDebugEnabled()) {
523 LOG.debug("\t"+key + ":" + entry.getValue()+"[treated as Class "+_c+"]");
524 }
525 mapping.put(key, _c);
526 }
527 }
528 }
529 } catch (Exception ex) {
530 LOG.error("Problem loading properties for " + clazz.getName(), ex);
531 }
532
533 // Process annotations
534 Annotation[] annotations = clazz.getAnnotations();
535
536 for (Annotation annotation : annotations) {
537 if (annotation instanceof Conversion) {
538 Conversion conversion = (Conversion) annotation;
539
540 for (TypeConversion tc : conversion.conversions()) {
541
542 String key = tc.key();
543
544 if (mapping.containsKey(key)) {
545 break;
546 }
547 if (LOG.isDebugEnabled()) {
548 LOG.debug(key + ":" + key);
549 }
550
551 if (key != null) {
552 try {
553 if (tc.type() == ConversionType.APPLICATION) {
554 defaultMappings.put(key, createTypeConverter(tc.converter()));
555 } else {
556 if (tc.rule().toString().equals(ConversionRule.KEY_PROPERTY) || tc.rule().toString().equals(ConversionRule.CREATE_IF_NULL)) {
557 mapping.put(key, tc.value());
558 }
559 //for properties of classes
560 else if (!(tc.rule().toString().equals(ConversionRule.ELEMENT.toString())) ||
561 tc.rule().toString().equals(ConversionRule.KEY.toString()) ||
562 tc.rule().toString().equals(ConversionRule.COLLECTION.toString())
563 ) {
564 mapping.put(key, createTypeConverter(tc.converter()));
565
566
567
568 }
569 //for keys of Maps
570 else if (tc.rule().toString().equals(ConversionRule.KEY.toString())) {
571 Class converterClass = Thread.currentThread().getContextClassLoader().loadClass(tc.converter());
572 if (LOG.isDebugEnabled()) {
573 LOG.debug("Converter class: " + converterClass);
574 }
575 //check if the converter is a type converter if it is one
576 //then just put it in the map as is. Otherwise
577 //put a value in for the type converter of the class
578 if (converterClass.isAssignableFrom(TypeConverter.class)) {
579 mapping.put(key, createTypeConverter(tc.converter()));
580 } else {
581 mapping.put(key, converterClass);
582 if (LOG.isDebugEnabled()) {
583 LOG.debug("Object placed in mapping for key "
584 + key
585 + " is "
586 + mapping.get(key));
587 }
588
589 }
590
591 }
592 //elements(values) of maps / lists
593 else {
594 mapping.put(key, Thread.currentThread().getContextClassLoader().loadClass(tc.converter()));
595 }
596 }
597 } catch (Exception e) {
598 }
599 }
600 }
601 }
602 }
603
604 Method[] methods = clazz.getMethods();
605
606 for (Method method : methods) {
607
608 annotations = method.getAnnotations();
609
610 for (Annotation annotation : annotations) {
611 if (annotation instanceof TypeConversion) {
612 TypeConversion tc = (TypeConversion) annotation;
613
614 String key = tc.key();
615 if (mapping.containsKey(key)) {
616 break;
617 }
618 // Default to the property name
619 if ( key != null && key.length() == 0) {
620 key = AnnotationUtils.resolvePropertyName(method);
621 LOG.debug("key from method name... " + key + " - " + method.getName());
622 }
623
624
625 if (LOG.isDebugEnabled()) {
626 LOG.debug(key + ":" + key);
627 }
628
629 if (key != null) {
630 try {
631 if (tc.type() == ConversionType.APPLICATION) {
632 defaultMappings.put(key, createTypeConverter(tc.converter()));
633 } else {
634 if (tc.rule().toString().equals(ConversionRule.KEY_PROPERTY)) {
635 mapping.put(key, tc.value());
636 }
637 //for properties of classes
638 else if (!(tc.rule().toString().equals(ConversionRule.ELEMENT.toString())) ||
639 tc.rule().toString().equals(ConversionRule.KEY.toString()) ||
640 tc.rule().toString().equals(ConversionRule.COLLECTION.toString())
641 ) {
642 mapping.put(key, createTypeConverter(tc.converter()));
643 }
644 //for keys of Maps
645 else if (tc.rule().toString().equals(ConversionRule.KEY.toString())) {
646 Class converterClass = Thread.currentThread().getContextClassLoader().loadClass(tc.converter());
647 if (LOG.isDebugEnabled()) {
648 LOG.debug("Converter class: " + converterClass);
649 }
650 //check if the converter is a type converter if it is one
651 //then just put it in the map as is. Otherwise
652 //put a value in for the type converter of the class
653 if (converterClass.isAssignableFrom(TypeConverter.class)) {
654 mapping.put(key, createTypeConverter(tc.converter()));
655 } else {
656 mapping.put(key, converterClass);
657 if (LOG.isDebugEnabled()) {
658 LOG.debug("Object placed in mapping for key "
659 + key
660 + " is "
661 + mapping.get(key));
662 }
663
664 }
665
666 }
667 //elements(values) of maps / lists
668 else {
669 mapping.put(key, Thread.currentThread().getContextClassLoader().loadClass(tc.converter()));
670 }
671 }
672 } catch (Exception e) {
673 }
674 }
675 }
676 }
677 }
678 }
679
680 /**
681 * Looks for converter mappings for the specified class, traversing up its class hierarchy and interfaces and adding
682 * any additional mappings it may find. Mappings lower in the hierarchy have priority over those higher in the
683 * hierarcy.
684 *
685 * @param clazz the class to look for converter mappings for
686 * @return the converter mappings
687 */
688 private Map<String, Object> buildConverterMapping(Class clazz) throws Exception {
689 Map<String, Object> mapping = new HashMap<String, Object>();
690
691 // check for conversion mapping associated with super classes and any implemented interfaces
692 Class curClazz = clazz;
693
694 while (!curClazz.equals(Object.class)) {
695 // add current class' mappings
696 addConverterMapping(mapping, curClazz);
697
698 // check interfaces' mappings
699 Class[] interfaces = curClazz.getInterfaces();
700
701 for (int x = 0; x < interfaces.length; x++) {
702 addConverterMapping(mapping, interfaces[x]);
703 }
704
705 curClazz = curClazz.getSuperclass();
706 }
707
708 if (mapping.size() > 0) {
709 mappings.put(clazz, mapping);
710 } else {
711 noMapping.add(clazz);
712 }
713
714 return mapping;
715 }
716
717 private Map<String, Object> conditionalReload(Class clazz, Map<String, Object> oldValues) throws Exception {
718 Map<String, Object> mapping = oldValues;
719
720 if (FileManager.isReloadingConfigs()) {
721 if (FileManager.fileNeedsReloading(buildConverterFilename(clazz))) {
722 mapping = buildConverterMapping(clazz);
723 }
724 }
725
726 return mapping;
727 }
728
729 TypeConverter createTypeConverter(String className) throws Exception {
730 // type converters are used across users
731 Object obj = objectFactory.buildBean(className, null);
732 if (obj instanceof TypeConverter) {
733 return (TypeConverter) obj;
734
735 // For backwards compatibility
736 } else if (obj instanceof ognl.TypeConverter) {
737 return new XWorkTypeConverterWrapper((ognl.TypeConverter)obj);
738 } else {
739 throw new IllegalArgumentException("Type converter class "+obj.getClass()+" doesn't implement com.opensymphony.xwork2.conversion.TypeConverter");
740 }
741 }
742
743 public void loadConversionProperties(String propsName) throws IOException {
744 Iterator<URL> resources = ClassLoaderUtil.getResources(propsName, getClass(), true);
745 while (resources.hasNext()) {
746 URL url = resources.next();
747 Properties props = new Properties();
748 props.load(url.openStream());
749
750 if (LOG.isDebugEnabled()) {
751 LOG.debug("processing conversion file ["+propsName+"]");
752 }
753
754 for (Iterator iterator = props.entrySet().iterator(); iterator.hasNext();) {
755 Map.Entry entry = (Map.Entry) iterator.next();
756 String key = (String) entry.getKey();
757
758 try {
759 TypeConverter _typeConverter = createTypeConverter((String) entry.getValue());
760 if (LOG.isDebugEnabled()) {
761 LOG.debug("\t"+key + ":" + entry.getValue()+" [treated as TypeConverter "+_typeConverter+"]");
762 }
763 defaultMappings.put(key, _typeConverter);
764 } catch (Exception e) {
765 LOG.error("Conversion registration error", e);
766 }
767 }
768 }
769 }
770
771 /**
772 * Recurses through a class' interfaces and class hierarchy looking for a TypeConverter in the default mapping that
773 * can handle the specified class.
774 *
775 * @param clazz the class the TypeConverter must handle
776 * @return a TypeConverter to handle the specified class or null if none can be found
777 */
778 TypeConverter lookupSuper(Class clazz) {
779 TypeConverter result = null;
780
781 if (clazz != null) {
782 result = (TypeConverter) defaultMappings.get(clazz.getName());
783
784 if (result == null) {
785 // Looks for direct interfaces (depth = 1 )
786 Class[] interfaces = clazz.getInterfaces();
787
788 for (int i = 0; i < interfaces.length; i++) {
789 if (defaultMappings.containsKey(interfaces[i].getName())) {
790 result = (TypeConverter) defaultMappings.get(interfaces[i].getName());
791 break;
792 }
793 }
794
795 if (result == null) {
796 // Looks for the superclass
797 // If 'clazz' is the Object class, an interface, a primitive type or void then clazz.getSuperClass() returns null
798 result = lookupSuper(clazz.getSuperclass());
799 }
800 }
801 }
802
803 return result;
804 }
805
806
807 }