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