001: /*
002: * Copyright (c) 2002-2006 by OpenSymphony
003: * All rights reserved.
004: */
005: package com.opensymphony.xwork.util;
006:
007: import java.io.IOException;
008: import java.io.InputStream;
009: import java.lang.reflect.Member;
010: import java.util.HashMap;
011: import java.util.HashSet;
012: import java.util.Iterator;
013: import java.util.Map;
014: import java.util.Properties;
015:
016: import ognl.DefaultTypeConverter;
017: import ognl.OgnlRuntime;
018: import ognl.TypeConverter;
019:
020: import org.apache.commons.logging.Log;
021: import org.apache.commons.logging.LogFactory;
022:
023: import com.opensymphony.util.FileManager;
024: import com.opensymphony.xwork.ActionContext;
025: import com.opensymphony.xwork.ObjectFactory;
026: import com.opensymphony.xwork.XWorkMessages;
027:
028: /**
029: * XWorkConverter is a singleton used by many of the WebWork's Ognl extention points,
030: * such as InstantiatingNullHandler, XWorkListPropertyAccessor etc to do object
031: * conversion.
032: *
033: * <!-- START SNIPPET: javadoc -->
034: *
035: * Type conversion is great for situations where you need to turn a String in to a more complex object. Because the web
036: * is type-agnostic (everything is a string in HTTP), WebWork's type conversion features are very useful. For instance,
037: * if you were prompting a user to enter in coordinates in the form of a string (such as "3, 22"), you could have
038: * WebWork do the conversion both from String to Point and from Point to String.
039: *
040: * <p/> Using this "point" example, if your action (or another compound object in which you are setting properties on)
041: * has a corresponding ClassName-conversion.properties file, WebWork will use the configured type converters for
042: * conversion to and from strings. So turning "3, 22" in to new Point(3, 22) is done by merely adding the following
043: * entry to <b>ClassName-conversion.properties</b> (Note that the PointConverter should impl the ognl.TypeConverter
044: * interface):
045: *
046: * <p/><b>point = com.acme.PointConverter</b>
047: *
048: * <p/> Your type converter should be sure to check what class type it is being requested to convert. Because it is used
049: * for both to and from strings, you will need to split the conversion method in to two parts: one that turns Strings in
050: * to Points, and one that turns Points in to Strings.
051: *
052: * <p/> After this is done, you can now reference your point (using <ww:property value="post"/> in JSP or ${point}
053: * in FreeMarker) and it will be printed as "3, 22" again. As such, if you submit this back to an action, it will be
054: * converted back to a Point once again.
055: *
056: * <p/> In some situations you may wish to apply a type converter globally. This can be done by editing the file
057: * <b>xwork-conversion.properties</b> in the root of your class path (typically WEB-INF/classes) and providing a
058: * 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
059: * the type converter on the right hand side. For example, providing a type converter for all Point objects would mean
060: * adding the following entry:
061: *
062: * <p/><b>com.acme.Point = com.acme.PointConverter</b>
063: *
064: * <!-- END SNIPPET: javadoc -->
065: *
066: * <p/>
067: *
068: * <!-- START SNIPPET: i18n-note -->
069: *
070: * Type conversion should not be used as a substitute for i18n. It is not recommended to use this feature to print out
071: * properly formatted dates. Rather, you should use the i18n features of WebWork (and consult the JavaDocs for JDK's
072: * MessageFormat object) to see how a properly formatted date should be displayed.
073: *
074: * <!-- END SNIPPET: i18n-note -->
075: *
076: * <p/>
077: *
078: * <!-- START SNIPPET: error-reporting -->
079: *
080: * Any error that occurs during type conversion may or may not wish to be reported. For example, reporting that the
081: * input "abc" could not be converted to a number might be important. On the other hand, reporting that an empty string,
082: * "", cannot be converted to a number might not be important - especially in a web environment where it is hard to
083: * distinguish between a user not entering a value vs. entering a blank value.
084: *
085: * <p/> By default, all conversion errors are reported using the generic i18n key <b>xwork.default.invalid.fieldvalue</b>,
086: * which you can override (the default text is <i>Invalid field value for field "xxx"</i>, where xxx is the field name)
087: * in your global i18n resource bundle.
088: *
089: * <p/>However, sometimes you may wish to override this message on a per-field basis. You can do this by adding an i18n
090: * key associated with just your action (Action.properties) using the pattern <b>invalid.fieldvalue.xxx</b>, where xxx
091: * is the field name.
092: *
093: * <p/>It is important to know that none of these errors are actually reported directly. Rather, they are added to a map
094: * called <i>conversionErrors</i> in the ActionContext. There are several ways this map can then be accessed and the
095: * errors can be reported accordingly.
096: *
097: * <!-- END SNIPPET: error-reporting -->
098: *
099: * @author <a href="mailto:plightbo@gmail.com">Pat Lightbody</a>
100: * @author Rainer Hermanns
101: * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
102: * @see XWorkBasicConverter
103: */
104: public class XWorkConverter extends DefaultTypeConverter {
105: private static XWorkConverter instance;
106: protected static final Log LOG = LogFactory
107: .getLog(XWorkConverter.class);
108: public static final String REPORT_CONVERSION_ERRORS = "report.conversion.errors";
109: public static final String CONVERSION_PROPERTY_FULLNAME = "conversion.property.fullName";
110: public static final String CONVERSION_ERROR_PROPERTY_PREFIX = "invalid.fieldvalue.";
111: public static final String CONVERSION_COLLECTION_PREFIX = "Collection_";
112:
113: public static final String LAST_BEAN_CLASS_ACCESSED = "last.bean.accessed";
114: public static final String LAST_BEAN_PROPERTY_ACCESSED = "last.property.accessed";
115:
116: HashMap defaultMappings = new HashMap();
117: HashMap mappings = new HashMap();
118: HashSet noMapping = new HashSet();
119: HashSet unknownMappings = new HashSet();
120: TypeConverter defaultTypeConverter = new XWorkBasicConverter();
121: ObjectTypeDeterminer objectTypeDeterminer = ObjectTypeDeterminerFactory
122: .getInstance();
123:
124: protected XWorkConverter() {
125: try {
126: // note: this file is deprecated
127: loadConversionProperties("xwork-default-conversion.properties");
128: } catch (Exception e) {
129: }
130:
131: try {
132: loadConversionProperties("xwork-conversion.properties");
133: } catch (Exception e) {
134: }
135: }
136:
137: public static String getConversionErrorMessage(String propertyName,
138: OgnlValueStack stack) {
139: String defaultMessage = LocalizedTextUtil.findDefaultText(
140: XWorkMessages.DEFAULT_INVALID_FIELDVALUE, ActionContext
141: .getContext().getLocale(),
142: new Object[] { propertyName });
143: String getTextExpression = "getText('"
144: + CONVERSION_ERROR_PROPERTY_PREFIX + propertyName
145: + "','" + defaultMessage + "')";
146: String message = (String) stack.findValue(getTextExpression);
147:
148: if (message == null) {
149: message = defaultMessage;
150: }
151:
152: return message;
153: }
154:
155: public static XWorkConverter getInstance() {
156: if (instance == null) {
157: try {
158: Class clazz = Thread
159: .currentThread()
160: .getContextClassLoader()
161: .loadClass(
162: "com.opensymphony.xwork.util.AnnotationXWorkConverter");
163: instance = (XWorkConverter) clazz.newInstance();
164: LOG
165: .info("Detected AnnotationXWorkConverter, initializing it...");
166: } catch (ClassNotFoundException e) {
167: // this is fine, just fall back to the default object type determiner
168: } catch (Exception e) {
169: LOG
170: .error(
171: "Exception when trying to create new AnnotationXWorkConverter",
172: e);
173: }
174: if (instance == null) {
175: instance = new XWorkConverter();
176: }
177: }
178:
179: return instance;
180: }
181:
182: public static void setInstance(XWorkConverter instance) {
183: XWorkConverter.instance = instance;
184: }
185:
186: public static String buildConverterFilename(Class clazz) {
187: String className = clazz.getName();
188: String resource = className.replace('.', '/')
189: + "-conversion.properties";
190:
191: return resource;
192: }
193:
194: public static void resetInstance() {
195: instance = null;
196: }
197:
198: public void setDefaultConverter(TypeConverter defaultTypeConverter) {
199: this .defaultTypeConverter = defaultTypeConverter;
200: }
201:
202: public Object convertValue(Map map, Object o, Class aClass) {
203: return convertValue(map, null, null, null, o, aClass);
204: }
205:
206: /**
207: * Convert value from one form to another.
208: * Minimum requirement of arguments:
209: * <ul>
210: * <li>supplying context, toClass and value</li>
211: * <li>supplying context, target and value.</li>
212: * </ul>
213: *
214: * @see ognl.TypeConverter#convertValue(java.util.Map, java.lang.Object, java.lang.reflect.Member, java.lang.String, java.lang.Object, java.lang.Class)
215: */
216: public Object convertValue(Map context, Object target,
217: Member member, String property, Object value, Class toClass) {
218: //
219: // Process the conversion using the default mappings, if one exists
220: //
221: TypeConverter tc = null;
222:
223: if ((value != null) && (toClass == value.getClass())) {
224: return value;
225: }
226:
227: // allow this method to be called without any context
228: // i.e. it can be called with as little as "Object value" and "Class toClass"
229: if (target != null) {
230: Class clazz = target.getClass();
231:
232: Object[] classProp = null;
233:
234: // this is to handle weird issues with setValue with a different type
235: if ((target instanceof CompoundRoot) && (context != null)) {
236: classProp = getClassProperty(context);
237: }
238:
239: if (classProp != null) {
240: clazz = (Class) classProp[0];
241: property = (String) classProp[1];
242: }
243:
244: tc = (TypeConverter) getConverter(clazz, property);
245: }
246:
247: if (tc == null && context != null) {
248: // ok, let's see if we can look it up by path as requested in XW-297
249: Object lastPropertyPath = context
250: .get(OgnlContextState.CURRENT_PROPERTY_PATH);
251: Class clazz = (Class) context
252: .get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
253: if (lastPropertyPath != null && clazz != null) {
254: String path = lastPropertyPath + "." + property;
255: tc = (TypeConverter) getConverter(clazz, path);
256: }
257: }
258:
259: if (tc == null) {
260: if (toClass.equals(String.class)
261: && (value != null)
262: && !(value.getClass().equals(String.class) || value
263: .getClass().equals(String[].class))) {
264: // when converting to a string, use the source target's class's converter
265: tc = lookup(value.getClass());
266: } else {
267: // when converting from a string, use the toClass's converter
268: tc = lookup(toClass);
269: }
270: }
271:
272: if (tc != null) {
273: try {
274: return tc.convertValue(context, target, member,
275: property, value, toClass);
276: } catch (Exception e) {
277: handleConversionException(context, property, value,
278: target);
279:
280: return OgnlRuntime.NoConversionPossible;
281: }
282: }
283:
284: if (defaultTypeConverter != null) {
285: try {
286: return defaultTypeConverter.convertValue(context,
287: target, member, property, value, toClass);
288: } catch (Exception e) {
289: handleConversionException(context, property, value,
290: target);
291:
292: return OgnlRuntime.NoConversionPossible;
293: }
294: } else {
295: try {
296: return super .convertValue(context, target, member,
297: property, value, toClass);
298: } catch (Exception e) {
299: handleConversionException(context, property, value,
300: target);
301:
302: return OgnlRuntime.NoConversionPossible;
303: }
304: }
305: }
306:
307: /**
308: * Looks for a TypeConverter in the default mappings.
309: *
310: * @param className name of the class the TypeConverter must handle
311: * @return a TypeConverter to handle the specified class or null if none can be found
312: */
313: public TypeConverter lookup(String className) {
314: if (unknownMappings.contains(className)) {
315: return null;
316: }
317:
318: TypeConverter result = (TypeConverter) defaultMappings
319: .get(className);
320:
321: //Looks for super classes
322: if (result == null) {
323: Class clazz = null;
324:
325: try {
326: clazz = Thread.currentThread().getContextClassLoader()
327: .loadClass(className);
328: } catch (ClassNotFoundException cnfe) {
329: }
330:
331: result = lookupSuper(clazz);
332:
333: if (result != null) {
334: //Register now, the next lookup will be faster
335: registerConverter(className, result);
336: } else {
337: // if it isn't found, never look again (also faster)
338: registerConverterNotFound(className);
339: }
340: }
341:
342: return result;
343: }
344:
345: /**
346: * Looks for a TypeConverter in the default mappings.
347: *
348: * @param clazz the class the TypeConverter must handle
349: * @return a TypeConverter to handle the specified class or null if none can be found
350: */
351: public TypeConverter lookup(Class clazz) {
352: return lookup(clazz.getName());
353: }
354:
355: protected Object getConverter(Class clazz, String property) {
356: if (LOG.isDebugEnabled()) {
357: LOG.debug("Property: " + property);
358: LOG.debug("Class: " + clazz.getName());
359: }
360: synchronized (clazz) {
361: if ((property != null) && !noMapping.contains(clazz)) {
362: try {
363: Map mapping = (Map) mappings.get(clazz);
364:
365: if (mapping == null) {
366: if (LOG.isDebugEnabled()) {
367: LOG.debug("Map is null.");
368: }
369: mapping = buildConverterMapping(clazz);
370: } else {
371: mapping = conditionalReload(clazz, mapping);
372: }
373:
374: Object converter = mapping.get(property);
375: if (LOG.isDebugEnabled() && converter == null) {
376: LOG.debug("converter is null for property "
377: + property + ". Mapping size: "
378: + mapping.size());
379: Iterator iter = mapping.keySet().iterator();
380: while (iter.hasNext()) {
381: Object next = iter.next();
382: LOG.debug(next + ":" + mapping.get(next));
383: }
384: }
385: return converter;
386: } catch (Throwable t) {
387: noMapping.add(clazz);
388: }
389: }
390: }
391:
392: return null;
393: }
394:
395: protected void handleConversionException(Map context,
396: String property, Object value, Object object) {
397: if ((Boolean.TRUE.equals(context.get(REPORT_CONVERSION_ERRORS)))) {
398:
399: String realProperty = property;
400: String fullName = (String) context
401: .get(CONVERSION_PROPERTY_FULLNAME);
402:
403: if (fullName != null) {
404: realProperty = fullName;
405: }
406:
407: Map conversionErrors = (Map) context
408: .get(ActionContext.CONVERSION_ERRORS);
409:
410: if (conversionErrors == null) {
411: conversionErrors = new HashMap();
412: context.put(ActionContext.CONVERSION_ERRORS,
413: conversionErrors);
414: }
415:
416: conversionErrors.put(realProperty, value);
417: }
418: }
419:
420: public synchronized void registerConverter(String className,
421: TypeConverter converter) {
422: defaultMappings.put(className, converter);
423: }
424:
425: public synchronized void registerConverterNotFound(String className) {
426: unknownMappings.add(className);
427: }
428:
429: private Object[] getClassProperty(Map context) {
430: return (Object[]) context.get("__link");
431: }
432:
433: /**
434: * not used
435: */
436: private Object acceptableErrorValue(Class toClass) {
437: if (!toClass.isPrimitive()) {
438: return null;
439: }
440:
441: if (toClass == int.class) {
442: return new Integer(0);
443: } else if (toClass == double.class) {
444: return new Double(0);
445: } else if (toClass == long.class) {
446: return new Long(0);
447: } else if (toClass == boolean.class) {
448: return Boolean.FALSE;
449: } else if (toClass == short.class) {
450: return new Short((short) 0);
451: } else if (toClass == float.class) {
452: return new Float(0);
453: } else if (toClass == byte.class) {
454: return new Byte((byte) 0);
455: } else if (toClass == char.class) {
456: return new Character((char) 0);
457: }
458:
459: return null;
460: }
461:
462: /**
463: * Looks for converter mappings for the specified class and adds it to an existing map. Only new converters are
464: * added. If a converter is defined on a key that already exists, the converter is ignored.
465: *
466: * @param mapping an existing map to add new converter mappings to
467: * @param clazz class to look for converter mappings for
468: */
469: void addConverterMapping(Map mapping, Class clazz) {
470: try {
471: InputStream is = FileManager.loadFile(
472: buildConverterFilename(clazz), clazz);
473:
474: if (is != null) {
475: Properties prop = new Properties();
476: prop.load(is);
477:
478: Iterator it = prop.entrySet().iterator();
479:
480: while (it.hasNext()) {
481: Map.Entry entry = (Map.Entry) it.next();
482: String key = (String) entry.getKey();
483:
484: if (mapping.containsKey(key)) {
485: break;
486: }
487: if (LOG.isDebugEnabled()) {
488: LOG.debug(key + ":" + entry.getValue());
489: }
490:
491: if (key
492: .startsWith(DefaultObjectTypeDeterminer.KEY_PROPERTY_PREFIX)
493: || key
494: .startsWith(DefaultObjectTypeDeterminer.CREATE_IF_NULL_PREFIX)) {
495: mapping.put(key, entry.getValue());
496: }
497: //for properties of classes
498: else if (!(key
499: .startsWith(DefaultObjectTypeDeterminer.ELEMENT_PREFIX)
500: || key
501: .startsWith(DefaultObjectTypeDeterminer.KEY_PREFIX) || key
502: .startsWith(DefaultObjectTypeDeterminer.DEPRECATED_ELEMENT_PREFIX))) {
503: mapping.put(key,
504: createTypeConverter((String) entry
505: .getValue()));
506: }
507: //for keys of Maps
508: else if (key
509: .startsWith(DefaultObjectTypeDeterminer.KEY_PREFIX)) {
510:
511: Class converterClass = Thread.currentThread()
512: .getContextClassLoader().loadClass(
513: (String) entry.getValue());
514: if (LOG.isDebugEnabled()) {
515: LOG.debug("Converter class: "
516: + converterClass);
517: }
518: //check if the converter is a type converter if it is one
519: //then just put it in the map as is. Otherwise
520: //put a value in for the type converter of the class
521: if (converterClass
522: .isAssignableFrom(TypeConverter.class)) {
523:
524: mapping.put(key,
525: createTypeConverter((String) entry
526: .getValue()));
527:
528: } else {
529:
530: mapping.put(key, converterClass);
531: if (LOG.isDebugEnabled()) {
532: LOG
533: .debug("Object placed in mapping for key "
534: + key
535: + " is "
536: + mapping.get(key));
537: }
538:
539: }
540:
541: }
542: //elements(values) of maps / lists
543: else {
544: mapping.put(key, Thread.currentThread()
545: .getContextClassLoader().loadClass(
546: (String) entry.getValue()));
547: }
548: }
549: }
550: } catch (Exception ex) {
551: LOG.error("Problem loading properties for "
552: + clazz.getName(), ex);
553: }
554: }
555:
556: /**
557: * Looks for converter mappings for the specified class, traversing up its class hierarchy and interfaces and adding
558: * any additional mappings it may find. Mappings lower in the hierarchy have priority over those higher in the
559: * hierarcy.
560: *
561: * @param clazz the class to look for converter mappings for
562: * @return the converter mappings
563: */
564: private Map buildConverterMapping(Class clazz) throws Exception {
565: Map mapping = new HashMap();
566:
567: // check for conversion mapping associated with super classes and any implemented interfaces
568: Class curClazz = clazz;
569:
570: while (!curClazz.equals(Object.class)) {
571: // add current class' mappings
572: addConverterMapping(mapping, curClazz);
573:
574: // check interfaces' mappings
575: Class[] interfaces = curClazz.getInterfaces();
576:
577: for (int x = 0; x < interfaces.length; x++) {
578: addConverterMapping(mapping, interfaces[x]);
579: }
580:
581: curClazz = curClazz.getSuperclass();
582: }
583:
584: if (mapping.size() > 0) {
585: mappings.put(clazz, mapping);
586: } else {
587: noMapping.add(clazz);
588: }
589:
590: return mapping;
591: }
592:
593: private Map conditionalReload(Class clazz, Map oldValues)
594: throws Exception {
595: Map mapping = oldValues;
596:
597: if (FileManager.isReloadingConfigs()) {
598: if (FileManager
599: .fileNeedsReloading(buildConverterFilename(clazz))) {
600: mapping = buildConverterMapping(clazz);
601: }
602: }
603:
604: return mapping;
605: }
606:
607: TypeConverter createTypeConverter(String className)
608: throws Exception {
609: // type converters are used across users
610: return (TypeConverter) ObjectFactory.getObjectFactory()
611: .buildBean(className, null);
612: }
613:
614: public void loadConversionProperties(String propsName)
615: throws IOException {
616: InputStream is = Thread.currentThread().getContextClassLoader()
617: .getResourceAsStream(propsName);
618: Properties props = new Properties();
619: props.load(is);
620:
621: for (Iterator iterator = props.entrySet().iterator(); iterator
622: .hasNext();) {
623: Map.Entry entry = (Map.Entry) iterator.next();
624: String key = (String) entry.getKey();
625:
626: try {
627: defaultMappings.put(key,
628: createTypeConverter((String) entry.getValue()));
629: } catch (Exception e) {
630: LOG.error("Conversion registration error", e);
631: }
632: }
633: }
634:
635: /**
636: * Recurses through a class' interfaces and class hierarchy looking for a TypeConverter in the default mapping that
637: * can handle the specified class.
638: *
639: * @param clazz the class the TypeConverter must handle
640: * @return a TypeConverter to handle the specified class or null if none can be found
641: */
642: TypeConverter lookupSuper(Class clazz) {
643: TypeConverter result = null;
644:
645: if (clazz != null) {
646: result = (TypeConverter) defaultMappings.get(clazz
647: .getName());
648:
649: if (result == null) {
650: // Looks for direct interfaces (depth = 1 )
651: Class[] interfaces = clazz.getInterfaces();
652:
653: for (int i = 0; i < interfaces.length; i++) {
654: if (defaultMappings.containsKey(interfaces[i]
655: .getName())) {
656: result = (TypeConverter) defaultMappings
657: .get(interfaces[i].getName());
658: break;
659: }
660: }
661:
662: if (result == null) {
663: // Looks for the superclass
664: // If 'clazz' is the Object class, an interface, a primitive type or void then clazz.getSuperClass() returns null
665: result = lookupSuper(clazz.getSuperclass());
666: }
667: }
668: }
669:
670: return result;
671: }
672:
673: public ObjectTypeDeterminer getObjectTypeDeterminer() {
674: return objectTypeDeterminer;
675: }
676:
677: /**
678: * @param determiner
679: */
680: public void setObjectTypeDeterminer(ObjectTypeDeterminer determiner) {
681: objectTypeDeterminer = determiner;
682: }
683:
684: }
|