...
| Bloque de código |
|---|
|
@Name("socketObserver")
@Scope(ScopeType.APPLICATION)
public class SocketObserver {
@Observer( SocketEvent.EVENT_SOCKET_OPENED )
public void onOpen(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.
}
@Observer( SocketEvent.EVENT_SOCKET_SWITCHED )
public void onSwitch(SocketEvent event) {
String channel = event.getChannel(); // Returns <o:socket channel>.
Long currentUserId = event.getUser(); // Returns current <o:socket user>, if any.
Long previousUserId = event.getPreviousUser(); // Returns previous <o:socket user>, if any.
// Do your thing with it. E.g. updating in a concurrent/synchronized collection.
}
@Observer( SocketEvent.EVENT_SOCKET_CLOSED )
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.
}
} |
Que nos da la oportunidad para enviar otro mensaje push a un socket de ámbito de aplicación, por ejemplo, "El usuario X ha iniciado sesión" (o cerrado) cuando se abre (o cierra) un socket de ámbito de sesión.
Consideraciones de Seguridad
Si el socket se declara en una página que tiene permitido el acceso a usuarios que han iniciado sesión con un rol específico, es posible agregar la URL de la solicitud de protocolo push al conjunto de las URL restringidas.
La URL de solicitud de protocolo push se compone del prefijo URI /fundeweb.push/, seguido del nombre del canal. Por ejemplo, si la seguridad administrada por el contenedor definida en el web.xml, ya ha controlado el acceso a la página /user/foo.xhtml, de los usuarios que hayan iniciado sesión con el rol USER en el patrón de URL de ejemplo /user/* como se muestra a continuación:
| Bloque de código |
|---|
|
<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> |
.. y la página /user/foo.xhtml a su vez contiene un <fw:socket channel="foo">, entonces necesita agregar una restricción en el patrón de URL de solicitud de protocolo push de /fundeweb.push/foo como se muestra a continuación:
| Bloque de código |
|---|
|
<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> |
Como seguridad adicional, particularmente para aquellos canales públicos que no pueden ser restringidos por restricciones de seguridad, <fw:socket> registrará todos los canales declarados hasta el momento en la sesión HTTP, y cualquier solicitud de apertura de websocket entrante será verificada contra los canales registrados hasta el momento en la sesión HTTP. En caso de que el canal sea desconocido (por ejemplo, adivinado al azar o falsificado por los usuarios finales o reconectado manualmente después de que expire la sesión), el websocket se cerrará inmediatamente con el código de motivo de cierre 1008 (CloseReason.CloseCodes.VIOLATED_POLICY). Además, cuando se destruye la sesión HTTP, todos los canales de sesión y vista que aún estén abiertos se cerrarán explícitamente desde el lado del servidor con el código de motivo de cierre 1000 (CloseReason.CloseCodes.NORMAL_CLOSURE). Solo los sockets de ámbito de aplicación permanecerán abiertos y accesibles desde el lado del servidor, incluso cuando la sesión o vista asociada con la página en el lado del cliente ha expirado.
Sugerencias de Diseño con EJBs
En caso de que desee activar un push desde el backend EAR/EJB a una conexión push con ámbito de aplicación, puede utilizar eventos de JBoss Seam. Primero, cree una clase de bean personalizada que represente el evento push, como por ejemplo, PushEvent, y que contenga lo necesario para pasar como mensaje push.
| Bloque de código |
|---|
|
public final class PushEvent implemments org.jboss.seam.core.EventObject {
private final String message;
public PushEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
} |
Después, utiliza org.jboss.seam.core.Events.raiseEvent(org.jboss.seam.core.EventObject), para lanzar el evento
| Bloque de código |
|---|
|
public void onSomeEntityChange(Entity entity) {
org.jboss.seam.core.Events.instance().raiseEvent((new PushEvent(entity.getSomeProperty()));
} |
Para terminar, usar la anotación @Observer en algún componente de ámbito EVENT, STATELESS o APPLICATION, y delegue en es.um.atica.faces.push.PushContext como se indica a continuación:
| Bloque de código |
|---|
|
@Push
private PushContext someChannel;
@Observer
public void onPushEvent(PushEvent event) {
someChannel.send(event.getMessage());
} |
Es importante advertir, que el componente con ámbito EVENT, no debe ser el mismo que el de la página de origen, por la sencilla razón de que no hay forma de realizar una solicitud HTTP en ese momento. Por esa misma razón, un componente con ámbito PAGE o SESSION, no funcionaría (ya que requieren respectivamente el estado de vista Faces y la sesión HTTP que solo se pueden identificar mediante una solicitud HTTP). Una conexión push con ámbito de view o session, tampoco funcionaría, por lo que la conexión push realmente debe tener ámbito application. El FacesContext tampoco estará disponible en el método que recibe el evento anterior.
En caso de que el disparador del evento del backend EAR/EJB sea un método de servicio asincrónico, que a su vez se inicia en el lado WAR, entonces puede hacer uso de callbacks del lado WAR. Deje que el método de servicio tome una instancia de callback como parámetro, como por ejemplo, la interfaz java.util.function.Consumer.
| Bloque de código |
|---|
|
@Asynchronous
public void someAsyncServiceMethod(Entity entity, Consumer<Object> callback) {
// ... (some long process)
callback.accept(entity.getSomeProperty());
} |
Y la invocación del método del servicio WAR es el siguiente:
| Bloque de código |
|---|
|
@In(create = true)
private SomeService someService;
@Push
private PushContext someChannel;
public void someAction() {
someService.someAsyncServiceMethod(entity, message -> someChannel.send(message));
} |
Esta sería la única forma en caso de que desee enviar de forma asincrónica un mensaje a una conexión push con ámbito de view o session, y/o desee pasar algo desde FacesContext o de ámbito EVENT/PAGE/SESSION como argumento (con modificador final).
Sugerencias de Diseño en Cluster