September 18th, 2008 by Lincoln Baxter III

Persist and pass FacesMessages over multiple page redirects

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:

Posted in Java, JSF

41 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.

      1. Richard says:

        This solution will only remove one of the messages so you end up with only 2 messages. This is because like Kaja mentioned, It seems like MyFaces returns only a copy of the messages and not the original ones.

  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!

      1. Andreas Christoforides says:

        Is this approach necessary when using JSF 2?

        I am using the following code:

        FacesContext facesContext = FacesContext.getCurrentInstance();
        ExternalContext externalContext = facesContext.getExternalContext();
        Flash flash = externalContext.getFlash();
        flash.setKeepMessages(true);

        The above code allows me to add FacesMessages in the queue that show after a post-redirect-get. However, this only works when performing the redirect using ExternalContext.redirect() and does NOT work when PrettyFaces performs the redirect navigation 🙁

      2. Lincoln says:

        @andreas – well, the thing is that the JSF2 flash is really not a complete solution, it does not work in this use-case. Seam Faces provides a custom FlashContext that does address the redirecting navigation issue – similar to (but more advanced than) the solution I posted in this article.

  9. Nice – You are right. I was happy to see the JSF2 flash message implementation but when I started using it, I came to know that it is not that useful. I used to implement my own Flash message by adding a utility method that saved the messages to the Session like:

    public String doAddUser() {
       //create user and populate...
       service.addUser(user)
       FacesUtil.addFlashMessage(FacesUtil.createInfoMessage("user.ADD_SUCCESS", new Object[] {user.name}));
       return SiteDirectory.LIST_USER + "?faces-redirect=true";
    }

    I use the getter of an dummy tag in template to copy the FacesMessage from the session. Mine looked more like what they do in rails or some php framework and is done on a “need” basis. However yours is cleaner since it removes unnecessary code from the action method.

    Hari Gangadharan

  10. Ryan says:

    This solution won’t work for JSF 2 View Parameters because when a view parameter fails validation or conversion a FacesMessage is automatically created, but with a clientId. This is unfortunate since only messages without cilentIds are saved. I’d like to redirect users in a preRenderView system event if they try making a GET request on a page with view parameters which are invalid. Any suggestions?

    1. Ryan says:

      Note: one workaround is to define a collection of clientIds which should have associated FacesMessages saved and add the view parameter clientIds to this collection.

  11. Ryan says:

    Another issue is that the MultiPageMessageSupport phase listener doesn’t save messages which were added in a preRenderView system event. This means if I have any multi-component validation that I perform on my view parameters in a preRenderView system event I must call the MultiPageMessageSupport.saveMessages method manually before executing a redirect.

    1. Ryan says:

      Note: another place where the MultiPageMessageSupport phase listener won’t save your messages is in a custom exception handler (ExceptionHandlerWrapper). Again, a workaround is to call the saveMessages method manually.

  12. Ulrich says:

    Hi,

    this works great except in the following case:
    when a user logs out, I try to add a message like “You logged out successfully”, call request.logout() and request.getSession().invalidate(). Then I send a redirect to the welcome page.

    I get

    12:43:36,474 ERROR [org.apache.catalina.core.ContainerBase.[jboss.web].[localhost].[/myapp].[Faces Servlet]] Servlet.service() for servlet Faces Servlet threw exception: java.lang.IllegalStateException: Cannot create a session after the response has been committed
    	at org.apache.catalina.connector.Request.doGetSession(Request.java:2576) [:6.0.0.Final]
    	at org.apache.catalina.connector.Request.getSession(Request.java:2315) [:6.0.0.Final]
    	at org.apache.catalina.connector.RequestFacade.getSession(RequestFacade.java:841) [:6.0.0.Final]
    	at com.sun.faces.context.SessionMap.getSession(SessionMap.java:235) [:2.1.1-FCS]
    	at com.sun.faces.context.SessionMap.put(SessionMap.java:126) [:2.1.1-FCS]
    	at com.sun.faces.context.SessionMap.put(SessionMap.java:61) [:2.1.1-FCS]
    	at com.myapp.web.phaselistener.MultiPageMessagesSupport.saveMessages(MultiPageMessagesSupport.java:101) [:]

    I could prevent the exception with a try catch:

         try {
    							sessionMap.put(sessionToken, messages);
    						} catch (IllegalStateException e) {
    							e.printStackTrace();
    						}

    But the message is lost. The problem is that it tries to create a new session and can’t because the response is already committed due to the redirect.

    Thanks

    1. Ah yes. For this use-case you would need something like a @RenderScoped bean to handle the message passing, since the session is obliterated. Seam Faces has such a feature, and also integrates with PrettyFaces 🙂 http://seamframework.org/Seam3/FacesModule

      Just @Inject Messages messages;

      Then messages.info(“My message”); and it will automatically be saved over any redirect. This is really a superior feature to PrettyFaces MultiPageMessagesSupport, but might be worth porting eventually.

      1. Ulrich says:

        Thanks for the reply. Unfortunately, in this project I use neither Seam nor PrettyFaces. But I found a workaround using the application map, see below. In theory, a conflict between two sessions could occur, but it is highly unlikely nor would it be critical.

        	@SuppressWarnings("unchecked")
        	private int saveMessages(final FacesContext facesContext) {
        		List messages = new ArrayList();
        		for (Iterator iter = facesContext.getMessages(null); iter
        				.hasNext();) {
        			messages.add(iter.next());
        			iter.remove();
        		}
         
        		if (messages.size() == 0) {
        			return 0;
        		}
         
        		Map sessionMap = facesContext.getExternalContext()
        				.getSessionMap();
        		List existingMessages = (List) sessionMap
        				.get(sessionToken);
        		if (existingMessages != null) {
        			existingMessages.addAll(messages);
        		}
        		else {
        			try {
        				sessionMap.put(sessionToken, messages);
        			} catch (IllegalStateException e) {
        				Map map = facesContext.getExternalContext()
        						.getApplicationMap();
        				List listMsg = (List) map.get(sessionToken);
        				if (listMsg == null) {
        					listMsg = new ArrayList(1);
        					map.put(sessionToken, listMsg);
        				}
        				else {
        					listMsg.clear();
        				}
        				listMsg.addAll(messages);
        			}
        		}
        		return messages.size();
        	}
         
        	@SuppressWarnings("unchecked")
        	private int restoreMessages(final FacesContext facesContext) {
        		Map sessionMap = facesContext.getExternalContext()
        				.getSessionMap();
        		List messages = (List) sessionMap
        				.remove(sessionToken);
         
        		if (messages == null) {
        			messages = (List) facesContext.getExternalContext()
        					.getApplicationMap().remove(sessionToken);
        			if (messages == null) {
        				return 0;
        			}
        		}
         
        		int restoredCount = messages.size();
        		for (Object element : messages) {
        			facesContext.addMessage(null, (FacesMessage) element);
        		}
        		return restoredCount;
        	}
    2. Ryan says:

      Hmm, I didn’t do anything special and I have a logout action that maintains the FacesMessages after a redirect (refresh current page: page is readonly when not logged in). I wonder why mine works (or rather why yours doesn’t)? Is it because I’m using the Servlet API to logout? Is it because I’m using GlassFish 3.1 instead of Tomcat? Or maybe because I’m invoking the action with Ajax (PrimeFaces p:commandLink)? Here is the request scoped bean code (action controller):

        public String logout() throws ServletException {
          FacesContext fc = FacesContext.getCurrentInstance();
          ExternalContext ec = fc.getExternalContext();
          HttpServletRequest request = (HttpServletRequest) ec.getRequest();
          request.logout;
          request.getSession().invalidate();
          fc.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Logout Successful", null));
          String viewId = fc.getViewRoot().getViewId();
          return viewId + "?faces-redirect=true&amp;includeViewParams=true"
        }
      1. Ulrich says:

        Return a viewId different than the current one (i.e. redirect to another page). Do you see the effect then?

      2. Ryan says:

        @Ulrich – Changing the viewId to something else (like to index.xhtml) doesn’t affect the outcome. The difference appears to be Ajax. If I invoke my logout action as a normal post (not Ajax) then I get the exception about not being able to create a new session once the response has already been committed. I do not get the exception if I use Ajax.

  13. Mike Dalsey says:

    Thank you, thank you. This was a huge help to me!

  14. Goutham says:

    This won’t work if I add a message and redirect to a different in a @PostConstruct method. Example:

    // This is a method in requestScoped bean and gets called from a payment gateway.
     @PostConstruct
        public void readPaymentResponse() throws IOException{
            logger.info("Post construct called");
            logger.info("order id "+orderId);
            
            addInfo("Your payment has been successfully made . Thank you!");
            
            FacesContext.getCurrentInstance().getExternalContext().redirect("/myApp/paymentThankYou.jsf");
    
       

    If in paymentThankYou.xhtml I have a H: messages component, nothing gets displayed there.

    1. Ryan says:

      As mentioned in other comments above there are some scenarios where the phase listener won’t get a chance to run so your messages won’t get automatically saved.

      Executing a redirect sounds like a good way to prevent phase listeners from running. The workaround is to manually call the save messages method. It is also a good idea to separate the phase listener and the message handling stuff into separate classes.

  15. […] I found a “solution” here: http://ocpsoft.com/java/persist-and-pass-facesmessages-over-page-redirects/ Seems to work pretty well. Still I have no idea why my code isn’t working. I mean it’s […]

  16. Cristiano says:

    Using prettyfaces MultiPageMessagesSupport on App Engine causes this error:

    javax.faces.FacesException: java.lang.NullPointerException: serialFactory
    at org.apache.myfaces.shared_impl.context.ExceptionHandlerImpl.wrap(ExceptionHandlerImpl.java:241)

    I cant figure it out

    1. Could you please post this question in the support forums? We will be able to help you more there:

      http://ocpsoft.com/support/

  17. Ryan says:

    Here is a scenario that I don’t think will work with this phase listener. I have a shopping cart scenario where the user is sent to a 3rd party payment system, then returns. The 3rd party site sends the user’s browser a redirect to my servlet with URL parameters. My servlet processes the data, then decides which screen to display next. When it needs to go to an error screen I want to add a FacesMessage, but I can’t because I’m in my own servlet, so FacesContext.getCurrentInstance() returns null.

  18. Raúl says:

    Hi Lincon
    I had the message problem after a redirect, and I solved it with your code but with a little modify.
    Here’s what I did.
    My scenario was a simple list that goes to the detail of an element.
    After insert in my project your PhaseListener ,I notice that the messages were not showed and debugging I noticed that passed two times into the method restoreMessages().
    At second access, the message list was always empty.
    When I inspected the object facescontext of the two calls, I noticed that the only difference between them was that the value of property renderReponse.
    So I added the condition && !facesContext.getRenderResponse() in the method beforePhase before restoring messages and seems to work fine.
    What do you think about my solution?
    Just for info, I work in a project with Icefaces 1.8 and OC4J as server
    Thanks a lot for your code, you saved me 🙂

  19. Luis Claudio says:

    It’s not working: (

    Here my code:

    bean:
    [Sourcecode lang = “xml”]
    public String save () {

    DAO dao = new DAO (Sistema.class);
    usuarioModel.getSource users = ();
    usuariosSelecionados usuarioModel.getTarget = ();

    if (sistema.getIdSistema () == null) {
    sistema.setUsuarios (usuariosSelecionados);
    dao.adiciona (system);
    FacesContext.getCurrentInstance (). AddMessage (“cadastro_sistema: msgSistema”, new FacesMessage (FacesMessage.SEVERITY_INFO, “System” sistema.getNomeSistema + () + “registered successfully.”, “”));
    this.sistema = new System ();
    else {}
    sistema.setUsuarios (usuariosSelecionados);
    dao.atualiza (system);
    FacesContext.getCurrentInstance (). AddMessage (“cadastro_sistema: msgSistema”, new FacesMessage (FacesMessage.SEVERITY_INFO, “System” sistema.getNomeSistema + () + “changed successfully.”, “”));
    this.sistema = new System ();
    }
    = new systems DAO (Sistema.class). listaTodos ();
    this.sistema = new System ();
    return “cadastrosistema? faces-redirect = true”;
    }

    
    

    MultiPageMessagesSupport I created the class in a given package of my project and soon after added this code in faces-config.xml …
    [Sourcecode lang = “xml”]

    br.com.sisapropriacao.util.MultiPageMessagesSupport

    
    

    The message is not appearing. Thank you in advance!

  20. Benoit says:

    Hi and thanks for this great post!

    is it possible so give me an example on how to implement it in the flow so I can show a message saying the page was save when I change from a flow to another

    example from profile page (saving you first name and last name) then going back to the main menu and saying You profile was saved succesfully.

    Thanks soo much!

    BenBen

  21. Note that for MyFaces, you not only have to check for duplicates before adding a message in restore, but you also have to NOT call iter.remove() in save.

    remove() will only remove the clientID/message association, not the message, leaving a duplicate message without a clientID association (the partially removed message won’t be considered a global message either).

    If you were to add() the message back checking for duplicates without redirecting, the message will exist, so the add() would fail, even though it exists as a disassociated message.

    This will result in X:messages globalOnly="true" never picking up such a message.

    You can also hit ConcurrentModificationException during restore if you are adding to the list you are iterating over when checking for duplicates, so be sure to make a copy of the original list for comparing.

  22. James says:

    Great work and many thanks!

    I slightly modified one line, to be able to save the messages before a redirect, since I didn’t figured how to call MultiPageMessageSupport.saveMessages method manually. Since I’m not a guru I was wondering if this could have any (bad) side effects?

    if (!PhaseId.RENDER_RESPONSE.equals(event.getPhaseId()))

    To

    if (facesContext.getResponseComplete() || !PhaseId.RENDER_RESPONSE.equals(event.getPhaseId()))

    Made

    1. That might be possible. I don’t know of any bad effects off the top of my head, but if it is working for you, then it seems like a viable solution.

  23. Vandana Acharya says:

    Hi,
    I have added the class in my faces.config.xml as below.

    <?xml version="1.0" encoding="UTF-8"?>
    <faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee"
    	xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
    	<application>
    		<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
    		<locale-config>
    			<default-locale>en</default-locale>
    		</locale-config>
    	</application>
    	<lifecycle>
    		<phase-listener>com.fhlbny.ipms.action.MultiPageMessagesSupport</phase-listener>
    	</lifecycle>
    </faces-config>

    But it is not able to find my class.

    How do I resolve this?

  24. Franka says:

    Still very useful, even in 2016!

    I’d modify this part:

    existingMessages.addAll(messages);

    … to this instead, to remove duplicate messages:

    for(FacesMessage fm: messages){
            if(!existingMessages.contains(fm)){
              existingMessages.add(fm);
            }
          }

Reply to David Causse




Please note: In order to submit code or special characters, wrap it in

[code lang="xml"][/code]
(for your language) - 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.