Chapter 4: Initializing PrimeFaces components via view parameters
View parameters (UIViewParameter) were introduced in JSF 2.0. They appear nested in the metadata section (<f:metadata>) and their main goal is to allow the developer to provide bookmarkable URLs. Although it is not mandatory, they are initialized via request parameters. As you will see in this chapter, view parameters can be used to transform a query string (the part after ? in an URL) into managed bean properties used to initialize PrimeFaces components. A quick code sketch will look like this:
// page A.xhtml
<h:link outcome="B.xhtml"...>
<f:param name="myParamName" value="myParamValue"/>
</h:link>
// page B.xhtml
<f:metadata>
// myProperty is initialized via myParamValue and it will further initialize
// PrimeFaces component property
<f:viewParam name=" myParamName " value="#{myBean.myProperty}"/>
</f:metadata>
View parameters are stateful and this raises several issues, of which will be the discussion of this chapter.
Suppressing the invocation of the setter/converter/validator at each postback
Again to reiterate, view parameters are stateful and hence they need to be initialized only once. This is a good thing from the perspective of availability. A stateful component is available in the current view over postbacks, and in the case of view parameters, this is true even if they no longer appear in the URL (they may appear only at initial request). But, there are a few drawbacks of this behavior:
In order to highlight these issues, let’s have the following scenario: we want to initialize the PrimeFaces Mindmap component via a query string of type: ...?nodes=root|node_A|node_B. This should produce the below figure (for simplicity we use a mindmap with a single level):
We have multiple approaches for accomplishing this, but obviously, we will choose to use view parameters for transforming the request parameters into mindmap nodes. The root, node_A, and node_B are strings, whereas mindmap relies on MindmapNode class.
In order to transform the query string into a MindmapNode instance, we will use a custom converter (NodesConverter) and in order to ensure that we do it right, we will use a custom validator (NodesValidator). But, let’s see some code lines (notice that the request parameter and the view parameter have the same name nodes):
<f:metadata>
<f:viewParam name="nodes" value="#{mindmapViewBean.root}" required="true"
converter="nodesConverter" validator="nodesValidator"/>
</f:metadata>
<h:body>
<h:panelGrid columns="2">
<p:messages/>
<h:form>
<p:commandButton value="Reset Mindmap"
action="#{mindmapViewBean.resetMindMap()}" ajax="false"/>
</h:form>
</h:panelGrid>
<p:mindmap value="#{mindmapViewBean.root}"
style="width: 450px; height: 400px; border: 1px solid black;">
<p:ajax event="select" listener="#{mindmapViewBean.onNodeSelect}" />
</p:mindmap>
<h:form>
<p:commandButton value="Unrelated to Mindmap" ajax="false"/>
</h:form>
</h:body>
The MindmapViewBean is responsible of storing the mindmap (nodes). Keeping up with simplicity and in order to highlight the issues at hand we implement a few logs:
@Named
@ViewScoped
public class MindmapViewBean implements Serializable {
private static final Logger LOG =
Logger.getLogger(MindmapViewBean.class.getName());
private MindmapNode root;
public MindmapViewBean() {
LOG.info("MindmapViewBean#constructor ...");
}
public MindmapNode getRoot() {
return root;
}
public void setRoot(MindmapNode root) {
LOG.info("MindmapViewBean#setter ...");
this.root = root;
}
public String resetMindMap(){
LOG.info("MindmapViewBean#resetMindMap() ...");
return "index?faces-redirect=true&includeViewParams=true";
}
public void onNodeSelect(SelectEvent event) {
LOG.info("MindmapViewBean#onNodeSelect() ...");
}
}
The root is populated from a custom converter. This is responsible of splitting the root|node_A|node_B string and creating the mindmap (getAsObject()) and viceversa (getAsString()). The code is pretty simple:
@FacesConverter("nodesConverter")
public class NodesConverter implements Converter {
private static final Logger LOG =
Logger.getLogger(NodesConverter.class.getName());
@Override
public Object getAsObject(FacesContext context,
UIComponent component, String value) {
LOG.info("NodesConverter#getAsObject() ...");
if ((value != null) && (!value.isEmpty())) {
String[] nodes = value.split("\\|");
// create mindmap root
MindmapNode root = new
DefaultMindmapNode(nodes[0], nodes[0], "FFCC00", false);
// add children to root
if (nodes.length > 1) {
for (int i = 1; i < nodes.length; i++) {
root.addNode(new DefaultMindmapNode(nodes[i],
nodes[i], "6e9ebf", true));
}
}
return root;
}
return null;
}
@Override
public String getAsString(FacesContext context,
UIComponent component, Object value) {
LOG.info("NodesConverter#getAsString() ...");
if (value != null) {
String queryString = ((DefaultMindmapNode) value).getLabel() + "|";
List<MindmapNode> nodes = ((DefaultMindmapNode) value).getChildren();
for (MindmapNode node : nodes) {
queryString = queryString + node.getLabel() + "|";
}
return queryString;
}
return "";
}
}
Besides the built-in required validator, you might in some cases need a validator. We can add a custom validator that would ensure that our mindmap has children. Furthermore, we will not accept a mindmap that contains only the root. Obviously, you could accomplish this check by using the above converter. But remember that we need to expose all the issues of view parameters, including calling valdiators at each postback. Moreover, the application is “shaped” for revealing what may happen if we don’t correctly understand how view parameters works. So the custom validator looks like this:
@FacesValidator("nodesValidator")
public class NodesValidator implements Validator {
private static final Logger LOG =
Logger.getLogger(NodesValidator.class.getName());
@Override
public void validate(FacesContext context, UIComponent component,
Object value) throws ValidatorException {
LOG.info("NodesValidator#validate() ...");
if (((MindmapNode)value).getChildren().isEmpty()) {
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
"You cannot have a mindmap only with a root !", null));
}
}
}
Assuming we have some hypothetical cases where the URLs are bookmarked:
- URL without query string (
http://localhost:8080/SuppressViewParamInvocations/). In this case, we see the error message caused by the built-inrequiredvalidator,j_idt1: Validation Error: Value is required.. This is the expected behavior:
Now, let’s focus on what happens if we press the Reset Mindmap button. When this button is pressed, we simply call a managed bean method that performs some tasks (not relevant) and redirect to the same view by including the view parameters. At postback, you see the same error message caused by the required built-in validator. This is normal since we never provided a query string. But the thing that may look strange (even if it shouldn’t) is the fact that this is not a redirect (the resetMindMap() method is not called). Since the required view parameter is not provided, each postback will “stop” in the Process Validation phase, and “jump” to the Render Response phase. The fact that the validators/converters are invoked at each postback (even if there is no query string) is also suggested by the logs:
// initial request - validation fails via 'required' built-in validator
[beans.MindmapViewBean] (default task-93) MindmapViewBean#constructor ...
[beans.NodesConverter] (default task-93) NodesConverter#getAsString() ...
// postback 1 - validation fails via 'required' built-in validator
[beans.NodesConverter] (default task-112) NodesConverter#getAsObject() ...
[beans.NodesConverter] (default task-112) NodesConverter#getAsString() ...
So, instead of being redirected and seeing the required built-in validator error message again, we just see this message without redirection. The resetMindMap() method is not called. This is happening for the Unrelated to Mindmap button also (dummy button to fire POST via forward mechanism). Let’s examine the second use case.
- URL with wrong query string (
http://.../SuppressViewParamInvocations/?nodes=root). In this case, we see the error message caused by the custom validator,You cannot have a mindmap only with a root !. This is the expected behavior:
Since we provide a value for the view parameter, the built-in required validator is successfully passed at initial request. But, the validation fails in our custom validator, which doesn’t accept a mindmap that contains only the root. This means that the flow “jumps” in Render Response phase, and the view parameter is not set in state. At postback (fired via Reset Mindmap button) we will receive the built-in required validator error message, j_idt1: Validation Error: Value is required.. This in normal since at initial request the view parameter was not set in state, and at postback the query string is not available, so there is no view parameter. The fact that the validators/converters are invoked at each postback (even if there is no query string) is suggested by the logs also:
// initial request - no setter is called because validation fails
// in the custom validator
[beans.NodesConverter] (default task-54) NodesConverter#getAsObject() ...
[beans.NodesValidator] (default task-54) NodesValidator#validate() ...
[beans.MindmapViewBean] (default task-54) MindmapViewBean#constructor ...
[beans.NodesConverter] (default task-54) NodesConverter#getAsString() ...
// postback 1 - validation fails via 'required' built-in validator
[beans.NodesConverter] (default task-76) NodesConverter#getAsObject() ...
[beans.NodesConverter] (default task-76) NodesConverter#getAsString() ...
Again instead of being redirected, we keep seeing the required built-in validator error message without redirection. The resetMindMap() method is not called. This is happening to the Unrelated to Mindmap button also (dummy button to fire POST via forward mechanism). Let’s examine the third use case.
- URL with valid query string (
http://.../?nodes=root|node_A|node_B). This query string will successfully pass through conversion and validation and the mindmap will be displayed on to screen. This is the expected behavior:
If we press the Reset Mindmap button, then everything will act as expected. The mindmap will be reset to the initial status via the query string attached due to the effect of the includeViewParams=true (see resetMindMap() method). But, if we click on one of the nodes (which fires an AJAX select event) and continue by pressing the Reset Mindmap button, then we will see the error message from the built-in required validator, j_idt1: Validation Error: Value is required.. This is an unexpected behavior. Click again on Reset Mindmap button and everything works fine.
Moreover, let’s press the Unrelated Mindmap button and check the logs. Even if this is a postback, there is no query string and this action is not even related to mindmap, the converters, validators and setters are still invoked at each request:
// initial request
[beans.NodesConverter] (default task-61) NodesConverter#getAsObject() ...
[beans.NodesValidator] (default task-61) NodesValidator#validate() ...
[beans.MindmapViewBean] (default task-61) MindmapViewBean#constructor ...
[beans.MindmapViewBean] (default task-61) MindmapViewBean#setter ...
[beans.NodesConverter] (default task-61) NodesConverter#getAsString() ...
// postback 1 - press the 'Unrelated Mindmap' button
[beans.NodesConverter] (default task-119) NodesConverter#getAsObject() ...
[beans.NodesValidator] (default task-119) NodesValidator#validate() ...
[beans.MindmapViewBean] (default task-119) MindmapViewBean#setter ...
[beans.NodesConverter] (default task-119) NodesConverter#getAsString() ...
So, in the above three cases we have identified several issues that disqualify view parameters for this job. But, if we add the OmniFaces namespace (xmlns:o="http://omnifaces.org/ui") in the XHTML page and replace the stateful <f:viewParam/> with the stateless <o:viewParam/> then all the issues will disappear like magic:
<f:metadata>
<o:viewParam name="nodes" value="#{mindmapViewBean.root}" required="true"
converter="nodesConverter" validator="nodesValidator"/>
</f:metadata>
Now, let’s see what happens in our three use cases:
- URL without the query string (
http://.../SuppressViewParamInvocations/). In this case, we see the error message caused by the built-inrequiredvalidatornodes: Validation Error: Value is required.. This is the expected behavior. This time, when you press theReset Mindmapbutton, the logs reveal that the redirect take place; the constructor ofMindmapViewBeanis invoked after each press and the built-inrequiredvalidator fails each time at initial request. TheresetMindMap()method is called as expected:
// initial request
[beans.NodesConverter] (default task-77) NodesConverter#getAsObject() ...
[beans.MindmapViewBean] (default task-77) MindmapViewBean#constructor ...
// postback 1 - press the 'Reset Mindmap' button
[beans.MindmapViewBean] (default task-97) MindmapViewBean#resetMindMap() ...
[beans.NodesConverter] (default task-95) NodesConverter#getAsObject() ...
[beans.MindmapViewBean] (default task-95) MindmapViewBean#constructor ...
- URL with the wrong query string (
http://.../SuppressViewParamInvocations/?nodes=root). In this case, at the initial request, we see the error message caused by our custom validator,You cannot only have a mindmap with a root !. This is the expected behavior. Since the view parameter doesn’t pass the validation process it will not be set in state. At postbacks when you press theReset Mindmapbutton, you will see the effect of the built-inrequiredvalidator. But, theresetMindMap()method is called as expected and the redirection takes place:
// intial request
[beans.NodesConverter] (default task-38) NodesConverter#getAsObject() ...
[beans.NodesValidator] (default task-38) NodesValidator#validate() ...
[beans.MindmapViewBean] (default task-38) MindmapViewBean#constructor ...
// postback 1 - press the 'Reset Mindmap' button
[beans.MindmapViewBean] (default task-3) MindmapViewBean#resetMindMap() ...
[beans.NodesConverter] (default task-19) NodesConverter#getAsObject() ...
[beans.MindmapViewBean] (default task-19) MindmapViewBean#constructor ...
This is confirmed by the Unrelated Mindmap button also. This button fires a POST requests via forward mechanism, but the required validator is not invoked!
- URL with valid query string (
http://.../?nodes=root|node_A|node_B). This query string will successfully pass through conversion and validation and the mindmap will be displayed on screen. This is the expected behavior.
This time pressing the Reset Mindmap button works as expected. Remember previously, the issue caused by clicking a node (firing an AJAX select event) and afterwards clicking the Reset Mindmap button ? Well, this issue is now solved. The POSTs via forward mechanism (e.g. pressing Unrelated Minmap button) will skip the invocation of converters/validators and setters attached to view parameters. This is a big win! Stateless mode avoids unnecessary conversions, validations and models updating on postbacks.
Supply a label for messages
Moreover, pay attention to the error messages label! OmniFaces provides a default value for the label attribute. When the label attribute is omitted, the name attribute will be used as the label. For example, when the label attribute is not set on <o:viewParam/> and a validation error occurs when using the required built-in validator, the following message will be generated:
nodes: Validation Error: Value is required.
Supply a default value
Note that, starting with OmniFaces 2.2, the ViewParam component also supports the optional default attribute. This attribute allows us to indicate a default value, and OmniFaces will in turn rely on this value in the case where the actual request parameter is null or empty. For example, you may want to initialize the mind map, as below:
<f:metadata>
<o:viewParam name="nodes" value="#{mindmapViewBean.root}" default="root|foo"
required="true" converter="nodesConverter"
validator="nodesValidator"/>
</f:metadata>
Now, if you don’t supply a query string (e.g. http://localhost:8080/SuppressViewParamInvocations) the mindmap will look like below (this way you don’t need the required built-in validator):
Suppressing invocation of the converter regardless the null values
OmniFaces doesn’t allow null view parameters to become empty strings nor does it allow null parameters from participating in query string. This is done automatically by the OmniFaces implementation, so you don’t have to configure anything. OmniFaces analyzes each
view parameter value and takes care of preventing any such occurrence and it does this by not calling the attached converters. By default, when you are using the includeViewParams, the null view parameters with attached converters are still null when the query string is prepared. For example, if we use <f:viewParam/>, remove validators from our view parameter, omit the nodes request parameter and click on the Reset Mindmap button, you will see this:
http://localhost:8080/SuppressViewParamInvocations/faces/index.xhtml?nodes=
Notice that nodes was attached with an empty string value. If we switch to <o:viewParam/> then we will obtain the expected result:
http://localhost:8080/SuppressViewParamInvocations/faces/index.xhtml
Support bean validation and triggering validate events on null value
The standard UIViewParameter implementation uses in JSF 2.0-2.2 an internal “is required” check when the submitted value is null, hereby completely bypassing the standard UIInput validation, including any bean validation annotations and even the PreValidateEvent and PostValidateEvent events. For example, in Mojarra, the UIViewParameter implementation is adjusted to deal with null values in presence of the built-in required validator. Well, UIInput assumes that a null value means don’t check, but UIViewParameter doesn’t accept null values and the built-in required validator:
// Mojarra 2.2.9, UIViewParameter source code
...
if (getSubmittedValue() == null && myIsRequired()) {
String requiredMessageStr = getRequiredMessage();
FacesMessage message;
if (null != requiredMessageStr) {
message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
requiredMessageStr,
requiredMessageStr);
} else {
message = MessageFactory.getMessage(context, REQUIRED_MESSAGE_ID,
MessageFactory.getLabel(context, this));
}
context.addMessage(getClientId(context), message);
setValid(false);
context.validationFailed();
context.renderResponse();
} else {
super.processValidators(context);
}
...
The myIsRequired() method is a private method that checks the well-known isRequired() and another private method, named isRequiredViaNestedRequiredValidator() - its name is self explanatory:
// Mojarra 2.2.9, UIViewParameter source code
private boolean myIsRequired() {
return super.isRequired() || isRequiredViaNestedRequiredValidator();
}
The workaround was added in OmniFaces 2.0. In JSF 2.3, this has been fixed and effectuated when javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL context param is set to true.
The complete application is named, SuppressViewParamInvocations.
Well, I hope you enjoyed this chapter and learned a bunch of interesting stuff. Not only are view parameters used to initialize PrimeFaces components but coupled with, OmniFaces’ stateless view parameters, you can create bookmarkable URLs that provide initial state of components.
Related articles: