Using WebSockets with Jakarta Faces Technology

This chapter describes using WebSockets in Jakarta Faces web applications.

About WebSockets in Jakarta Faces

You use the f:websocket tag in a view to allow server-side communications to be pushed to all instances of a socket containing the same channel name. When the communication is received, an onmessage, client-side JavaScript event handler can be set that is called whenever a push arrives from the server.

The server side of a WebSocket communication has the ability to push out messages. You can do this using jakarta.faces.push.PushContext, which is an injectable context, allowing a server push to a named channel.

Configuring WebSockets

To configure WebSockets for use in faces web applications, first enable the WebSocket endpoint using the context parameter in web.xml:

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

If your server is configured to run a WebSocket container on a different TCP port than the HTTP container, you can use the optional jakarta.faces.WEBSOCKET_ENDPOINT_PORT integer context parameter to explicitly specify the port:

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

WebSocket Usage: Client Side

Declare the f:websocket tag in the faces view with a channel name and an onmessage JavaScript listener function.

The following example refers to an existing JavaScript listener function:

<f:websocket channel="someChannel" onmessage="someWebsocketListener" />

function someWebsocketListener(message, channel, event) { console.log(message); }

This example declares an inline JavaScript listener function:

<f:websocket channel="someChannel" onmessage="function(m){console.log(m);}" />

The onmessage JavaScript listener function is invoked with three arguments:

  • message: The push message as a JSON object

  • channel: The channel name

  • event: The MessageEvent instance

When successfully connected, the WebSocket is open by default for as long as the document is open, and it will auto-reconnect, at increasing intervals, when the connection is closed, or aborted, as a result of events such as a network error or server restart. It will not auto-reconnect when the very first connection attempt fails. The WebSocket will be implicitly closed after the document is unloaded.

WebSocket Usage: Server Side

On the Java programming side, inject a PushContext using the @Push annotation on the given channel in any CDI or container managed artifact, such as @Named, or @WebServlet, where you want to send a push message. Then invoke PushContext.send(Object) with any Java object representing the push message.

For example:

@Inject @Push
private PushContext someChannel;

public void sendMessage(Object message) {
    someChannel.send(message);
}

By default, the name of the channel is taken from the name of the variable into which the injection takes place.

Optionally, the channel name can be specified using the channel attribute. The following example injects the push context for channel name foo into a variable named bar.

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

The message object will be encoded as JSON and delivered as a message argument of the onmessage JavaScript listener function associated with the channel name. It can be a String, but it can also be a collection, map, or a JavaBean.

Using the f:websocket Tag

Attributes of the f:websocket Tag describes the attributes of the f:websocket tag.

Attributes of the f:websocket Tag
Name Type Description

channel

String

Required. The name of the WebSocket channel. It may not be an EL expression and may only contain alphanumeric characters, hyphens, underscores, and periods. All open WebSockets on the same channel name will receive the same push notification from the server.

id

String

Optional. The identifier of the UIWebSocket component to be created.

scope

String

Optional. The scope of the WebSocket channel. It may not be an EL expression. Allowed values (case insensitive) are: application, session, and view.

When the value is application, all channels with the same name throughout the application receive the same push message. When the value is session, only the channels with the same name in the current user session receive the same push message. When the value is view, only the channel in the current view receives the push message.

The default scope is application. When the user attribute is specified, then the default scope is session.

user

Serializable

Optional. The user identifier of the WebSocket channel, so that user-targeted push messages can be sent. It must implement Serializable and preferably have a low memory footprint.

Hint: Use #{request.remoteUser} or #{someLoggedInUser.id}.

All open WebSockets on the same channel and user will receive the same push message from the server.

onopen

String

Optional. The JavaScript event handler function that is invoked when the WebSocket is opened. The function is invoked with one argument: the channel name.

onmessage

String

Optional. The JavaScript event handler function that is invoked when a push message is received from the server. The function is invoked with three arguments: the push message, the channel name, and the MessageEvent instance.

onclose

String

Optional. The JavaScript event handler function that is invoked when the WebSocket is closed. The function is invoked with three arguments: the close reason code, the channel name, and the CloseEvent.

Note that this will also be invoked on errors. If an error occurred, you can inspect the close reason code and which code was given (for example, when the code is not 1000).

connected

Boolean

