Anonymous avatar Anonymous committed 4f89b6c

Extracting type conversion classes into new XWork type conversion system, separate but
interoperable with the OGNL type conversion API
XW-561

git-svn-id: http://svn.opensymphony.com/svn/xwork/trunk@1592 e221344d-f017-0410-9bd5-d282ab1896d7

Comments (0)

Files changed (64)

src/java/com/opensymphony/xwork2/conversion/ObjectTypeDeterminer.java

+/*
+ * Copyright (c) 2002-2007 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2.conversion;
+
+/**
+ * Determines what the key and and element class of a Map or Collection should be. For Maps, the elements are the
+ * values. For Collections, the elements are the elements of the collection.
+ * <p/>
+ * See the implementations for javadoc description for the methods as they are dependent on the concrete implementation.
+ *
+ * @author Gabriel Zimmerman
+ */
+public interface ObjectTypeDeterminer {
+
+    public Class getKeyClass(Class parentClass, String property);
+
+    public Class getElementClass(Class parentClass, String property, Object key);
+
+    public String getKeyProperty(Class parentClass, String property);
+    
+    public boolean shouldCreateIfNew(Class parentClass,  String property,  Object target, String keyProperty, boolean isIndexAccessed);
+
+}

src/java/com/opensymphony/xwork2/conversion/ObjectTypeDeterminerFactory.java

+/*
+ * Copyright (c) 2002-2007 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2.conversion;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.opensymphony.xwork2.conversion.impl.DefaultObjectTypeDeterminer;
+import com.opensymphony.xwork2.conversion.impl.GenericsObjectTypeDeterminer;
+
+/**
+ * Factory for getting an instance of {@link ObjectTypeDeterminer}.
+ * <p/>
+ * Will use <code>com.opensymphony.xwork2.util.GenericsObjectTypeDeterminer</code> by default.
+ *
+ * @see com.opensymphony.xwork2.conversion.impl.GenericsObjectTypeDeterminer
+ * @see com.opensymphony.xwork2.conversion.ObjectTypeDeterminer
+ * @see com.opensymphony.xwork2.conversion.impl.DefaultObjectTypeDeterminer
+ *
+ * @author plightbo
+ * @author Rainer Hermanns
+ * @author Rene Gielen
+ */
+public class ObjectTypeDeterminerFactory {
+    private static final Log LOG = LogFactory.getLog(ObjectTypeDeterminerFactory.class);
+
+    private static ObjectTypeDeterminer instance = new DefaultObjectTypeDeterminer();
+
+    static {
+        LOG.info("Setting DefaultObjectTypeDeterminer as default ...");
+    }
+
+    /**
+     * Sets a new instance of ObjectTypeDeterminer to be used.
+     *
+     * @param instance  instance of ObjectTypeDeterminer
+     */
+    public static void setInstance(ObjectTypeDeterminer instance) {
+        if (instance != null) {
+            if (!instance.getClass().equals(ObjectTypeDeterminerFactory.instance.getClass())) {
+                LOG.info("Switching to ObjectTypeDeterminer of type " + instance.getClass().getName());
+            }
+            ObjectTypeDeterminerFactory.instance = instance;
+        }
+    }
+
+    /**
+     * Gets the instance of ObjectTypeDeterminer to be used.
+     *
+     * @return instance of ObjectTypeDeterminer
+     */
+    public static ObjectTypeDeterminer getInstance() {
+        return instance;
+    }
+
+}

src/java/com/opensymphony/xwork2/conversion/OgnlTypeConverterWrapper.java

+package com.opensymphony.xwork2.conversion;
+
+import java.lang.reflect.Member;
+import java.util.Map;
+
+public class OgnlTypeConverterWrapper implements ognl.TypeConverter {
+
+    private TypeConverter typeConverter;
+    
+    public OgnlTypeConverterWrapper(TypeConverter conv) {
+        this.typeConverter = conv;
+    }
+    
+    public Object convertValue(Map context, Object target, Member member,
+            String propertyName, Object value, Class toType) {
+        return typeConverter.convertValue(context, target, member, propertyName, value, toType);
+    }
+    
+    public TypeConverter getTarget() {
+        return typeConverter;
+    }
+}

src/java/com/opensymphony/xwork2/conversion/TypeConversionException.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2.conversion;
+
+import com.opensymphony.xwork2.XWorkException;
+
+
+/**
+ * TypeConversionException should be thrown by any TypeConverters which fail to convert values
+ *
+ * @author Jason Carreira
+ *         Created Oct 3, 2003 12:18:33 AM
+ */
+public class TypeConversionException extends XWorkException {
+
+    /**
+     * Constructs a <code>XWorkException</code> with no detail  message.
+     */
+    public TypeConversionException() {
+    }
+
+    /**
+     * Constructs a <code>XWorkException</code> with the specified
+     * detail message.
+     *
+     * @param s the detail message.
+     */
+    public TypeConversionException(String s) {
+        super(s);
+    }
+
+    /**
+     * Constructs a <code>XWorkException</code> with no detail  message.
+     */
+    public TypeConversionException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructs a <code>XWorkException</code> with the specified
+     * detail message.
+     *
+     * @param s the detail message.
+     */
+    public TypeConversionException(String s, Throwable cause) {
+        super(s, cause);
+    }
+}

src/java/com/opensymphony/xwork2/conversion/TypeConverter.java

+//--------------------------------------------------------------------------
+//  Copyright (c) 1998-2004, Drew Davidson and Luke Blanshard
+//  All rights reserved.
+//
+//  Redistribution and use in source and binary forms, with or without
+//  modification, are permitted provided that the following conditions are
+//  met:
+//
+//  Redistributions of source code must retain the above copyright notice,
+//  this list of conditions and the following disclaimer.
+//  Redistributions in binary form must reproduce the above copyright
+//  notice, this list of conditions and the following disclaimer in the
+//  documentation and/or other materials provided with the distribution.
+//  Neither the name of the Drew Davidson nor the names of its contributors
+//  may be used to endorse or promote products derived from this software
+//  without specific prior written permission.
+//
+//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+//  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+//  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+//  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+//  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+//  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+//  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+//  OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+//  AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+//  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+//  DAMAGE.
+//--------------------------------------------------------------------------
+package com.opensymphony.xwork2.conversion;
+
+import java.lang.reflect.Member;
+import java.util.Map;
+
+/**
+ * Interface for accessing the type conversion facilities within a context.
+ * 
+ * This interface was copied from OGNL's ognl.TypeConverter
+ * 
+ * @author Luke Blanshard (blanshlu@netscape.net)
+ * @author Drew Davidson (drew@ognl.org)
+ */
+public interface TypeConverter
+{
+    /**
+       * Converts the given value to a given type.  The OGNL context, target, member and
+       * name of property being set are given.  This method should be able to handle
+       * conversion in general without any context, target, member or property name specified.
+       * @param context context under which the conversion is being done
+       * @param target target object in which the property is being set
+       * @param member member (Constructor, Method or Field) being set
+       * @param propertyName property name being set
+       * @param value value to be converted
+       * @param toType type to which value is converted
+       * @return Converted value of type toType or TypeConverter.NoConversionPossible to indicate that the
+                 conversion was not possible.
+     */
+    public Object convertValue(Map context, Object target, Member member, String propertyName, Object value, Class toType);
+    
+    public static final Object NO_CONVERSION_POSSIBLE = "ognl.NoConversionPossible";
+    
+    public static final String TYPE_CONVERTER_CONTEXT_KEY = "_typeConverter";
+}

src/java/com/opensymphony/xwork2/conversion/XWorkTypeConverterWrapper.java

+package com.opensymphony.xwork2.conversion;
+
+import java.lang.reflect.Member;
+import java.util.Map;
+
+public class XWorkTypeConverterWrapper implements TypeConverter {
+
+    private ognl.TypeConverter typeConverter;
+    
+    public XWorkTypeConverterWrapper(ognl.TypeConverter conv) {
+        this.typeConverter = conv;
+    }
+    
+    public Object convertValue(Map context, Object target, Member member,
+            String propertyName, Object value, Class toType) {
+        return typeConverter.convertValue(context, target, member, propertyName, value, toType);
+    }
+}

src/java/com/opensymphony/xwork2/conversion/annotations/TypeConversion.java

      * The ConversionRule can be a PROPERTY, KEY, KEY_PROPERTY, ELEMENT, COLLECTION (deprecated) or a MAP.
      * Note: Collection and Map vonversion rules can be determined via com.opensymphony.xwork2.util.DefaultObjectTypeDeterminer.
      *
-     * @see com.opensymphony.xwork2.util.DefaultObjectTypeDeterminer
+     * @see com.opensymphony.xwork2.conversion.impl.DefaultObjectTypeDeterminer
      */
     ConversionRule rule() default ConversionRule.PROPERTY;
 

src/java/com/opensymphony/xwork2/conversion/impl/DefaultObjectTypeDeterminer.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2.conversion.impl;
+
+import java.util.Map;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.lang.reflect.ParameterizedType;
+import java.beans.IntrospectionException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.opensymphony.xwork2.conversion.ObjectTypeDeterminer;
+import com.opensymphony.xwork2.util.CreateIfNull;
+import com.opensymphony.xwork2.util.Element;
+import com.opensymphony.xwork2.util.Key;
+import com.opensymphony.xwork2.util.KeyProperty;
+import com.opensymphony.xwork2.util.XWorkCollectionPropertyAccessor;
+import com.opensymphony.xwork2.util.XWorkListPropertyAccessor;
+import com.opensymphony.xwork2.util.XWorkMapPropertyAccessor;
+
+import ognl.OgnlRuntime;
+import ognl.OgnlException;
+
+/**
+ * <!-- START SNIPPET: javadoc -->
+ *
+ * This {@link ObjectTypeDeterminer} looks at the <b>Class-conversion.properties</b> for entries that indicated what
+ * objects are contained within Maps and Collections. For Collections, such as Lists, the element is specified using the
+ * pattern <b>Element_xxx</b>, where xxx is the field name of the collection property in your action or object. For
+ * Maps, both the key and the value may be specified by using the pattern <b>Key_xxx</b> and <b>Element_xxx</b>,
+ * respectively.
+ *
+ * <p/> From WebWork 2.1.x, the <b>Collection_xxx</b> format is still supported and honored, although it is deprecated
+ * and will be removed eventually.
+ *
+ * <!-- END SNIPPET: javadoc -->
+ * 
+ *
+ * @author Gabriel Zimmerman
+ * @see XWorkListPropertyAccessor
+ * @see XWorkCollectionPropertyAccessor
+ * @see XWorkMapPropertyAccessor
+ */
+public class DefaultObjectTypeDeterminer implements ObjectTypeDeterminer {
+    
+
+    protected static final Log LOG = LogFactory.getLog(DefaultObjectTypeDeterminer.class);
+
+    public static final String KEY_PREFIX = "Key_";
+    public static final String ELEMENT_PREFIX = "Element_";
+    public static final String KEY_PROPERTY_PREFIX = "KeyProperty_";
+    public static final String CREATE_IF_NULL_PREFIX = "CreateIfNull_";
+    public static final String DEPRECATED_ELEMENT_PREFIX = "Collection_";
+
+    /**
+     * Determines the key class by looking for the value of @Key annotation for the given class.
+     * If no annotation is found, the key class is determined by using the generic parametrics.
+     *
+     * As fallback, it determines the key class by looking for the value of Key_${property} in the properties
+     * file for the given class.
+     *
+     * @param parentClass the Class which contains as a property the Map or Collection we are finding the key for.
+     * @param property    the property of the Map or Collection for the given parent class
+     * @see com.opensymphony.xwork2.conversion.ObjectTypeDeterminer#getKeyClass(Class, String)
+     */
+    public Class getKeyClass(Class parentClass, String property) {
+        Key annotation = getAnnotation(parentClass, property, Key.class);
+
+        if (annotation != null) {
+            return annotation.value();
+        }
+
+        Class clazz = getClass(parentClass, property, false);
+
+        if (clazz != null) {
+            return clazz;
+        }
+
+        return (Class) XWorkConverter.getInstance().getConverter(parentClass, KEY_PREFIX + property);
+    }
+
+
+    /**
+     * Determines the element class by looking for the value of @Element annotation for the given
+     * class.
+     * If no annotation is found, the element class is determined by using the generic parametrics.
+     *
+     * As fallback, it determines the key class by looking for the value of Element_${property} in the properties
+     * file for the given class. Also looks for the deprecated Collection_${property}
+     *
+     * @param parentClass the Class which contains as a property the Map or Collection we are finding the key for.
+     * @param property    the property of the Map or Collection for the given parent class
+     * @see com.opensymphony.xwork2.conversion.ObjectTypeDeterminer#getElementClass(Class, String, Object)
+     */
+    public Class getElementClass(Class parentClass, String property, Object key) {
+        Element annotation = getAnnotation(parentClass, property, Element.class);
+
+        if (annotation != null) {
+            return annotation.value();
+        }
+
+        Class clazz = getClass(parentClass, property, true);
+
+        if (clazz != null) {
+            return clazz;
+        }
+
+        clazz = (Class) XWorkConverter.getInstance().getConverter(parentClass, ELEMENT_PREFIX + property);
+
+        if (clazz == null) {
+            clazz = (Class) XWorkConverter.getInstance()
+                    .getConverter(parentClass, DEPRECATED_ELEMENT_PREFIX + property);
+
+            if (clazz != null) {
+                LOG.info("The Collection_xxx pattern for collection type conversion is deprecated. Please use Element_xxx!");
+            }
+        }
+        return clazz;
+
+    }
+
+
+    /**
+     * Determines the key property for a Collection by getting it from the @KeyProperty annotation.
+     *
+     * As fallback, it determines the String key property for a Collection by getting it from the conversion properties
+     * file using the KeyProperty_ prefix. KeyProperty_${property}=somePropertyOfBeansInTheSet
+     *
+     * @param parentClass the Class which contains as a property the Map or Collection we are finding the key for.
+     * @param property    the property of the Map or Collection for the given parent class
+     * @see com.opensymphony.xwork2.conversion.ObjectTypeDeterminer#getKeyProperty(Class, String)
+     */
+    public String getKeyProperty(Class parentClass, String property) {
+        KeyProperty annotation = getAnnotation(parentClass, property, KeyProperty.class);
+
+        if (annotation != null) {
+            return annotation.value();
+        }
+
+        return (String) XWorkConverter.getInstance().getConverter(parentClass, KEY_PROPERTY_PREFIX + property);
+    }
+
+
+    /**
+     * Determines the createIfNull property for a Collection or Map by getting it from the @CreateIfNull annotation.
+     *
+     * As fallback, it determines the boolean CreateIfNull property for a Collection or Map by getting it from the
+     * conversion properties file using the CreateIfNull_ prefix. CreateIfNull_${property}=true|false
+     *
+     * @param parentClass     the Class which contains as a property the Map or Collection we are finding the key for.
+     * @param property        the property of the Map or Collection for the given parent class
+     * @param target          the target object
+     * @param keyProperty     the keyProperty value
+     * @param isIndexAccessed <tt>true</tt>, if the collection or map is accessed via index, <tt>false</tt> otherwise.
+     * @return <tt>true</tt>, if the Collection or Map should be created, <tt>false</tt> otherwise.
+     * @see ObjectTypeDeterminer#getKeyProperty(Class, String)
+     */
+    public boolean shouldCreateIfNew(Class parentClass,
+                                     String property,
+                                     Object target,
+                                     String keyProperty,
+                                     boolean isIndexAccessed) {
+
+        CreateIfNull annotation = getAnnotation(parentClass, property, CreateIfNull.class);
+
+        if (annotation != null) {
+            return annotation.value();
+        }
+
+        String configValue = (String) XWorkConverter.getInstance().getConverter(parentClass, CREATE_IF_NULL_PREFIX + property);
+        //check if a value is in the config
+        if (configValue!=null) {
+            if (configValue.equals("true")) {
+                return true;
+            }
+            if (configValue.equals("false")) {
+                return false;
+            }
+        }
+
+        //default values depend on target type
+        //and whether this is accessed by an index
+        //in the case of List
+        if ((target instanceof Map) || isIndexAccessed) {
+            return true;
+        }	else {
+            return false;
+        }
+
+    }
+
+    /**
+     * Retrieves an annotation for the specified property of field, setter or getter.
+     *
+     * @param <T>             the annotation type to be retrieved
+     * @param parentClass     the class
+     * @param property        the property
+     * @param annotationClass the annotation
+     * @return the field or setter/getter annotation or <code>null</code> if not found
+     */
+    protected <T extends Annotation> T getAnnotation(Class parentClass, String property, Class<T> annotationClass) {
+        T annotation = null;
+        Field field = OgnlRuntime.getField(parentClass, property);
+
+        if (field != null) {
+            annotation = field.getAnnotation(annotationClass);
+        }
+        if (annotation == null) { // HINT: try with setter
+            annotation = getAnnotationFromSetter(parentClass, property, annotationClass);
+        }
+        if (annotation == null) { // HINT: try with getter
+            annotation = getAnnotationFromGetter(parentClass, property, annotationClass);
+        }
+
+        return annotation;
+    }
+
+    /**
+     * Retrieves an annotation for the specified field of getter.
+     *
+     * @param parentClass     the Class which contains as a property the Map or Collection we are finding the key for.
+     * @param property        the property of the Map or Collection for the given parent class
+     * @param annotationClass The annotation
+     * @return concrete Annotation instance or <tt>null</tt> if none could be retrieved.
+     */
+    private <T extends Annotation>T getAnnotationFromGetter(Class parentClass, String property, Class<T> annotationClass) {
+        try {
+            Method getter = OgnlRuntime.getGetMethod(null, parentClass, property);
+
+            if (getter != null) {
+                return getter.getAnnotation(annotationClass);
+            }
+        }
+        catch (OgnlException ognle) {
+            ; // ignore
+        }
+        catch (IntrospectionException ie) {
+            ; // ignore
+        }
+        return null;
+    }
+
+    /**
+     * Retrieves an annotation for the specified field of setter.
+     *
+     * @param parentClass     the Class which contains as a property the Map or Collection we are finding the key for.
+     * @param property        the property of the Map or Collection for the given parent class
+     * @param annotationClass The annotation
+     * @return concrete Annotation instance or <tt>null</tt> if none could be retrieved.
+     */
+    private <T extends Annotation>T getAnnotationFromSetter(Class parentClass, String property, Class<T> annotationClass) {
+        try {
+            Method setter = OgnlRuntime.getSetMethod(null, parentClass, property);
+
+            if (setter != null) {
+                return setter.getAnnotation(annotationClass);
+            }
+        }
+        catch (OgnlException ognle) {
+            ; // ignore
+        }
+        catch (IntrospectionException ie) {
+            ; // ignore
+        }
+        return null;
+    }
+
+    /**
+     * Returns the class for the given field via generic type check.
+     *
+     * @param parentClass the Class which contains as a property the Map or Collection we are finding the key for.
+     * @param property    the property of the Map or Collection for the given parent class
+     * @param element     <tt>true</tt> for indexed types and Maps.
+     * @return Class of the specified field.
+     */
+    private Class getClass(Class parentClass, String property, boolean element) {
+
+
+        try {
+
+            Field field = OgnlRuntime.getField(parentClass, property);
+
+            Type genericType = null;
+
+            // Check fields first
+            if (field != null) {
+                genericType = field.getGenericType();
+            }
+
+            // Try to get ParameterType from setter method
+            if (genericType == null || !(genericType instanceof ParameterizedType)) {
+                try {
+                    Method setter = OgnlRuntime.getSetMethod(null, parentClass, property);
+                    genericType = setter.getGenericParameterTypes()[0];
+                }
+                catch (OgnlException ognle) {
+                    ; // ignore
+                }
+                catch (IntrospectionException ie) {
+                    ; // ignore
+                }
+            }
+
+            // Try to get ReturnType from getter method
+            if (genericType == null || !(genericType instanceof ParameterizedType)) {
+                try {
+                    Method getter = OgnlRuntime.getGetMethod(null, parentClass, property);
+                    genericType = getter.getGenericReturnType();
+                }
+                catch (OgnlException ognle) {
+                    ; // ignore
+                }
+                catch (IntrospectionException ie) {
+                    ; // ignore
+                }
+            }
+
+            if (genericType instanceof ParameterizedType) {
+
+
+                ParameterizedType type = (ParameterizedType) genericType;
+
+                int index = (element && type.getRawType().toString().contains(Map.class.getName())) ? 1 : 0;
+
+                Type resultType = type.getActualTypeArguments()[index];
+
+                if ( resultType instanceof ParameterizedType) {
+                    return resultType.getClass();
+                }
+                return (Class) resultType;
+
+            }
+        } catch (Exception e) {
+            if ( LOG.isDebugEnabled()) {
+                LOG.debug("Error while retrieving generic property class for property=" + property, e);
+            }
+        }
+        return null;
+    }
+}

src/java/com/opensymphony/xwork2/conversion/impl/DefaultTypeConverter.java

