May 17, 2015

RESTful JSF with POST-redirect-GET (part 3 of 5)


Pages: 1 2 3 4 5

RESTful navigation (continued)

Implementation

GET without parameter

In the simplest case, a view doesn’t take any GET parameter. In above state diagram, these use cases are colored in blue. As an example, this is the case for /customers/list.xhtml.

However, the view has to be initialized on page load. Therefore, JSF 2.2 provides the new <f:viewAction> tag which offers some advantages over the older (but still valid and useful, as we’ll see later) <f:event> tag. We can specify
<f:metadata>
    <f:viewAction action="#{customerController.initEntities}"/>
</f:metadata>
Note that <f:viewAction>, <f:event> and <f:viewParam> must be placed inside <f:metadata>!

The view action is executed on page load and can point to any backing bean method. In customers/list.xhtml, it initializes the customer table (by fetching the data from the service):
public void initEntities() {
    this.entities = getService().findAll();
}
This method is implement in BaseController.

This mechanism is completely unrelated to annotating an “init” method with @PostConstruct as it is common practice in non-RESTful JSF application design. Actually, we will never use @PostConstruct throught this application as that mechanism is driven by the backing bean’s life cycle whereas <f:viewAction> / <f:event> is driven by actual view events. Thanks to REST, we have complete control over the latter whilst being independent of the former, and we don’t want to mix those two concepts.

How do you trigger GET navigation without parameter? Of course, this is trivial. GET navigation happens if the user types the URL (here, /customers/list.xhtml) and hits enter. This is how it works in the example application.

Creating an actual navigation link of course is trivial, too. Just any <a href="/customers/list.xhtml">Customer list</a> will do. However, we prefer using “proper” JSF components <h:link> and <h:button>, respectively, for full JSF support. The former will actually be rendered as an <a>-tag, the latter as a <button>.

For example, this is how the Cancel button on the /customers/edit.xhtml view is implemented, which navigates back to the list view:
<h:button value="Cancel" outcome="list.xhtml"/>
Yes, implementing Cancel really just means leaving the page. You don’t even need to put those links / buttons inside a <form>.

Also note that the application now is implicitly lazy-loading ready, e.g. when customers/list.xhtml does #initEntities(), it would respect FetchType.LAZY entity relations; the whole entity is loaded only on demand (e.g. in customers/edit.xhtml’s #initCurrentEntity().

GET with parameter

The interesting part is dealing with GET parameters. In above state diagram, these use cases are colored in magenta. Let’s begin with implementing the view, again. As an example, /customers/edit.xhtml takes an “id” parameter.

This will initialize the view:
<f:metadata>
    <f:viewParam name="id" value="#{customerController.currentEntityId}"/>
    <f:viewAction action="#{customerController.initCurrentEntity}"/>
</f:metadata>
<f:viewParam> takes the values of the GET parameter with the given name and binds it to the given backing bean property. Based on that value (plus any other information available), <f:viewAction> can then trigger the view initialization.
public String initCurrentEntity() {
    // without id param: CREATE
    if (currentEntityId == null) {
        currentEntity = createNewEntity();
    }
    else {
        // with id param: READ
        currentEntity = getService().findById(currentEntityId);
        if (currentEntity == null) {
            Messages.addGlobalError("Entity with id " + currentEntityId + " not found!");
            return "list.xhtml";
        }
    }
    return "edit.xhtml";
}
Just as we specified earlier:
  • Without an id param, a new entity is created.
  • With id param, the requested entity is fetched.
  • If the entity is not found, the user is redirected to the list view and an error message is shown.
Note that <f:viewAction> can optionally return a String (instead of void) which can be used to trigger a redirect rather than opening the requested view. This is useful for the “illegal id” case. This is implicitly a “proper” redirect with URL rewrite.

The method is implement in BaseController.

Triggering GET with parameter is of course trivial as well: For the user, it’s a matter of navigating to e.g. /customers/edit.xhtml?id=1.