Optional. Specifies whether to auto-reconnect the WebSocket. Defaults to true. It is interpreted as a JavaScript instruction to open or close the WebSocket push connection.

This attribute is implicitly re-evaluated on every ajax request by a PreRenderViewEvent listener on the UIViewRoot. You can also explicitly set it to false and then manually control it in JavaScript using jsf.push.open(clientId) and jsf.push.close(clientId).

rendered

Boolean

Optional. Specifies whether to render the WebSocket scripts. Defaults to true.

This attribute is implicitly re-evaluated on every ajax request by a PreRenderViewEvent listener on the UIViewRoot. If the value changes to false while the WebSocket is already opened, then the WebSocket will implicitly be closed.

binding

UIComponent

Optional. The value binding expression to a backing bean property bound to the component instance for the UIComponent created by this tag.

WebSocket Scopes and Users

By default, the WebSocket is application-scoped. For example, any view or session throughout the web application having the same WebSocket channel open will receive the same push message. The push message can be sent by all users and the application. To restrict the push messages to all views in the current user session only, set the optional scope attribute to session . In this case, the push message can only be sent by the user and not by the application.

<f:websocket channel="someChannel" scope="session" ... />

To restrict the push messages to the current view only, you can set the scope attribute to view. The push message will not show up in other views in the same session, even if it has the same URL. This push message can be sent only by the user and not by the application.

<f:websocket channel="someChannel" scope="view" ... />

The scope attribute may not be an EL expression.

Additionally, you can set the optional user attribute to the unique identifier of the logged-in user, usually the login name or the user ID.As such, the push message can be targeted to a specific user and can also be sent by other users and the application. The value of the user attribute must implement Serializable and have a low memory footprint, so an entire user entity is not recommended.

For example, when you are using container managed authentication or a related framework or library:

<f:websocket channel="someChannel"user="#{request.remoteUser}" ... />

Or, when you have a custom user entity accessible via EL, such as #{someLoggedInUser} which has an id property representing its identifier:

<f:websocket channel="someChannel" user="#{someLoggedInUser.id}" ... />

When the user attribute is specified, the scope defaults to session and cannot be set to application.

On the server side, the push message can be targeted to the user specified in the user attribute using PushContext.send(Object, Serializable). The push message can be sent by all users and the application.

@Inject @Push
private PushContext someChannel;

public void sendMessage(Object message, User recipientUser) {
    Long recipientUserId = recipientUser.getId();
    someChannel.send(message, recipientUserId);
}

Multiple users can be targeted by passing a Collection holding user identifiers to PushContext.send(Object, Collection).

public void sendMessage(Object message, Group recipientGroup) {
    Collection<Long> recipientUserIds = recipientGroup.getUserIds();
    someChannel.send(message, recipientUserIds);
}

Conditionally Connecting WebSockets

You can use the optional connected attribute to control whether to auto-reconnect the WebSocket.

<f:websocket ... connected="#{bean.pushable}" />

The connected attribute defaults to true and is interpreted as a JavaScript instruction to open or close the WebSocket push connection. If the value is an EL expression and it becomes false during an ajax request, then the push connection will explicitly be closed during oncomplete of that ajax request.

You can also explicitly set it to false and manually open the push connection on the client side by invoking jsf.push.open(clientId), passing the component’s client ID.

<h:commandButton ... onclick="jsf.push.open('foo')">
    <f:ajax ... />
</h:commandButton>
<f:websocket id="foo" channel="bar" scope="view" ... connected="false" />

If you intend to have a one-time push and do not expect more messages, you can optionally explicitly close the push connection from the client side by invoking jsf.push.close(clientId), passing the component’s client ID. For example, in the onmessage JavaScript listener function, as seen below:

function someWebsocketListener(message) {
// ... jsf.push.close('foo');
}

WebSocket Events: Server

When a session or view-scoped socket is automatically closed with close reason code 1000 by the server (and thus, not manually closed by the client via jsf.push.close(clientId)), it means that the session or view has expired.

@ApplicationScoped
public class WebsocketObserver {

    public void onOpen(@Observes @Opened WebsocketEvent event) {
        String channel = event.getChannel();
        // Returns <f:websocket channel>. Long userId = event.getUser();
        // Returns <f:websocket user>, if any.
        // ...
    }

