Tuesday, May 12, 2009

Showing not shown Faces Messages

Most of us using JSF 1.2 RI are familiar with the warning message in our logs:

[lifecycle] WARNING: FacesMessage(s) have been enqueued, but may not have been displayed.

JavaServer Faces has a mechanism to assign messages to specific components. In the common case, if an input field has a validation error, the error message is queued to that field, and a h:message put next to the input field can show the message on the page:

<h:inputtext id="firstName"/>
<h:message for="firstName"/>

The JSF Reference Implementation has a solution to help us find out if we forgot to put a h:message tag for some components. Unfortunately, it only shows a warning message in the log, and we have no programmatic way to process those messages. Well, almost no way.

If you dare take a look in com.sun.faces.lifecycle.RenderResponsePhase, you can see that if there are any messages queued, a special variable of the Set is added to the request map. All client ids of controls with faces messages are added to this set. When h:message tags render, they access the messages queued for their associated client ids using the getMessages(String clientId) method in com.sun.faces.context.FacesContextImpl. This method removes the client id from the set, thus signalling that the messages for that client id are displayed. When finished rendering, messages for the client ids remaining in the Set are those that are not displayed. The class RenderResponsePhase simply logs these messages and then disposes the Set.

In one of my applications, I wanted to present these messages at the top of my forms. This can come really useful especially in development phase. As seen from the above, I have the necessary information ready at the very end of each render phase. I created a Seam component with a method to handle it:

public class MessageHandler {

public String handleUnAssignedMessages() {
    // copy-paste from com.sun.faces.lifecycle.RenderResponsePhase
        Set clientIds = TypedCollections.dynamicallyCastSet(
                (Set) FacesContext.getCurrentInstance().getExternalContext().getRequestMap()
                .remove(RIConstants.CLIENT_ID_MESSAGES_NOT_DISPLAYED), String.class);
        if (clientIds != null)
            for (String clientId : clientIds) {
                Iterator messages = 
                while (messages.hasNext()) {
                    FacesMessage message = messages.next();
                    if (message.getSeverity() == FacesMessage.SEVERITY_ERROR ||
                            message.getSeverity() == FacesMessage.SEVERITY_FATAL) {
                        String text = message.getSummary() + " " + message.getDetail();
                                new FacesMessage(FacesMessage.SEVERITY_ERROR, text, text));
    return "";

This method simply collects all nondisplayed error messages and enqueues them without assigning them to a specific client id. Note that you'll have to include the jsf-impl.jar in your classpath for this code to compile. Having these messages enqueued, you can present them by a h:messages tag, or you can do anything programmatically with them.

Note that as you only have this information at the end of the render phase, you have to put the invocation of this method at the end of your page. If you want also to present them on the page with a h:messages tag, you have to put it after the invocation of the method above. You can then move the html output to the desired place in your document (above your forms, probably) with a simple javascript code:

#{messageHandler.handleUnAssignedMessages()} <h:messages layout="table" styleclass="msg_gl" infoclass="info-message" warnclass="warning-message" errorclass="error-message" globalonly="#{true}" showsummary="#{false}" showdetail="#{true}"> </h:messages>

So that's it.

No comments:

Post a Comment