Creating a navigation link is easy, too. It’s just a <a href="/customers/edit.xhtml?id=1">Edit customer 1</a>. However, JSF provides a means of creating those links more explicitly, as for example in the customer list view pointing to a single customer in the dataTable:
<h:link outcome="edit.xhtml" value="#{item.id}">
    <f:param name="id" value="#{item.id}"/>
</h:link>
This works for both <h:link> and <h:button> and is the preferred way of creating a GET link with parameter.

POST-redirect-GET

This is a key ingredient of RESTful navigation. Luckily, it is easily realized with JSF. It’s important to understand that POST-redirect-GET actually involves two JSF lifecycle runs. In above state diagram, these use cases are colored in red. As an example, let’s take the Save action on /customers/edit.xhtml.

Let’s take a look at the implementation.

As we are doing POST, we use a default <commandButton> / <commandLink>:
<h:commandButton value="Save" action="#{customerController.save(customerController.currentEntity)}"/>
Important: If you use PrimeFaces components, make sure to set the ajax="false" attribute: PrimeFaces components are implicitly “ajaxified” meaning that they would not trigger a page refresh, but update parts of the view instead. This is not what we want for RESTful navigation!

The save method triggers the service call and returns the navigation outcome:
public String save(T currentEntity) {
    getService().save(currentEntity);
    return "list.xhtml?faces-redirect=true";
}
The important part here is to set the faces-redirect=true request parameter in the navigation outcome. This is what actually triggers redirect, thus URL rewrite.

Let’s observe now how this fits in the JSF lifecycle:


The save method redirects to the list.xhtml page. Only after the page is opened, it’s <f:viewAction> / <f:event> is fired, which allows the page to initialize itself again. Thus, with a POST form submit, a GET to another page is triggered: POST-redirect-GET.

In order to navigate to a <a> anchor, e.g. for /customers/edit.xhtml#payments?id=1, you’ll have to apply some JavaScript hacks as a JSF action outcome must not contain an anchor reference. One possible solution is to add an auxiliary request parameter, e.g. ?anchor=payments, and on the target page onload, parse the URL for that parameter and make a redirect to the anchor. I will eventually build a similar solution in the “final” version of this application, featuring true RESTful URLs with PrettyFaces.

This is the last piece of the puzzle. With this knowledge, you can build any RESTful navigation case backed by a @ViewScoped controller. Please read on for more sophisticated solutions.

NullPointerException in <h:button> / <h:link> with parameter

The presence of a <h:button> / <h:link> with a <f:param> whose value's EL expression raises a NullPointerException immeditaely throws this NullPointerException all the ways up, without stacktrace:
FATAL:   JSF1073: java.lang.NullPointerException caught during processing of RENDER_RESPONSE 6 : UIComponent-ClientId=, Message=null
FATAL:   No associated message
java.lang.NullPointerException
On another occasion, I’ve met the following more chatty Exception in this situation which isn’t any more helpful though:
java.lang.NullPointerException
 at java.net.URLEncoder.encode(URLEncoder.java:204)
 at com.sun.faces.context.UrlBuilder.addValuesToParameter(UrlBuilder.java:318)
 at com.sun.faces.context.UrlBuilder.addParameters(UrlBuilder.java:127)
 at com.sun.faces.context.ExternalContextImpl.encodeBookmarkableURL(ExternalContextImpl.java:1054)
 at com.sun.faces.application.view.MultiViewHandler.getBookmarkableURL(MultiViewHandler.java:407)
 at javax.faces.application.ViewHandlerWrapper.getBookmarkableURL(ViewHandlerWrapper.java:272)
 at org.jboss.weld.jsf.ConversationAwareViewHandler.getBookmarkableURL(ConversationAwareViewHandler.java:132)
 at javax.faces.application.ViewHandlerWrapper.getBookmarkableURL(ViewHandlerWrapper.java:272)
 at com.sun.faces.renderkit.html_basic.OutcomeTargetRenderer.getEncodedTargetURL(OutcomeTargetRenderer.java:194)
 ...
 at java.lang.Thread.run(Thread.java:745)
