Preserving Messages Across a Redirect in Struts 2

July 2, 2008

In Struts 2 when your result type is a redirect (or redirectAction) you will loose any error messages that your action created unless you take special steps to preserve them.

The standard response to the question “How do you preserve messages across a redirect?” is to point the person to the MessageStoreInterceptor“.  I took a look at using this in my own projects but frankly it was a major pain to configure it.  You need to manually configure the interceptor to store the messages in every action that might have a redirect result and you have to also configure it to retrieve the message for every action that is the target of the redirect…. yuck.  Too much work.

So I wrote the RedirectMessageInterceptor (see below) to simplify the solution.   It makes the assumption that you always want to preserve messages across a redirect and restore them to the next action if they exist.   The way it works is… it looks at the result type after a action has executed and if the result was a redirect (ServletRedirectResult) or a redirectAction (ServletActionRedirectResult) and there were any errors, messages, or fieldErrors they are stored in the session.  Before the next action executes it will check if there are any messages stored in the session and add them to the next action.

The one thing you need to be aware of is: The action you are redirecting towards will need to configure a result with name=”input” as the added messages will trigger the ‘workflow’ interceptor to return a result of “input”.

To use this you simply need to declare it and add it to your stack.  It extends MethodFilterInterceptor so you can configure ‘excludeMethods’ and ‘includeMethods’ (I haven’t been able to think of a good reason why you would want to do this but it comes for free so you can if you like.)

<interceptor name="redirectMessage"
      class="my.struts.interceptor.RedirectMessageInterceptor" />
<interceptor-stack name="myStack">
    <interceptor-ref name="redirectMessage" />
    <interceptor-ref name="paramsPrepareParamsStack" />
</interceptor-stack>
<default-interceptor-ref name="myStack" />

Here is the Interceptor feel free to use it or modify it as you see fit.

This has been updated to work with Struts 2.1.6. If you are using an earlier version of Struts you may need to add some @SuppressWarnings(“unchecked”)  annotations. 3/13/09

/*
 * Licensed 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 my.struts.interceptor;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.struts2.StrutsStatics;
import org.apache.struts2.dispatcher.ServletActionRedirectResult;
import org.apache.struts2.dispatcher.ServletRedirectResult;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.ValidationAware;
import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;

/**
 * An Interceptor to preserve an actions ValidationAware messages across a
 * redirect result.
 *
 * It makes the assumption that you always want to preserve messages across a
 * redirect and restore them to the next action if they exist.
 *
 * The way this works is it looks at the result type after a action has executed
 * and if the result was a redirect (ServletRedirectResult) or a redirectAction
 * (ServletActionRedirectResult) and there were any errors, messages, or
 * fieldErrors they are stored in the session. Before the next action executes
 * it will check if there are any messages stored in the session and add them to
 * the next action.
 *
 */
public class RedirectMessageInterceptor extends MethodFilterInterceptor
{
    private static final long  serialVersionUID    = -1847557437429753540L;

    public static final String FIELD_ERRORS_KEY    = "RedirectMessageInterceptor_FieldErrors";
    public static final String ACTION_ERRORS_KEY   = "RedirectMessageInterceptor_ActionErrors";
    public static final String ACTION_MESSAGES_KEY = "RedirectMessageInterceptor_ActionMessages";

    public RedirectMessageInterceptor()
    {
    }

    public String doIntercept(ActionInvocation invocation) throws Exception
    {
        Object action = invocation.getAction();
        if (action instanceof ValidationAware)
        {
            before(invocation, (ValidationAware) action);
        }

        String result = invocation.invoke();

        if (action instanceof ValidationAware)
        {
            after(invocation, (ValidationAware) action);
        }
        return result;
    }

