© Bauke Scholtz, Arjan Tijms 2018

Bauke Scholtz and Arjan Tijms, The Definitive Guide to JSF in Java EE 8, https://doi.org/10.1007/978-1-4842-3387-0_10

10. WebSocket Push

Bauke Scholtz and Arjan Tijms2

(1)Willemstad, Curaçao

(2)Amsterdam, Noord-Holland, The Netherlands

JSF 1.0 introduced HTML form-based POST action support. JSF 2.0 introduced AJAX-based POST support. JSF 2.0 introduced GET query string parameter mapping support. JSF 2.2 introduced GET-based action support. JSF 2.3 introduces WebSocket support.

JSF’s WebSocket support is represented by the new <f:websocket> tag, the PushContext interface , and the @Push annotation. It is built on top of the JSR-356 WebSockets specification, introduced in Java EE 7. Therefore, it is technically possible to use it in a Java EE 7 environment as well. JSR-356 is even natively supported in Tomcat since 7.0.27 and in Jetty since 9.1.0.

In Mojarra , the <f:websocket> has an additional Java EE 7 dependency: JSON-P (JSR-353). In case you’re targeting Tomcat or Jetty instead of a Java EE application server, you might need to install it separately. JSON-P is internally used to convert Java objects to a JSON string so that it can, without much hassle, be transferred to the client side and be provided as an argument of JavaScript listener function attached to <f:websocket>.

Configuration

The JSR-356 WebSocket specification does not officially support programmatic initialization of the socket end point during runtime. So we cannot initialize it by simply declaring <f:websocket> in the view and wait until a JSF page referencing it is being opened for the first time. We really need to initialize it explicitly during deployment time. We could do that by default, but having an unused WebSocket end point open forever is not really nice if it’s never used by the web application. So we cannot avoid having a context parameter to explicitly initialize it during deployment time.

<context-param>
    <param-name>javax.faces.ENABLE_WEBSOCKET_ENDPOINT</param-name>
    <param-value>true</param-value>
</context-param>

If you prefer programmatic initialization over declarative initialization, then you can always use ServletContext#setInitParameter() in a ServletContainerInitializer of your web fragment library as follows:

public class YourInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> types, ServletContext context) {
        context.setInitParameter(
            PushContext.ENABLE_WEBSOCKET_ENDPOINT_PARAM_NAME, "true");
    }
}

Note that it is not possible to perform this task in a ServletContextListener as JSF will actually register the WebSocket end point in its own ServletContainerInitializer implementation which always runs before any ServletContextListener.

Once the WebSocket end point is enabled and successfully initialized during deployment, it will listen for WebSocket handshake requests on the URL (uniform resource locator) pattern /javax.faces.push/*. The first path element will represent the WebSocket channel name.

Coming back to “officially,” some WebSocket implementations do, however, support programmatic initialization, such as the one provided by Undertow , which is in turn used in WildFly. Unfortunately, the spec doesn’t say so, and there may be WebSocket implementations that simply do not support programmatic initialization, such as Tyrus as used in Payara.1

The WebSocket container will, by default, listen for handshake requests on the same port as the application server is listening for HTTP requests. You can optionally change the port with another web.xml context parameter,

<context-param>
    <param-name>javax.faces.WEBSOCKET_ENDPOINT_PORT</param-name>
    <param-value>8000</param-value>
</context-param>

or programmatically in a ServletContainerInitializer:

context.setInitParameter(
    PushContext.WEBSOCKET_ENDPOINT_PORT_PARAM_NAME, "8000");

Usage

In your JSF page , just declare the <f:websocket> tag along with the required channel attribute representing the channel name and the optional onmessage attribute representing a reference to a JavaScript function.

<f:websocket channel="test" onmessage="logMessage" />

<script>
    function logMessage(message, channel, event) {
        console.log(message);
    }
</script>

The JavaScript function will be invoked with three arguments.

  1. message: the push message as JSON object.

  2. channel: the WebSocket channel name. This may be useful in case you intend to have a global listener, or want to manually control the close of the WebSocket.

  3. event: the original MessageEvent object. This may be useful in case you intend to inspect it in the JavaScript function.

On the WAR side, you can inject the PushContext via the @Push annotation in any web artifact that supports CDI injection. This can be a simple CDI managed bean, but it can also be a @WebServlet, @WebFilter or @WebListener.

import javax.inject.Named;
import javax.enterprise.context.RequestScoped;


import javax.inject.Inject;
import javax.faces.push.Push;
import javax.faces.push.PushContext;


@Named @RequestScoped
public class Bean {


    @Inject @Push
    private PushContext test;


    public void submit() {
        test.send("Hello World!");
    }
}

The PushContext variable name test must match the channel name declared in the JSF page. In case you cannot match the variable name with the channel name, you can always specify the channel name in the optional channel attribute of the @Push annotation.

@Inject @Push(channel="test")
private PushContext foo;

Once the submit() method of the bean shown before is invoked by some JSF command component, even in a different JSF page, the push message “Hello World!” will be sent to all opened sockets on the very same channel name, application wide.

Scopes and Users

As you may have realized, <f:websocket> is thus, by default, application scoped. You can control the scope by the optional scope attribute. Allowed values are application, session, and view.

When set to session, the message will be sent to all opened sockets on the very same channel in the current session only.

<f:websocket channel="progress" scope="session" />

This is particularly useful for progress messages coming from long-running session-scoped background tasks initiated by the user itself. This way the user can just continue browsing the site without the need to wait for the result on the very same page.

Alternatively, you can also set the optional user attribute to a serializable value representing the unique user identifier, which can be a String representing the user login name or a Long representing the user ID. When this attribute is set, the scope of the socket will automatically default to session and it cannot be set to application.

<f:websocket channel="chat" user="#{loggedInUser.id}" />

This offers the opportunity to send a message to a specific user as follows:

private String message;
private User recipient;


@Inject @Push
private PushContext chat;


public void sendMessage() {
    Long recipientId = recipient.getId();
    chat.send(message, recipientId);
}

You can even send it to multiple users by providing a Set argument.

private String message;
private Set<User> recipients;


@Inject @Push
private PushContext chat;


public void sendMessage() {
    Set<Long> recipientIds = recipients.stream()
        .map(User::getId)
        .collect(Collectors.toSet());
    chat.send(message, recipientIds);
}

In other words, you can easily implement a chat box this way. Incidentally, real-time user targeted notifications at, for example, Stack Overflow and Facebook work this way.

When the scope is set to view, the message will be sent to the opened socket on the specified channel in the current view only. This won’t affect any sockets on the same channel in all other views throughout the application.

<f:websocket channel="push" scope="view" />

This is also supported in combination with the user attribute.

<f:websocket channel="chat" user="#{user.id}" scope="view" />

This construct is somewhat unusual though and should only be used if the logged-in user represented by user attribute can have a shorter lifetime than the HTTP session. This is, however, in turn considered a poor security practice. The best security practice is, namely, that the HTTP session is invalidated during login and during logout. Invalidating the HTTP session during login prevents session fixation attacks and invalidating the session during logout prevents dirty user-specific data lingering around in HTTP session .

Channel Design Hints

You can declare multiple push channels on different scopes with or without user target throughout the application. However, be aware that the same channel name can easily be reused across multiple views, even if it is view scoped. It’s more efficient if you use as few different channel names as possible and tie the channel name to a specific push socket scope/user combination, not to a specific JSF view. In case you intend to have multiple view-scoped channels for different purposes, it is best to use only one view-scoped channel and have a global JavaScript listener which can distinguish its task based on the delivered message, for example, by sending the message in a server as follows,

Map<String, Object> message = new HashMap<>();
message.put("functionName", "someFunction");
message.put("functionData", functionData); // Can be Map or Bean.
someChannel.send(message);

which is then processed in the onmessage JavaScript listener function as follows:

function someSocketListener(message) {
    window[message.functionName](message.functionData);
}


function someFunction(data) {
    // ...
}


function otherFunction(data) {
    // ...
}


// ...

One-Time Push

You can use the connected attribute to prevent the socket from automatically connecting during page load .

<f:websocket ... connected="false" />

This is particularly useful when you want to perform a one-time push of the result after invoking a view-scoped Ajax action method which might take a bit more time to complete, and you’d like the user to immediately continue using the very same page without being annoyed about a “slow web site” experience. This approach only requires a bit of additional work with the jsf.push JavaScript API (application programming interface).2 It has three functions, but only two are of interest to us: jsf.push.open(...) and jsf.push.close(...). The third one, jsf.push.init(...), basically initializes the socket and that’s up to the renderer of the <f:websocket> tag.

Right before invoking the Ajax action method, you’d need to explicitly open the socket by invoking the jsf.push.open(...) function with the socket client ID as argument. And right after the push message arrives, you’d need to explicitly close the socket by invoking the jsf.push.close(...) function with the socket client ID as argument. The following example demonstrates this approach:

<script>
    function startLongRunningProcess() {
        jsf.push.open("push");
        document.getElementById("status").innerHTML =
            "Long running process has started ...";
    }
    function endLongRunningProcess(result) {
        jsf.push.close("push");
        document.getElementById("status").innerHTML = result;
    }
</script>
<h:form>
    <h:commandButton value="submit"
        onclick="startLongRunningProcess()"
        action="#{longRunningProcess.submit}">
        <f:ajax />
    </h:commandButton>
</h:form>
<div id="status" />
<f:websocket id="push" channel="push" scope="view"
    connected="false" onmessage="endLongRunningProcess">
</f:websocket>

It must be said that it’s a poor practice to put JavaScript code right in the HTML source as shown above. It’s, of course, for demonstration purposes only. For better maintenance, performance, and tooling support, you should, in real-world code, put JavaScript code in a JS file and include it via <h:outputScript>. And then I’m not talking about the lack of jQuery magic for demonstration purposes.

In the example, opening the socket is performed during the onclick of the command button. The onmessage listener function in turn closes the socket. Of course, you can also keep the socket open all the time without fiddling with JavaScript, but it may be a waste of resources if the socket isn’t used for purposes other than presenting the result of a view-scoped Ajax action method. Here is what the associated backing bean looks like.

@Named @RequestScoped
public class LongRunningProcess {


    @Inject
    private LongRunningProcessService service;


    @Inject @Push
    private PushContext push;


    public void submit() {
        service.asyncSubmit(result -> push.send(result));
    }
}

And here is what the service class looks like.

@Stateless
public class LongRunningProcessService {


    @Asynchronous
    public void asyncSubmit(Consumer<String> callback) {
        String result = someLongRunningProcess();
        callback.accept(result);
    }


}

Note the EJB @Asynchronous annotation. This is very important in this construct. It will ensure that the EJB (Enterprise Java Bean) method is executed in a separate thread. This allows the backing bean method to return immediately without waiting for the EJB method to complete.

Stateful UI Updates

As you may have noticed, the onmessage JavaScript listener function is generally only useful for small stateless tasks, such as displaying a feedback message or adding a new item to some stateless list using JavaScript. It isn’t terribly useful when you want to update a stateful UI (user interface) represented by another JSF component. Think of replacing a trivial loading image with a whole JSF table.

For that you’d better nest <f:ajax> listening on a specific push message. Via its render attribute you have the opportunity to automatically update an arbitrary JSF component in an incoming push message. Following is an example which initially shows a loading image and then the table when it’s ready to load:

<h:form>
    <f:websocket channel="push" scope="view">
        <f:ajax event="loaded" render=":results" />
    </f:websocket>
</h:form>
<h:panelGroup id="results" layout="block">
    <h:graphicImage name="images/loading.gif"
        rendered="#{empty longRunningSearch.results}">
    </h:graphicImage>
    <h:dataTable value="#{longRunningSearch.results}" var="result"
        rendered="#{not empty longRunningSearch.results}">
        <h:column>#{result.id}</h:column>
        <h:column>#{result.name}</h:column>
        <h:column>#{result.value}</h:column>
    </h:dataTable>
</h:panelGroup>

Note that <f:websocket> is placed in <h:form>. This is mandatory when it has <f:ajax> nested. Normally this is not required. Here is what the backing bean looks like.

@Named @ViewScoped
public class LongRunningSearch implements Serializable {


    private List<Result> results;

    @Inject
    private LongRunningSearchService service;


    @Inject @Push
    private PushContext push;


    @PostConstruct
    public void init() {
        service.asyncLoadResults(results -> {
            this.results = results;
            push.send("loaded");
        });
    }


    public List<Result> getResults() {
        return results;
    }
}

Note that the push message "loaded" matches exactly the <f:ajax event> value. You can use any value you want and you can nest as many <f:ajax> tags as you need. It’s important that the managed bean is @ViewScoped as the Ajax call is basically performed in a different request within the same view. Finally the service class looks as follows:

@Stateless
public class LongRunningSearchService {


    @Asynchronous
    public void asyncLoadResults(Consumer<List<Result>> callback) {
        List<Result> results = someLongRunningProcess();
        callback.accept(results);
    }
}

The someLongRunningProcess() method represents your implementation of some long-running process (e.g., calling a third-party web service API).

Site-Wide Push Notifications

For this, you can use an application-scoped socket . Such a socket is particularly useful for application-wide feedback messages triggered by the web application itself on a particular event which may be interest to all application users. Think of site-wide statistics, real-time lists, stock updates, etc. The following example shows the case of a real-time top 10 list:

<h:dataTable id="top10" value="#{bean.top10}" var="item">
    <h:column>#{item.ranking}</h:column>
    <h:column>#{item.name}</h:column>
    <h:column>#{item.score}</h:column>
</h:dataTable>
<h:form>
    <f:websocket channel="top10Observer">
        <f:ajax event="updated" render=":top10" />
    </f:websocket>
</h:form>

Here is what the service class looks like, with a little help from CDI events.

@Stateless
public class ItemService {


    @Inject
    private EntityManager entityManager;


    @Inject
    private BeanManager beanManager;


    public void update(Item item) {
        List<Item> previousTop10 = getTop10();
        entityManager.merge(item);
        List<Item> currentTop10 = getTop10();


        if (!currentTop10.equals(previousTop10)) {
            beanManager.fireEvent(new Top10UpdatedEvent());
        }
    }


    pulic List<Item> getTop10() {
        return entityManager
            .createNamedQuery("Item.top10", Item.class)
            .getResultList();
    }
}

Note that the Top10UpdatedEvent is, in this specific example, basically just an empty class like public class Top10UpdatedEvent {}. Also note that we’re not injecting the PushContext here. This is otherwise considered tight coupling of layers. All JSF-related code belongs in the front end, not in the back end. This way the back-end service classes are better reusable across all kinds of front-end frameworks other than JSF, such as JAX-RS or even plain vanilla JSP/Servlet. In other words, you should ensure that none of your back-end classes directly or indirectly use any front-end-specific classes such as those from javax.faces.*, javax.ws.*, and javax.servlet.* packages.

Any event fired with the BeanManager#fireEvent() method can be observed using CDI @Observes annotation. This works across all layers. In other words, even when it’s fired in the back end, you can observe it in the front end. The only requirement is that the backing bean must be @ApplicationScoped. That is, there’s not necessarily any means of an HTTP request, HTTP session, or JSF view anywhere at that moment.

@Named @ApplicationScoped
public class Bean {


    private List<Item> top10;

    @Inject
    private ItemService service;


    @Inject @Push
    private PushContext top10Observer;


    @PostConstruct
    public void load() {
        top10 = service.getTop10();
    }


    public void onTop10Updated(@Observes Top10UpdatedEvent event) {
        load();
        top10Observer.send("updated");
    }


    public List<Item> getTop10() {
        return top10;
    }
}

Keeping Track of Active Sockets

In order to keep track of active sockets , you can in an application-scoped bean observe @WebsocketEvent.Opened and @WebsocketEvent.Closed events. The following example assumes that you have <f:websocket channel="chat" user="..."> and that you intend to collect “active chat users”:

@ApplicationScoped
public class WebsocketEventObserver {


    private Map<Serializable, AtomicInteger> users;

    @PostConstruct
    public void init() {
        users = new ConcurrentHashMap<>();
    }


    public void onopen(@Observes @Opened WebsocketEvent event) {
        if ("chat".equals(event.getChannel())) {
            getCounter(event.getUser()).incrementAndGet();
        }
    }


    public void onclose(@Observes @Closed WebsocketEvent event) {
        if ("chat".equals(event.getChannel())) {
            getCounter(event.getUser()).decrementAndGet();
        }
    }


    private AtomicInteger getCounter(Serializable user) {
        return users.computeIfAbsent(user, k -> new AtomicInteger());
    }


    public Set<Serializable> getActiveUsers() {
        return users.entrySet().stream()
            .filter(entry -> entry.getValue().intValue() > 0)
            .map(entry -> entry.getKey())
            .collect(Collectors.toSet());
    }
}

You can use the above getActiveUsers() method to obtain a set of “active chat users.” Do note that a single user can open the same web page multiple times within the same session (e.g., multiple browser tabs) and that’s exactly why a counter is used instead simply adding and removing users from a Set.

Detecting Session and View Expiration

The <f:websocket> tag will by default keep the connection open forever, as long as the document is open—as long as there’s no connected="false" being set, or jsf.push.close(clientId) being invoked, of course. When the first connection attempt fails, it will immediately report an error. You can optionally use the onclose attribute to reference a JavaScript function which acts as a close listener.

<f:websocket ... onclose="logClose" />

<script>
    function logClose(code, channel, event) {
        if (code == -1) {
            // WebSocket API not supported by client. E.g. IE9.
        }
        else if (code == 1000) {
            // Normal close as result of expired view or session.
        }
        else {
            // Abnormal close as result of a client or server error.
        }
    }
</script>

The JavaScript function will be invoked with three arguments.

  1. code: the close reason code as integer. If this is -1, then the WebSocket JavaScript API is simply not supported3 by the client. If this is 1000, then a normal closure has occurred as consequence of an expired view or session in the server side.

  2. channel: the WebSocket channel name. This may be useful in case you intend to have a global listener.

  3. event: the original CloseEvent object. This may be useful in case you intend to inspect it in the JavaScript function.

When the first connection attempt succeeds but it later gets disconnected for some reason (e.g., because the server is restarting), then it will by default keep trying to reconnect. In the case of Mojarra, it will keep retrying up to 25 times, with an interval which is incremented 500ms each time, and it will eventually report an error.

As you might have noticed in the aforementioned onclose listener function example, you could just check if the close code of a <f:websocket> equals 1000 in order to perform some client-side action via JavaScript (e.g., displaying a warning message and/or redirecting to some “Session expired” page).

<f:websocket channel="push" scope="session" onclose="closeListener" />

<script>
    function closeListener(code) {
        if (code == 1000) {
            window.location = jsf.contextPath + "/expired.xhtml";
        }
    }
</script>

This works for both view- and session-scoped sockets. Application-scoped sockets, however, remain open forever as long as the document is still open on client side, even when the underlying view or session has expired.

Breaking Down Mojarra’s f:websocket Implementation

The <f:websocket> API specifies the following classes and methods:

  • javax.faces.push.Push, a CDI qualifier to for @Inject. With help of this qualifier the socket channel name can be specified.

  • javax.faces.push.PushContext, an interface with three send() methods: send(String message), send(String message, S user), and send(String message, Collection<S> users). All those methods accept the push message as Object and will for JavaScript convert it to a JSON string. All those methods return Future<Void> for each message. If it returns null, then the target socket isn’t open at all. If it doesn’t throw ExecutionException on Future#get() method call, then the message was successfully delivered.

  • javax.faces.component.UIWebsocket, a component which implements ClientBehaviorHolder in order to support nested <f:ajax>. Historically, the prototype tag used a TagHandler instead of UIComponent. It was later decided to let the tag support <f:ajax> as that would make complex and stateful UI updates much easier. However, it isn’t possible to let a TagHandler implement ClientBehaviorHolder and benefit all of built-in Ajax magic, hence the conversion to UIComponent.

  • ViewHandler#getWebsocketURL() method which takes a channel name and returns the absolute WebSocket URL in form of ws://host:port/context/javax.faces.push/channel with help of ExternalContext#encodeWebsocketURL().

  • ExternalContext#encodeWebsocketURL() method which basically takes a relative WebSocket URI in form of /context/javax.faces.push/channel and returns the absolute WebSocket URL.

The actual implementation is fairly extensive. It’s directly based on OmniFaces <o:socket> 4 with, here and there, a few adjustments such as using a component’s client ID instead of a channel name in JavaScript API functions.

  • com.sun.faces.renderkit.html_basic.WebsocketRenderer, a faces renderer class which registers during encoding the socket channel, scope and user in WebsocketChannelManager and retrieves the WebSocket URL from it. Then it auto-includes the jsf.js script containing the necessary javax.push.* functions, and renders the jsf.push.init(...) inline script function call with among others the WebSocket URL as an argument. This function should in turn in JavaScript create a new WebSocket(url). The WebsocketRenderer will also subscribe the WebsocketFacesListener to the current view.

  • com.sun.faces.push.WebsocketChannelManager, a session-scoped CDI managed bean which keeps track of all so far registered <f:websocket> channels, scopes, and users and ensures that each socket gets its own unique channel identifier. It will register every channel identifier in WebsocketSessionManager and those of user-targeted sockets in WebsocketUserManager.

  • com.sun.faces.push. WebsocketFacesListener, a system event listener which listens on PreRenderViewEvent and renders if necessary the jsf.push.open(...) or jsf.push.close(...) inline script function calls depending on whether the connected attribute represents a dynamic EL expression which got changed during an Ajax request.

  • com.sun.faces.push.WebsocketEndpoint, a class which implements JSR-356 javax.websocket.Endpoint and listens on the URI template /javax.faces.push/{channel}. When a new WebSocket(url) is created and opened on client-side JavaScript, then a new javax.websocket.Session is created on server-side Java and the WebsocketEndpoint will add this Session to WebsocketSessionManager. Equivalently, when a socket is closed, then the WebsocketEndpoint will remove it from WebsocketSessionManager.

  • com.sun.faces.push.WebsocketSessionManager, an application-scoped CDI managed bean which collects all so far opened socket sessions and validates that their unique WebSocket URL has been registered by WebsocketChannelManager.

  • com.sun.faces.push.WebsocketUserManager, an application-scoped CDI managed bean which collects the channel identifiers of all so far opened user-targeted sockets.

  • com.sun.faces.push.WebsocketPushContext, the concrete implementation of the PushContext interface. It will send the push message via WebsocketSessionManager and if necessary obtain the user-targeted channels via WebsocketUserManager.

  • com.sun.faces.push.WebsocketPushContextProducer, the CDI producer which creates the WebsocketPushContext instance based on channel name as obtained from @Push qualifier, the WebsocketSessionManager and WebsocketUserManager.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.218.105