"Simple Software"

OcpSoft

Persist and pass FacesMessages over multiple page redirects

September 18th, 2008 by Lincoln

Very Simple

In a JSF Reference Implementation, passing global faces messages between pages doesn’t work. It’s not designed that way “out of the box.” Fortunately there is a way to do this, which will even support redirects between pages, forwards through a RequestDispatcher, and also through standard JSF navigation cases.

There is a 5 minute solution to this problem.

Messages should be displayed IF:

  • …the RENDER_RESPONSE phase has been reached, and JSF completed all phases “naturally.” This means that messages should not be displayed if the HttpResponse has been completed BEFORE the RENDER_RESPONSE phase has been reached.
  • …the RENDER_RESPONSE phase is reached, and the HttpResponse is already completed, then FacesMessages could not have been rendered; they need to be saved again for the next RENDER_RESPONSE phase.

I found an almost solution to this problem in a mailing list that I’ve long since forgotten, but I saved the original accreditation, fixed the bugs (messages would not originally save through a redirect,) and here you go.

It’s a MultiPageMessagesSupport PhaseListener:

Copy this file into your project classpath.

package com.yoursite.jsf;
 
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
 
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
 
/**
 * Enables messages to be rendered on different pages from which they were set.
 *
 * After each phase where messages may be added, this moves the messages
 * from the page-scoped FacesContext to the session-scoped session map.
 *
 * Before messages are rendered, this moves the messages from the
 * session-scoped session map back to the page-scoped FacesContext.
 *
 * Only global messages, not associated with a particular component, are
 * moved. Component messages cannot be rendered on pages other than the one on
 * which they were added.
 *
 * To enable multi-page messages support, add a <code>lifecycle</code> block to your
 * faces-config.xml file. That block should contain a single
 * <code>phase-listener</code> block containing the fully-qualified classname
 * of this file.
 *
 * @author Jesse Wilson jesse[AT]odel.on.ca
 * @secondaryAuthor Lincoln Baxter III lincoln[AT]ocpsoft.com 
 */
public class MultiPageMessagesSupport implements PhaseListener
{
 
    private static final long serialVersionUID = 1250469273857785274L;
    private static final String sessionToken = "MULTI_PAGE_MESSAGES_SUPPORT";
 
    public PhaseId getPhaseId()
    {
        return PhaseId.ANY_PHASE;
    }
 
    /*
     * Check to see if we are "naturally" in the RENDER_RESPONSE phase. If we
     * have arrived here and the response is already complete, then the page is
     * not going to show up: don't display messages yet.
     */
    // TODO: Blog this (MultiPageMessagesSupport)
    public void beforePhase(final PhaseEvent event)
    {
        FacesContext facesContext = event.getFacesContext();
        this.saveMessages(facesContext);
 
        if (PhaseId.RENDER_RESPONSE.equals(event.getPhaseId()))
        {
            if (!facesContext.getResponseComplete())
            {
                this.restoreMessages(facesContext);
            }
        }
    }
 
    /*
     * Save messages into the session after every phase.
     */
    public void afterPhase(final PhaseEvent event)
    {
        if (!PhaseId.RENDER_RESPONSE.equals(event.getPhaseId()))
        {
            FacesContext facesContext = event.getFacesContext();
            this.saveMessages(facesContext);
        }
    }
 
    @SuppressWarnings("unchecked")
    private int saveMessages(final FacesContext facesContext)
    {
        List<FacesMessage> messages = new ArrayList<FacesMessage>();
        for (Iterator<FacesMessage> iter = facesContext.getMessages(null); iter.hasNext();)
        {
            messages.add(iter.next());
            iter.remove();
        }
 
        if (messages.size() == 0)
        {
            return 0;
        }
 
        Map<String, Object> sessionMap = facesContext.getExternalContext().getSessionMap();
        List<FacesMessage> existingMessages = (List<FacesMessage>) sessionMap.get(sessionToken);
        if (existingMessages != null)
        {
            existingMessages.addAll(messages);
        }
        else
        {
            sessionMap.put(sessionToken, messages);
        }
        return messages.size();
    }
 
    @SuppressWarnings("unchecked")
    private int restoreMessages(final FacesContext facesContext)
    {
        Map<String, Object> sessionMap = facesContext.getExternalContext().getSessionMap();
        List<FacesMessage> messages = (List<FacesMessage>) sessionMap.remove(sessionToken);
 
        if (messages == null)
        {
            return 0;
        }
 
        int restoredCount = messages.size();
        for (Object element : messages)
        {
            facesContext.addMessage(null, (FacesMessage) element);
        }
        return restoredCount;
    }
}

Configuration:

This needs to be in your faces-config.xml.

		<phase-listener>
			com.yoursite.jsf.MultiPageMessagesSupport
		</phase-listener>

That’s it. You’re done.

References:

Post to Twitter Post to Delicious Post to Digg Post to StumbleUpon

Posted in JSF, Java

11 Comments

  1. This is just what I need! Thanks!

  2. rua says:

    Thanks a lot! That saved my day!!!

  3. Thom says:

    Works great! One question: I get duplicated FacesMessages when returning a String value of null from a JSF action method, to indicate the previous page should be rerendered. Returning a specific String works fine. Is there a way to work around this, or is it generally bad practice to return null from such methods?

  4. Lincoln says:

    Hmmm… Can you check to see if the method is being called twice? If so… why? What phases? It may depend on your implementation of JSF… You’re probably using MyFaces?

  5. David Causse says:

    Hi,

    Very nice, helped a lot…
    I tried to implement a CDI version of your idea, see my questions on the weld forum :
    http://www.seamframework.org/Community/PersistAndPassFacesMessagesOverMultiplePageRedirects

    Thank you.

  6. Kaja says:

    for (Iterator iter = facesContext.getMessages(null); iter.hasNext();) {
    messages.add(iter.next());
    iter.remove();
    }

    iter.remove(); doesn’t work on MyFaces, it’s probably because MyFaces return copy of collection so you remove item from copy not from orginal collection.

    First message is saved in INVOKE_APPLICATION, second in RENDER_RESPONSE and also there is orginal message in facesContext so I get 3 same messages on page after all.

    1. Lincoln says:

      You could add a check to see if the message already exists. Only add the message if it is not already present in the list.

  7. Jonathan DB says:

    Thanks man, this solves my problem! PrettyFaces rocks

    1. Lincoln says:

      My pleasure, thank you!

  8. Thanks Lincoln, this looks great! – it should tide me over until the Seam 3 Faces module is ready.

    This will work with JSF2 won’t it?

    1. Lincoln says:

      Sure will :) I’m actually writing up the docs on the Seam 3 docs right now. Also — those silly exceptions should be resolved in Alpha 3, as well. Thanks again for your help!

Leave a Comment




Please note: In order to submit code or special characters, wrap it in <pre lang="java"> </pre> - or your tags will be eaten.

Please note: Comment moderation is enabled and may delay your comment from appearing. There is no need to resubmit your comment.

Search

Recent Posts

Recent Comments

OcpSoft Agile Management

Recent Tweets:

Posting tweet...

Sponsored By:

Resources

Meta