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:
- JSF-RI: Specification Document
This is just what I need! Thanks!
Thanks a lot! That saved my day!!!
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?
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?
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.
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.
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.
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.
Thanks man, this solves my problem! PrettyFaces rocks
My pleasure, thank you!
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?
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!
Is this approach necessary when using JSF 2?
I am using the following code:
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 🙁
@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.
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:
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
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?
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.
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.
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.
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
I could prevent the exception with a try catch:
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
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.
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.
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):
Return a viewId different than the current one (i.e. redirect to another page). Do you see the effect then?
@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.
Thank you, thank you. This was a huge help to me!
This won’t work if I add a message and redirect to a different in a @PostConstruct method. Example:
If in paymentThankYou.xhtml I have a H: messages component, nothing gets displayed there.
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.
[…] 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 […]
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
Could you please post this question in the support forums? We will be able to help you more there:
http://ocpsoft.com/support/
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.
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 🙂
[…] http://ocpsoft.org/java/persist-and-pass-facesmessages-over-page-redirects/ Posted 20 minutes ago # […]
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!
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
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.
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?
To
Made
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.
Hi,
I have added the class in my faces.config.xml as below.
But it is not able to find my class.
How do I resolve this?
Still very useful, even in 2016!
I’d modify this part:
… to this instead, to remove duplicate messages: