Commits

Anonymous committed fd8bc1b

When looking up resources (properties, converters, validators), look up class hierarchy as well. Improve support for ModelDriven when looking up properties.
Resolves XW-166 & XW-164.

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

Comments (0)

Files changed (14)

src/java/com/opensymphony/xwork/util/LocalizedTextUtil.java

  */
 package com.opensymphony.xwork.util;
 
+import com.opensymphony.xwork.Action;
 import com.opensymphony.xwork.ActionContext;
+import com.opensymphony.xwork.ModelDriven;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
      * </li>
      * <li>If the message text is not found, each parent class of the action is used as above until
      * java.lang.Object is found.<li>
+     * <li>If the message text has still not been found and the action is {@link ModelDriven},
+     * the class hierarchy of the model will be traversed (interface and parent classes) and
+     * used as above until java.lang.Object is found.</li>
      * <li>If the message text has still not been found, findDefaultText(aTextName, locale) is called to
      * search the default message bundles</li>
      * <li>If the text has still not been found, the provided defaultMessage is returned.<li>
      * @return
      */
     public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) {
-        OgnlValueStack valueStack = ActionContext.getContext().getValueStack();
+        ActionContext context = ActionContext.getContext();
+        OgnlValueStack valueStack = context.getValueStack();
+
+        // search up class hierarchy
+        Class clazz = aClass;
 
         do {
             try {
-                ResourceBundle bundle = findResourceBundle(aClass.getName(), locale);
-
+                ResourceBundle bundle = findResourceBundle(clazz.getName(), locale);
                 String message = TextParseUtil.translateVariables(bundle.getString(aTextName), valueStack);
 
                 return MessageFormat.format(message, args);
             } catch (MissingResourceException ex) {
-                aClass = aClass.getSuperclass();
+                clazz = clazz.getSuperclass();
             }
-        } while (!aClass.equals(Object.class));
+        } while (!clazz.equals(Object.class));
+
+        if (ModelDriven.class.isAssignableFrom(aClass)) {
+            Action action = context.getActionInvocation().getAction();
+
+            // make sure action is ModelDriven
+            if (action instanceof ModelDriven) {
+                clazz = ((ModelDriven) action).getModel().getClass();
+
+                while (!clazz.equals((Object.class))) {
+                    // look for resource bundle for implemented interfaces
+                    Class[] interfaces = clazz.getInterfaces();
+
+                    for (int x = 0; x < interfaces.length; x++) {
+                        try {
+                            ResourceBundle bundle = findResourceBundle(interfaces[x].getName(), locale);
+                            String message = TextParseUtil.translateVariables(bundle.getString(aTextName), valueStack);
+
+                            return MessageFormat.format(message, args);
+                        } catch (MissingResourceException ex) {
+                        }
+                    }
+
+                    // search up model class hierarchy
+                    try {
+                        ResourceBundle bundle = findResourceBundle(clazz.getName(), locale);
+                        String message = TextParseUtil.translateVariables(bundle.getString(aTextName), valueStack);
+
+                        return MessageFormat.format(message, args);
+                    } catch (MissingResourceException ex) {
+                    }
+
+                    clazz = clazz.getSuperclass();
+                }
+            }
+        }
 
         return getDefaultText(aTextName, locale, valueStack, args, defaultMessage);
     }

src/java/com/opensymphony/xwork/util/XWorkConverter.java

         // 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 = null;
-
-            clazz = target.getClass();
+            Class clazz = target.getClass();
 
             Object[] classProp = null;
 
         }
     }
 
+    /**
+     * Looks for a TypeConverter in the default mappings.
+     *
+     * @param className name of the class the TypeConverter must handle
+     * @return a TypeConverter to handle the specified class or null if none can
+     * be found
+     */
     public TypeConverter lookup(String className) {
         if (unknownMappings.contains(className)) {
             return null;
         return result;
     }
 
+    /**
+     * Looks for a TypeConverter in the default mappings.
+     *
+     * @param clazz the class the TypeConverter must handle
+     * @return a TypeConverter to handle the specified class or null if none can
+     * be found
+     */
     public TypeConverter lookup(Class clazz) {
         return lookup(clazz.getName());
     }
         return null;
     }
 
