Using WebSockets with Jakarta Faces Technology
We are working on a fresh, updated Jakarta EE Tutorial. This section hasn’t yet been updated. |
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
: TheMessageEvent
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.
Name | Type | Description |
---|---|---|
|
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. |
|
String |
Optional. The identifier of the UIWebSocket component to be created. |
|
String |
Optional.
The scope of the WebSocket channel.
It may not be an EL expression.
Allowed values (case insensitive) are: When the value is The default scope is |
|
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 All open WebSockets on the same channel and user will receive the same push message from the server. |
|
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. |
|
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 |
|
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 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 |
|
Boolean |
Optional.
Specifies whether to auto-reconnect the WebSocket.
Defaults to This attribute is implicitly re-evaluated on every ajax request by a |
|
Boolean |
Optional.
Specifies whether to render the WebSocket scripts.
Defaults to This attribute is implicitly re-evaluated on every ajax request by a |
|
UIComponent |
Optional.
The value binding expression to a backing bean property bound to the component instance for the |
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 faces.push.open(clientId)
, passing the component’s client ID.
<h:commandButton ... onclick="faces.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 faces.push.close(clientId)
, passing the component’s client ID.
For example, in the onmessage
JavaScript listener function, as seen below:
function someWebsocketListener(message) {
// ... faces.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 faces.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 is1000
, then it was normally closed. Otherwise, if it is not1000
, then there might be an error. -
channel
: The channel name -
event
: TheCloseEvent
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}
.