Friday, April 1, 2016

OmniFaces 2.3 brings JSF and WebSockets together!

OmniFaces 2.3 has been released!

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.

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

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

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.

<o:socket channel="top10" onmessage="top10Listener" />
function top10Listener(top10) {
    console.log(top10); // Do your thing with HTML DOM.
}
@Startup @Singleton
@ConcurrencyManagement(BEAN)
public class SomeTop10Manager {

    private List<Some> top10;

    @PersistenceContext
    private EntityManager entityManager;

    @Inject
    private BeanManager beanManager;

    @PostConstruct
    @Schedule(hour="*", minute="*/1", second="0", persistent=false)
    public void load() {
        List<Some> top10 = entityManager.createNamedQuery("Some.top10", Some.class).getResultList();

        if (!top10.equals(this.top10)) {
            this.top10 = top10;
            beanManager.fireEvent(new SomeTop10Event(top10));
        }
    }

    public List<Some> list() {
        return top10;
    }

}
@ApplicationScoped
public class SomeTop10Observer {

    @Inject @Push
    private PushContext top10;

    public void onTop10Change(@Observes SomeTop10Event event) {
        top10.send(event.getTop10());
    }

}

Example 2

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.

<o:socket channel="chat" user="#{user.id}" onmessage="chatListener" />
function chatListener(message) {
    console.log(message); // Do your thing with HTML DOM.
}
<h:form>
    <p:inputText value="#{someChatBacking.message}" required="true" />
    <p:selectOneMenu value="#{someChatBacking.recipientUser}" required="true">
        <f:selectItems value="#{someSocketObserver.onlineUsers}" var="user" itemLabel="#{user.name}" />
    </p:selectOneMenu>
    <p:commandButton value="send" action="#{someChatBacking.send}" />
</h:form>
@Named
@RequestScoped
public class SomeChatBacking {

    @Inject @Push
    private PushContext chat;

    private String message;
    private User recipientUser;

    public void send() {
        Set<Future<Void>> sent = chat.send(message, recipientUser.getId());

        // TIPS:
        // sent.size() represents amount of open web sockets at send time.
        // future.get() will return null if message is successfully delivered.
        // future.get() will throw ExecutionException if message delivery failed.
        // You can observe @Opened/@Closed websockets with CDI. See o:socket showcase/javadoc.
    }

    // ...
}

Example 3

The below conditionally connected view scoped socket example will receive the push message as result of a user-initiated asynchronous task only in the current view.

<h:form>
    <p:commandButton widgetVar="asyncCommand" value="submit"
        action="#{someTaskBacking.start}" onclick="startAsync()" />
</h:form>
<o:socket channel="async" scope="view" onmessage="asyncListener" connected="false" />
function startAsync() {
    PF("asyncCommand").disable();
    OmniFaces.Push.open("async");
}
function asyncListener(result) {
    OmniFaces.Push.close("async");
    console.log(result); // Do your thing with HTML DOM.
    PF("asyncCommand").enable();
}
@Named
@RequestScoped
public class SomeTaskBacking {

    @Inject
    private SomeTaskService service;

    @Inject @Push
    private PushContext async;

    public void start() {
        service.someLongRunningAsyncTask(result -> async.send(result));
    }

}
@Stateless
public class SomeTaskService {

    @Asynchronous
    public void someLongRunningAsyncTask(Consumer<Object> callback) {
        // ...
        // ... (some long process)
        // ...

        callback.accept(result);
    }

}

Complex UI updates

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.

<o:socket ... onmessage="commandScriptName" />
<o:commandScript name="commandScriptName" action="#{bean.pushed}" render="result" />

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.
    }
}

For an elaborate documentation and several live demos, checkout the <o:socket> showcase.

Skip validators

Contributed by Michele Mariotti, <o:skipValidators> allows developers to entirely skip JSF validation (required, <f:validateXxx>, custom validators, etc) when executing a specific UICommand or ClientBehaviorHolder action.

For example, when adding a new row to the data table, you'd like to not immediately validate all empty rows.

<h:form>
    <h:dataTable value="#{bean.items}" var="item">
        <h:column>
            <h:inputText value="#{item.value}" required="true" />
        </h:column>
    </h:dataTable>
    <h:commandButton value="add new row" action="#{bean.add}">
        <o:skipValidators />
    </h:commandButton>
    <h:commandButton value="save all data" action="#{bean.save}" />
    <h:messages />
</h:form>

Under the covers, it will basically during PreValidateEvent remove and remember all validators and re-attach them during the PostValidateEvent.

Only converters will still run. Conversion failures can still be suppressed with help of the existing <o:ignoreValidationFailed>.

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.

A good use case is described in OmniFaces issue 208:

  1. 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).
  2. 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.
  3. Open this page in a tab and keep it open all time.
  4. Open the same page in another tab and then immediately close this tab.
  5. Open the same page in another tab and then immediately close this tab.
  6. Open the same page in another tab and then immediately close this tab.
  7. 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.

Maven download stats

Here are the Maven download stats after previous release:

  • November 2015: 5906
  • December 2015: 6068
  • January 2016: 6135
  • February 2016: 6635
  • March 2016: 7265

Below is the version pie of March 2016: