All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy