Friday, April 1, 2016

OmniFaces 2.3 brings JSF and WebSockets together!

This version brings a fairly important new tag to the world: <o:socket> for WebSocket based server-side push. Not only because it integrates JSR-356 WebSocket API seamlessly into JSF API with help of the CDI API, but also because it is the base for JSF 2.3's upcoming new <f:websocket> tag. Basically, OmniFaces offers you the opportunity to benefit of it long before JSF 2.3 itself is released (not before 2017). Afterwards, any migration should be a piece of cake.

Next to the <o:socket> and a lot of utility methods and functions, there's another new tag <o:skipValidators> and a further improved @ViewScoped which now also really physically destroys the associated JSF view state during unload (and thus not only the view scoped bean itself as it did before).

Note: whilst perhaps confusing, OmniFaces version 2.3 is still targeted at JSF 2.2. All OmniFaces 2.x versions are targeted at JSF 2.2. Only the future OmniFaces 3.x will be targeted at JSF 2.3.

Installation

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

For users who don't want to use CDI, there's the CDI-less 1.13 with all 2.3 enhancements and fixes (but no brand new 2.x additions nor the o:socket!).

Easy scoped and user-targeted push

Besides "easy to use", an important design decision of <o:socket> was being able to send push messages from the server side to a specific scope (application, session or view) and/or to a specific user.

Example 1

The below application scoped socket example will receive the same push message from the application in all pages having the same socket channel.

The below user-targeted (implicitly session scoped) socket example will receive the same push message in all pages having the same socket channel and user. This example assumes that the logged-in user is available via an EL variable as #{user} which has an id property representing its identifier.

The only disadvantage from the JSF perspective is that the target JSF view state isn't anywhere available at the moment the push message is being sent. So, no partial rendering based on JSF view could be performed. That's why it isn't possible to perform complex UI updates as easily as with JSF ajax. If you're not that fluent with e.g. jQuery, then you could combine <o:socket> with <o:commandScript> which in turn uses ajax to obtain the desired rendering based on the message.

If you pass a Map<String, V> or a JavaBean as push message object, then all entries/properties will transparently be available as request parameters in the command script action method.

Session/view expired

Oh, if you ever wanted to immediately warn the user about an expired session, then you could now use onclose of a session scoped <o:socket> for that. If it returns code 1000 while you didn't manually close the socket, then it means that session has been expired on the server side. How useful! This also works on view scoped sockets (but indicates "View expired" instead and should actually be a very rare case if it isn't caused by an expired session).

<o:socket ... scope="session" onclose="sessionCloseListener" />

function sessionCloseListener(code) {
if (code == 1000) {
alert("Session has expired! Page will be reloaded.");
window.location.reload(true); // Or just go to some login/home page.
}
}

Further improved @ViewScoped

The OmniFaces CDI @ViewScoped annotation has been further improved to also physically destroy the associated JSF view state in session during an unload event. I.e. during an unload it does not only destroy view scoped beans in order to eagerly free memory, but it will now also immediately push out unused JSF view states from the session. This is important in order to prevent "older" JSF view states from expiring too soon as they are basically held in a LRU map.

Set number of physical views to 3 (in Mojarra, use com.sun.faces.numberOfLogicalViews context param and in MyFaces use org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION context param).

Create a page which immediately initializes an OmniFaces @ViewScoped bean during load, and has a form which submits to it. For clarity, add @PostConstruct and @PreDestroy methods which log a debug line.

Open this page in a tab and keep it open all time.

Open the same page in another tab and then immediately close this tab.

Open the same page in another tab and then immediately close this tab.

Open the same page in another tab and then immediately close this tab.

Submit the form in the first tab.

Previously, this would have failed with ViewExpiredException, but since OmniFaces 2.3 not anymore. Currently, a fairly convoluted hack has to be used, as the way to explicitly destroy a physical view is implementation specific. Both MyFaces and Mojarra required a different way. Currently, it only works when partial state saving is kept turned on (as by default). Those who still use full state saving should better ask themselves if it's still fruitful to keep using full state saving in JSF 2.2. During JSF 2.0/2.1 there have been reasons to keep using full state saving, but all of them are workarounds for partial state saving bugs which have long been fixed in JSF 2.2.

While at it I noticed in MyFaces org.apache.myfaces.spi.ViewScopeProvider source code a TODO comment saying that something should be provided to cleanup the session when a view is discarded. This is exactly what we want! It would be even nicer if there was a standard API which explicitly destroys the view state. Perhaps a new removeState or destroyState method in ResponseStateManager. That would make things simpler, also for the JSF implementation itself.