    /**
     * Retrieve the errors and messages from the session and add them to the
     * action.
     */
    protected void before(ActionInvocation invocation,
                          ValidationAware validationAware)
        throws Exception
    {
        @SuppressWarnings("unchecked")
        Map<String, ?> session = invocation.getInvocationContext().getSession();

        @SuppressWarnings("unchecked")
        Collection<String> actionErrors =
            (Collection) session.remove(ACTION_ERRORS_KEY);
        if (actionErrors != null && actionErrors.size() > 0)
        {
            for (String error : actionErrors)
            {
                validationAware.addActionError(error);
            }
        }

        @SuppressWarnings("unchecked")
        Collection<String> actionMessages =
            (Collection) session.remove(ACTION_MESSAGES_KEY);
        if (actionMessages != null && actionMessages.size() > 0)
        {
            for (String message : actionMessages)
            {
                validationAware.addActionMessage(message);
            }
        }

        @SuppressWarnings("unchecked")
        Map<String, List<String>> fieldErrors =
            (Map) session.remove(FIELD_ERRORS_KEY);
        if (fieldErrors != null && fieldErrors.size() > 0)
        {
            for (Map.Entry<String, List<String>> fieldError : fieldErrors.entrySet())
            {
                for (String message : fieldError.getValue())
                {
                    validationAware.addFieldError(fieldError.getKey(), message);
                }
            }
        }
    }

    /**
     * If the result is a redirect then store error and messages in the session.
     */
    protected void after(ActionInvocation invocation,
                         ValidationAware validationAware)
        throws Exception
    {
        Result result = invocation.getResult();

        if (result != null 
            && (result instanceof ServletRedirectResult ||
                result instanceof ServletActionRedirectResult))
        {
            Map<String, Object> session = invocation.getInvocationContext().getSession();

            Collection<String> actionErrors = validationAware.getActionErrors();
            if (actionErrors != null && actionErrors.size() > 0)
            {
                session.put(ACTION_ERRORS_KEY, actionErrors);
            }

            Collection<String> actionMessages = validationAware.getActionMessages();
            if (actionMessages != null && actionMessages.size() > 0)
            {
                session.put(ACTION_MESSAGES_KEY, actionMessages);
            }

            Map<String, List<String>> fieldErrors = validationAware.getFieldErrors();
            if (fieldErrors != null && fieldErrors.size() > 0)
            {
                session.put(FIELD_ERRORS_KEY, fieldErrors);
            }
        }
    }
}
Advertisements

Unit Testing Struts 2 Actions

June 30, 2008

Hopefully this will help others who are trying to unit test Struts 2 Actions.

My goal is to be able to unit test my actions in the full Struts 2 context with the Interceptor stack being run which includes validation.  The big advantage of this type of testing is that it allows you test you validation logic, the Interceptor configuration for you actions, and the results configuration.

The current information on Struts 2 website regarding unit testing was not very helpful.  The guides page has 2 links to external blogs with some example code for unit testing with Spring.  I used these as starting points but since I’m not using Spring and the examples were heavily dependent on Spring I ended up spending a lot of time in the debugger figuring out how to make this work.

Below is my StrutsTestContext class it makes use of Mockrunner mock Http Servlet classes (mockrunner-servlet.jar).  (If you wish to use a different mock package it should be easy enough to make the change.)

The way this works is you first have to create a Dispatcher which reads your struts configuration. You then use the Dispatcher to create an ActionProxy with the desired request parameters and session attributes.  The ActionProxy will give you access to the Action object so you can set properties or inject mock objects for your test.  You next execute the ActionProxy to run the Interceptor stack and your action, this returns the result so you can test it for correctness.  You can also test the mock Http servlet objects to ensure other result effects have occured (e.g. a session attribute was changed.)

This has been updated for Struts 2.1.6 on 3/5/2009.

