Tabla de contenidos
| Advertencia |
|---|
No añadir código en la aplicación, hasta leer toda la documentación. |
| Advertencia |
|---|
EN CONSTTRUCCIÓN ... |
...
<fw:socket> es un componente UI que abre una conexión push basada en websocket unidireccional (de servidor a cliente) en el lado del cliente a la que se puede acceder desde el lado del servidor a través de la interfaz PushContext inyectada mediante la anotación @Push.
...
| Bloque de código | ||
|---|---|---|
| ||
function socketOpenListener(channel) {
// ...
} |
El listener de Javascript onopen acepta un único parámetro:
- channel: el nombre del canal, útil si se intenta tener un listener global.
El listener opcional de Javascript onerror, se usa para detectar los errores de conexión antes de un nuevo reintento de conexión. Se invocará cuando el socket web pueda realizar un intento de reconexión automática en una conexión interrumpida después de la primera conexión correcta. No se invocará cuando falle el primer intento de conexión, o el servidor haya devuelto el código de motivo de cierre 1000 (cierre normal) o 1008 (política violada), o se haya excedido el número máximo de intentos de reconexión. En su lugar, se invocará listener Javascript onclose.
...
El listener de Javascript onerror acepta tres parámetros:
- code: el código que indica el tipo de cierre. Mirar RFC 6455 sección 7.4.1 y CloseReason.CloseCodes.
- channel: el nombre del canal, útil si se intenta tener un listener global.
- event: la instancia de CloseEvent, útil en caso de revisión de querer más información sobre el error.
...
El listener de Javascript onclose acepta tres parámetros:
- code: el código que indica el tipo de cierre. Si el código es -1, entonces el cliente no soporta los websockets. Si es 1000, el websocket es cerrado por la expiración de una sesión o vista. El resto de valores que no son 1000, indican algún tipo de error. Mirar RFC 6455 sección 7.4.1 y CloseReason.CloseCodes.
- channel: el nombre del canal, útil si se intenta tener un listener global.
- event: la instancia de CloseEvent, útil en caso de revisión de querer más información sobre el error.
...
Cuando una conexión de ámbito sesión o vista, es cerrado automáticamente por el servidor con un código de 1000 (y no cerrado manualmente con FundeWeb.Push.close(channel)), indica que la sesión o la vista a expirado. Si la conexión que expira tiene ámbito de sesión, podemos aprovechar la oportunidad para mostrar un mensaje de información y redirigir a la página de inicio o autenticación usando window.location. Para el caso de que una conexión con ámbito vista expire, la gestión dependerá del tipo de expiración. Una vista puede expirar cuando la sesión asociada expira, pero además se puede producir con una navegación o recarga accidental, o cuando la configuración Faces "view per session" tiene un valor bajo y el cliente tiene bastantes vistas (ventanas/pestañas) abiertas en la misma sesión. Puedes aprovechar la oportunidad para advertir al cliente y/o dejar que JavaScript vuelva a cargar la página, ya que enviar cualquier formulario generaría una excepción ViewExpiredException.
Eventos en el Servidor
Tenemos tres eventos en la parte servidor:
- Cuando un websocket es abierto, se lanza un
...
- evento es.umatica.faces.push.socketEvent.opened (definido en la constante SocketEvent.EVENT_SOCKET_OPENED).
- Cuando el
...
- atributo user de <fw:socket> cambia, se lanza un evento es.umatica.faces.push.socketEvent.switched (definido en la constante SocketEvent.EVENT_SOCKET_SWITCHED).
...
- Cuando un websocket es cerrado, se lanza un
...
- evento es.umatica.faces.push.socketEvent.closed (definido en la constante SocketEvent.EVENT_SOCKET_CLOSED).
Observar estos eventos en componentes de ámbito EVENT/PAGE/SESSION no tiene sentido, ya que no se puede realizar una solicitud HTTP en ese momento.
| 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<fw:socket channel>.
Long userId = event.getUser(); // Returns <o<fw:socket user>, if any.
SocketEvent.EventType type = event.getEventType(); // DoValid yourvalues: thing OPENED, SWITCHED, CLOSED
// 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<fw:socket channel>.
Long currentUserId = event.getUser(); // Returns current <o<fw:socket user>, if any.
Long previousUserId = event.getPreviousUser(); // Returns previous <o<fw:socket user>, if any.
SocketEvent.EventType type = event.getEventType(); // Valid values: OPENED, SWITCHED, CLOSED
// 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<fw:socket channel>.
Long userId = event.getUser(); // Returns <o<fw:socket user>, if any.
SocketEvent.EventType type CloseCode code = event.getCloseCodegetEventType(); // ReturnsValid close values: OPENED, SWITCHED, CLOSED
CloseCode code = event.getCloseCode(); // Returns close reason code.
// Do your thing with it. E.g. removing them from collection.
}
} |
...
| Bloque de código | ||
|---|---|---|
| ||
public final class PushEventMyPushEvent implemmentsimplements orges.um.jbossatica.seamfaces.corepush.EventObjectPushEvent { private final String user; private final String message; public PushEventMyPushEvent(String message) { this.message =(null, message); } public MyPushEvent(String publicuser, String getMessage(message) { this.user = user; this.messag e= message; } public String getMessage() { return message; } public String getUser() { return user; } } |
Después, utiliza org.jboss.seam.core.Events.raiseEvent(org.jboss.seam.core.EventObject), para lanzar el evento
...
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.
| Info |
|---|
Ejemplos de código, no llevar a la aplicación hasta leer toda la documentación. |
En 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.
...
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
...
de la UI
Si se quieren realizar actualizaciones complejas de la UI, la forma más sencilla es usando <f:ajax> dentro de <fw:socket>. Ejemplo:
| Bloque de código | ||
|---|---|---|
| ||
<h:panelGroup id="foo">
... (some complex UI here) ...
</h:panelGroup>
<h:form>
<fw:socket channel="someChannel" scope="view">
| ||
| Bloque de código | ||
| ||
<jms:managed-topic-publisher name="pruebaPushPublisher" <f:ajax event="someEvent" auto-create="true" listener="#{bean.pushed}" render=":foo" /> topic-jndi-name="topic/pruebaPushTopic"/> |
...
</fw:socket>
</h:form> |
Donde el mensaje push, solo contien el nombre del evento Ajax. Puedes usar cualquier nombre de evento personalizado.
| Bloque de código | ||
|---|---|---|
| ||
@NamesomeChannel.send("pushChangeNotifier") public class PushManager { someEvent"); |
Otra alternativa, es combinar <fw:socket> con <p:remoteCommand>, ejemplo:
| Bloque de código | ||
|---|---|---|
| ||
<h:panelGroup id="foo"> @In ... (some privatecomplex TopicPublisherUI pruebaPushPublisher; @In private TopicSession topicSession; here) ... </h:panelGroup> <fw:socket channel="someChannel" scope="view" onmessage="someCommandScript" /> <h:form> public void fireEvent(PushEvent event) { try { pruebaPushPublisher.publish(topicSession.createObjectMessage(event.getMessage())); } catch (Exception ex) { throw new RuntimeException(ex); } } } |
...
<p:remoteCommand name="someCommandScript" action="#{bean.pushed}" update=":foo" />
</h:form> |
Si se pasa un Map<String, V> o un POJO (JAVABEAN) como el objeto en el mensaje push, después todas las entradas/propiedades estarán disponibles como parámetros de la solicitud en la acción del comando #{bean.pushed}.
Sugerencias de Diseño en Cluster
En el caso que la aplicación este desplegada en un cluster con varios nodos, y el evento push se lanza desde un nodo diferente al que esta conectado el cliente (Navegador Web), entonces este no será notificado. Una solución es usar temas (topic) JMS, lanzar el evento push a través de JMS, y usar MDB (message driven bean) para reenviar el evento push a JBoss Seam en cada nodo.
Con JBoss Seam tenemos varios componentes para ayudar a trabajar con JMS. Ejemplo para la configuración de JMS con JBoss Seam, en el fichero components.xml registramos el manager del tema (topic) JMS, como norma general, el nombre del tema comenzara con el nombre de la aplicación en minúsculas seguido de PushTopic, en el ejemplo usaremos pruebaPushTopic. Para el nombre del publisher, podemos usar el nombre que queramos. Veamos un ejemplo de configuración:
| Bloque de código | ||
|---|---|---|
| ||
<jms:managed-topic-publisher name="pushPublisher"
| ||
| Bloque de código | ||
| ||
@MessageDriven(activationConfig = { @ActivationConfigProperty( propertyName = "destinationType", propertyValue = "javax.jms.Topic" ), @ActivationConfigProperty( propertyName = "destination", propertyValue = "topic/pruebaPushTopic" ) }) @Name("pushReceiver") public class pushReceiver implements MessageListener { @Logger private Log log; @Override public void onMessage(Message jmsMessage) { auto-create="true" installDependencies="es.um.atica.faces.push.pushInitialization" topic-jndi-name="jms/pruebaPushTopic"/> <jms:topic-connection topic-connection-factory-jndi-name="jms/pruebaPushConnectionFactory" try { String message = jmsMessage.getStringProperty("message"); Events.instance().raiseEvent(new PushEvent(message)); } catch (JMSException ex) { log.error("Problem sending push event message.", ex); } } } |
...
installDependencies="es.um.atica.faces.push.pushInitialization"/> |
Ahora se puede inyectar el componente pushPublisher en cualquier componente:
| Bloque de código | ||
|---|---|---|
| ||
@Name("pushEventsManager ")
public class PushEventsManager {
@In
private TopicPublisher pushPublisher;
@In
private TopicSession topicSession;
public void fireEvent(PushEvent event) {
try {
pushPublisher.publish(topicSession.createObjectMessage(event.getMessage()));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
} |
Para recibir los mensajes JMS tenemos que configurar un MDB:
| Bloque de código | ||
|---|---|---|
| ||
package es.um.atica.prueba.push;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.core.Events;
import org.jboss.seam.log.Log;
import org.jboss.seam.util.Strings;
import es.um.atica.faces.push.PushEvent;
@MessageDriven(activationConfig = {
@ActivationConfigProperty(
propertyName = "destinationType",
propertyValue = "javax.jms.Topic"
),
@ActivationConfigProperty(
propertyName = "destinationLookup",
propertyValue = "jms/pruebaPushTopic"
)
})
@Name("pushReceiver")
@TransactionAttribute(value = TransactionAttributeType.NOT_SUPPORTED)
public class PushReceiver implements MessageListener {
@Logger
private Log log;
protected void raiseEvent(String eventType, PushEvent event) {
if (Strings.isEmpty(eventType)) {
log.info("Raise push event '#0'", event.getClass().toString());
Events.instance().raiseEvent(event);
} else {
log.info("Raise push event '#0'", eventType);
Events.instance().raiseEvent(eventType, event);
}
}
@Override
public void onMessage(Message jmsMessage) {
try {
String eventType = jmsMessage.getStringProperty("eventType");
PushEvent event = jmsMessage.getBody(PushEvent.class);
raiseEvent(eventType, event);
} catch (JMSException ex) {
log.error("Problem sending push event message.", ex);
}
}
} |
Ahora, usamos PushManager#fireEvent() para lanzar eventos JMS desde un nodo del cluster, que permite después lanzar los eventos de JBoss Seam en todos los nodos el cluster y asi enviar el mensaje al canal push correspondiente:
| Bloque de código | ||
|---|---|---|
| ||
@In
private PushEventsManager pushEventsManager;
public void onSomeEntityChange(Entity entity) {
pushEventsManager.fireEvent(new MyPushEvent(entity.getSomeProperty()));
} |
| Info |
|---|
IMPORTANTE EN FUNDEWEB |
Todas estas clases, están añadidas en las librerías de FundeWeb, y el sistema también esta añadido a partir del Arquetipo versión 2.0.11, que añade un perfil llamada cluster que permite terne fácilmente esta funcionalidad. Solo tendréis que usar el PushEventsManager para enviar los mensajes a través de las conexiones push. Para añadir esa configuración a una aplicación existente hay que descargar el fichero push-cluster.zip y descomprimir con Extraer aquí en la raíz del proyecto. Con esto crearemos la carpeta web/src/main/profiles/cluster.
En esta carpeta tendremos la carpeta java donde se encuentra la clase PushReceiver (la única que no esta en las librerías de FundeWeb). La ruta a esta clase, es/um/atica/__rootArtifactId__/push (que es una ruta de paquete de código fuente), la tendremos que modificar, cambiando __rootArtifactId__ por el identificador de la aplicación. En la propia clase PushReceiver, tendremos que cambiar los valores ${rootArtifactId} por el identificador de la aplicación (igual que antes).
También tenemos que modificar el fichero web/src/main/profiles/cluster/components.xml, tendremos que cambiar los valores ${rootArtifactId} por el identificador de la aplicación (igual que antes).
Para finalizar, en el POM del módulo Web, tendremos que añadir un perfil a <profiles> (el elemento XML puede estar comentado, por lo que dejamos sin comentar solo las etiquetas <profiles> y </profiles>):
| Bloque de código | ||
|---|---|---|
| ||
<profile>
<id>cluster</id>
<properties>
<cluster.profile.folder>src/main/profiles/cluster</cluster.profile.folder>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-cluster-java</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${cluster.profile.folder}/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>be.hikage.maven.plugins</groupId>
<artifactId>maven-xmlmerger-plugin</artifactId>
<executions>
<execution>
<id>merge-cluster-components_xml</id>
<phase>generate-resources</phase>
<goals>
<goal>mergexml</goal>
</goals>
<configuration>
<baseDirectory>${basedir}/src/main/webapp/WEB-INF</baseDirectory>
<inputDirectory>${cluster.profile.folder}/xmlmerge</inputDirectory>
<outputDirectory>${basedir}/src/main/webapp/WEB-INF</outputDirectory>
<mergeFilenamePattern>()(components\.xml)</mergeFilenamePattern>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>es.um.atica.fundeweb</groupId>
<artifactId>fundeweb-cluster</artifactId>
</dependency>
</dependencies>
</profile> |
Para finalizar, tenemos que poner un JIRA a DJ-AT-SIST-MIDDLE (MIDWEB), como Petición de Servicio y al componente JMS, indicando que hay que crear una cola JMS para la aplicación XXXXX con rutas JNDI:
- Para el Tema (TOPIC): jms/_nombre_aplicacion_minusculas_PushTopic
- Para la fábrica de conexiones: jms/_nombre_aplicacion_minusculas_PushConnectionFactory
Más información sobre JMS:
- JMS Setup In Weblogic 12c
- Video - Weblogic JMS Configuration
- Como configurar un tema (topic) JMS en el servidor Weblogic 12.2.
- Como ver los mensjaes de un tema (topic) JMS en el servidor Weblogic 12.2.
Configuraciones Adicionales Obligatorias
Para que las peticiones push puedan ser interceptadas por JBoss Seam, tenemos que hacer una configuración adicionales en los ficheros components.properties y components.xml.
Tenemos que ver el atributo para especificar la URL que se intercepta para los filtros: <web:exception-filter>, <web:logging-filter>, <web:character-encoding-filter>.
Si se usa el atributo url-pattern, para este caso añadimos la propiedad default_url_pattern en el fichero components.properties, con el valor definido para la propiedad junto con la que se usa para las peticiones push, si por ejemplo, el valor de la propiedad es *.seam, entonces añadimos:
| Bloque de código |
|---|
default_url_pattern=*.seam|/fundeweb.push/* |
Ahora en el fichero components.xml, para los filtros;<web:exception-filter>, <web:logging-filter>, <web:character-encoding-filter>; cambiamos el valor del atributo por @default_url_pattern@. Ejemplo:
| Bloque de código | ||
|---|---|---|
| ||
<!-- Exception handling -->
<web:exception-filter url-pattern="@default_url_pattern@" installed="true" />
<!-- Identity Logging -->
<web:logging-filter url-pattern="@default_url_pattern@" installed="true" />
<!-- Character encoding -->
<web:character-encoding-filter encoding="UTF-8"
override-client="true" installed="true" url-pattern="@default_url_pattern@" /> |
Si se usa el atributo regex-url-pattern, para este caso añadimos la propiedad default_regex_url_pattern en el fichero components.properties, con el valor definido para la propiedad junto con la que se usa para las peticiones push, si por ejemplo, el valor de la propiedad es .*\.seam, entonces añadimos:
| Bloque de código |
|---|
default_regex_url_pattern=.*\.seam|/fundeweb.push/.* |
Ahora en el fichero components.xml, para los filtros;<web:exception-filter>, <web:logging-filter>, <web:character-encoding-filter>; cambiamos el valor del atributo por @default_regex_url_pattern@
| Bloque de código | ||
|---|---|---|
| ||
@In
private PushManager pushManager;
public void onSomeEntityChange(Entity entity) {
pushManager.fireEvent(new PushEvent(entity.getSomeProperty()));
} |
...
- Como configurar un tema (topic) JMS en el servidor Weblogic 12.2.
- Como ver los mensjaes de un tema (topic) JMS en el servidor Weblogic 12.2.
Sugerencias de Diseño de la UI
Si se quieren realizar actualizaciones complejas de la UI, la forma más sencilla es usando <f:ajax> dentro de <fw:socket>. Ejemplo:
| Bloque de código | ||
|---|---|---|
| ||
<h:panelGroup id="foo">
... (some complex UI here) ...
</h:panelGroup>
<h:form>
<fw:socket channel="someChannel" scope="view">
<f:ajax event="someEvent" listener="#{bean.pushed}" render=":foo" />
</fw:socket>
</h:form> |
Donde el mensaje push, solo contien el nombre del evento Ajax. Puedes usar cualquier nombre de evento personalizado.
| Bloque de código | ||
|---|---|---|
| ||
someChannel.send("someEvent"); |
...
<!-- Exception handling -->
<web:exception-filter regex-url-pattern="@default_regex_url_pattern@" installed="true" />
<!-- Identity Logging -->
<web:logging-filter regex-url-pattern="@default_regex_url_pattern@" installed="true" />
<!-- Character encoding -->
<web:character-encoding-filter encoding="UTF-8"
override-client="true" installed="true" regex-url-pattern="@default_regex_url_pattern@" /> |
Además, en el fichero componens.xml, añadimos la siguiente línea (junto al resto de definiciones de <web:context-filter>):
| Bloque de código | ||
|---|---|---|
| ||
<web:context-filter name="pushSockets" url-pattern="/fundeweb.push/*" /> |
...
| Bloque de código | ||
|---|---|---|
| ||
<h:panelGroup id="foo">
... (some complex UI here) ...
</h:panelGroup>
<fw:socket channel="someChannel" scope="view" onmessage="someCommandScript" />
<h:form>
<p:remoteCommand name="someCommandScript" action="#{bean.pushed}" update=":foo" />
</h:form> |
Si se pasa un Map<String, V> o un POJO (JAVABEAN) como el objeto en el mensaje push, después todas las entradas/propiedades estarán disponibles como parámetros de la solicitud en la acción del comando #{bean.pushed}.