You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by mu...@apache.org on 2007/04/27 03:51:08 UTC

svn commit: r532928 - in /struts/struts2/trunk/core/src: main/java/org/apache/struts2/interceptor/validation/ main/resources/ main/resources/org/apache/struts2/static/ main/resources/template/css_xhtml/ main/resources/template/xhtml/ test/java/org/apac...

Author: musachy
Date: Thu Apr 26 18:51:07 2007
New Revision: 532928

URL: http://svn.apache.org/viewvc?view=rev&rev=532928
Log:
WW-1897 Create JSONValidationInterceptor

Added:
    struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptor.java
    struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/utils.js
    struts/struts2/trunk/core/src/test/java/org/apache/struts2/TestUtils.java
    struts/struts2/trunk/core/src/test/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptorTest.java
Removed:
    struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/validationClient.js
Modified:
    struts/struts2/trunk/core/src/main/resources/struts-default.xml
    struts/struts2/trunk/core/src/main/resources/template/css_xhtml/validation.js
    struts/struts2/trunk/core/src/main/resources/template/xhtml/validation.js

Added: struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptor.java
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptor.java?view=auto&rev=532928
==============================================================================
--- struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptor.java (added)
+++ struts/struts2/trunk/core/src/main/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptor.java Thu Apr 26 18:51:07 2007
@@ -0,0 +1,207 @@
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.interceptor.validation;
+
+import java.text.CharacterIterator;
+import java.text.StringCharacterIterator;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.struts2.ServletActionContext;
+
+import com.opensymphony.xwork2.Action;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.ValidationAware;
+
+/**
+ * <p>Extends the annotations validator and returns a JSON string with the
+ * validation errors. If validation succeeds the action is invoked.</p> 
+ * 
+ * <p>If 'validationFailedStatus' is set it will be used as the Response status
+ * when validation fails.</p>
+ * 
+ * <p>If the request has a parameter 'validateOnly' execution will return after 
+ * validation (action won't be executed).</p>
+ */
+public class JSONValidationInterceptor extends AnnotationValidationInterceptor {
+    private static final Log LOG = LogFactory
+        .getLog(JSONValidationInterceptor.class);
+    static char[] hex = "0123456789ABCDEF".toCharArray();
+
+    private int validationFailedStatus = -1;
+
+    /**
+     * HTTP status that will be set in the response if validation fails
+     * @param validationFailedStatus
+     */
+    public void setValidationFailedStatus(int validationFailedStatus) {
+        this.validationFailedStatus = validationFailedStatus;
+    }
+
+    @Override
+    protected String doIntercept(ActionInvocation invocation) throws Exception {
+        //validate
+        doBeforeInvocation(invocation);
+
+        HttpServletResponse response = ServletActionContext.getResponse();
+        HttpServletRequest request = ServletActionContext.getRequest();
+
+        Object action = invocation.getAction();
+
+        if (action instanceof ValidationAware) {
+            // generate json
+            ValidationAware validationAware = (ValidationAware) action;
+            if (validationAware.hasErrors()) {
+                if (validationFailedStatus >= 0)
+                    response.setStatus(validationFailedStatus);
+                response.getWriter().print(buildResponse(validationAware));
+                return Action.NONE;
+            }
+        }
+
+        String validateOnly = request.getParameter("validateOnly");
+        if (validateOnly != null && "true".equals(validateOnly)) {
+            return Action.NONE;
+        } else {
+            return invocation.invoke();
+        }
+    }
+
+    /**
+     * @return JSON string that contains the errors and field errors
+     */
+    @SuppressWarnings("unchecked")
+    protected String buildResponse(ValidationAware validationAware) {
+        //should we use FreeMarker here?
+        StringBuilder sb = new StringBuilder();
+        sb.append("/* { ");
+
+        if (validationAware.hasErrors()) {
+            //action errors
+            if (validationAware.hasActionErrors()) {
+                sb.append("\"errors\":");
+                sb.append(buildArray(validationAware.getActionErrors()));
+                sb.append(",");
+            }
+
+            //field errors
+            if (validationAware.hasFieldErrors()) {
+                sb.append("\"fieldErrors\": {");
+                Map<String, List<String>> fieldErrors = validationAware
+                    .getFieldErrors();
+                for (Map.Entry<String, List<String>> fieldError : fieldErrors
+                    .entrySet()) {
+                    sb.append("\"");
+                    sb.append(fieldError.getKey());
+                    sb.append("\":");
+                    sb.append(buildArray(fieldError.getValue()));
+                    sb.append(",");
+                }
+                //remove trailing comma, IE creates an empty object, duh
+                sb.deleteCharAt(sb.length() - 1);
+                sb.append("}");
+            }
+        }
+
+        sb.append("} */");
+        /*response should be something like:
+         * {
+         *      "errors": ["this", "that"],
+         *      "fieldErrors": {
+         *            field1: "this",
+         *            field2: "that"
+         *      }
+         * }
+         */
+        return sb.toString();
+    }
+
+    private String buildArray(Collection<String> values) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        for (String value : values) {
+            sb.append("\"");
+            sb.append(escapeJSON(value));
+            sb.append("\",");
+        }
+        if (values.size() > 0)
+            sb.deleteCharAt(sb.length() - 1);
+        sb.append("]");
+        return sb.toString();
+    }
+
+    private String escapeJSON(Object obj) {
+        StringBuilder sb = new StringBuilder();
+
+        CharacterIterator it = new StringCharacterIterator(obj.toString());
+
+        for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
+            if (c == '"') {
+                sb.append("\\\"");
+            } else if (c == '\\') {
+                sb.append("\\\\");
+            } else if (c == '/') {
+                sb.append("\\/");
+            } else if (c == '\b') {
+                sb.append("\\b");
+            } else if (c == '\f') {
+                sb.append("\\f");
+            } else if (c == '\n') {
+                sb.append("\\n");
+            } else if (c == '\r') {
+                sb.append("\\r");
+            } else if (c == '\t') {
+                sb.append("\\t");
+            } else if (Character.isISOControl(c)) {
+                sb.append(unicode(c));
+            } else {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Represent as unicode
+     * @param c character to be encoded
+     */
+    private String unicode(char c) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("\\u");
+
+        int n = c;
+
+        for (int i = 0; i < 4; ++i) {
+            int digit = (n & 0xf000) >> 12;
+
+            sb.append(hex[digit]);
+            n <<= 4;
+        }
+        return sb.toString();
+    }
+
+}