/*
 * Licensed 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 test.struts;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.dispatcher.Dispatcher;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import com.mockrunner.mock.web.MockHttpServletRequest;
import com.mockrunner.mock.web.MockHttpServletResponse;
import com.mockrunner.mock.web.MockHttpSession;
import com.mockrunner.mock.web.MockServletContext;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.ActionProxyFactory;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.ValueStackFactory;

public class StrutsTestContext
{
    public StrutsTestContext(Dispatcher dispatcher,
                             MockServletContext servletContext)
    {
        this.dispatcher = dispatcher;
        this.mockServletContext = servletContext;
    }

    private Dispatcher              dispatcher;
    private MockServletContext      mockServletContext;
    private MockHttpServletRequest  mockRequest;
    private MockHttpServletResponse mockResponse;

    public static Dispatcher prepareDispatcher()
    {
        return prepareDispatcher(null, null);
    }

    public static Dispatcher prepareDispatcher(
           ServletContext servletContext,
           Map<String, String> params)
    {
        if (params == null)
        {
            params = new HashMap<String, String>();
        }
        Dispatcher dispatcher = new Dispatcher(servletContext, params);
        dispatcher.init();
        Dispatcher.setInstance(dispatcher);
        return dispatcher;
    }

    public static ActionProxy createActionProxy(
          Dispatcher dispatcher,
          String namespace,
          String actionName,
          HttpServletRequest request,
          HttpServletResponse response,
          ServletContext servletContext) throws Exception
    {
        // BEGIN: Change for Struts 2.1.6
        Container container = dispatcher.getContainer();
        ValueStack stack = container.getInstance(ValueStackFactory.class).createValueStack();
        ActionContext.setContext(new ActionContext(stack.getContext()));
        // END: Change for Struts 2.1.6

        ServletActionContext.setRequest(request);
        ServletActionContext.setResponse(response);
        ServletActionContext.setServletContext(servletContext);

        ActionMapping mapping = null;
        return dispatcher.getContainer()
           .getInstance(ActionProxyFactory.class)
           .createActionProxy(
            namespace,
            actionName,
            dispatcher.createContextMap(
                request, response, mapping, servletContext),
            true, // execute result
            false);
    }

    public ActionProxy createActionProxy(
               String namespace,
               String actionName,
               Map<String, String> requestParams,
               Map<String, Object> sessionAttributes) throws Exception
    {
        mockRequest = new MockHttpServletRequest();
        mockRequest.setSession(new MockHttpSession());
        mockResponse = new MockHttpServletResponse();

        if (requestParams != null)
        {
            for (Map.Entry<String, String> param :
                       requestParams.entrySet())
            {
                mockRequest.setupAddParameter(param.getKey(),
                                              param.getValue());
            }
        }
        if (sessionAttributes != null)
        {
            for (Map.Entry<String, ?> attribute :
                      sessionAttributes.entrySet())
            {
                mockRequest.getSession().setAttribute(
                    attribute.getKey(),
                    attribute.getValue());
            }
        }

        return createActionProxy(
            dispatcher, namespace, actionName,
            mockRequest, mockResponse, mockServletContext);
    }

    public Dispatcher getDispatcher()
    {
        return dispatcher;
    }

    public MockHttpServletRequest getMockRequest()
    {
        return mockRequest;
    }

    public MockHttpServletResponse getMockResponse()
    {
        return mockResponse;
    }

    public MockServletContext getMockServletContext()
    {
        return mockServletContext;
    }

}

Here is an example of using this class to unit test a Login Action.

/*
** Create a Dispatcher.
** This is an expensive operation as it has to load all
** the struts configuration so you will want to reuse the Dispatcher for
** multiple tests instead of re-creating it each time.
**
** In this example I'm setting configuration parameter to override the
** values in struts.xml.
*/
  HashMap<String, String> params = new HashMap<String, String>();
  // Override struts.xml config constants to use a guice test module
  params.put("struts.objectFactory", "guice");
  params.put("guice.module", "test.MyModule");

  MockServletContext servletContext = new MockServletContext();
  Dispatcher dispatcher = StrutsTestContext.prepareDispatcher(
       servletContext, params);

/*
**  Create an ActionProxy based on the namespace and action.
**  Pass in request parameters and session attributes needed for this
**  test.
*/
  StrutsTestContext context = new StrutsTestContext(
      dispatcher, servletContext);
  Map<String, String> requestParams = new HashMap<String, String>();
  Map<String, Object> sessionAttributes = new HashMap<String, Object>();
  requestParams.put("username", "test");
  requestParams.put("password", "test");

  ActionProxy proxy = context.createActionProxy(
      "/admin",      // namespace
      "LoginSubmit", // Action
      requestParams,
      sessionAttributes);

  assertTrue(proxy.getAction() instanceof LoginAction);

  // Get the Action object from the proxy
  LoginAction action = (LoginAction) proxy.getAction();

  // Inject any mock objects or set any action properties needed
  action.setXXX(new MockXXX());

  // Run the Struts Interceptor stack and the Action
  String result = proxy.execute();

  // Check the results
  assertEquals("success", result);

  // Check the user was redirected as expected
  assertEquals(true, context.getMockResponse().wasRedirectSent());
  assertEquals("/admin/WelcomeUser.action",
      context.getMockResponse().getHeader("Location"));

  // Check the session Login object was set
  assertEquals(mockLogin,
      context.getMockRequest().getSession().getAttribute(
         Constants.SESSION_LOGIN));