Tuesday, November 24, 2015

OmniFaces 2.2 released!

OmniFaces 2.2 has been released!

This version brings a bunch of new utility methods in Faces, Messages and Components classes, a new <o:viewAction> component, a new @ContextParam annotation, a whole FileServlet, and a second life for the @ViewScoped annotation.

As usual, in the What's new page of the showcase site you can find an overview of all what's been added/changed/fixed for 2.2. The top three additions are the second life for the @ViewScoped annotation, the new and refactored "BalusC FileServlet", and the new <o:viewAction>.

Installation

Non-Maven users: download OmniFaces 2.2 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.

Maven users: use <version>2.2</version>.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>2.2</version>
</dependency>

For users who don't want to use CDI, there's the CDI-less 1.12.1 with all 2.2 enhancements and fixes (but no brand new 2.x additions!). UPDATE: FacesViews was broken in 1.12, so a 1.12.1 has been baked with the hotfix. FacesViews continues to work fine in 2.2, so there's no 2.2.1.

Second life for @ViewScoped

In standard JSF 2.0/2.1, the @PreDestroy annotated method on a standard JSF view scoped bean was never invoked when the session expires. This was solved since OmniFaces 1.6 with its new CDI @ViewScoped annotation. However, since JSF 2.2 this problem is solved on JSF's own @ViewScoped beans, hereby basically making the OmniFaces CDI @ViewScoped annotation superflous in JSF 2.2. You could as good just stick to JSF's own @ViewScoped annotation.

Instead being potentially deprecated, the OmniFaces @ViewScoped got a second life: immediately invoke @PreDestroy when the browser unloads the page instead of waiting until the session is expired. In other words, when the user navigates away by a GET link, or closes the browser tab/window, or refreshes the page (by GET), then the OmniFaces @ViewScoped bean will immediately be destroyed. None of the both JSF 2.2 @ViewScoped annotations support this.

The trick is done by a synchronous XHR request during beforeunload event (thus not on the main thread! ;) ) via a helper script omnifaces:unload.js which is automatically included when an OmniFaces @ViewScoped bean is created. The XHR request sends a special request parameter omnifaces.event=unload along with the OmniFaces specific view scope ID and the JSF javax.faces.ViewState ID:

var xhr = new XMLHttpRequest();
xhr.open("POST", window.location.href.split(/[?#;]/)[0], false);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send("omnifaces.event=unload&id=" + id + "&" + VIEW_STATE_PARAM + "=" + viewState);

The existing OmniFaces RestorableViewHandler has been extended with below lines:

if (isUnloadRequest(context)) {
    UIViewRoot createdView = createView(context, viewId);
    createdView.restoreViewScopeState(context, getRenderKit(context).getResponseStateManager().getState(context, viewId));
    BeanManager.INSTANCE.getReference(ViewScopeManager.class).preDestroyView();
    responseComplete();
    return createdView;
}

In a nutshell: when the omnifaces.event=unload parameter is present, then only create the view and restore any view scoped beans (and thus don't build the view! which might be time consuming and unnecessarily create new beans) and finally destroy those beans, hereby immediately invoking the @PreDestroy and freeing unused memory space on demand. After that, JSF is instructed to immediately render the response and return the dummy (non-built!) view, preventing potential unnecessary ViewExpiredException.

There's however a small caveat: on slow network and/or poor server hardware, there may be a noticeable lag between the enduser action of unloading the page and the desired result, even though the server side action may take place in only a few milliseconds (in case you're doing expensive tasks inside @PreDestroy method, better delegate it to an @Asynchronous EJB method or so). If the slow network/hardware lag is in your case noticeable and thus undesireable, then better stick to JSF 2.2's own @ViewScoped annotation and accept the postponed destroy.

New FileServlet with advanced HTTP range and caching support

The well known "BalusC FileServlet" from the blog article FileServlet supporting resume and caching has been cleaned, refactored and reworked into an abstract template. Now, instead of copypasting and editing the code from the blog, you can simply extend from org.omnifaces.servlet.FileServlet and implement the getFile() method in its simplest form as below:

@WebServlet("/media/*")
public class MediaFileServlet extends FileServlet {

    @Override
    protected File getFile(HttpServletRequest request) throws IllegalArgumentException {
        return new File("/var/webapp/media", request.getPathInfo());
    }

}

When referenced in e.g. a HTML5 <video> tag like below:

<video src="#{request.contextPath}/media/video.mp4" controls="controls" />

Then the FileServlet will worry about properly streaming the media range requests. I.e. the servlet is able to return specific parts of the media file on a request with a HTTP Range header. For example, only the bytes at exactly the index 1000 until 2000 on a file of 10MB long. This is mandatory for many media players in order to be able to skip a certain range of the media stream quickly enough and/or to improve buffering speed by creating multiple connections which each requests different parts of the file. Also download accelerators will take benefit of this on large files. They will then simply open multiple HTTP connections which each downloads a specific range of the file and afterwards put the pieces together.

Fix unintuitive "if" attribute of <f:viewAction>

The if attribute of the standard <f:viewAction> not really intuitive. It is checked during APPLY_REQUEST_VALUES phase instead of INVOKE_APPLICATION phase. This would make several straightforward use cases to fail.

In below example, the FooConverter may convert a non-null parameter to null without causing a validation or conversion error, and the intent is to redirect the current page to otherpage.xhtml when the converted result is null.

<f:viewParam name="foo" value="#{bean.foo}" converter="fooConverter" />
<f:viewAction action="otherpage" if="#{bean.foo eq null}" />

However, this fails because the if attribute runs before the conversion has taken place, when the component wants to check whether it should queue the action event. This happens during APPLY_REQUEST_VALUES phase. The OmniFaces <o:viewAction> solves this by postponing the evaluation of the if attribute to the INVOKE_APPLICATION phase.

<f:viewParam name="foo" value="#{bean.foo}" converter="fooConverter" />
<o:viewAction action="otherpage" if="#{bean.foo eq null}" />

The implementation of the extended component is really simple. It's nothing more than below:

@Override
public void broadcast(FacesEvent event) throws AbortProcessingException {
    if (super.isRendered()) {
        super.broadcast(event);
    }
}

@Override
public boolean isRendered() {
    return !isImmediate() || super.isRendered();
}

It gives a bit of thinking as the if attribute is basically a renamed rendered attribute, and one would wonder why !isImmediate() is checked instead of e.g. the current phase ID or a hardcoded true. But this way the original and intented behavior of immediate attribute is maintained. Actually, the <o:viewAction immediate="true"> behaves exactly the same as a <f:viewAction immediate="true">.

Maven download stats

Here are the Maven download stats after previous release:

  • June 2015: 6949
  • July 2015: 6574
  • August 2015: 5906
  • September 2015: 6561
  • October 2015: 5904

Below is the version pie of October 2015:

Surprising how many 1.8.1 users are still there. To those who can't upgrade to 2.2 or 1.12, I'd like to point out that 1.8.3 contains an important memory leak fix in case you're using UnmappedResourceHandler while having composite components on the pages. So, if you can't upgrade to 2.2 or 1.12, then please at least upgrade to 1.8.3. And those few old RC1/RC2 and M1 users, upgrade to the final version without any RC/M suffix!