Added: struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/utils.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/utils.js?view=auto&rev=532928
==============================================================================
--- struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/utils.js (added)
+++ struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/utils.js Thu Apr 26 18:51:07 2007
@@ -0,0 +1,68 @@
+var StrutsUtils = {};
+
+// gets an object with validation errors from string returned by 
+// the ajaxValidation interceptor
+StrutsUtils.getValidationErrors = function(data) {
+  if(data.indexOf("/* {") == 0) {
+    return eval("( " + data.substring(2, data.length - 2) + " )");
+  } else {
+    return null;
+  }  
+};
+
+StrutsUtils.clearValidationErrors = function(form) {
+  var firstNode = StrutsUtils.firstElement(form);
+  var xhtml = firstNode.tagName.toLowerCase() == "table";
+  
+  if(xhtml) {
+    clearErrorMessagesXHTML(form);
+    clearErrorLabelsXHTML(form);
+  } else {
+    clearErrorMessagesCSS(form);
+    clearErrorLabelsCSS(form);
+  }
+};  
+
+// shows validation errors using functions from xhtml/validation.js
+// or css_xhtml/validation.js
+StrutsUtils.showValidationErrors = function(form, errors) {
+  StrutsUtils.clearValidationErrors(form, errors);
+
+  var firstNode = StrutsUtils.firstElement(form);
+  var xhtml = firstNode.tagName.toLowerCase() == "table";  
+  if(errors.fieldErrors) {
+    for(var fieldName in errors.fieldErrors) {
+      for(var i = 0; i < errors.fieldErrors[fieldName].length; i++) {
+        if(xhtml) {
+          addErrorXHTML(form.elements[fieldName], errors.fieldErrors[fieldName][i]);
+        } else {
+          addErrorCSS(form.elements[fieldName], errors.fieldErrors[fieldName][i]);
+        }  
+      }
+    }
+  }
+};
+
+
+StrutsUtils.firstElement  = function(parentNode, tagName) {
+  var node = parentNode.firstChild;
+  while(node && node.nodeType != 1){
+    node = node.nextSibling;
+  }
+  if(tagName && node && node.tagName && node.tagName.toLowerCase() != tagName.toLowerCase()) {
+    node = dojo.dom.nextElement(node, tagName);
+  }
+  return node;  
+};
+
+StrutsUtils.nextElement = function(node, tagName){
+  if(!node) { return null; }
+  do {
+    node = node.nextSibling;
+  } while(node && node.nodeType != 1);
+
+  if(node && tagName && tagName.toLowerCase() != node.tagName.toLowerCase()) {
+    return StrutsUtils.nextElement(node, tagName);
+  }
+  return node;  
+}

