/* * Copyright 2017 OmniFaces * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ package org.omnifaces.cdi.push; import static java.lang.Boolean.TRUE; import static java.lang.Boolean.parseBoolean; import static java.lang.String.format; import static java.util.Collections.unmodifiableList; import static java.util.logging.Logger.getLogger; import static javax.faces.component.behavior.ClientBehaviorContext.createClientBehaviorContext; import static org.omnifaces.util.Beans.getReference; import static org.omnifaces.util.FacesLocal.getApplicationAttribute; import static org.omnifaces.util.FacesLocal.getRequestContextPath; import static org.omnifaces.util.FacesLocal.getRequestParameter; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Pattern; import javax.el.ValueExpression; import javax.enterprise.event.Observes; import javax.faces.FacesException; import javax.faces.application.ResourceDependency; import javax.faces.component.FacesComponent; import javax.faces.component.UIComponent; import javax.faces.component.behavior.ClientBehavior; import javax.faces.component.behavior.ClientBehaviorHolder; import javax.faces.context.FacesContext; import javax.faces.event.ComponentSystemEvent; import javax.faces.event.ListenerFor; import javax.faces.event.PostAddToViewEvent; import javax.servlet.ServletContext; import javax.websocket.CloseReason.CloseCodes; import javax.websocket.server.ServerContainer; import javax.websocket.server.ServerEndpointConfig; import org.omnifaces.cdi.Push; import org.omnifaces.cdi.PushContext; import org.omnifaces.cdi.push.SocketEvent.Closed; import org.omnifaces.cdi.push.SocketEvent.Opened; import org.omnifaces.component.script.ScriptFamily; import org.omnifaces.util.Beans; import org.omnifaces.util.Callback; import org.omnifaces.util.Json; import org.omnifaces.util.State; /** * <p> * The <code><o:socket></code> is an {@link UIComponent} whith opens an one-way (server to client) web socket * based push connection in client side which can be reached from server side via {@link PushContext} interface injected * in any CDI/container managed artifact via <code>@</code>{@link Push} annotation. * * * <h3 id="configuration"><a href="#configuration">Configuration</a></h3> * <p> * First enable the web socket endpoint by below boolean context parameter in <code>web.xml</code>: * <pre> * <context-param> * <param-name>org.omnifaces.SOCKET_ENDPOINT_ENABLED</param-name> * <param-value>true</param-value> * </context-param> * </pre> * <p> * It will install the {@link SocketEndpoint}. Lazy initialization of the endpoint via component is unfortunately not * possible across all containers (yet). * See also <a href="https://java.net/jira/browse/WEBSOCKET_SPEC-211">WS spec issue 211</a>. * * * <h3 id="usage-client"><a href="#usage-client">Usage (client)</a></h3> * <p> * Declare <strong><code><o:socket></code></strong> tag in the JSF view with at least a * <strong><code>channel</code></strong> name and an <strong><code>onmessage</code></strong> JavaScript listener * function. The channel name may not be an EL expression and it may only contain alphanumeric characters, hyphens, * underscores and periods. * <p> * Here's an example which refers an existing JavaScript listener function (do not include the parentheses!). * <pre> * <o:socket channel="someChannel" onmessage="socketListener" /> * </pre> * <pre> * function socketListener(message, channel, event) { * console.log(message); * } * </pre> * <p> * Here's an example which declares an inline JavaScript listener function. * <pre> * <o:socket channel="someChannel" onmessage="function(message) { console.log(message); }" /> * </pre> * <p> * The <code>onmessage</code> JavaScript listener function will be invoked with three arguments: * <ul> * <li><code>message</code>: the push message as JSON object.</li> * <li><code>channel</code>: the channel name, useful in case you intend to have a global listener, or want to manually * control the close.</li> * <li><code>event</code>: the raw <a href="https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent"><code> * MessageEvent</code></a> instance, useful in case you intend to inspect it.</li> * </ul> * <p> * 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 <strong><code>port</code></strong> attribute to explicitly specify the port. * <pre> * <o:socket port="8000" ... /> * </pre> * <p> * When successfully connected, the web socket 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 web socket will * be implicitly closed once the document is unloaded (e.g. navigating away, close of browser window/tab, etc). * * * <h3 id="usage-server"><a href="#usage-server">Usage (server)</a></h3> * <p> * In WAR side, you can inject <strong>{@link PushContext}</strong> via <strong><code>@</code>{@link Push}</strong> * annotation on the given channel name in any CDI/container managed artifact such as <code>@Named</code>, * <code>@WebServlet</code>, etc wherever you'd like to send a push message and then invoke * <strong>{@link PushContext#send(Object)}</strong> with any Java object representing the push message. * <pre> * @Inject @Push * private PushContext someChannel; * * public void sendMessage(Object message) { * someChannel.send(message); * } * </pre> * <p> * 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 <code>channel</code> attribute. The example below injects the push * context for channel name <code>foo</code> into a variable named <code>bar</code>. * <pre> * @Inject @Push(channel="foo") * private PushContext bar; * </pre> * <p> * The message object will be encoded as JSON and be delivered as <code>message</code> argument of the * <code>onmessage</code> JavaScript listener function associated with the <code>channel</code> name. It can be a * plain vanilla <code>String</code>, but it can also be a collection, map and even a javabean. For supported argument * types, see also {@link Json#encode(Object)}. * <p> * Although web sockets support two-way communication, the <code><o:socket></code> push is designed for one-way * communication, from server to client. In case you intend to send some data from client to server, just continue * using JSF ajax the usual way, if necessary from JavaScript on with <code><o:commandScript></code> or perhaps * <code><p:remoteCommand></code> or similar. This has among others the advantage of maintaining the JSF view * state, the HTTP session and, importantingly, all security constraints on business service methods. Namely, those * security constraints are not available during an incoming web socket message per se. See also a.o. * <a href="https://java.net/jira/browse/WEBSOCKET_SPEC-238">WS spec issue 238</a>. * * * <h3 id="scopes-and-users"><a href="#scopes-and-users">Scopes and users</a></h3> * <p> * By default the web socket is <code>application</code> scoped, i.e. any view/session throughout the web application * having the same web socket channel open will receive the same push message. The push message can be sent by all users * and the application itself. This is useful for application-wide feedback triggered by site itself such as real time * updates of a certain page (e.g. site-wide statistics, top100 lists, stock updates, etc). * <p> * The optional <strong><code>scope</code></strong> attribute can be set to <code>session</code> 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. This is useful for session-wide feedback triggered by user itself (e.g. as result of asynchronous * tasks triggered by user specific action). * <pre> * <o:socket channel="someChannel" scope="session" ... /> * </pre> * <p> * The <code>scope</code> attribute can also be set to <code>view</code> 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. This is useful for view-wide feedback * triggered by user itself (e.g. progress bar tied to a user specific action on current view). * <pre> * <o:socket channel="someChannel" scope="view" ... /> * </pre> * <p> * The <code>scope</code> attribute may not be an EL expression and allowed values are <code>application</code>, * <code>session</code> and <code>view</code>, case insensitive. * <p> * Additionally, the optional <strong><code>user</code></strong> 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 <code>user</code> attribute must at * least implement {@link Serializable} and have a low memory footprint, so putting entire user entity is not * recommended. * <p> * E.g. when you're using container managed authentication or a related framework/library: * <pre> * <o:socket channel="someChannel" user="#{request.remoteUser}" ... /> * </pre> * <p> * Or when you have a custom user entity around in EL as <code>#{someLoggedInUser}</code> which has an <code>id</code> * property representing its identifier: * <pre> * <o:socket channel="someChannel" user="#{someLoggedInUser.id}" ... /> * </pre> * <p> * When the <code>user</code> attribute is specified, then the <code>scope</code> defaults to <code>session</code> and * cannot be set to <code>application</code>. It can be set to <code>view</code>, but this is kind of unusual and should * only be used if the logged-in user represented by <code>user</code> has a shorter lifetime than the HTTP session * (e.g. when your application allows changing a logged-in user during same HTTP session without invaliding it — * which is in turn poor security practice). If in such case a session scoped socket is reused, undefined behavior may * occur when user-targeted push message is sent. It may target previously logged-in user only. This can be solved by * setting the scope to <code>view</code>, but better is to fix the logout to invalidate the HTTP session altogether. * <p> * In the server side, the push message can be targeted to the user specified in the <code>user</code> attribute via * <strong>{@link PushContext#send(Object, Serializable)}</strong>. The push message can be sent by all users and the * application itself. This is useful for user-specific feedback triggered by other users (e.g. chat, admin messages, * etc) or by application's background tasks (e.g. notifications, event listeners, etc). * <pre> * @Inject @Push * private PushContext someChannel; * * public void sendMessage(Object message, User recipientUser) { * Long recipientUserId = recipientUser.getId(); * someChannel.send(message, recipientUserId); * } * </pre> * <p> * Multiple users can be targeted by passing a {@link Collection} holding user identifiers to * <strong>{@link PushContext#send(Object, Collection)}</strong>. * <pre> * public void sendMessage(Object message, Group recipientGroup) { * Collection<Long> recipientUserIds = recipientGroup.getUserIds(); * someChannel.send(message, recipientUserIds); * } * </pre> * * * <h3 id="channels"><a href="#channels">Channel design hints</a></h3> * <p> * You can declare multiple push channels on different scopes with or without user target throughout the application. * Be however aware that the same channel name can easily be reused across multiple views, even if it's view scoped. * It's more efficient if you use as few different channel names as possible and tie the channel name to a specific * push socket scope/user combination, not to a specific JSF view. In case you intend to have multiple view scoped * channels for different purposes, best is to use only one view scoped channel and have a global JavaScript listener * which can distinguish its task based on the delivered message. E.g. by sending the message in server as below: * <pre> * Map<String, Object> message = new HashMap<>(); * message.put("functionName", "someFunction"); * message.put("functionData", functionData); // Can be Map or Bean. * someChannel.send(message); * </pre> * <p> * Which is processed in the <code>onmessage</code> JavaScript listener function as below: * <pre> * function someSocketListener(message) { * window[message.functionName](message.functionData); * } * * function someFunction(data) { * // ... * } * * function otherFunction(data) { * // ... * } * * // ... * </pre> * * * <h3 id="connecting"><a href="#connecting">Conditionally connecting</a></h3> * <p> * You can use the optional <strong><code>connected</code></strong> attribute to control whether to auto-connect the web * socket or not. * <pre> * <o:socket ... connected="#{bean.pushable}" /> * </pre> * <p> * It defaults to <code>true</code> and it's under the covers interpreted as a JavaScript instruction whether to open or * close the web socket push connection. If the value of the <code>connected</code> or <code>rendered</code> attribute * is an EL expression and it becomes <code>false</code> during an ajax request, then any opened push connection will * explicitly be closed during oncomplete of that ajax request, even though you did not cover the * <code><o:socket></code> component in ajax render/update. So make sure the value is tied to at least a view * scoped property in case you intend to control it during the view scope. * <p> * You can also explicitly set it to <code>false</code> and manually open the push connection in client side by * invoking <strong><code>OmniFaces.Push.open(channel)</code></strong>, passing the channel name, for example in an * onclick listener function of a command button which initiates a long running asynchronous task in server side. This * is particularly useful on view scoped sockets which doesn't necessarily need to immediately open on page load. * <pre> * <h:commandButton ... onclick="OmniFaces.Push.open('foo')"> * <f:ajax ... /> * </h:commandButton> * <o:socket channel="foo" scope="view" ... connected="false" /> * </pre> * <p> * In case you intend to have an one-time push and don't expect more messages, usually because you only wanted to * present the result of an one-time asynchronous action in a manually opened view scoped push socket as in above * example, you can optionally explicitly close the push connection from client side by invoking * <strong><code>OmniFaces.Push.close(channel)</code></strong>, passing the channel name. For example, in the * <code>onmessage</code> JavaScript listener function as below: * <pre> * function someSocketListener(message, channel) { * // ... * OmniFaces.Push.close(channel); * } * </pre> * <p> * Noted should be that both ways should not be mixed. Choose either the server side way of an EL expression in * <code>connected</code> attribute, or the client side way of explicitly setting <code>connected="false"</code> and * manually invoking <code>OmniFaces.Push</code> functions. Mixing them ends up in undefined behavior because the * associated JSF view state in the server side can't be notified if a socket is manually opened in client side. * * * <h3 id="events-client"><a href="#events-client">Events (client)</a></h3> * <p> * The optional <strong><code>onopen</code></strong> JavaScript listener function can be used to listen on open of a web * socket 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 web socket auto-reconnects a broken connection after the first * successful connection. * <pre> * <o:socket ... onopen="socketOpenListener" /> * </pre> * <pre> * function socketOpenListener(channel) { * // ... * } * </pre> * <p> * The <code>onopen</code> JavaScript listener function will be invoked with one argument: * <ul> * <li><code>channel</code>: the channel name, useful in case you intend to have a global listener.</li> * </ul> * <p> * The optional <strong><code>onclose</code></strong> JavaScript listener function can be used to listen on (ab)normal * close of a web socket. This will be invoked when the very first connection attempt fails, or the server has returned * close reason code <code>1000</code> (normal closure) or <code>1008</code> (policy violated), or the maximum reconnect * attempts has exceeded. This will not be invoked when the web socket can make an auto-reconnect attempt on a broken * connection after the first successful connection. * <pre> * <o:socket ... onclose="socketCloseListener" /> * </pre> * <pre> * function socketCloseListener(code, channel, event) { * if (code == -1) { * // Web sockets 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). * } * } * </pre> * <p> * The <code>onclose</code> JavaScript listener function will be invoked with three arguments: * <ul> * <li><code>code</code>: the close reason code as integer. If this is <code>-1</code>, then the web socket * is simply not <a href="http://caniuse.com/websockets">supported</a> by the client. If this is <code>1000</code>, * then it was normally closed. Else if this is not <code>1000</code>, then there may be an error. See also * <a href="http://tools.ietf.org/html/rfc6455#section-7.4.1">RFC 6455 section 7.4.1</a> and {@link CloseCodes} API for * an elaborate list of all close codes.</li> * <li><code>channel</code>: the channel name, useful in case you intend to have a global listener.</li> * <li><code>event</code>: the raw <a href="https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent"><code> * CloseEvent</code></a> instance, useful in case you intend to inspect it.</li> * </ul> * <p> * When a session or view scoped socket is automatically closed with close reason code <code>1000</code> by the server * (and thus not manually by the client via <code>OmniFaces.Push.close(channel)</code>), then it means that the session * or view has expired. In case of a session scoped socket you could take the opportunity to let JavaScript show a * "Session expired" message and/or immediately redirect to the login page via <code>window.location</code>. In case of * a view scoped socket the handling depends on the reason of the view expiration. A view can be expired when the * associated session has expired, but it can also be expired as result of (accidental) navigation or rebuild, or when * the JSF "views per session" configuration setting is set relatively low and the client has many views (windows/tabs) * open in the same session. You might take the opportunity to warn the client and/or let JavaScript reload the page as * submitting any form in it would throw <code>ViewExpiredException</code> anyway. * * * <h3 id="events-server"><a href="#events-server">Events (server)</a></h3> * <p> * When a web socket has been opened, a new CDI <strong>{@link SocketEvent}</strong> will be fired with * <strong><code>@</code>{@link Opened}</strong> qualifier. When a web socket has been closed, a new CDI * {@link SocketEvent} will be fired with <strong><code>@</code>{@link Closed}</strong> qualifier. They can only be * observed and collected in an application scoped CDI bean as below. Observing in a request/view/session scoped CDI * bean is not possible as there's no means of a HTTP request anywhere at that moment. * <pre> * @ApplicationScoped * public class SocketObserver { * * public void onOpen(@Observes @Opened SocketEvent event) { * String channel = event.getChannel(); // Returns <o:socket channel>. * Long userId = event.getUser(); // Returns <o:socket user>, if any. * // Do your thing with it. E.g. collecting them in a concurrent/synchronized collection. * // Do note that a single person can open multiple sockets on same channel/user. * } * * public void onClose(@Observes @Closed SocketEvent event) { * String channel = event.getChannel(); // Returns <o:socket channel>. * Long userId = event.getUser(); // Returns <o:socket user>, if any. * CloseCode code = event.getCloseCode(); // Returns close reason code. * // Do your thing with it. E.g. removing them from collection. * } * * } * </pre> * <p> * You could take the opportunity to send another push message to an application scoped socket, e.g. "User X has been * logged in" (or out) when a session scoped socket is opened (or closed). * * * <h3 id="security"><a href="#security">Security considerations</a></h3> * <p> * If the socket 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. * <p> * The push handshake request URL is composed of the URI prefix <strong><code>/omnifaces.push/</code></strong>, followed * by channel name. So, in case of for example container managed security which has already restricted an example page * <code>/user/foo.xhtml</code> to logged-in users with the example role <code>USER</code> on the example URL pattern * <code>/user/*</code> in <code>web.xml</code> like below, * <pre> * <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> * </pre> * <p> * .. and the page <code>/user/foo.xhtml</code> in turn contains a <code><o:socket channel="foo"></code>, then you * need to add a restriction on push handshake request URL pattern of <code>/omnifaces.push/foo</code> like below. * <pre> * <security-constraint> * <web-resource-collection> * <web-resource-name>Restrict access to role USER.</web-resource-name> * <url-pattern>/user/*</url-pattern> * <url-pattern>/omnifaces.push/foo</url-pattern> * </web-resource-collection> * <auth-constraint> * <role-name>USER</role-name> * </auth-constraint> * </security-constraint> * </pre> * <p> * As extra security, particularly for those public channels which can't be restricted by security constraints, the * <code><o:socket></code> will register all so far declared channels in the current HTTP session, and any * incoming web socket 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 web socket will immediately be closed with close reason code * <code>1008</code> ({@link CloseCodes#VIOLATED_POLICY}). 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 * <code>1000</code> ({@link CloseCodes#NORMAL_CLOSURE}). Only application scoped sockets remain open and are still * reachable from server end even when the session or view associated with the page in client side is expired. * * * <h3 id="ejb"><a href="#ejb">EJB design hints</a></h3> * <p> * In case you'd like to trigger a push from EAR/EJB side to an application scoped push socket, then you could make use * of CDI events. First create a custom bean class representing the push event something like <code>PushEvent</code> * below taking whatever you'd like to pass as push message. * <pre> * public final class PushEvent { * * private final String message; * * public PushEvent(String message) { * this.message = message; * } * * public String getMessage() { * return message; * } * } * </pre> * <p> * Then use {@link javax.enterprise.inject.spi.BeanManager#fireEvent(Object, java.lang.annotation.Annotation...)} to * fire the CDI event. * <pre> * @Inject * private BeanManager beanManager; * * public void onSomeEntityChange(Entity entity) { * beanManager.fireEvent(new PushEvent(entity.getSomeProperty())); * } * </pre> * <p> * Note that OmniFaces own {@link Beans#fireEvent(Object, java.lang.annotation.Annotation...)} utility method is * insuitable as it is not allowed to use WAR (front end) frameworks and libraries like JSF and OmniFaces in EAR/EJB * (back end) side. * <p> * Finally just <code>@</code>{@link Observes} it in some request or application scoped CDI managed bean in WAR and * delegate to {@link PushContext} as below. * <pre> * @Inject @Push * private PushContext someChannel; * * public void onPushEvent(@Observes PushEvent event) { * someChannel.send(event.getMessage()); * } * </pre> * <p> * Note that a request scoped bean wouldn't be the same one as from the originating page for the simple reason that * there's no means of a HTTP request anywhere at that moment. For exactly this reason a view and session scoped bean * would not work (as they require respectively the JSF view state and HTTP session which can only be identified by a * HTTP request). A view and session scoped push socket would also not work, so the push socket really needs to be * application scoped. The {@link FacesContext} will also be unavailable in the above event listener method. * <p> * In case the trigger in EAR/EJB side is an asynchronous service method which is in turn initiated in WAR side, then * you could make use of callbacks from WAR side. Let the business service method take a callback instance as argument, * e.g. the <code>java.util.function.Consumer</code> functional interface. * <pre> * @Asynchronous * public void someAsyncServiceMethod(Entity entity, Consumer<Object> callback) { * // ... (some long process) * callback.accept(entity.getSomeProperty()); * } * </pre> * <p> * And invoke the asynchronous service method in WAR as below. * <pre> * @Inject * private SomeService someService; * * @Inject @Push * private PushContext someChannel; * * public void someAction() { * someService.someAsyncServiceMethod(entity, message -> someChannel.send(message)); * } * </pre> * <p> * This would be the only way in case you intend to asynchronously send a message to a view or session scoped push * socket, and/or want to pass something from {@link FacesContext} or the initial request/view/session scope along as * (<code>final</code>) argument. * <p> * In case you're not on Java 8 yet, then you can make use of {@link Runnable} as callback instance instead of the * above <code>Consumer</code> functional interface example. * <pre> * @Asynchronous * public void someAsyncServiceMethod(Entity entity, Runnable callback) { * // ... (some long process) * entity.setSomeProperty(someProperty); * callback.run(); * } * </pre> * <p> * Which is invoked in WAR as below. * <pre> * public void someAction() { * someService.someAsyncServiceMethod(entity, new Runnable() { * public void run() { * someChannel.send(entity.getSomeProperty()); * } * }); * } * </pre> * <p> * Note that OmniFaces own {@link Callback} interfaces are insuitable as it is not allowed to use WAR (front end) * frameworks and libraries like JSF and OmniFaces in EAR/EJB (back end) side. * * * <h3 id="ui"><a href="#ui">UI update design hints</a></h3> * <p> * In case you'd like to perform complex UI updates, then easiest would be to put <code><f:ajax></code> inside * <code><o:socket></code>. The support was added in OmniFaces 2.6. Here's an example: * <pre> * <h:panelGroup id="foo"> * ... (some complex UI here) ... * </h:panelGroup> * * <h:form> * <o:socket channel="someChannel" scope="view"> * <f:ajax event="someEvent" listener="#{bean.pushed}" render=":foo" /> * </o:socket> * </h:form> * </pre> * <p> * Here, the push message simply represents the ajax event name. You can use any custom event name. * <pre> * someChannel.send("someEvent"); * </pre> * <p> * An alternative is to combine <code><o:socket></code> with <code><o:commandScript></code>. E.g. * <pre> * <h:panelGroup id="foo"> * ... (some complex UI here) ... * </h:panelGroup> * * <o:socket channel="someChannel" scope="view" onmessage="someCommandScript" /> * <h:form> * <o:commandScript name="someCommandScript" action="#{bean.pushed}" render=":foo" /> * </h:form> * </pre> * <p> * If you pass a <code>Map<String,V></code> or a JavaBean as push message object, then all entries/properties will * transparently be available as request parameters in the command script method <code>#{bean.pushed}</code>. * * * @author Bauke Scholtz * @see SocketEndpoint * @see SocketChannelManager * @see SocketUserManager * @see SocketSessionManager * @see SocketEvent * @see Push * @see PushContext * @see SocketPushContext * @see SocketPushContextProducer * @since 2.3 */ @FacesComponent(Socket.COMPONENT_TYPE) @ListenerFor(systemEventClass=PostAddToViewEvent.class) @ResourceDependency(library="omnifaces", name="omnifaces.js", target="head") public class Socket extends ScriptFamily implements ClientBehaviorHolder { // Public constants ----------------------------------------------------------------------------------------------- /** The standard component type. */ public static final String COMPONENT_TYPE = "org.omnifaces.cdi.push.Socket"; /** The boolean context parameter name to register web socket endpoint during startup. */ public static final String PARAM_SOCKET_ENDPOINT_ENABLED = "org.omnifaces.SOCKET_ENDPOINT_ENABLED"; /** Naming convention was wrong. Use {@link #PARAM_SOCKET_ENDPOINT_ENABLED} instead. @deprecated */ @Deprecated // TODO: remove in 3.0. public static final String PARAM_ENABLE_SOCKET_ENDPOINT = "org.omnifaces.ENABLE_SOCKET_ENDPOINT"; // Private constants ---------------------------------------------------------------------------------------------- private static final Pattern PATTERN_CHANNEL = Pattern.compile("[\\w.-]+"); private static final String WARNING_PARAM_DEPRECATED = "Context parameter name '" + PARAM_ENABLE_SOCKET_ENDPOINT + "' is deprecated. It has been renamed to '" + PARAM_SOCKET_ENDPOINT_ENABLED + "'." + " Please update web.xml accordingly."; private static final String ERROR_EXPRESSION_DISALLOWED = "o:socket 'channel' and 'scope' attributes may not contain an EL expression."; private static final String ERROR_INVALID_USER = "o:socket 'user' attribute '%s' does not represent a valid user identifier. It must implement Serializable and" + " preferably have low memory footprint. Suggestion: use #{request.remoteUser} or #{someLoggedInUser.id}."; private static final String ERROR_INVALID_CHANNEL = "o:socket 'channel' attribute '%s' does not represent a valid channel name." + " It is required and it may only contain alphanumeric characters, hyphens, underscores and periods."; private static final String ERROR_ENDPOINT_NOT_ENABLED = "o:socket endpoint is not enabled." + " You need to set web.xml context param '" + PARAM_SOCKET_ENDPOINT_ENABLED + "' with value 'true'."; private static final String SCRIPT_INIT = "OmniFaces.Util.addOnloadListener(function(){OmniFaces.Push.init('%s','%s',%s,%s,%s);});"; private static final Collection<String> CONTAINS_EVERYTHING = unmodifiableList(new ArrayList<String>() { private static final long serialVersionUID = 1L; @Override public boolean contains(Object object) { return true; } }); private enum PropertyKeys { // Cannot be uppercased. They have to exactly match the attribute names. port, channel, scope, user, onopen, onmessage, onclose, connected; } // Variables ------------------------------------------------------------------------------------------------------ private final State state = new State(getStateHelper()); // Actions -------------------------------------------------------------------------------------------------------- /** * After adding component to view, subscribe {@link SocketFacesListener} if necessary. */ @Override public void processEvent(ComponentSystemEvent event) { if (event instanceof PostAddToViewEvent) { SocketFacesListener.subscribeIfNecessary(); } } /** * An override which checks if this isn't been invoked on <code>channel</code> or <code>scope</code> attribute, and * if the <code>user</code> attribute is <code>Serializable</code>. * Finally it delegates to the super method. * @throws IllegalArgumentException When this value expression is been set on <code>channel</code> or * <code>scope</code> attribute, or when the <code>user</code> attribute is not <code>Serializable</code>. */ @Override public void setValueExpression(String name, ValueExpression binding) { if (PropertyKeys.channel.toString().equals(name) || PropertyKeys.scope.toString().equals(name)) { throw new IllegalArgumentException(ERROR_EXPRESSION_DISALLOWED); } if (PropertyKeys.user.toString().equals(name)) { Object user = binding.getValue(getFacesContext().getELContext()); if (user != null && !(user instanceof Serializable)) { throw new IllegalArgumentException(format(ERROR_INVALID_USER, user)); } } super.setValueExpression(name, binding); } /** * Accept all event names. */ @Override public Collection<String> getEventNames() { return CONTAINS_EVERYTHING; } /** * First check if the web socket endpoint is enabled in <code>web.xml</code> and the channel name and scope is * valid, then register it in {@link SocketChannelManager} and get the channel ID, then render the * <code>init()</code> script. This scripts will in turn hit {@link SocketEndpoint}. * @throws IllegalStateException When the web socket endpoint is not enabled in <code>web.xml</code>. * @throws IllegalArgumentException When the channel name, scope or user is invalid. * The channel name may only contain alphanumeric characters, hyphens, underscores and periods. * The allowed channel scope values are "application", "session" and "view", case insensitive. * The channel name must be uniquely tied to the channel scope. * The user, if any, must implement <code>Serializable</code>. */ @Override public void encodeChildren(FacesContext context) throws IOException { if (!TRUE.equals(getApplicationAttribute(context, Socket.class.getName()))) { throw new IllegalStateException(ERROR_ENDPOINT_NOT_ENABLED); } if (SocketFacesListener.register(context, this)) { String channel = getChannel(); if (channel == null || !PATTERN_CHANNEL.matcher(channel).matches()) { throw new IllegalArgumentException(format(ERROR_INVALID_CHANNEL, channel)); } Integer port = getPort(); String host = (port != null ? ":" + port : "") + getRequestContextPath(context); String channelId = getReference(SocketChannelManager.class).register(channel, getScope(), getUser()); String functions = getOnopen() + "," + getOnmessage() + "," + getOnclose(); String behaviors = getBehaviorScripts(); boolean connected = isConnected(); String script = format(SCRIPT_INIT, host, channelId, functions, behaviors, connected); context.getResponseWriter().write(script); } } private String getBehaviorScripts() { Map<String, List<ClientBehavior>> clientBehaviorsByEvent = getClientBehaviors(); if (clientBehaviorsByEvent.isEmpty()) { return "{}"; } String clientId = getClientId(getFacesContext()); StringBuilder scripts = new StringBuilder("{"); for (Entry<String, List<ClientBehavior>> entry : clientBehaviorsByEvent.entrySet()) { String event = entry.getKey(); List<ClientBehavior> clientBehaviors = entry.getValue(); scripts.append(scripts.length() > 1 ? "," : "").append(event).append(":["); for (int i = 0; i < clientBehaviors.size(); i++) { scripts.append(i > 0 ? "," : "").append("function(event){"); scripts.append(clientBehaviors.get(i).getScript(createClientBehaviorContext(getFacesContext(), this, event, clientId, null))); scripts.append("}"); } scripts.append("]"); } return scripts.append("}").toString(); } @Override public void decode(FacesContext context) { Map<String, List<ClientBehavior>> clientBehaviors = getClientBehaviors(); if (clientBehaviors.isEmpty()) { return; } if (!getClientId(context).equals(getRequestParameter(context, "javax.faces.source"))) { return; } List<ClientBehavior> behaviors = clientBehaviors.get(getRequestParameter(context, "javax.faces.behavior.event")); if (behaviors == null) { return; } for (ClientBehavior behavior : behaviors) { behavior.decode(context, this); } } // Attribute getters/setters -------------------------------------------------------------------------------------- /** * Returns the port number of the web socket host. * @return The port number of the web socket host. */ public Integer getPort() { return state.get(PropertyKeys.port); } /** * Sets the port number of the web socket host, in case it is different from the port number in the request URI. * Defaults to the port number of the request URI. * @param port The port number of the web socket host. */ public void setPort(Integer port) { state.put(PropertyKeys.port, port); } /** * Returns the name of the web socket channel. * @return The name of the web socket channel. */ public String getChannel() { return state.get(PropertyKeys.channel); } /** * Sets the name of the web socket channel. * It may not be an EL expression and it may only contain alphanumeric characters, hyphens, underscores and periods. * All open websockets on the same channel will receive the same push message from the server. * @param channel The name of the web socket channel. */ public void setChannel(String channel) { state.put(PropertyKeys.channel, channel); } /** * Returns the scope of the web socket channel. * @return The scope of the web socket channel. */ public String getScope() { return state.get(PropertyKeys.scope); } /** * Sets the scope of the web socket channel. * It may not be an EL expression and allowed values are <code>application</code>, <code>session</code> and * <code>view</code>, case insensitive. When the value is <code>application</code>, then all channels with the same * name throughout the application will receive the same push message. When the value is <code>session</code>, then * only the channels with the same name in the current user session will receive the same push message. When the * value is <code>view</code>, then only the channel in the current view will receive the push message. The default * scope is <code>application</code>. When the <code>user</code> attribute is specified, then the default scope is * <code>session</code>. * @param scope The scope of the web socket channel. */ public void setScope(String scope) { state.put(PropertyKeys.scope, scope); } /** * Returns the user identifier of the web socket channel. * @return The user identifier of the web socket channel. */ public Serializable getUser() { return state.get(PropertyKeys.user); } /** * Sets the user identifier of the web socket channel, so that user-targeted push messages can be sent. * All open websockets on the same channel and user will receive the same push message from the server. * It must implement <code>Serializable</code> and preferably have low memory footprint. * Suggestion: use <code>#{request.remoteUser}</code> or <code>#{someLoggedInUser.id}</code>. * @param user The user identifier of the web socket channel. */ public void setUser(Serializable user) { state.put(PropertyKeys.user, user); } /** * Returns the JavaScript event handler function that is invoked when the web socket is opened. * @return The JavaScript event handler function that is invoked when the web socket is opened. */ public String getOnopen() { return state.get(PropertyKeys.onopen); } /** * Sets the JavaScript event handler function that is invoked when the web socket is opened. * The function will be invoked with one argument: the channel name. * @param onopen The JavaScript event handler function that is invoked when the web socket is opened. */ public void setOnopen(String onopen) { state.put(PropertyKeys.onopen, onopen); } /** * Returns the JavaScript event handler function that is invoked when a push message is received from the server. * @return The JavaScript event handler function that is invoked when a push message is received from the server. */ public String getOnmessage() { return state.get(PropertyKeys.onmessage); } /** * Sets the JavaScript event handler function that is invoked when a push message is received from the server. * The function will be invoked with three arguments: the push message, the channel name and the raw MessageEvent itself. * @param onmessage The JavaScript event handler function that is invoked when a push message is received from the server. */ public void setOnmessage(String onmessage) { state.put(PropertyKeys.onmessage, onmessage); } /** * Returns the JavaScript event handler function that is invoked when the web socket is closed. * @return The JavaScript event handler function that is invoked when the web socket is closed. */ public String getOnclose() { return state.get(PropertyKeys.onclose); } /** * Sets the JavaScript event handler function that is invoked when the web socket is closed. * The function will be invoked with three arguments: the close reason code, the channel name and the raw * <code>CloseEvent</code> itself. Note that this will also be invoked on errors and that you can inspect the close * reason code if an error occurred and which one (i.e. when the code is not 1000). See also * <a href="http://tools.ietf.org/html/rfc6455#section-7.4.1">RFC 6455 section 7.4.1</a> and {@link CloseCodes} API * for an elaborate list of all close codes. * @param onclose The JavaScript event handler function that is invoked when the web socket is closed. */ public void setOnclose(String onclose) { state.put(PropertyKeys.onclose, onclose); } /** * Returns whether to (auto)connect the web socket or not. * @return Whether to (auto)connect the web socket or not. */ public boolean isConnected() { return state.get(PropertyKeys.connected, TRUE); } /** * Sets whether to (auto)connect the web socket or not. Defaults to <code>true</code>. It's interpreted as a * JavaScript instruction whether to open or close the web socket push connection. Note that this attribute is * re-evaluated on every ajax request. You can also explicitly set it to <code>false</code> and then manually * control in JavaScript by <code>OmniFaces.Push.open("channelName")</code> and * <code>OmniFaces.Push.close("channelName")</code>. * @param connected Whether to (auto)connect the web socket or not. */ public void setConnected(boolean connected) { state.put(PropertyKeys.connected, connected); } // Helpers -------------------------------------------------------------------------------------------------------- /** * Register web socket endpoint if necessary, i.e. when it's enabled via context param and not already installed. * @param context The involved servlet context. */ public static void registerEndpointIfNecessary(ServletContext context) { if (TRUE.equals(context.getAttribute(Socket.class.getName()))) { return; } if (!parseBoolean(context.getInitParameter(PARAM_SOCKET_ENDPOINT_ENABLED))) { if (parseBoolean(context.getInitParameter(PARAM_ENABLE_SOCKET_ENDPOINT))) { // TODO: remove in OmniFaces 3.0. getLogger(Socket.class.getName()).warning(WARNING_PARAM_DEPRECATED); } else { return; } } try { ServerContainer container = (ServerContainer) context.getAttribute(ServerContainer.class.getName()); ServerEndpointConfig config = ServerEndpointConfig.Builder.create(SocketEndpoint.class, SocketEndpoint.URI_TEMPLATE).build(); container.addEndpoint(config); context.setAttribute(Socket.class.getName(), TRUE); } catch (Exception e) { throw new FacesException(e); } } }