Friday, May 6, 2011

JSF/ICEfaces Validation Messages and Liferay Portlets

Turning this:





Into this:






Getting JSF and Liferay to play nice together in the sandbox is sometimes an adventure. Even things that should be straightforward sometimes... aren't.


Take validation. JSF 1.2 has a pretty nice validation component. It's easy to use and has some built-in validators with custom validators easy to handle.

There are some things to watch out for, however, especially when using this functionality in your portlets.

Our environment today:
Liferay 5.2
ICEfaces 1.8.1 (built on JSF 1.2)

Let's say we have 3 fields... two of them are for inputting a name (first and last separately) and one is for an E-mail address. All 3 fields are required and we're going to use a regex to validate the E-mail address is properly formatted.

The text fields are simple.

<ice:inputText id="lname"
label="Last Name"
partialSubmit="false"
required="true"
value="#{backingBean.lname}"/>

Notice the required="true" attribute. That tells JSF to use its built-in required field validator to check this field during the Process Validation phase of the JSF lifecycle. (That isn't just technical mumbo-jumbo, boys and girls. It's going to matter later.)

We do the same for first name.

<ice:inputText id="fname"
label="First Name"
partialSubmit="false"
required="true"
value="#{backingBean.fname}"/>

Now, for E-mail address we don't just want it to be required. We also want to check to be sure the format is correct.

<ice:inputText id="email"
label="E-mail Address"
partialSubmit="false"
required="true"
validator="#{backingBean.validateEmail}"
value="#{backingBean.email}"/>

Notice that we used the required="true" attribute, but we're ALSO adding our custom validator with the attribute validator="#{backingBean.validateEmail}"

Now, sometimes it's better to create a separate validator class to define a bean whose sole purpose is validation. In this case, we're only defining a single custom validator so there's no need to create a separate class just to handle it. We certainly could have, and in a more complex form with more custom validators we would.

The custom validator method we're calling here is pretty straightforward.

public void validateEmail(FacesContext context, UIComponent toValidate, Object value) {
String email = (String) value;
String regex = "regexstring";

if(!email.matches(regex)){
((UIInput)toValidate).setValid(false);
FacesMessage message = new FacesMessage("Invalid Email Adddress");
context.addMessage(toValidate.getClientId(context), message);
}
}

I left out the actual regex string, replacing it in the code with just regexstring. I did this because there are a lot of different ways to do this and you may have a favorite you'd prefer. Either way, I stink at writing regex strings so anything I put there would be written by somebody else and I'm not gonna plagiarize. Here's a nice regex library where you can find all sorts of user uploaded regex strings as well as a regex tester.

So here's what's happening...

We take 3 parameters in our validator method. The FacesContext object, the UI component that's triggering the validation, and the Object that is the subject to be validated. We're casting that Object to a String and comparing it to our regex. If it matches, life goes on as normal. if it doesn't, the UI component is marked as being invalid and we add a new FacesMessage to the FacesContext object.

So if any is not valid, the JSF lifecycle bypasses all but the Render Response phase.

Nice, huh?

Now all we need is to display the validation messages back to the user. There are two ways to do this. The first way is to have a message for each control, so that it can be placed right next to the control. This is done by means of an <ice:message/> tag with the for attribute set to the control being validated. You can also display all of the validation messages in one place with the<ice:messages/> tag which has attributes for setting the display layout and so on.

We're not done yet, friends.

Here's the problem. FacesMessage objects have a severity associated with them. By default, the built-in required field validator is of SEVERITY_ERROR. In our custom validator we didn't set a severity, did we? Well the default is not SEVERITY_ERROR. That means it's going to be different from the required validators.

Why is that a problem?

Because Liferay assigns a different look and feel to the way it displays these messages based on severity.

Normally, we could just set the severity level of the message when we create it in our custom validator so it matches. At least then they'd all look the same. Personally, I don't really like the big red box look. The default severity level is handled by Liferay as a SEVERITY_INFO which is a much more pleasant blue box.

Liferay also sometimes tacks on a lot of extra, very user unfriendly info like the uid for the portlet instance and we don't want all that displaying in the error message.

Note: it only does this for the built-in JSF validators. When we create our own, it's no problem.

That means in order to get them all the same, we need to override the messages created by the built-in validators.

I prefer to do this right after the Process Validation phase.

That means creating a phase listener.

public void afterPhase(PhaseEvent pe) {
if (pe.getPhaseId() == PhaseId.PROCESS_VALIDATIONS) {
FacesContext facesContext = FacesContext.getCurrentInstance();
Iterator messages = facesContext.getMessages();
while(messages.hasNext()){
String control, detail;
FacesMessage message = messages.next();
message.setSeverity(FacesMessage.SEVERITY_INFO);
reformatMessage(message);
}
}
}

So in our phase listener (Which you did remember to register in the faces-config.xml, right?) we get the messages in the FacesContext. We then turn each of them to SEVERITY_INFO for that nicer blue look. Then we reformat each message...

private void reformatMessage(FacesMessage message){
String[] messageElements = message.getSummary().split(":");
if(messageElements.length > 1){
message.setSummary(messageElements[messageElements.length - 3] + " field is blank. Please enter a value.");
}
}

As you can see from the image at the top of this post, the exact length of the messages isn't the same so we need a little flexibility in our String handling here. We also want it to ignore our custom messages so we have it ignore the messages that don't contain that ":" in them.