Modified: struts/struts2/trunk/core/src/main/resources/struts-default.xml
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/resources/struts-default.xml?view=diff&rev=532928&r1=532927&r2=532928
==============================================================================
--- struts/struts2/trunk/core/src/main/resources/struts-default.xml (original)
+++ struts/struts2/trunk/core/src/main/resources/struts-default.xml Thu Apr 26 18:51:07 2007
@@ -93,6 +93,7 @@
             <interceptor name="checkbox" class="org.apache.struts2.interceptor.CheckboxInterceptor" />
             <interceptor name="profiling" class="org.apache.struts2.interceptor.ProfilingActivationInterceptor" />
             <interceptor name="roles" class="org.apache.struts2.interceptor.RolesInterceptor" />
+            <interceptor name="jsonValidation" class="org.apache.struts2.interceptor.validation.JSONValidationInterceptor" />
 
             <!-- Basic stack -->
             <interceptor-stack name="basicStack">
@@ -108,6 +109,15 @@
             <interceptor-stack name="validationWorkflowStack">
                 <interceptor-ref name="basicStack"/>
                 <interceptor-ref name="validation"/>
+                <interceptor-ref name="workflow"/>
+            </interceptor-stack>
+            
+            <!-- Sample JSON validation stack -->
+            <interceptor-stack name="jsonValidationWorkflowStack">
+                <interceptor-ref name="basicStack"/>
+                <interceptor-ref name="jsonValidation">
+                    <param name="excludeMethods">input,back,cancel</param>
+                </interceptor-ref>
                 <interceptor-ref name="workflow"/>
             </interceptor-stack>
 

