Bauke Scholtz, Arjan Tijms 2018
Bauke Scholtz and Arjan Tijms , The Definitive Guide to JSF in Java EE 8 ,
10. WebSocket Push
Bauke Scholtz 1 and Arjan Tijms 2
(1) Willemstad, Curaao
(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.
JSFs WebSocket support is represented by the new 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 has an additional Java EE 7 dependency: JSON-P (JSR-353). In case youre 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 .
Configuration
The JSR- WebSocket specification does not officially support programmatic initialization of the socket end point during runtime. So we cannot initialize it by simply declaring 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 its never used by the web application. So we cannot avoid having a context parameter to explicitly initialize it during deployment time.
javax.faces.ENABLE_WEBSOCKET_ENDPOINT
true
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> 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 doesnt say so, and there may be WebSocket implementations that simply do not support programmatic initialization, such as Tyrus as used in Payara.
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,
javax.faces.WEBSOCKET_ENDPOINT_PORT
8000
or programmatically in a ServletContainerInitializer :
context.setInitParameter(
PushContext.WEBSOCKET_ENDPOINT_PORT_PARAM_NAME, "8000");
Usage
In your JSF page , just declare the tag along with the required channel attribute representing the channel name and the optional onmessage attribute representing a reference to a JavaScript function.
function logMessage(message, channel, event) {
console.log(message);
}
The JavaScript function will be invoked with three arguments.
message : the push message as JSON object.
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.
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, 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.
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 .
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 recipients;
@Inject @Push
private PushContext chat;
public void sendMessage() {
Set 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.