For example, this is the case for the Reset button in payments/edit.xhtml when declared like this:
<h:button value="Reset" outcome="edit.xhtml">
    <f:param name="id" value="#{paymentController.currentEntity.id}"/>
    <f:param name="customer" value="#{paymentController.currentEntity.customer.id}"/>
</h:button>
In order to overcome the error, you have to use <f:param>’s disable flag to conditionally include the param or not, like this:
<h:button value="Reset" outcome="edit.xhtml">
    <f:param name="id" value="#{paymentController.currentEntity.id}" 
        disable="#{empty paymentController.currentEntity.id}"/>
    <f:param name="customer" value="#{paymentController.currentEntity.customer.id}" 
        disable="#{not empty paymentController.currentEntity.id}"/>
</h:button>
According to the domain model, id must be specified for the Read operation, customer must be specified for the Create operation.

Note that this problem may as well apply to <h:button>s / <h:link>s which are in a subsection of the page which has a conditional rendered attribute! That doesn’t prevent them from throwing this exception.

Bugfix: Restoring invalid null input

When setting the web.xml context parameter javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL to true, undesired behavior on validation error will been introduced: if a null input triggers @NotNull / required="true" validation, it will be reset to its previous non-null value, if available, instead of keeping the submitted null value.

This can be observed by omitting the required “name” input for /customers/edit.xhtml?id=1: After validation error, the view is reloaded with the validation error message displayed (“form:name: must not be empty“), but in the form, the previously present non-null “name” value has been restored. This does not match the behavior when violating a Bean Validation constraint, e.g. for illegal input in “name” (e.g. “1234”): in that case, the invalid value is kept after the page refresh. This is confusing for the user. Expected JSF default behavior is that invalid inputs are kept after page refresh so that the user can see the error he made. The exact problem is described in this stackoverflow answer.

I will here present a workaround which doesn’t force you to patch the JSF library: I registered yet another <f:event> with the exclusive duty to set invalid input component’s values to their (invalid) submitted values. It is registered within <f:metadata>:
<f:metadata>
    <f:viewParam name="id" value="#{customerController.currentEntityId}"/>
    <f:event type="preValidate" listener="#{customerController.initCurrentEntity}"/>
    <f:event type="postValidate" listener="#{customerController.postValidate}"/>
    <f:viewAction action="#{customerController.initCurrentEntity}"/>
</f:metadata>
And implemented in BaseController:
public void postValidate() {
        Components.forEachComponent().invoke(new VisitCallback() {
            @Override
            public VisitResult visit(VisitContext context, UIComponent target) {
                if (target instanceof UIInput && !((UIInput)target).isValid()) {
                    ((UIInput) target).setValue(((UIInput) target).getSubmittedValue());
                    return VisitResult.REJECT;
                }
                return VisitResult.ACCEPT;
            }
        });
    }

Bugfix: Keep request parameters on validation error

There is one particular annoyance of the <h:form> component which can be fixed by using OmniFaces’ <o:form> instead. From the OmniFaces showcase: “The standard UIForm doesn't put the original view parameters in the action URL that's used for the post-back. The disadvantage of this invisible retention of view parameters is that the user doesn't see them anymore in the address bar of the browser”. In the example application, this behavior shows up if user input triggers a validation error, e.g. omit “name” in /customers/edit.xhtml?id=1: After form submit, the id param is removed from the URL.

Strictly speaking, because the parameter is still stored in the respective UIViewParameter component, this really is just a usability issue: If e.g. the user corrects the illegal input and re-submits the form, the parameter is still present, and everything works. Nonetheless, the behavior looks buggy and it breaks the premise that the user should be able to hit ENTER in the address bar at any time and be able to refresh the page.

Thus we use <o:form> instead of <h:form>:
<o:form id="form" includeRequestParams="true">
It’s the includeRequestParams="true" attribute which keeps GET parameters in the URL.

Pages: 1 2 3 4 5