Modified: struts/struts2/trunk/core/src/main/resources/template/css_xhtml/validation.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/resources/template/css_xhtml/validation.js?view=diff&rev=532928&r1=532927&r2=532928
==============================================================================
--- struts/struts2/trunk/core/src/main/resources/template/css_xhtml/validation.js (original)
+++ struts/struts2/trunk/core/src/main/resources/template/css_xhtml/validation.js Thu Apr 26 18:51:07 2007
@@ -1,4 +1,8 @@
 function clearErrorMessages(form) {
+    clearErrorMessagesCSS(form);
+}
+
+function clearErrorMessagesCSS(form) {
 	// clear out any rows with an "errorFor" attribute
 	var divs = form.getElementsByTagName("div");
     var paragraphsToDelete = new Array();
@@ -19,6 +23,10 @@
 }
 
 function clearErrorLabels(form) {
+    clearErrorLabelsCSS(form);
+}
+
+function clearErrorLabelsCSS(form) {
     // set all labels back to the normal class
     var labels = form.getElementsByTagName("label");
     for (var i = 0; i < labels.length; i++) {
@@ -34,6 +42,10 @@
 }
 
 function addError(e, errorText) {
+    addErrorCSS(e, errorText);
+}
+
+function addErrorCSS(e, errorText) {
     try {
         var ctrlDiv = e.parentNode; // wwctrl_ div or span
         var enclosingDiv = ctrlDiv.parentNode; // wwgrp_ div

Modified: struts/struts2/trunk/core/src/main/resources/template/xhtml/validation.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/resources/template/xhtml/validation.js?view=diff&rev=532928&r1=532927&r2=532928
==============================================================================
--- struts/struts2/trunk/core/src/main/resources/template/xhtml/validation.js (original)
+++ struts/struts2/trunk/core/src/main/resources/template/xhtml/validation.js Thu Apr 26 18:51:07 2007
@@ -1,4 +1,8 @@
 function clearErrorMessages(form) {
+    clearErrorMessagesXHTML(form);
+}
+
+function clearErrorMessagesXHTML(form) {
 
     var table = form.childNodes[1];
     if( typeof table == "undefined" ) {
@@ -28,6 +32,10 @@
 }
 
 function clearErrorLabels(form) {
+    clearErrorLabelsXHTML(form);
+}
+
+function clearErrorLabelsXHTML(form) {
     // set all labels back to the normal class
     var elements = form.elements;
     for (var i = 0; i < elements.length; i++) {
@@ -45,6 +53,10 @@
 }
 
 function addError(e, errorText) {
+    addErrorXHTML(e, errorText);
+}
+
+function addErrorXHTML(e, errorText) {
     try {
         // clear out any rows with an "errorFor" of e.id
         var row = e.parentNode.parentNode;

Added: struts/struts2/trunk/core/src/test/java/org/apache/struts2/TestUtils.java
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/test/java/org/apache/struts2/TestUtils.java?view=auto&rev=532928
==============================================================================
--- struts/struts2/trunk/core/src/test/java/org/apache/struts2/TestUtils.java (added)
+++ struts/struts2/trunk/core/src/test/java/org/apache/struts2/TestUtils.java Thu Apr 26 18:51:07 2007
@@ -0,0 +1,96 @@
+/*
+ * $Id: TestResult.java 471756 2006-11-06 15:01:43Z husted $
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.StringTokenizer;
+
+/**
+ * Utility methods for test classes
+ *
+ */
+public class TestUtils {
+    /**
+     * normalizes a string so that strings generated on different platforms can be compared.  any group of one or more
+     * space, tab, \r, and \n characters are converted to a single space character
+     *
+     * @param obj the object to be normalized.  normalize will perform its operation on obj.toString().trim() ;
+     * @param appendSpace
+     * @return the normalized string
+     */
+    public static String normalize(Object obj, boolean appendSpace) {
+        StringTokenizer st =
+            new StringTokenizer(obj.toString().trim(), " \t\r\n");
+        StringBuffer buffer = new StringBuffer(128);
+
+        while(st.hasMoreTokens()) {
+            buffer.append(st.nextToken());
+        }
+
+        return buffer.toString();
+    }
+
+    
+    public static String normalize(URL url) throws Exception {
+        return normalize(readContent(url), true);
+    }
+    /**
+     * Attempt to verify the contents of text against the contents of the URL specified. Performs a
+     * trim on both ends
+     *
+     * @param url the HTML snippet that we want to validate against
+     * @throws Exception if the validation failed
+     */
+    public static boolean compare(URL url, String text)
+        throws Exception {
+        /**
+         * compare the trimmed values of each buffer and make sure they're equivalent.  however, let's make sure to
+         * normalize the strings first to account for line termination differences between platforms.
+         */
+        String writerString = TestUtils.normalize(text, true);
+        String bufferString = TestUtils.normalize(readContent(url), true);
+
+        return bufferString.equals(writerString);
+    }
+    
+    
+
+    public static String readContent(URL url)
+        throws Exception {
+        if(url == null) {
+            throw new Exception("unable to verify a null URL");
+        }
+
+        StringBuffer buffer = new StringBuffer(128);
+        InputStream in = url.openStream();
+        byte[] buf = new byte[4096];
+        int nbytes;
+
+        while((nbytes = in.read(buf)) > 0) {
+            buffer.append(new String(buf, 0, nbytes));
+        }
+
+        in.close();
+
+        return buffer.toString();
+    }
+}

Added: struts/struts2/trunk/core/src/test/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptorTest.java
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/test/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptorTest.java?view=auto&rev=532928
==============================================================================
--- struts/struts2/trunk/core/src/test/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptorTest.java (added)
+++ struts/struts2/trunk/core/src/test/java/org/apache/struts2/interceptor/validation/JSONValidationInterceptorTest.java Thu Apr 26 18:51:07 2007
@@ -0,0 +1,184 @@
+/*
+ * $Id$
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.interceptor.validation;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.struts2.StrutsStatics;
+import org.apache.struts2.StrutsTestCase;
+import org.apache.struts2.TestUtils;
+import org.apache.struts2.views.jsp.StrutsMockHttpServletRequest;
+import org.apache.struts2.views.jsp.StrutsMockHttpServletResponse;
+import org.apache.struts2.views.jsp.StrutsMockServletContext;
+
+import com.opensymphony.xwork2.Action;
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionSupport;
+import com.opensymphony.xwork2.mock.MockActionInvocation;
+import com.opensymphony.xwork2.mock.MockActionProxy;
+import com.opensymphony.xwork2.util.ValueStack;
+import com.opensymphony.xwork2.util.ValueStackFactory;
+import com.opensymphony.xwork2.validator.annotations.EmailValidator;
+import com.opensymphony.xwork2.validator.annotations.IntRangeFieldValidator;
+import com.opensymphony.xwork2.validator.annotations.StringLengthFieldValidator;
+import com.opensymphony.xwork2.validator.annotations.Validation;
+
+public class JSONValidationInterceptorTest extends StrutsTestCase {
+    private MockActionInvocation invocation;
+    private StringWriter stringWriter;
+    private TestAction action;
+    private StrutsMockHttpServletResponse response;
+    private JSONValidationInterceptor interceptor;
+    private StrutsMockHttpServletRequest request;
+
+    public void testValidationFails() throws Exception {
+
+        action.addActionError("General error");
+        interceptor.setValidationFailedStatus(HttpServletResponse.SC_BAD_REQUEST);
+        interceptor.intercept(invocation);
+
+        String json = stringWriter.toString();
+
+        String normalizedActual = TestUtils.normalize(json, true);
+        String normalizedExpected = TestUtils
+            .normalize(JSONValidationInterceptorTest.class.getResource("json-1.txt"));
+        //json
+        assertEquals(normalizedExpected, normalizedActual);
+        //execution
+        assertFalse(invocation.isExecuted());
+        //http status
+        assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
+    }
+
+    public void testValidationSucceeds() throws Exception {
+        JSONValidationInterceptor interceptor = new JSONValidationInterceptor();
+
+        action.setText("abcd@ggg.com");
+        action.setValue(10);
+
+        interceptor.intercept(invocation);
+
+        String json = stringWriter.toString();
+
+        String normalizedActual = TestUtils.normalize(json, true);
+        assertEquals("", normalizedActual);
+        assertTrue(invocation.isExecuted());
+    }
+    
+    public void testValidationSucceedsValidateOnly() throws Exception {
+        JSONValidationInterceptor interceptor = new JSONValidationInterceptor();
+
+        action.setText("abcd@ggg.com");
+        action.setValue(10);
+
+        //just validate
+        Map parameters = new HashMap();
+        parameters.put("validateOnly", "true");
+        request.setParameterMap(parameters);
+        
+        interceptor.intercept(invocation);
+
+        String json = stringWriter.toString();
+
+        String normalizedActual = TestUtils.normalize(json, true);
+        assertEquals("", normalizedActual);
+        assertFalse(invocation.isExecuted());
+    }
+
+    protected void setUp() throws Exception {
+        super.setUp();
+        this.action = new TestAction();
+        this.interceptor = new JSONValidationInterceptor();
+        
+        this.request = new StrutsMockHttpServletRequest();
+        stringWriter = new StringWriter();
+        PrintWriter writer = new PrintWriter(stringWriter);
+        this.response = new StrutsMockHttpServletResponse();
+        response.setWriter(writer);
+
+        ValueStack stack = ValueStackFactory.getFactory().createValueStack();
+        ActionContext context = new ActionContext(stack.getContext());
+
+        ActionContext.setContext(context);
+        context.put(StrutsStatics.HTTP_REQUEST, request);
+        context.put(StrutsStatics.HTTP_RESPONSE, response);
+
+        StrutsMockServletContext servletContext = new StrutsMockServletContext();
+
+        context.put(StrutsStatics.SERVLET_CONTEXT, servletContext);
+        invocation = new MockActionInvocation() {
+            private boolean executed;
+
+            public String invoke() throws Exception {
+                executed = true;
+                return "success";
+            }
+
+            public boolean isExecuted() {
+                return executed;
+            }
+        };
+        invocation.setAction(action);
+        invocation.setInvocationContext(context);
+        MockActionProxy proxy = new MockActionProxy();
+        proxy.setAction(action);
+        invocation.setProxy(proxy);
+    }
+
+    @Validation
+    public static class TestAction extends ActionSupport {
+        private String text = "x";
+        private int value = -10;
+
+        public String execute() {
+            return Action.SUCCESS;
+        }
+
+        @SkipValidation
+        public String skipMe() {
+            return "skipme";
+        }
+
+        public String getText() {
+            return text;
+        }
+
+        @StringLengthFieldValidator(minLength = "2", message = "Too short")
+        @EmailValidator(message = "This is no email")
+        public void setText(String text) {
+            this.text = text;
+        }
+
+        public int getValue() {
+            return value;
+        }
+
+        @IntRangeFieldValidator(min = "-1", message = "Min value is -1")
+        public void setValue(int value) {
+            this.value = value;
+        }
+    }
+}