+//--------------------------------------------------------------------------
+//  Copyright (c) 1998-2004, Drew Davidson and Luke Blanshard
+//  All rights reserved.
+//
+//  Redistribution and use in source and binary forms, with or without
+//  modification, are permitted provided that the following conditions are
+//  met:
+//
+//  Redistributions of source code must retain the above copyright notice,
+//  this list of conditions and the following disclaimer.
+//  Redistributions in binary form must reproduce the above copyright
+//  notice, this list of conditions and the following disclaimer in the
+//  documentation and/or other materials provided with the distribution.
+//  Neither the name of the Drew Davidson nor the names of its contributors
+//  may be used to endorse or promote products derived from this software
+//  without specific prior written permission.
+//
+//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+//  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+//  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+//  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+//  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+//  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+//  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+//  OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+//  AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+//  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+//  DAMAGE.
+//--------------------------------------------------------------------------
+package com.opensymphony.xwork2.conversion.impl;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Member;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.opensymphony.xwork2.conversion.TypeConverter;
+import com.opensymphony.xwork2.conversion.XWorkTypeConverterWrapper;
+
+/**
+ * Default type conversion. Converts among numeric types and also strings.  Contains the basic 
+ * type mapping code from OGNL.
+ * 
+ * @author Luke Blanshard (blanshlu@netscape.net)
+ * @author Drew Davidson (drew@ognl.org)
+ */
+public class DefaultTypeConverter implements TypeConverter {
+    private static final String NULL_STRING = "null";
+
+    private final Map<Class, Object> primitiveDefaults;
+
+    public DefaultTypeConverter() {
+        Map<Class, Object> map = new HashMap<Class, Object>();
+        map.put(Boolean.TYPE, Boolean.FALSE);
+        map.put(Byte.TYPE, new Byte((byte) 0));
+        map.put(Short.TYPE, new Short((short) 0));
+        map.put(Character.TYPE, new Character((char) 0));
+        map.put(Integer.TYPE, new Integer(0));
+        map.put(Long.TYPE, new Long(0L));
+        map.put(Float.TYPE, new Float(0.0f));
+        map.put(Double.TYPE, new Double(0.0));
+        map.put(BigInteger.class, new BigInteger("0"));
+        map.put(BigDecimal.class, new BigDecimal(0.0));
+        primitiveDefaults = Collections.unmodifiableMap(map);
+    }
+
+    public Object convertValue(Map context, Object value, Class toType) {
+        return convertValue(value, toType);
+    }
+
+    public Object convertValue(Map context, Object target, Member member,
+            String propertyName, Object value, Class toType) {
+        return convertValue(context, value, toType);
+    }
+    
+    public TypeConverter getTypeConverter( Map context )
+    {
+        Object obj = context.get(TypeConverter.TYPE_CONVERTER_CONTEXT_KEY);
+        if (obj instanceof TypeConverter) {
+            return (TypeConverter) obj;
+        } else if (obj instanceof ognl.TypeConverter) {
+            return new XWorkTypeConverterWrapper((ognl.TypeConverter) obj);
+        }
+        return null; 
+    }
+
+    /**
+     * Returns the value converted numerically to the given class type
+     * 
+     * This method also detects when arrays are being converted and converts the
+     * components of one array to the type of the other.
+     * 
+     * @param value
+     *            an object to be converted to the given type
+     * @param toType
+     *            class type to be converted to
+     * @return converted value of the type given, or value if the value cannot
+     *         be converted to the given type.
+     */
+    public Object convertValue(Object value, Class toType) {
+        Object result = null;
+
+        if (value != null) {
+            /* If array -> array then convert components of array individually */
+            if (value.getClass().isArray() && toType.isArray()) {
+                Class componentType = toType.getComponentType();
+
+                result = Array.newInstance(componentType, Array
+                        .getLength(value));
+                for (int i = 0, icount = Array.getLength(value); i < icount; i++) {
+                    Array.set(result, i, convertValue(Array.get(value, i),
+                            componentType));
+                }
+            } else {
+                if ((toType == Integer.class) || (toType == Integer.TYPE))
+                    result = new Integer((int) longValue(value));
+                if ((toType == Double.class) || (toType == Double.TYPE))
+                    result = new Double(doubleValue(value));
+                if ((toType == Boolean.class) || (toType == Boolean.TYPE))
+                    result = booleanValue(value) ? Boolean.TRUE : Boolean.FALSE;
+                if ((toType == Byte.class) || (toType == Byte.TYPE))
+                    result = new Byte((byte) longValue(value));
+                if ((toType == Character.class) || (toType == Character.TYPE))
+                    result = new Character((char) longValue(value));
+                if ((toType == Short.class) || (toType == Short.TYPE))
+                    result = new Short((short) longValue(value));
+                if ((toType == Long.class) || (toType == Long.TYPE))
+                    result = new Long(longValue(value));
+                if ((toType == Float.class) || (toType == Float.TYPE))
+                    result = new Float(doubleValue(value));
+                if (toType == BigInteger.class)
+                    result = bigIntValue(value);
+                if (toType == BigDecimal.class)
+                    result = bigDecValue(value);
+                if (toType == String.class)
+                    result = stringValue(value);
+            }
+        } else {
+            if (toType.isPrimitive()) {
+                result = primitiveDefaults.get(toType);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Evaluates the given object as a boolean: if it is a Boolean object, it's
+     * easy; if it's a Number or a Character, returns true for non-zero objects;
+     * and otherwise returns true for non-null objects.
+     * 
+     * @param value
+     *            an object to interpret as a boolean
+     * @return the boolean value implied by the given object
+     */
+    public static boolean booleanValue(Object value) {
+        if (value == null)
+            return false;
+        Class c = value.getClass();
+        if (c == Boolean.class)
+            return ((Boolean) value).booleanValue();
+        // if ( c == String.class )
+        // return ((String)value).length() > 0;
+        if (c == Character.class)
+            return ((Character) value).charValue() != 0;
+        if (value instanceof Number)
+            return ((Number) value).doubleValue() != 0;
+        return true; // non-null
+    }
+
+    /**
+     * Evaluates the given object as a long integer.
+     * 
+     * @param value
+     *            an object to interpret as a long integer
+     * @return the long integer value implied by the given object
+     * @throws NumberFormatException
+     *             if the given object can't be understood as a long integer
+     */
+    public static long longValue(Object value) throws NumberFormatException {
+        if (value == null)
+            return 0L;
+        Class c = value.getClass();
+        if (c.getSuperclass() == Number.class)
+            return ((Number) value).longValue();
+        if (c == Boolean.class)
+            return ((Boolean) value).booleanValue() ? 1 : 0;
+        if (c == Character.class)
+            return ((Character) value).charValue();
+        return Long.parseLong(stringValue(value, true));
+    }
+
+    /**
+     * Evaluates the given object as a double-precision floating-point number.
+     * 
+     * @param value
+     *            an object to interpret as a double
+     * @return the double value implied by the given object
+     * @throws NumberFormatException
+     *             if the given object can't be understood as a double
+     */
+    public static double doubleValue(Object value) throws NumberFormatException {
+        if (value == null)
+            return 0.0;
+        Class c = value.getClass();
+        if (c.getSuperclass() == Number.class)
+            return ((Number) value).doubleValue();
+        if (c == Boolean.class)
+            return ((Boolean) value).booleanValue() ? 1 : 0;
+        if (c == Character.class)
+            return ((Character) value).charValue();
+        String s = stringValue(value, true);
+
+        return (s.length() == 0) ? 0.0 : Double.parseDouble(s);
+        /*
+         * For 1.1 parseDouble() is not available
+         */
+        // return Double.valueOf( value.toString() ).doubleValue();
+    }
+
+    /**
+     * Evaluates the given object as a BigInteger.
+     * 
+     * @param value
+     *            an object to interpret as a BigInteger
+     * @return the BigInteger value implied by the given object
+     * @throws NumberFormatException
+     *             if the given object can't be understood as a BigInteger
+     */
+    public static BigInteger bigIntValue(Object value)
+            throws NumberFormatException {
+        if (value == null)
+            return BigInteger.valueOf(0L);
+        Class c = value.getClass();
+        if (c == BigInteger.class)
+            return (BigInteger) value;
+        if (c == BigDecimal.class)
+            return ((BigDecimal) value).toBigInteger();
+        if (c.getSuperclass() == Number.class)
+            return BigInteger.valueOf(((Number) value).longValue());
+        if (c == Boolean.class)
+            return BigInteger.valueOf(((Boolean) value).booleanValue() ? 1 : 0);
+        if (c == Character.class)
+            return BigInteger.valueOf(((Character) value).charValue());
+        return new BigInteger(stringValue(value, true));
+    }
+
+    /**
+     * Evaluates the given object as a BigDecimal.
+     * 
+     * @param value
+     *            an object to interpret as a BigDecimal
+     * @return the BigDecimal value implied by the given object
+     * @throws NumberFormatException
+     *             if the given object can't be understood as a BigDecimal
+     */
+    public static BigDecimal bigDecValue(Object value)
+            throws NumberFormatException {
+        if (value == null)
+            return BigDecimal.valueOf(0L);
+        Class c = value.getClass();
+        if (c == BigDecimal.class)
+            return (BigDecimal) value;
+        if (c == BigInteger.class)
+            return new BigDecimal((BigInteger) value);
+        if (c.getSuperclass() == Number.class)
+            return new BigDecimal(((Number) value).doubleValue());
+        if (c == Boolean.class)
+            return BigDecimal.valueOf(((Boolean) value).booleanValue() ? 1 : 0);
+        if (c == Character.class)
+            return BigDecimal.valueOf(((Character) value).charValue());
+        return new BigDecimal(stringValue(value, true));
+    }
+
+    /**
+     * Evaluates the given object as a String and trims it if the trim flag is
+     * true.
+     * 
+     * @param value
+     *            an object to interpret as a String
+     * @return the String value implied by the given object as returned by the
+     *         toString() method, or "null" if the object is null.
+     */
+    public static String stringValue(Object value, boolean trim) {
+        String result;
+
+        if (value == null) {
+            result = NULL_STRING;
+        } else {
+            result = value.toString();
+            if (trim) {
+                result = result.trim();
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Evaluates the given object as a String.
+     * 
+     * @param value
+     *            an object to interpret as a String
+     * @return the String value implied by the given object as returned by the
+     *         toString() method, or "null" if the object is null.
+     */
+    public static String stringValue(Object value) {
+        return stringValue(value, false);
+    }
+}

src/java/com/opensymphony/xwork2/conversion/impl/GenericsObjectTypeDeterminer.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+
+package com.opensymphony.xwork2.conversion.impl;
+
+
+/**
+ * GenericsObjectTypeDeterminer
+ *
+ * @author Patrick Lightbody
+ * @author Rainer Hermanns
+ * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
+ * 
+ * @deprecated Use DefaultObjectTypeDeterminer instead. Since XWork 2.0.4 the DefaultObjectTypeDeterminer handles the
+ *             annotation processing.
+ */
+public class GenericsObjectTypeDeterminer extends DefaultObjectTypeDeterminer {
+}

src/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java

+/*
+ * Copyright (c) 2002-2007 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2.conversion.impl;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Member;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.text.DateFormat;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.XWorkException;
+import com.opensymphony.xwork2.conversion.TypeConverter;
+import com.opensymphony.xwork2.util.TextUtils;
+import com.opensymphony.xwork2.util.XWorkList;
+
+
+/**
+ * <!-- START SNIPPET: javadoc -->
+ * <p/>
+ * XWork will automatically handle the most common type conversion for you. This includes support for converting to
+ * and from Strings for each of the following:
+ * <p/>
+ * <ul>
+ * <li>String</li>
+ * <li>boolean / Boolean</li>
+ * <li>char / Character</li>
+ * <li>int / Integer, float / Float, long / Long, double / Double</li>
+ * <li>dates - uses the SHORT format for the Locale associated with the current request</li>
+ * <li>arrays - assuming the individual strings can be coverted to the individual items</li>
+ * <li>collections - if not object type can be determined, it is assumed to be a String and a new ArrayList is
+ * created</li>
+ * </ul>
+ * <p/> Note that with arrays the type conversion will defer to the type of the array elements and try to convert each
+ * item individually. As with any other type conversion, if the conversion can't be performed the standard type
+ * conversion error reporting is used to indicate a problem occured while processing the type conversion.
+ * <p/>
+ * <!-- END SNIPPET: javadoc -->
+ *
+ * @author <a href="mailto:plightbo@gmail.com">Pat Lightbody</a>
+ * @author Mike Mosiewicz
+ * @author Rainer Hermanns
+ * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
+ */
+public class XWorkBasicConverter extends DefaultTypeConverter {
+
+    private static String MILLISECOND_FORMAT = ".SSS";
+
+    public Object convertValue(Map context, Object o, Member member, String s, Object value, Class toType) {
+        Object result = null;
+
+        if (value == null || toType.isAssignableFrom(value.getClass())) {
+            // no need to convert at all, right?
+            return value;
+        }
+
+        if (toType == String.class) {
+            /* the code below has been disabled as it causes sideffects in Strtus2 (XW-512)
+            // if input (value) is a number then use special conversion method (XW-490)
+            Class inputType = value.getClass();
+            if (Number.class.isAssignableFrom(inputType)) {
+                result = doConvertFromNumberToString(context, value, inputType);
+                if (result != null) {
+                    return result;
+                }
+            }*/
+            // okay use default string conversion
+            result = doConvertToString(context, value);
+        } else if (toType == boolean.class) {
+            result = doConvertToBoolean(value);
+        } else if (toType == Boolean.class) {
+            result = doConvertToBoolean(value);
+        } else if (toType.isArray()) {
+            result = doConvertToArray(context, o, member, s, value, toType);
+        } else if (Date.class.isAssignableFrom(toType)) {
+            result = doConvertToDate(context, value, toType);
+        } else if (Calendar.class.isAssignableFrom(toType)) {
+            Date dateResult = (Date) doConvertToDate(context, value, Date.class);
+            if (dateResult != null) {
+                Calendar calendar = Calendar.getInstance();
+                calendar.setTime(dateResult);
+                result = calendar;
+            } 
+        } else if (Collection.class.isAssignableFrom(toType)) {
+            result = doConvertToCollection(context, o, member, s, value, toType);
+        } else if (toType == Character.class) {
+            result = doConvertToCharacter(value);
+        } else if (toType == char.class) {
+            result = doConvertToCharacter(value);
+        } else if (Number.class.isAssignableFrom(toType)) {
+            result = doConvertToNumber(context, value, toType);
+        } else if (toType == Class.class) {
+            result = doConvertToClass(value);
+        }
+
+        if (result == null) {
+            if (value instanceof Object[]) {
+                Object[] array = (Object[]) value;
+
+                if (array.length >= 1) {
+                    value = array[0];
+                }
+
+                // let's try to convert the first element only
+                result = convertValue(context, o, member, s, value, toType);
+            } else if (!"".equals(value)) { // we've already tried the types we know
+                result = super.convertValue(context, value, toType);
+            }
+
+            if (result == null && value != null && !"".equals(value)) {
+                throw new XWorkException("Cannot create type " + toType + " from value " + value);
+            }
+        }
+
+        return result;
+    }
+
+    private Locale getLocale(Map context) {
+        if (context == null) {
+            return Locale.getDefault();
+        }
+
+        Locale locale = (Locale) context.get(ActionContext.LOCALE);
+
+        if (locale == null) {
+            locale = Locale.getDefault();
+        }
+
+        return locale;
+    }
+
+    /**
+     * Creates a Collection of the specified type.
+     *
+     * @param fromObject
+     * @param propertyName
+     * @param toType       the type of Collection to create
+     * @param memberType   the type of object elements in this collection must be
+     * @param size         the initial size of the collection (ignored if 0 or less)
+     * @return a Collection of the specified type
+     */
+    private Collection createCollection(Object fromObject, String propertyName, Class toType, Class memberType, int size) {
+//        try {
+//            Object original = Ognl.getValue(OgnlUtil.compile(propertyName),fromObject);
+//            if (original instanceof Collection) {
+//                Collection coll = (Collection) original;
+//                coll.clear();
+//                return coll;
+//            }
+//        } catch (Exception e) {
+//            // fail back to creating a new one
+//        }
+
+        Collection result;
+
+        if (toType == Set.class) {
+            if (size > 0) {
+                result = new HashSet(size);
+            } else {
+                result = new HashSet();
+            }
+        } else if (toType == SortedSet.class) {
+            result = new TreeSet();
+        } else {
+            if (size > 0) {
+                result = new XWorkList(memberType, size);
+            } else {
+                result = new XWorkList(memberType);
+            }
+        }
+
+        return result;
+    }
+
+    private Object doConvertToArray(Map context, Object o, Member member, String s, Object value, Class toType) {
+        Object result = null;
+        Class componentType = toType.getComponentType();
+
+        if (componentType != null) {
+            TypeConverter converter = getTypeConverter(context);
+
+            if (value.getClass().isArray()) {
+                int length = Array.getLength(value);
+                result = Array.newInstance(componentType, length);
+
+                for (int i = 0; i < length; i++) {
+                    Object valueItem = Array.get(value, i);
+                    Array.set(result, i, converter.convertValue(context, o, member, s, valueItem, componentType));
+                }
+            } else {
+                result = Array.newInstance(componentType, 1);
+                Array.set(result, 0, converter.convertValue(context, o, member, s, value, componentType));
+            }
+        }
+
+        return result;
+    }
+
+    private Object doConvertToCharacter(Object value) {
+        if (value instanceof String) {
+            String cStr = (String) value;
+
+            return (cStr.length() > 0) ? new Character(cStr.charAt(0)) : null;
+        }
+
+        return null;
+    }
+
+    private Object doConvertToBoolean(Object value) {
+        if (value instanceof String) {
+            String bStr = (String) value;
+
+            return Boolean.valueOf(bStr);
+        }
+
+        return null;
+    }
+
+    private Class doConvertToClass(Object value) {
+        Class clazz = null;
+
+        if (value instanceof String && value != null && ((String) value).length() > 0) {
+            try {
+                clazz = Class.forName((String) value);
+            } catch (ClassNotFoundException e) {
+                throw new XWorkException(e.getLocalizedMessage(), e);
+            }
+        }
+
+        return clazz;
+    }
+
+    private Collection doConvertToCollection(Map context, Object o, Member member, String prop, Object value, Class toType) {
+        Collection result;
+        Class memberType = String.class;
+
+        if (o != null) {
+            //memberType = (Class) XWorkConverter.getInstance().getConverter(o.getClass(), XWorkConverter.CONVERSION_COLLECTION_PREFIX + prop);
+            memberType = XWorkConverter.getInstance().getObjectTypeDeterminer().getElementClass(o.getClass(), prop, null);
+
+            if (memberType == null) {
+                memberType = String.class;
+            }
+        }
+
+        if (toType.isAssignableFrom(value.getClass())) {
+            // no need to do anything
+            result = (Collection) value;
+        } else if (value.getClass().isArray()) {
+            Object[] objArray = (Object[]) value;
+            TypeConverter converter = getTypeConverter(context);
+            result = createCollection(o, prop, toType, memberType, objArray.length);
+
+            for (int i = 0; i < objArray.length; i++) {
+                result.add(converter.convertValue(context, o, member, prop, objArray[i], memberType));
+            }
+        } else if (Collection.class.isAssignableFrom(value.getClass())) {
+            Collection col = (Collection) value;
+            TypeConverter converter = getTypeConverter(context);
+            result = createCollection(o, prop, toType, memberType, col.size());
+
+            for (Iterator it = col.iterator(); it.hasNext();) {
+                result.add(converter.convertValue(context, o, member, prop, it.next(), memberType));
+            }
+        } else {
+            result = createCollection(o, prop, toType, memberType, -1);
+            result.add(value);
+        }
+
+        return result;
+    }
+
+    private Object doConvertToDate(Map context, Object value, Class toType) {
+        Date result = null;
+
+        if (value instanceof String && value != null && ((String) value).length() > 0) {
+            String sa = (String) value;
+            Locale locale = getLocale(context);
+
+            DateFormat df = null;
+            if (java.sql.Time.class == toType) {
+                df = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale);
+            } else if (java.sql.Timestamp.class == toType) {
+                Date check = null;
+                SimpleDateFormat dtfmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT,
+                        DateFormat.MEDIUM,
+                        locale);
+                SimpleDateFormat fullfmt = new SimpleDateFormat(dtfmt.toPattern() + MILLISECOND_FORMAT,
+                        locale);
+
+                SimpleDateFormat dfmt = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT,
+                        locale);
+
+                SimpleDateFormat[] fmts = {fullfmt, dtfmt, dfmt};
+                for (int i = 0; i < fmts.length; i++) {
+                    try {
+                        check = fmts[i].parse(sa);
+                        df = fmts[i];
+                        if (check != null) {
+                            break;
+                        }
+                    } catch (ParseException ignore) {
+                    }
+                }
+            } else if (java.util.Date.class == toType) {
+                Date check = null;
+                SimpleDateFormat d1 = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG, locale);
+                SimpleDateFormat d2 = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, locale);
+                SimpleDateFormat d3 = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
+                SimpleDateFormat rfc3399 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+                SimpleDateFormat[] dfs = {d1, d2, d3, rfc3399}; //added RFC 3339 date format (XW-473)
+                for (int i = 0; i < dfs.length; i++) {
+                    try {
+                        check = dfs[i].parse(sa);
+                        df = dfs[i];
+                        if (check != null) {
+                            break;
+                        }
+                    }
+                    catch (ParseException ignore) {
+                    }
+                }
+            }
+            //final fallback for dates without time
+            if (df == null) {
+                df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
+            }
+            try {
+                df.setLenient(false); // let's use strict parsing (XW-341)
+                result = df.parse(sa);
+                if (!(Date.class == toType)) {
+                    try {
+                        Constructor constructor = toType.getConstructor(new Class[]{long.class});
+                        return constructor.newInstance(new Object[]{new Long(result.getTime())});
+                    } catch (Exception e) {
+                        throw new XWorkException("Couldn't create class " + toType + " using default (long) constructor", e);
+                    }
+                }
+            } catch (ParseException e) {
+                throw new XWorkException("Could not parse date", e);
+            }
+        } else if (Date.class.isAssignableFrom(value.getClass())) {
+            result = (Date) value;
+        }
+        return result;
+    }
+
+    private Object doConvertToNumber(Map context, Object value, Class toType) {
+        if (value instanceof String) {
+            if (toType == BigDecimal.class) {
+                return new BigDecimal((String) value);
+            } else if (toType == BigInteger.class) {
+                return new BigInteger((String) value);
+            } else {
+                String stringValue = (String) value;
+                if (!toType.isPrimitive() && (stringValue == null || stringValue.length() == 0)) {
+                    return null;
+                }
+                NumberFormat numFormat = NumberFormat.getInstance(getLocale(context));
+                ParsePosition parsePos = new ParsePosition(0);
+                if (isIntegerType(toType)) {
+                    numFormat.setParseIntegerOnly(true);
+                }
+                numFormat.setGroupingUsed(true);
+                Number number = numFormat.parse(stringValue, parsePos);
+
+                if (parsePos.getIndex() != stringValue.length()) {
+                    throw new XWorkException("Unparseable number: \"" + stringValue + "\" at position "
+                            + parsePos.getIndex());
+                } else {
+                    value = super.convertValue(context, number, toType);
+                }
+            }
+        } else if (value instanceof Object[]) {
+            Object[] objArray = (Object[]) value;
+
+            if (objArray.length == 1) {
+                return doConvertToNumber(context, objArray[0], toType);
+            }
+        }
+
+        // pass it through DefaultTypeConverter
+        return super.convertValue(context, value, toType);
+    }
+
+    protected boolean isIntegerType(Class type) {
+        if (double.class == type || float.class == type || Double.class == type || Float.class == type
+                || char.class == type || Character.class == type) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Converts the input as a number using java's number formatter to a string output.
+     */
+    private String doConvertFromNumberToString(Map context, Object value, Class toType) {
+        // XW-409: If the input is a Number we should format it to a string using the choosen locale and use java's numberformatter
+        if (Number.class.isAssignableFrom(toType)) {
+            NumberFormat numFormat = NumberFormat.getInstance(getLocale(context));
+            if (isIntegerType(toType)) {
+                numFormat.setParseIntegerOnly(true);
+            }
+            numFormat.setGroupingUsed(true);
+            numFormat.setMaximumFractionDigits(99); // to be sure we include all digits after decimal seperator, otherwise some of the fractions can be chopped
+
+            String number = numFormat.format(value);
+            if (number != null) {
+                return number;
+            }
+        }
+
+        return null; // no number
+    }
+
+
+    private String doConvertToString(Map context, Object value) {
+        String result = null;
+
+        if (value instanceof int[]) {
+            int[] x = (int[]) value;
+            List intArray = new ArrayList(x.length);
+
+            for (int i = 0; i < x.length; i++) {
+                intArray.add(new Integer(x[i]));
+            }
+
+            result = TextUtils.join(", ", intArray);
+        } else if (value instanceof long[]) {
+            long[] x = (long[]) value;
+            List intArray = new ArrayList(x.length);
+
+            for (int i = 0; i < x.length; i++) {
+                intArray.add(new Long(x[i]));
+            }
+
+            result = TextUtils.join(", ", intArray);
+        } else if (value instanceof double[]) {
+            double[] x = (double[]) value;
+            List intArray = new ArrayList(x.length);
+
+            for (int i = 0; i < x.length; i++) {
+                intArray.add(new Double(x[i]));
+            }
+
+            result = TextUtils.join(", ", intArray);
+        } else if (value instanceof boolean[]) {
+            boolean[] x = (boolean[]) value;
+            List intArray = new ArrayList(x.length);
+
+            for (int i = 0; i < x.length; i++) {
+                intArray.add(new Boolean(x[i]));
+            }
+
+            result = TextUtils.join(", ", intArray);
+        } else if (value instanceof Date) {
+            DateFormat df = null;
+            if (value instanceof java.sql.Time) {
+                df = DateFormat.getTimeInstance(DateFormat.MEDIUM, getLocale(context));
+            } else if (value instanceof java.sql.Timestamp) {
+                SimpleDateFormat dfmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT,
+                        DateFormat.MEDIUM,
+                        getLocale(context));
+                df = new SimpleDateFormat(dfmt.toPattern() + MILLISECOND_FORMAT);
+            } else {
+                df = DateFormat.getDateInstance(DateFormat.SHORT, getLocale(context));
+            }
+            result = df.format(value);
+        } else if (value instanceof String[]) {
+            result = TextUtils.join(", ", (String[]) value);
+        }
+
+        return result;
+    }
+}

src/java/com/opensymphony/xwork2/conversion/impl/XWorkConverter.java

+/*
+ * Copyright (c) 2002-2006 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.xwork2.conversion.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.lang.annotation.Annotation;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.util.AnnotationUtils;
+import com.opensymphony.xwork2.util.CompoundRoot;
+import com.opensymphony.xwork2.util.FileManager;
+import com.opensymphony.xwork2.util.LocalizedTextUtil;
+import com.opensymphony.xwork2.util.OgnlContextState;
+import com.opensymphony.xwork2.util.ValueStack;
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.XWorkMessages;
+import com.opensymphony.xwork2.conversion.ObjectTypeDeterminer;
+import com.opensymphony.xwork2.conversion.ObjectTypeDeterminerFactory;
+import com.opensymphony.xwork2.conversion.OgnlTypeConverterWrapper;
+import com.opensymphony.xwork2.conversion.TypeConverter;
+import com.opensymphony.xwork2.conversion.XWorkTypeConverterWrapper;
+import com.opensymphony.xwork2.conversion.annotations.Conversion;
+import com.opensymphony.xwork2.conversion.annotations.ConversionType;
+import com.opensymphony.xwork2.conversion.annotations.ConversionRule;
+import com.opensymphony.xwork2.conversion.annotations.TypeConversion;
+
+
+/**
+ * XWorkConverter is a singleton used by many of the Struts 2's Ognl extention points,
+ * such as InstantiatingNullHandler, XWorkListPropertyAccessor etc to do object 
+ * conversion.
+ * 
+ * <!-- START SNIPPET: javadoc -->
+ *
+ * Type conversion is great for situations where you need to turn a String in to a more complex object. Because the web
+ * is type-agnostic (everything is a string in HTTP), Struts 2's type conversion features are very useful. For instance,
+ * if you were prompting a user to enter in coordinates in the form of a string (such as "3, 22"), you could have
+ * Struts 2 do the conversion both from String to Point and from Point to String.
+ *
+ * <p/> Using this "point" example, if your action (or another compound object in which you are setting properties on)
+ * has a corresponding ClassName-conversion.properties file, Struts 2 will use the configured type converters for
+ * conversion to and from strings. So turning "3, 22" in to new Point(3, 22) is done by merely adding the following
+ * entry to <b>ClassName-conversion.properties</b> (Note that the PointConverter should impl the ognl.TypeConverter
+ * interface):
+ *
+ * <p/><b>point = com.acme.PointConverter</b>
+ *
+ * <p/> Your type converter should be sure to check what class type it is being requested to convert. Because it is used
+ * for both to and from strings, you will need to split the conversion method in to two parts: one that turns Strings in
+ * to Points, and one that turns Points in to Strings.
+ *
+ * <p/> After this is done, you can now reference your point (using &lt;s:property value="point"/&gt; in JSP or ${point}
+ * in FreeMarker) and it will be printed as "3, 22" again. As such, if you submit this back to an action, it will be
+ * converted back to a Point once again.
+ *
+ * <p/> In some situations you may wish to apply a type converter globally. This can be done by editing the file
+ * <b>xwork-conversion.properties</b> in the root of your class path (typically WEB-INF/classes) and providing a
+ * 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
+ * the type converter on the right hand side. For example, providing a type converter for all Point objects would mean
+ * adding the following entry:
+ *
+ * <p/><b>com.acme.Point = com.acme.PointConverter</b>
+ *
+ * <!-- END SNIPPET: javadoc -->
+ *
+ * <p/>
+ *
+ * <!-- START SNIPPET: i18n-note -->
+ *
+ * Type conversion should not be used as a substitute for i18n. It is not recommended to use this feature to print out
+ * properly formatted dates. Rather, you should use the i18n features of Struts 2 (and consult the JavaDocs for JDK's
+ * MessageFormat object) to see how a properly formatted date should be displayed.
+ *
+ * <!-- END SNIPPET: i18n-note -->
+ *
+ * <p/>
+ *
+ * <!-- START SNIPPET: error-reporting -->
+ *
+ * Any error that occurs during type conversion may or may not wish to be reported. For example, reporting that the
+ * input "abc" could not be converted to a number might be important. On the other hand, reporting that an empty string,
+ * "", cannot be converted to a number might not be important - especially in a web environment where it is hard to
+ * distinguish between a user not entering a value vs. entering a blank value.
+ *
+ * <p/> By default, all conversion errors are reported using the generic i18n key <b>xwork.default.invalid.fieldvalue</b>,
+ * which you can override (the default text is <i>Invalid field value for field "xxx"</i>, where xxx is the field name)
+ * in your global i18n resource bundle.
+ *
+ * <p/>However, sometimes you may wish to override this message on a per-field basis. You can do this by adding an i18n
+ * key associated with just your action (Action.properties) using the pattern <b>invalid.fieldvalue.xxx</b>, where xxx
+ * is the field name.
+ *
+ * <p/>It is important to know that none of these errors are actually reported directly. Rather, they are added to a map
+ * called <i>conversionErrors</i> in the ActionContext. There are several ways this map can then be accessed and the
+ * errors can be reported accordingly.
+ *
+ * <!-- END SNIPPET: error-reporting -->
+ *
+ * @author <a href="mailto:plightbo@gmail.com">Pat Lightbody</a>
+ * @author Rainer Hermanns
+ * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
+ * @author tm_jee
+ * 
+ * @version $Date$ $Id$
+ * 
+ * @see XWorkBasicConverter
+ */
+public class XWorkConverter extends DefaultTypeConverter {
+
+    private static XWorkConverter instance;
+    protected static final Log LOG = LogFactory.getLog(XWorkConverter.class);
+    public static final String REPORT_CONVERSION_ERRORS = "report.conversion.errors";
+    public static final String CONVERSION_PROPERTY_FULLNAME = "conversion.property.fullName";
+    public static final String CONVERSION_ERROR_PROPERTY_PREFIX = "invalid.fieldvalue.";
+    public static final String CONVERSION_COLLECTION_PREFIX = "Collection_";
+
+    public static final String LAST_BEAN_CLASS_ACCESSED = "last.bean.accessed";
+    public static final String LAST_BEAN_PROPERTY_ACCESSED = "last.property.accessed";
+
+    /**
+     * Target class conversion Mappings. 
+     * <pre>
+     * Map<Class, Map<String, Object>>
+     *  - Class -> convert to class
+     *  - Map<String, Object>
+     *    - String -> property name 
+     *                eg. Element_property, property etc.
+     *    - Object -> String to represent properties 
+     *                eg. value part of 
+     *                    KeyProperty_property=id
+     *             -> TypeConverter to represent an Ognl TypeConverter
+     *                eg. value part of 
+     *                    property=foo.bar.MyConverter
+     *             -> Class to represent a class
+     *                eg. value part of 
+     *                    Element_property=foo.bar.MyObject
+     * </pre>               
+     */
+    protected HashMap<Class,Map<String,Object>> mappings = new HashMap<Class,Map<String,Object>>(); // action 			
+    
+    /**
+     * Unavailable target class conversion mappings, serves as a simple cache.
+     */
+    protected HashSet<Class> noMapping = new HashSet<Class>(); // action
+    
+    /**
+     * Record class and its type converter mapping.
+     * <pre>
+     * - String - classname as String
+     * - TypeConverter - instance of TypeConverter
+     * </pre>
+     */
+    protected HashMap<String, TypeConverter> defaultMappings = new HashMap<String, TypeConverter>();  // non-action (eg. returned value)
+    
+    /**
+     * Record classes that doesn't have conversion mapping defined.
+     * <pre>
+     * - String -> classname as String
+     * </pre>
+     */
+    protected HashSet<String> unknownMappings = new HashSet<String>(); 	// non-action (eg. returned value)
+    
+    protected TypeConverter defaultTypeConverter = new XWorkBasicConverter();
+    protected ObjectTypeDeterminer objectTypeDeterminer = null;
+
+
+    protected XWorkConverter() {
+        try {
+            // note: this file is deprecated
+            loadConversionProperties("xwork-default-conversion.properties");
+        } catch (Exception e) {
+        }
+
+        try {
+            loadConversionProperties("xwork-conversion.properties");
+        } catch (Exception e) {
+        }
+    }
+
+    public static String getConversionErrorMessage(String propertyName, ValueStack stack) {
+        String defaultMessage = LocalizedTextUtil.findDefaultText(XWorkMessages.DEFAULT_INVALID_FIELDVALUE,
+                ActionContext.getContext().getLocale(),
+                new Object[]{
+                        propertyName
+                });
+        String getTextExpression = "getText('" + CONVERSION_ERROR_PROPERTY_PREFIX + propertyName + "','" + defaultMessage + "')";
+        String message = (String) stack.findValue(getTextExpression);
+
+        if (message == null) {
+            message = defaultMessage;
+        }
+
+        return message;
+    }
+
+
+    public static XWorkConverter getInstance() {
+         if (instance == null) {
+             instance = new XWorkConverter();
+         }
+
+         return instance;
+     }
+    
+    public static ognl.TypeConverter getOgnlInstance() {
+        //return getInstance();
+        return new OgnlTypeConverterWrapper(getInstance());
+    }
+
+    
+    @Inject
+    public static void setInstance(XWorkConverter instance) {
+        XWorkConverter.instance = instance;
+    }
+
+    public static String buildConverterFilename(Class clazz) {
+        String className = clazz.getName();
+        String resource = className.replace('.', '/') + "-conversion.properties";
+
+        return resource;
+    }
+
+    public static void resetInstance() {
+        instance = null;
+    }
+
+    public void setDefaultConverter(TypeConverter defaultTypeConverter) {
+        this.defaultTypeConverter = defaultTypeConverter;
+    }
+
+    public Object convertValue(Map map, Object o, Class aClass) {
+        return convertValue(map, null, null, null, o, aClass);
+    }
+
+    /**
+     * Convert value from one form to another.
+     * Minimum requirement of arguments:
+     * <ul>
+     * 		<li>supplying context, toClass and value</li>
+     * 		<li>supplying context, target and value.</li>
+     * </ul>
+     * 
+     * @see ognl.TypeConverter#convertValue(java.util.Map, java.lang.Object, java.lang.reflect.Member, java.lang.String, java.lang.Object, java.lang.Class)
+     */
+    public Object convertValue(Map context, Object target, Member member, String property, Object value, Class toClass) {
+        //
+        // Process the conversion using the default mappings, if one exists
+        //
+        TypeConverter tc = null;
+
+        if ((value != null) && (toClass == value.getClass())) {
+            return value;
+        }
+
+        // allow this method to be called without any context
+        // i.e. it can be called with as little as "Object value" and "Class toClass"
+        if (target != null) {
+            Class clazz = target.getClass();
+
+            Object[] classProp = null;
+
+            // this is to handle weird issues with setValue with a different type
+            if ((target instanceof CompoundRoot) && (context != null)) {
+                classProp = getClassProperty(context);
+            }
+
+            if (classProp != null) {
+                clazz = (Class) classProp[0];
+                property = (String) classProp[1];
+            }
+
+            tc = (TypeConverter) getConverter(clazz, property);
+            
+            if (LOG.isDebugEnabled())