Sharing Our Passion for Technology
& continuous learning
〈  Back to Blog

Rendering Global t:messages After Redirect

A common problem when working with JSF is getting global info messages  via <t:messages globalOnly="true"> or <f:messages globalOnly="true"> to display messages set in the previous request when you have a <redirect/> in your faces-config for a particular page You will not see your <t:messages> that are set on the previous page.

The Problem

For instance, say you have two pages - page1.xhtml and page2.xhtml. In your faces-config.xml, you will have 2 entries.

<navigation-case>
	<from-outcome>page1</from-outcome>
	<to-view-id>/pages/page1.xhtml</to-view-id>
	<redirect/>
</navigation-case>

<navigation-case>
	<from-outcome>page2</from-outcome>
	<to-view-id>/pages/page2.xhtml</to-view-id>
	<redirect/>
</navigation-case>

Let’s say page1Bean is called in page1 and you want it to display messages in page2. So your bean would have something like this:

public class page1Bean {
	...
	public String page1Action{
		...
		/** Add message */
		FacesMessage facesMessage = new FacesMessage();
		...
		FacesContext.getCurrentInstance().addMessage(null, facesMessage);
		...
		return "page2";
	}
	...
}

Your page2.xhtml has like the following somewhere in it.

<t:messages globalOnly="true">

One would expect that this would display the message on the next page, however these messages are request scoped and so are not available in this second request and you do not see your message unless you remove the on page2.

Fortunately, there is a nifty and quick solution. Basically, it is a phase listener that saves the messages from the previous request and then restores them just before the RENDER_RESPONSE phase of the second request.

The Solution

To solve the problem, save the following class into some package where your faces-config will be able to access it

package com.myproject.web.jsf.phaselistener;

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.
 * To produce this behaviour, this class acts as a <code>PhaseListener</code>.
 *
 * This is performed by moving the FacesMessage objects:
 *
	<li>After each phase where messages may be added, this moves the messages from
 * the page-scoped FacesContext to the session-scoped session map.
 *</li>
	<li>Before messages are rendered, this moves the messages from the session-scoped
 * session map back to the page-scoped FacesContext.
 *</li>
 * Only messages that are not associated with a particular component are ever
 * moved. These are the only messages that can be rendered on a page that is different
 * from where they originated.
 *
 * To enable this behaviour, 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.
 *
 * EDIT: This code was minimally modified by Max Kuipers to address some of the Java 1.6
 * compiler warnings.  All code was originally written by Jesse Wilson.
 *
 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>
 * @author <a href="mailto:mkuipers@sourceallies.com">Max Kuipers</a>
 */

public class MessageHandler implements PhaseListener {
	private static final long serialVersionUID = 1L;

	/**
	 * a name to save messages in the session under
	 */
	private static final String sessionToken = "MULTI_PAGE_MESSAGES_SUPPORT";

	/**
	 * Return the identifier of the request processing phase during which this
	 * listener is interested in processing PhaseEvent events.
	 */
	public PhaseId getPhaseId() {
		return PhaseId.ANY_PHASE;
	}

	/**
	 * Handle a notification that the processing for a particular phase of the
	 * request processing lifecycle is about to begin.
	 */
	public void beforePhase(PhaseEvent event) {

		if(event.getPhaseId() == PhaseId.RENDER_RESPONSE) {
			FacesContext facesContext = event.getFacesContext();
			restoreMessages(facesContext);
		}
	}

	/**
	 * Handle a notification that the processing for a particular phase has just
	 * been completed.
	 */
	public void afterPhase(PhaseEvent event) {

		if(event.getPhaseId() == PhaseId.APPLY_REQUEST_VALUES ||
				event.getPhaseId() == PhaseId.PROCESS_VALIDATIONS ||
				event.getPhaseId() == PhaseId.INVOKE_APPLICATION) {

			FacesContext facesContext = event.getFacesContext();
			saveMessages(facesContext);
		}

	}

	/**
	 * Remove the messages that are not associated with any particular component
	 * from the faces context and store them to the user's session.
	 *
	 * @return the number of removed messages.
	 */
	private int saveMessages(FacesContext facesContext) {
		// remove messages from the context
		List messages = new ArrayList();
		for(Iterator i = facesContext.getMessages(null); i.hasNext(); ) {
			messages.add(i.next());
			i.remove();
		}
		// store them in the session
		if(messages.size() == 0) {
			return 0;
		}
		Map sessionMap = facesContext.getExternalContext().getSessionMap();
		// if there already are messages
		@SuppressWarnings("unchecked")
		List existingMessages = (List) sessionMap.get(sessionToken);
		if(existingMessages != null) {
			existingMessages.addAll(messages);
		}
		else {
			sessionMap.put(sessionToken, messages); // if these are the first messages
		}

		return messages.size();
	}

	/**
	 * Remove the messages that are not associated with any particular component
	 * from the user's session and add them to the faces context.
	 *
	 * @return the number of removed messages.
	 */
	private int restoreMessages(FacesContext facesContext) {
		// remove messages from the session
		Map sessionMap = facesContext.getExternalContext().getSessionMap();
		@SuppressWarnings("unchecked")
		List messages = (List)sessionMap.remove(sessionToken);
		// store them in the context
		if(messages == null) {
			return 0;
		}
		int restoredCount = messages.size();
		for(Iterator i = messages.iterator(); i.hasNext(); ) {
			facesContext.addMessage(null, i.next());
		}

		return restoredCount;
	}
}

And then add something like the following in your faces-config.xml. Be sure to replace the example package location with your own.

<lifecycle>
	<phase-listener>com.myproject.web.jsf.phaselistener.MessageHandler</phase-listener>
</lifecycle>

The forum post that documented this solution is here.

〈  Back to Blog