jakarta.faces.push.Push Maven / Gradle / Ivy
/*
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package jakarta.faces.push;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Collection;
import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.enterprise.util.Nonbinding;
import jakarta.faces.component.UIWebsocket;
import jakarta.faces.event.WebsocketEvent;
import jakarta.faces.event.WebsocketEvent.Closed;
import jakarta.faces.event.WebsocketEvent.Opened;
import jakarta.inject.Qualifier;
import jakarta.websocket.CloseReason.CloseCodes;
/**
*
* The CDI annotation @
{@link Push} allows you to inject a {@link PushContext} associated with a given
* <f:websocket>
channel in any container managed artifact in WAR.
*
*
* @Inject
* @Push
* private PushContext channelName;
*
*
*
* Configuration
*
* First enable the websocket endpoint by below boolean context parameter in web.xml
.
*
*
* <context-param>
* <param-name>jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT</param-name>
* <param-value>true</param-value>
* </context-param>
*
*
*
* Usage (client)
*
* Declare <f:websocket>
tag in the Jakarta Faces view with at least a
* channel
name and an onmessage
JavaScript listener
* function. The channel name may not be a Jakarta Expression Language expression and it may only contain alphanumeric
* characters, hyphens, underscores and periods.
*
* Here's an example which refers an existing JavaScript listener function.
*
*
* <f:websocket channel="someChannel" onmessage="someWebsocketListener" />
*
*
*
* function someWebsocketListener(message, channel, event) {
* console.log(message);
* }
*
*
* Here's an example which declares an inline JavaScript listener function.
*
*
* <f:websocket channel="someChannel" onmessage="function(message) { console.log(message); }" />
*
*
* The onmessage
JavaScript listener function will be invoked with three arguments:
*
* message
: the push message as JSON object.
* channel
: the channel name.
* event
: the raw
* MessageEvent
instance.
*
*
* In case your server is configured to run WS container on a different TCP port than the HTTP container, then you can
* use the optional jakarta.faces.WEBSOCKET_ENDPOINT_PORT
integer context parameter in
* web.xml
to explicitly specify the port.
*
*
* <context-param>
* <param-name>jakarta.faces.WEBSOCKET_ENDPOINT_PORT</param-name>
* <param-value>8000</param-value>
* </context-param>
*
*
* When successfully connected, the websocket is by default open as long as the document is open, and it will
* auto-reconnect at increasing intervals when the connection is closed/aborted as result of e.g. a network error or
* server restart. It will not auto-reconnect when the very first connection attempt already fails. The websocket will
* be implicitly closed once the document is unloaded.
*
*
*
Usage (server)
*
* In WAR side, you can inject {@link PushContext} via @
{@link Push}
* annotation on the given channel name in any CDI/container managed artifact such as @Named
,
* @WebServlet
, etc wherever you'd like to send a push message and then invoke
* {@link PushContext#send(Object)} with any Java object representing the push message.
*
*
* @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 injection takes place. The
* channel name can be optionally specified via the channel
attribute. The example below 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 be delivered as message
argument of the
* onmessage
JavaScript listener function associated with the channel
name. It can be a plain
* vanilla String
, but it can also be a collection, map and even a javabean.
*
* Although websockets support two-way communication, the <f:websocket>
push is designed for one-way
* communication, from server to client. In case you intend to send some data from client to server, continue using
* Jakarta Faces ajax the usual way. This has among others the advantage of maintaining the Jakarta Faces
* view state, the HTTP session and, importantly, all security constraints on business service methods.
*
*
*
Scopes and users
*
* By default the websocket is application
scoped, i.e. any view/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 itself.
*
* The optional scope
attribute can be set to session
to restrict the push
* messages to all views in the current user session only. The push message can only be sent by the user itself and not
* by the application.
*
*
* <f:websocket channel="someChannel" scope="session" ... />
*
*
* The scope
attribute can also be set to view
to restrict the push messages to the current
* view only. The push message will not show up in other views in the same session even if it's the same URL. The push
* message can only be sent by the user itself and not by the application.
*
*
* <f:websocket channel="someChannel" scope="view" ... />
*
*
* The scope
attribute may not be a Jakarta Expression Language expression and allowed values are
* application
, session
and view
, case insensitive.
*
* Additionally, the optional user
attribute can be set to the unique identifier of the
* logged-in user, usually the login name or the user ID. This way the push message can be targeted to a specific user
* and can also be sent by other users and the application itself. The value of the user
attribute must at
* least implement {@link Serializable} and have a low memory footprint, so putting entire user entity is not
* recommended.
*
* E.g. when you're using container managed authentication or a related framework/library:
*
*
* <f:websocket channel="someChannel" user="#{request.remoteUser}" ... />
*
*
* Or when you have a custom user entity around in Jakarta Expression Language as #{someLoggedInUser}
which
* has an id
property representing its identifier:
*
*
* <f:websocket channel="someChannel" user="#{someLoggedInUser.id}" ... />
*
*
* When the user
attribute is specified, then the scope
defaults to session
and
* cannot be set to application
.
*
* In the server side, the push message can be targeted to the user specified in the user
attribute via
* {@link PushContext#send(Object, Serializable)}. The push message can be sent by all users and the
* application itself.
*
*
* @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 {@link Collection} holding user identifiers to
* {@link PushContext#send(Object, Collection)}.
*
*
* public void sendMessage(Object message, Group recipientGroup) {
* Collection<Long> recipientUserIds = recipientGroup.getUserIds();
* someChannel.send(message, recipientUserIds);
* }
*
*
*
* Conditionally connecting
*
* You can use the optional connected
attribute to control whether to auto-connect the
* websocket or not.
*
*
* <f:websocket ... connected="#{bean.pushable}" />
*
*
* It defaults to true
and it's under the covers interpreted as a JavaScript instruction whether to open or
* close the websocket push connection. If the value is a Jakarta Expression Language 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 in 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" />
*
*
* In case you intend to have an one-time push and don't expect more messages, you can optionally explicitly close the
* push connection from client side by invoking faces.push.close(clientId)
, passing the
* component's client ID. For example, in the onmessage
JavaScript listener function as below:
*
*
* function someWebsocketListener(message) {
* // ...
* faces.push.close('foo');
* }
*
*
*
* Events (client)
*
* The optional onopen
JavaScript listener function can be used to listen on open of a
* websocket in client side. This will be invoked on the very first connection attempt, regardless of whether it will be
* successful or not. This 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 will be invoked with one argument:
*
* channel
: the channel name, useful in case you intend to have a global listener.
*
*
* The optional onerror
JavaScript listener function can be used to listen on a connection
* error whereby the websocket will attempt to reconnect. This will be invoked when the websocket can make an
* auto-reconnect attempt on a broken connection after the first successful connection. This will be not
* 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 has exceeded. Instead,
* the onclose
will be invoked.
*
* <o:socket ... onerror="websocketErrorListener" />
*
*
* function websocketErrorListener(code, channel, event) {
* if (code == 1001) {
* // Server has returned an unexpected response code. E.g. 503, because it's shutting down.
* } else if (code == 1006) {
* // Server is not reachable anymore. I.e. it's not anymore listening on TCP/IP requests.
* } else {
* // Any other reason which is usually not -1, 1000 or 1008, as the onclose will be invoked instead.
* }
*
* // In any case, the websocket will attempt to reconnect. This function will be invoked again.
* // Once the websocket gives up reconnecting, the onclose will finally be invoked.
* }
*
*
* The onerror
JavaScript listener function will be invoked with three arguments:
*
* code
: the close reason code as integer. See also
* RFC 6455 section 7.4.1 and {@link CloseCodes} API for
* an elaborate list of all close codes.
* channel
: the channel name, useful in case you intend to have a global listener.
* event
: the raw
* CloseEvent
instance, useful in case you intend to inspect it.
*
*
* The optional onclose
JavaScript listener function can be used to listen on (ab)normal
* close of a websocket. This will be 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 has exceeded. This will not be invoked when the websocket can make an auto-reconnect attempt on a
* broken connection after the first successful connection. Instead, the onerror
will be invoked.
*
*
* <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 will be invoked with three arguments:
*
* code
: the close reason code as integer. If this is -1
, then the websocket is simply
* not supported by the client. If this is 1000
, then it was
* normally closed due to an expired session or view. Else if this is not 1000
, then there may be an error.
* See also RFC 6455 section 7.4.1 and {@link CloseCodes}
* API for an elaborate list of all close codes.
* channel
: the channel name.
* event
: the raw
* CloseEvent
instance.
*
*
* When a session or view scoped websocket is automatically closed with close reason code 1000
by the server
* (and thus not manually by the client via faces.push.close(clientId)
), then it means that the session or
* view has expired.
*
*
*
Events (server)
*
* When a websocket has been opened, a new CDI {@link WebsocketEvent} will be fired with
* @
{@link Opened} qualifier. When a websocket has been closed, a new CDI
* {@link WebsocketEvent} will be fired with @
{@link Closed} qualifier. They can only
* be observed and collected in an application scoped CDI bean as below.
*
*
* @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.
* // ...
* }
*
* }
*
*
*
* Security considerations
*
* If the websocket is declared in a page which is only restricted to logged-in users with a specific role, then you may
* want to add the URL of 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 channel name. So, in case of for example 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
like 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>
*
*
* .. and the page /user/foo.xhtml
in turn contains a <f:websocket channel="foo">
, then
* you need to add a restriction on push handshake request URL pattern of /jakarta.faces.push/foo
like
* below.
*
*
* <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 can't be restricted by security constraints, the
* <f:websocket>
will register all so far declared channels in the current HTTP session, and any
* incoming websocket open request will be checked whether they match the so far registered channels in the current
* HTTP session. In case the channel is unknown (e.g. randomly guessed or spoofed by endusers or manually reconnected
* after the session is expired), then the websocket will immediately be closed with close reason code
* {@link 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 server side with close reason code
* {@link CloseCodes#NORMAL_CLOSURE} (1000
). Only application scoped websockets remain open and are still
* reachable from server end even when the session or view associated with the page in client side is expired.
*
*
*
Ajax support
*
* In case you'd like to perform complex UI updates depending on the received push message, then you can nest
* <f:ajax>
inside <f:websocket>
. Here's an 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 <w:websocket>
with <h:commandScript>
. E.g.
*
*
* <h:panelGroup id="foo">
* ... (some complex UI here) ...
* </h:panelGroup>
*
* <f:websocket channel="someChannel" scope="view" onmessage="someCommandScript" />
* <h:form>
* <h:commandScript name="someCommandScript" action="#{bean.pushed}" render=":foo" />
* </h:form>
*
*
* 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 method #{bean.pushed}
.
*
*
* @see PushContext
* @see UIWebsocket
* @see WebsocketEvent
* @since 2.3
*/
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface Push {
/**
* (Optional) The name of the push channel. If not specified the name of the injection target field will be used.
*
* @return The name of the push channel.
*/
@Nonbinding
String channel() default "";
/**
*
* Supports inline instantiation of the {@link Push} qualifier.
*
*
* @since 4.0
*/
public static final class Literal extends AnnotationLiteral implements Push {
private static final long serialVersionUID = 1L;
/**
* Instance of the {@link Push} qualifier.
*/
public static final Literal INSTANCE = of("");
private final String channel;
public static Literal of(String channel) {
return new Literal(channel);
}
private Literal(String channel) {
this.channel = channel;
}
@Override
public String channel() {
return channel;
}
}
}