+    /**
+     * Looks for converter mappings for the specified class and adds it to an
+     * existing map.  Only new converters are added.  If a converter is defined
+     * on a key that already exists, the converter is ignored.
+     *
+     * @param mapping an existing map to add new converter mappings to
+     * @param clazz class to look for converter mappings for
+     */
+    private void addConverterMapping(Map mapping, Class clazz) {
+        try {
+            InputStream is = FileManager.loadFile(buildConverterFilename(clazz), clazz);
+
+            if (is != null) {
+                Properties prop = new Properties();
+                prop.load(is);
+
+                Iterator it = prop.entrySet().iterator();
+
+                while (it.hasNext()) {
+                    Map.Entry entry = (Map.Entry) it.next();
+                    String key = (String) entry.getKey();
+
+                    if (mapping.containsKey(key)) {
+                        break;
+                    }
+
+                    if (!key.startsWith("Collection_")) {
+                        mapping.put(key, createTypeConverter((String) entry.getValue()));
+                    } else {
+                        mapping.put(key, entry.getValue());
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            LOG.error("Problem loading properties for " + clazz.getName(), ex);
+        }
+    }
+
+    /**
+     * Looks for converter mappings for the specified class, traversing up its
+     * class hierarchy and interfaces and adding any additional mappings it may
+     * find.  Mappings lower in the hierarchy have priority over those higher
+     * in the hierarcy.
+     *
+     * @param clazz the class to look for converter mappings for
+     * @return the converter mappings
+     */
     private Map buildConverterMapping(Class clazz) throws Exception {
         Map mapping = new HashMap();
 
-        String resource = buildConverterFilename(clazz);
-        InputStream is = FileManager.loadFile(resource, clazz);
+        // check for conversion mapping associated with super classes and any implemented interfaces
+        Class curClazz = clazz;
 
-        if (is != null) {
-            Properties props = new Properties();
-            props.load(is);
-            mapping.putAll(props);
+        while (!curClazz.equals(Object.class)) {
+            // add current class' mappings
+            addConverterMapping(mapping, clazz);
 
-            for (Iterator iterator = mapping.entrySet().iterator();
-                    iterator.hasNext();) {
-                Map.Entry entry = (Map.Entry) iterator.next();
-                String propName = (String) entry.getKey();
-                String className = (String) entry.getValue();
+            // check interfaces' mappings
+            Class[] interfaces = curClazz.getInterfaces();
 
-                if (!propName.startsWith("Collection_")) {
-                    entry.setValue(createTypeConverter(className));
-                }
+            for (int x = 0; x < interfaces.length; x++) {
+                addConverterMapping(mapping, interfaces[x]);
             }
 
+            curClazz = curClazz.getSuperclass();
+        }
+
+        if (mapping.size() > 0) {
             mappings.put(clazz, mapping);
         } else {
             noMapping.add(clazz);
         }
     }
 
+    /**
+     * Recurses through a class' interfaces and class hierarchy looking for a
+     * TypeConverter in the default mapping that can handle the specified class.
+     *
+     * @param clazz the class the TypeConverter must handle
+     * @return a TypeConverter to handle the specified class or null if none can
+     * be found
+     */
     private TypeConverter lookupSuper(Class clazz) {
         TypeConverter result = null;
 

src/java/com/opensymphony/xwork/validator/ActionValidatorManager.java

 
     //~ Methods ////////////////////////////////////////////////////////////////
 
+    /**
+     * Primary method for validator lookup.
+     */
     public static synchronized List getValidators(Class clazz, String context) {
         final String validatorKey = buildValidatorKey(clazz, context);
 
     * This method 'collects' all the validators for a given action invocation.
     *
     * It will traverse up the class hierarchy looking for validators for every super class
-    * of the current action, as well as adding validators for any alias of this invocation. Nifty!
+    * and interface of the current action, as well as adding validators for any alias of
+    * this invocation. Nifty!
     */
     private static List buildValidators(Class clazz, String context, boolean checkFile) {
         List validators = new ArrayList();
 
-        // validators for the action class validators.addAll(buildClassValidators(actionClass, checkFile)); validators.addAll(buildAliasValidators(actionClass, invocation, checkFile));
-        // looking for validators for every super class
-        Class anActionClass = clazz;
-        anActionClass = anActionClass.getSuperclass();
+        // look for validators for implemented interfaces
+        Class[] interfaces = clazz.getInterfaces();
+
+        for (int x = 0; x < interfaces.length; x++) {
+            validators.addAll(buildClassValidators(interfaces[x], checkFile));
+        }
+
+        // looking for validators in class hierarchy
+        Class anActionClass = clazz.getSuperclass();
 
         while (!anActionClass.equals(Object.class)) {
+            // look for validators for implemented interfaces
+            interfaces = anActionClass.getInterfaces();
+
+            for (int x = 0; x < interfaces.length; x++) {
+                validators.addAll(buildClassValidators(interfaces[x], checkFile));
+                validators.addAll(buildAliasValidators(interfaces[x], context, checkFile));
+            }
+
+            // search up class hierarchy
             validators.addAll(buildClassValidators(anActionClass, checkFile));
             anActionClass = anActionClass.getSuperclass();
         }

src/test/com/opensymphony/xwork/test/DataAware-conversion.properties

+barObj=com.opensymphony.xwork.util.FooBarConverter

src/test/com/opensymphony/xwork/test/DataAware-validation.xml

+<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.dtd">
+<validators>
+    <field name="data">
+        <field-validator type="requiredstring">
+            <message>You must enter a value for data.</message>
+        </field-validator>
+    </field>
+</validators>

src/test/com/opensymphony/xwork/test/DataAware.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+/*
+ * This file is copyright (c) 2001-2004, Board of Trustees of Stanford
+ * University.  Created in the laboratory of Professor Russ B. Altman
+ * (russ.altman@stanford.edu), Stanford University, Department of
+ * Medicine, as part of the NIH PharmGKB knowledge base development
+ * effort.  This work is supported by NIH U01GM61374.  Contact
+ * help@pharmgkb.org for assistance, questions or suggestions.
+ */
+package com.opensymphony.xwork.test;
+
+import com.opensymphony.xwork.util.Bar;
+
+
+/**
+ * Implemented by SimpleAction3 and TestBean2 to test class hierarchy traversal.
+ *
+ * @author Mark Woon
+ */
+public interface DataAware {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    void setBarObj(Bar b);
+
+    Bar getBarObj();
+
+    void setData(String data);
+
+    String getData();
+}

src/test/com/opensymphony/xwork/test/DataAware.properties

+test.foo = Foo!
+test.bar = Bar!

src/test/com/opensymphony/xwork/test/ModelDrivenAction2.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+/*
+ * This file is copyright (c) 2001-2004, Board of Trustees of Stanford
+ * University.  Created in the laboratory of Professor Russ B. Altman
+ * (russ.altman@stanford.edu), Stanford University, Department of
+ * Medicine, as part of the NIH PharmGKB knowledge base development
+ * effort.  This work is supported by NIH U01GM61374.  Contact
+ * help@pharmgkb.org for assistance, questions or suggestions.
+ */
+package com.opensymphony.xwork.test;
+
+import com.opensymphony.xwork.ModelDrivenAction;
+
+
+/**
+ * Extend ModelDrivenAction to test class hierarchy traversal.
+ *
+ * @author Mark Woon
+ */
+public class ModelDrivenAction2 extends ModelDrivenAction {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private TestBean2 model = new TestBean2();
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * @return the model to be pushed onto the ValueStack after the Action itself
+     */
+    public Object getModel() {
+        return model;
+    }
+}

src/test/com/opensymphony/xwork/test/SimpleAction3.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+/*
+ * This file is copyright (c) 2001-2004, Board of Trustees of Stanford
+ * University.  Created in the laboratory of Professor Russ B. Altman
+ * (russ.altman@stanford.edu), Stanford University, Department of
+ * Medicine, as part of the NIH PharmGKB knowledge base development
+ * effort.  This work is supported by NIH U01GM61374.  Contact
+ * help@pharmgkb.org for assistance, questions or suggestions.
+ */
+package com.opensymphony.xwork.test;
+
+import com.opensymphony.xwork.SimpleAction;
+import com.opensymphony.xwork.util.Bar;
+
+
+/**
+ * Extend SimpleAction to test class hierarchy traversal.
+ *
+ * @author Mark Woon
+ */
+public class SimpleAction3 extends SimpleAction implements DataAware {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private Bar m_bar;
+    private String m_data;
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void setBarObj(Bar b) {
+        m_bar = b;
+    }
+
+    public Bar getBarObj() {
+        return m_bar;
+    }
+
+    public void setData(String data) {
+        m_data = data;
+    }
+
+    public String getData() {
+        return m_data;
+    }
+}

src/test/com/opensymphony/xwork/test/TestBean2.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+/*
+ * This file is copyright (c) 2001-2004, Board of Trustees of Stanford
+ * University.  Created in the laboratory of Professor Russ B. Altman
+ * (russ.altman@stanford.edu), Stanford University, Department of
+ * Medicine, as part of the NIH PharmGKB knowledge base development
+ * effort.  This work is supported by NIH U01GM61374.  Contact
+ * help@pharmgkb.org for assistance, questions or suggestions.
+ */
+package com.opensymphony.xwork.test;
+
+import com.opensymphony.xwork.TestBean;
+import com.opensymphony.xwork.util.Bar;
+
+
+/**
+ * Extend TestBean to test class hierarchy traversal.
+ *
+ * @author Mark Woon
+ */
+public class TestBean2 extends TestBean implements DataAware {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private Bar m_bar;
+    private String m_data;
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void setBarObj(Bar b) {
+        m_bar = b;
+    }
+
+    public Bar getBarObj() {
+        return m_bar;
+    }
+
+    public void setData(String data) {
+        m_data = data;
+    }
+
+    public String getData() {
+        return m_data;
+    }
+}

src/test/com/opensymphony/xwork/util/LocalizedTextUtilTest.java

  */
 package com.opensymphony.xwork.util;
 
+import com.mockobjects.dynamic.Mock;
+
+import com.opensymphony.xwork.Action;
 import com.opensymphony.xwork.ActionContext;
+import com.opensymphony.xwork.ActionInvocation;
 import com.opensymphony.xwork.XWorkMessages;
 import com.opensymphony.xwork.config.ConfigurationManager;
+import com.opensymphony.xwork.test.ModelDrivenAction2;
 
 import junit.framework.TestCase;
 
 
     public void testAddDefaultResourceBundle() {
         try {
-            String message = LocalizedTextUtil.findDefaultText("foo.range", Locale.getDefault());
+            LocalizedTextUtil.findDefaultText("foo.range", Locale.getDefault());
             fail("Found message when it should not be available.");
         } catch (MissingResourceException e) {
         }
         }
     }
 
+    public void testFindText() {
+        try {
+            Action action = new ModelDrivenAction2();
+            Mock mockActionInvocation = new Mock(ActionInvocation.class);
+            mockActionInvocation.expectAndReturn("getAction", action);
+            ActionContext.getContext().setActionInvocation((ActionInvocation) mockActionInvocation.proxy());
+
+            String message = LocalizedTextUtil.findText(ModelDrivenAction2.class, "test.foo", Locale.getDefault());
+            assertEquals("Foo!", message);
+        } catch (MissingResourceException ex) {
+            ex.printStackTrace();
+            fail(ex.getMessage());
+        }
+    }
+
     public void testParameterizedDefaultMessage() {
         try {
             String message = LocalizedTextUtil.findDefaultText(XWorkMessages.MISSING_ACTION_EXCEPTION, Locale.getDefault(), new String[] {

src/test/com/opensymphony/xwork/util/XWorkConverterTest.java

 import com.opensymphony.xwork.SimpleAction;
 import com.opensymphony.xwork.TestBean;
 import com.opensymphony.xwork.config.ConfigurationManager;
+import com.opensymphony.xwork.test.ModelDrivenAction2;
 
 import junit.framework.TestCase;
 
         assertEquals("Invalid field value for field \"foo\".", message);
     }
 
+    public void testFindConversionMappingForInterface() {
+        ModelDrivenAction2 action = new ModelDrivenAction2();
+        OgnlValueStack stack = new OgnlValueStack();
+        stack.push(action);
+        stack.push(action.getModel());
+
+        Map context = stack.getContext();
+        context.put(XWorkConverter.REPORT_CONVERSION_ERRORS, Boolean.TRUE);
+
+        String value = "asdf:123";
+        Object o = converter.convertValue(context, action.getModel(), null, "barObj", value, Bar.class);
+        assertNotNull(o);
+        assertTrue(o instanceof Bar);
+
+        Bar b = (Bar) o;
+        assertEquals(value, b.getTitle() + ":" + b.getSomethingElse());
+    }
+
     public void testLocalizedDateConversion() throws Exception {
         Date date = new Date(System.currentTimeMillis());
         Locale locale = Locale.GERMANY;

src/test/com/opensymphony/xwork/validator/ActionValidatorManagerTest.java

  */
 package com.opensymphony.xwork.validator;
 
-import com.opensymphony.xwork.Action;
 import com.opensymphony.xwork.SimpleAction;
 import com.opensymphony.xwork.test.SimpleAction2;
+import com.opensymphony.xwork.test.SimpleAction3;
 import com.opensymphony.xwork.validator.validators.DateRangeFieldValidator;
 import com.opensymphony.xwork.validator.validators.ExpressionValidator;
 import com.opensymphony.xwork.validator.validators.IntRangeFieldValidator;
 import com.opensymphony.xwork.validator.validators.RequiredFieldValidator;
+import com.opensymphony.xwork.validator.validators.RequiredStringValidator;
 
 import junit.framework.TestCase;
 
 public class ActionValidatorManagerTest extends TestCase {
     //~ Instance fields ////////////////////////////////////////////////////////
 
-    protected Action action;
     protected final String alias = "validationAlias";
 
     //~ Methods ////////////////////////////////////////////////////////////////
 
     public void testBuildValidatorKey() {
-        String validatorKey = ActionValidatorManager.buildValidatorKey(action.getClass(), alias);
-        assertEquals(action.getClass().getName() + "/" + alias, validatorKey);
+        String validatorKey = ActionValidatorManager.buildValidatorKey(SimpleAction.class, alias);
+        assertEquals(SimpleAction.class.getName() + "/" + alias, validatorKey);
     }
 
     public void testBuildsValidatorsForAlias() {
-        List validatorList = ActionValidatorManager.getValidators(action.getClass(), alias);
+        List validatorList = ActionValidatorManager.getValidators(SimpleAction.class, alias);
 
         // 2 in the class level + 2 in the alias
         assertEquals(7, validatorList.size());
         assertTrue(bazValidator2 instanceof IntRangeFieldValidator);
     }
 
-    public void testSameAliasWithDifferentClass() {
-        List validatorList = ActionValidatorManager.getValidators(action.getClass(), alias);
-        Action action2 = new SimpleAction2();
-        List validatorList2 = ActionValidatorManager.getValidators(action2.getClass(), alias);
-        assertFalse(validatorList.size() == validatorList2.size());
+    public void testGetValidatorsForInterface() {
+        List validatorList = ActionValidatorManager.getValidators(SimpleAction3.class, alias);
+
+        // 5 in the class hierarchy + 1 in the interface
+        assertEquals(6, validatorList.size());
+
+        final FieldValidator dataValidator = (FieldValidator) validatorList.get(0);
+        assertEquals("data", dataValidator.getFieldName());
+        assertTrue(dataValidator instanceof RequiredStringValidator);
+
+        final FieldValidator barValidator1 = (FieldValidator) validatorList.get(1);
+        assertEquals("bar", barValidator1.getFieldName());
+        assertTrue(barValidator1 instanceof RequiredFieldValidator);
+
+        final FieldValidator barValidator2 = (FieldValidator) validatorList.get(2);
+        assertEquals("bar", barValidator2.getFieldName());
+        assertTrue(barValidator2 instanceof IntRangeFieldValidator);
+
+        final FieldValidator dateValidator = (FieldValidator) validatorList.get(3);
+        assertEquals("date", dateValidator.getFieldName());
+        assertTrue(dateValidator instanceof DateRangeFieldValidator);
+
+        final FieldValidator fooValidator = (FieldValidator) validatorList.get(4);
+        assertEquals("foo", fooValidator.getFieldName());
+        assertTrue(fooValidator instanceof IntRangeFieldValidator);
+
+        final Validator expressionValidator = (Validator) validatorList.get(5);
+        assertTrue(expressionValidator instanceof ExpressionValidator);
     }
 
-    protected void setUp() {
-        action = new SimpleAction();
+    public void testSameAliasWithDifferentClass() {
+        List validatorList = ActionValidatorManager.getValidators(SimpleAction.class, alias);
+        List validatorList2 = ActionValidatorManager.getValidators(SimpleAction2.class, alias);
+        assertFalse(validatorList.size() == validatorList2.size());
     }
 }

src/test/com/opensymphony/xwork/validator/VisitorFieldValidatorModelTest.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
 package com.opensymphony.xwork.validator;
 
+import com.opensymphony.xwork.ActionContext;
+import com.opensymphony.xwork.TestBean;
+import com.opensymphony.xwork.test.TestBean2;
+import com.opensymphony.xwork.util.OgnlValueStack;
+
 import junit.framework.TestCase;
 
 import java.util.*;
 
-import com.opensymphony.xwork.TestBean;
-import com.opensymphony.xwork.ActionContext;
-import com.opensymphony.xwork.util.OgnlValueStack;
 
 /**
  * VisitorFieldValidatorModelTest
  * Date: Mar 18, 2004 2:51:42 PM
  */
 public class VisitorFieldValidatorModelTest extends TestCase {
+    //~ Instance fields ////////////////////////////////////////////////////////
 
     protected VisitorValidatorModelAction action;
     private Locale origLocale;
     }
 
     public void testModelFieldErrorsAddedWithoutFieldPrefix() throws Exception {
-        ActionValidatorManager.validate(action,null);
+        ActionValidatorManager.validate(action, null);
         assertTrue(action.hasFieldErrors());
 
         Map fieldErrors = action.getFieldErrors();
+
         // the required string validation inherited from the VisitorValidatorTestAction
         assertTrue(fieldErrors.containsKey("context"));
+
         // the bean validation which is now at the top level because we set the appendPrefix to false
         assertTrue(fieldErrors.containsKey("name"));
 
         assertEquals("You must enter a name.", nameMessage);
     }
 
+    public void testModelFieldErrorsAddedWithoutFieldPrefixForInterface() throws Exception {
+        TestBean origBean = action.getBean();
+        TestBean2 bean = new TestBean2();
+        bean.setBirth(origBean.getBirth());
+        bean.setCount(origBean.getCount());
+        action.setBean(bean);
+        assertTrue(action.getBean() instanceof TestBean2);
+
+        ActionValidatorManager.validate(action, null);
+        assertTrue(action.hasFieldErrors());
+
+        Map fieldErrors = action.getFieldErrors();
+
+        // the required string validation inherited from the VisitorValidatorTestAction
+        assertTrue(fieldErrors.containsKey("context"));
+
+        // the bean validation which is now at the top level because we set the appendPrefix to false
+        assertTrue(fieldErrors.containsKey("name"));
+
+        List nameMessages = (List) fieldErrors.get("name");
+        assertEquals(1, nameMessages.size());
+
+        String nameMessage = (String) nameMessages.get(0);
+        assertEquals("You must enter a name.", nameMessage);
+
+        // should also have picked up validation check for DataAware interface
+        assertTrue(fieldErrors.containsKey("data"));
+
+        List dataMessages = (List) fieldErrors.get("data");
+        assertEquals(1, dataMessages.size());
+
+        String dataMessage = (String) dataMessages.get(0);
+        assertEquals("You must enter a value for data.", dataMessage);
+    }
+
     protected void tearDown() throws Exception {
         super.tearDown();
         ActionContext.setContext(null);
         Locale.setDefault(origLocale);
     }
-
-
 }