    public void onClose(@Observes @Closed WebsocketEvent event) { String channel = event.getChannel();
        // Returns <f:websocket channel>. Long userId = event.getUser();
        // Returns <f:websocket user>, if any. CloseCode code = event.getCloseCode();
        // Returns close reason code.
       // ...
    }
}

WebSocket Events: Clients

You can use the optional onopen JavaScript listener function to listen for the open of a WebSocket on the client side. This function is invoked on the very first connection attempt, regardless of whether it will be successful. It will not be invoked when the WebSocket auto-reconnects a broken connection after the first successful connection.

<f:websocket ... onopen="websocketOpenListener" />
function websocketOpenListener(channel) {
// ...
}

The onopen JavaScript listener function is invoked with one argument: channel (the channel name, particularly useful if you have a global listener).

You can use the optional onclose JavaScript listener function to listen for a normal or abnormal close of a WebSocket. This function is invoked when the very first connection attempt fails, or the server has returned close reason code 1000 (normal closure) or 1008 (policy violated), or the maximum reconnect attempts have been exceeded. It will not be invoked if the WebSocket makes an auto-reconnect attempt on a broken connection after the first successful connection.

<f:websocket ... onclose="websocketCloseListener" />
function websocketCloseListener(code, channel, event) {
    if (code == -1) {
        // Websockets not supported by client.
    } else if (code == 1000) {
        // Normal close (as result of expired session or view).
    } else {
        // Abnormal close reason (as result of an error).
    }
}

The onclose JavaScript listener function is invoked with three arguments:

  • code: The close reason code as an integer. If it is -1, the WebSocket is not supported by the client. If it is 1000, then it was normally closed. Otherwise, if it is not 1000, then there might be an error.

  • channel: The channel name

  • event: The CloseEvent instance

WebSocket Security Considerations

If the WebSocket is declared in a page which is restricted to logged-in users only with a specific role, then you might want to add the push handshake request URL to the set of restricted URLs.

The push handshake request URL is composed of the URI prefix, /jakarta.faces.push/, followed by the channel name. In the example of container managed security, which has already restricted an example page, /user/foo.xhtml, to logged-in users with the example role, USER, on the example URL pattern, /user/*, in web.xml, see below:

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Restrict access to role USER.</web-resource-name>
        <url-pattern>/user/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>USER</role-name>
    </auth-constraint>
</security-constraint>

If the page, /user/foo.xhtml, contains <f:websocket channel="foo">, then you must add a restriction on the push handshake request URL pattern of /jakarta.faces.push/foo, as shown next:

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Restrict access to role USER.</web-resource-name>
        <url-pattern>/user/*</url-pattern>
        <url-pattern>/jakarta.faces.push/foo</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>USER</role-name>
    </auth-constraint>
</security-constraint>

As extra security, particularly for those public channels which cannot be restricted by security constraints, the f:websocket tag will register all previously declared channels in the current HTTP session, and any incoming WebSocket open request will be checked for whether it matches these channels in the current HTTP session. If the channel is unknown (for example, randomly guessed or spoofed by end users or manually reconnected after the session is expired), then the WebSocket will immediately be closed with close reason code, CloseCodes.VIOLATED_POLICY (1008). Also, when the HTTP session gets destroyed, all session and view-scoped channels which are still open will explicitly be closed from the server side with close reason code, CloseCodes.NORMAL_CLOSURE (1000). Only application-scoped sockets remain open and are still reachable from the server even when the session or view associated with the page in the client side is expired.

Using Ajax With WebSockets

If you want to perform complex UI updates depending on the received push message, you can nest the f:ajax tag inside the f:websocket tag. See the following example:

<h:panelGroup id="foo">
    ... (some complex UI here) ...
</h:panelGroup>
<h:form>
    <f:websocket channel="someChannel" scope="view">
        <f:ajax event="someEvent" listener="#{bean.pushed}" render=":foo" />
    </f:websocket>
</h:form>

Here, the push message simply represents the ajax event name. You can use any custom event name.

someChannel.send("someEvent");

An alternative is to combine the f:websocket tag with the h:commandScript tag. The <f:websocket onmessage> references exactly the <h:commandScript name>.

For example:

<h:panelGroup id="foo">
    ... (some complex UI here) ...
</h:panelGroup>
<f:websocket channel="someChannel" scope="view" onmessage="pushed" />
<h:form>
    <h:commandScript name="pushed" action="#{bean.pushed}" render=":foo" />
</h:form>

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