Tabla de Contenidos

Gestión de Conversaciones en Seam

PEDRO DELGADO YARZA 2014/02/03 18:58

El ámbito conversación, proviso por Seam, es uno de los más moldeables de entre los ámbitos que tenemos disponibles en nuestro contexto de aplicación. Las conversaciones nos permiten definir rangos mayores o menores donde el contenido de las clases que contienen será mantenido en memoria, dándonos la posibilidad de destruir dicho contexto y su contenido, cuando el flujo de la aplicación nos lleve por un camino que ya no necesita dichos valores.

Existen diferentes tipos de conversaciones, y debemos conocer cómo gestionarlas para poder aprovechar el rendimiento que nos brindan.

A modo de resumen, podemos indicar que una conversación es una unidad de trabajo, compuesta por varias páginas y sus clases java asociadas, que tienen sentido bajo un contexto determinado, fuera de ese contexto el contenido cargado en memoria no tiene sentido por lo que es prescindible.

Tipos de conversaciones

Pese a que hemos estado hablando de las conversaciones como si sólo fuesen de un tipo, realmente nos encontramos con dos tipos básicos:

El proceso de creación y destrucción de conversaciones de manera esquemática quedaría como la siguiente figura:

El Modelo de Conversación de Seam

El modelo de conversación de Seam, sigue las siguientes reglas:

Si se quiere propagar una conversación de Seam a lo largo de solicitudes no-Faces, se necesita especificar explicitamente el identificador de la conversación como un parametro de solicitud. El ejemplo muestra como hacerlo con la etiqueta conversationId de la libreria de Seam:

  <h:outputLink value="main.jsf">
     <s:conversationId/>
     <h:outputText value="Continue"/>
  </h:outputLink>

Si se desea deshabilitar la propagación del contexto de conversación para un postback de JSF, utilizarmeos la etiqueta conversationPropagation de la libreria de Seam:

  <h:commandLink action="main" value="Exit">
     <s:conversationPropagation type="none"/>
  </h:commandLink>

Hay que destacar, que deshabilitar la propagación del contexto de conversación no es absolutamente lo mismo que finalizar la conversación. La etiqueta conversationPropagation, también sirve para iniciar, finalizar o unirse a una conversación, o iniciar un anidado (nested) de conversaciones.

<h:commandLink action="main" value="Select Hotel">
   <s:conversationPropagation type="begin"/>
</h:commandLink>
 
<h:commandLink action="main" value="Exit">
   <s:conversationPropagation type="end"/>
</h:commandLink>
 
<h:commandLink action="main" value="Select Hotel">
   <s:conversationPropagation type="join"/>
</h:commandLink>
 
<h:commandLink action="main" value="Select Child">
   <s:conversationPropagation type="nested"/>
</h:commandLink>

Este modelo de conversación, simplifica la construcción de aplicaciones que se comportan correctamente con respecto a una operación multi-ventana. Para muchas aplicaciones, esto es todo lo que se necesita. Algunas aplicaciones más complejas tienen uno o ambos de los comportamientos siguientes:

Anidado (Nested) de Conversaciones

El anidado de conversaciones se crea invocando un método anotado con @Begin(nested=true) dentro del ámbito de una conversación existente. El anidado de conversaciones tiene su propio contexto de conversación, pero puede leer valores de otros contextos de conversación externos. Estos contextos son de solo lectura para el anidado de conversación, pero como los objetos se obtienen por referencia, cambios de los propios objetos pueden ser reflejados en el contexto externo.

Cuando posteriormente se encuentra un método anotado con @End, la conversación anidada será destruida, y la conversación exterior se reanudará, obteniendola mediante un pop de la pila de conversaciones. Las conversaciones pueden ser anidadas a cualquier profundidad.

Ciertas actividades del usuario (como utilizar el botón retroceder back) puede causar que la conversación exterior se reanude antes de que la conversación interna sea finalizada. En este caso es posible mantener concurrentemente conversaciones anidadas a lo largo de la misma conversación exterior. Si la conversación exterior finaliza antes de que la conversación anidada finalice, Seam destruye todas los contextos de conversación anidades con el contexto exterior.

La conversación del fondo de la pila de conversaciones es la conversación raíz. Destruir esta conversación, siempre destruirá a todos sus descendientes. Se puede lograr declarativamente especificando @End(root=true) en un método.

Una conversación puede permanecer en un estado continuable. Las conversaciones anidadas permiten a una aplicación capturar un estado continuable y consistente en varias puntos de la interacción del usuario, por lo tanto, asegurar un comportamiento correcto en la utilización del botón de retroceso.

Como se ha dicho anteriormente, si un componente existe en una conversación padre de una conversación actual anidada, la conversación anidada puede utilizar la misma instancia. Ocasionalmente, es util tener diferentes instancias en cada conversación anidada, pero esto hara que la instancia que existe en la conversación padre sea invisible para sus conversaciones hijas. Se puede lograr este comportamiento, anotando a un componente con @PerNestedConversation.

Iniciar una Conversación con una solicitud GET

JSF no define ninguna acción que se pueda lanzar cuando una pagina es accedida mediante una solicitud no-faces (como por ejemplo, una solicitud HTTP GET). Esto suele ocurrir cuando se utilizan marcadores (bookmark) de pagina o si navegamos mediante un <h:outputlink/>.

Muchas veces necesitamos iniciar una conversación inmediatamente al acceder a una página. Ya que no tenemos un método action de JSF, no podemos resolver el problema de manera normal, es decir, anotar el método con la anotación @Begin.

Un problema posterior surge si la pagina necesita que algún estado sea cargado dentro de una variable de contexto. Pare resolver esto podemos actuar de la siguiente manera:

@Create
public void init() {
	// Logica para cargar el estado
	...
}
 
@Factory("lista")
public List<String> getLista() {
	// Logica para cargar la lista
	...
    return lista;
}
 

Si ninguna de estas opciones funciona, Seam permite definir un método llamado page action en el fichero pages.xml, en la propiedad action del elemento page. Esta acción se lanzará al inicio de la fase de renderizado, si el método devuelve un valor no nulo como resultado, Seam procesará las reglas de navegación, navegando a lo que indique la regla que encaje con la respuesta del método lanzado.

<pages>
   <page view-id="/messageList.jsp" action="#{messageManager.list}"/>
   ...
</pages>

Si lo único que se quiere es iniciar una conversación antes de que la pagina sea renderizada, se puede utilizar el componente de Seam conversation, llamando al método begin.

<pages>
   <page view-id="/messageList.jsp" action="#{conversation.begin}"/>
   ...
</pages>

Si lo que queremos en finalizar la conversación, podemos utilizar el método end del componente conversation.

<pages>
   <page view-id="/messageList.jsp" action="#{conversation.end}"/>
   ...
</pages>

Si se quiere más control, para por ejemplo, unirse a una conversación existente, iniciar una conversación anidada, se puede utilizar el elemento <begin-conversation>.

<pages>
    <page view-id="/messageList.jsp">
        <begin-conversation join="true" />
    <page>
    ...
</pages>
 

Tambien podemos utilizar el elemnto <end-conversation> para finalizar una conversación.

<pages>
    <page view-id="/home.jsp">
        <end-conversation/>
    <page>
    ...
</pages>

En resumen, para solventar el problema de iniciar una conversación en una solicitud no-faces, ahora dispones de cinco opciones:

Los command link de JSF siempre realizan un sumisión mediante JavaScript, que rompe la carasteristica “abrir en una nueva ventana” o “abrir en una nueva pestaña” del navegador web. En JSF plano, se necesita utilizar <h:outputLink> si se necesita esta funcionalidad. Pero tenemos dos limitaciones importantes:

Podemos utilizar los page action de Seam para resolver el primer problema. Pero para resolver el segundo problema tendremos que utilizar la etiqueta JSF <s:link> de Seam. Para esta etiqueta podemos especificar el identificador de una vista a la que navegar en la propiedad viewId.

<s:link view="/login.xhtml" value="Login"/>

También se puede especificar un método action, en este caso el resultado del método determina la página resultado en la propiedad action.

<s:link action="#{login.logout}" value="Login"/>

Si se especifican tanto el identificador de la vista como el método action, se utilizara el identificador de la vista a no ser que el método devuelva valores no nulos como resultado.

<s:link view="/login.xhtml" action="#{login.logout}" value="Login"/>

El enlace, propagará automáticamente la fila seleccionada en un DataModel utilizado dentro de un <h:dataTable>.

<s:link view="/hotel.xhtml" action="#{hotelSearch.selectHotel}" value="#{hotel.name}"/>

Se puede iniciar, unir, finalizar, anidar conversaciones o abandonar el ámbito de una conversación utilizando la propiedad propagation, que puede tener los valores.

Finalmente, si necesitamos que el enlace se dibuje como un botón, utilizaremos la etiqueta <s:button>

<s:button action="#{login.logout}" value="Logout"/>

También hay que indicar, que <s:button> y <s:link>, no realizan submit del formulario. Si se necesitan pasar valores, se puede utilizar las etiquetas <ui:param> de Facelets o <f:param> de JSF.

<s:link view="/paginas/varias_tablas/ComentarioList.xhtml"
	rendered="#{comentarioList.previousExists}"
       value="#{messages.left}#{messages.left} First Page"
          id="firstPage">
    <f:param name="firstResult" value="0"/>
</s:link>

Identificadores de Conversación Naturales o Personalizables

Cuando se trabaja con conversaciones que tratan con objetos persistentes, a veces, es deseable utilizar la clave de negocio natural del objeto, en lugar de la estándar, sustituyendo el identificador de la conversación. En estos casos podemos encontrar dos tipos de escenarios diferentes:

Para crear conversaciones naturales, utilizamos el fichero pages.xml. En el siguiente ejemplo definimos una conversación llamada PlaceBid.

 <conversation name="PlaceBid"
               parameter-name="auctionId"
               parameter-value="#{auction.auctionId}"/>

Los atributos a rellenar son los siguientes:

A continuación,definimos qué paginas pueden participar en la conversación nombrada. Esto se hace, especificando el atributo de la conversación en la definición de la página. Como podemos observar, en la propiedad conversation del elemento page, especificamos el nombre de la conversación nombrada.

<page view-id="/bid.xhtml" conversation="PlaceBid" login-required="true">
    <navigation from-action="#{bidAction.confirmBid}">        
        <rule if-outcome="success">
            <redirect view-id="/auction.xhtml">
                <param name="id" value="#{bidAction.bid.auction.auctionId}"/>
            </redirect>
        </rule>        
    </navigation>
</page>

Cuando iniciemos o redireccionemos a una conversación natural, hay varias opciones para especificar el nombre. Vamos a comenzar viendo el siguiente ejemplo de definición de una página.

<page view-id="/auction.xhtml">
    <param name="id" value="#{auctionDetail.selectedAuctionId}"/>    
    <navigation from-action="#{bidAction.placeBid}">
        <redirect view-id="/bid.xhtml"/>
    </navigation>
</page>

Podemos observar como cuando se invoca el método action #{bidAction.placeBid} seremos redirigidos a la página /bid.xhtml, que como hemos vista antes, está configurada con la conversación PlaceBid. La declaración del método action puede ser la siguiente.

@Begin(join = true)
public void placeBid() {
    ...
}

Cuando las conversaciones nombradas son especificadas en el elemento <page>, la redirección a la conversación tiene lugar como parte de las reglas de navegación, después de que el método action haya sido invocado.

Existe un problema cuando se redirige a una conversación existente, si la redirección tiene que ocurrir antes que el método sea invocado. En ese momento, es necesario especificar el nombre de la conversación cuando el método action sea invocado.

Una forma de hacer esto, es utilizar la etiqueta JSF de Seam s:conversationName, donde especificamos el nombre de la conversación a utilizar.

<h:commandButton id="placeBidWithAmount" styleClass="placeBid" action="#{bidAction.placeBid}">
    <s:conversationName value="PlaceBid"/>
</h:commandButton>

Otra alternativa es especificar el atributo conversationName cuando se utilizen las etiquetas JSF de Seam s:link o s:button.

<s:link value="Place Bid" action="#{bidAction.placeBid}" conversationName="PlaceBid"/>
...
<s:button value="Place Bid" action="#{bidAction.placeBid}" conversationName="PlaceBid"/>

Finalizar conversacion existente e iniciar una nueva, en un mismo enlace

Cuando queremos acceder a una página destruyendo la conversación previa existente y comenzando una nueva nos encontramos con un problema. Como sabemos, si usamos la directiva <begin join=“true”…> en el fichero page.xml para comenzar una conversación de largo recorrido. Por otro lado en la página que redirige a esta nueva anotamos el método clickAndKillLastConversation, que enlaza a la página con un @End o usamos propagation=“end” en el <s:link que enlaza a la página.

En principio parece todo correcto, la página “padre” mata la conversación y la hija crea una nueva, pero el resultado real es que no se finaliza la conversación existente, por lo tanto no se crea una nueva, sino que nos unimos a la que había.

El problema viene porque a Seam le llegan dos órdenes contradictorias, por un lado <begin join=“true”…> que inidica que cree o se una a una conversación y por otro que la finalice. En ese momento Seam ha de escoger entre las dos opciones conservando la conversación existente.

Para solucionar esto, lo que se debe hacer es finalizar manualmente la conversación y mantener el begin, para que se inicie una conversación nueva, en el momento en que finaliza la fase de renderizado de la página. Para ello tenemos a disposición las clase MenuManagerBean, con el método clickAndKillLastConversation.

Si el enlace se encuentra en el menú principal de la aplicación, deberá de utilzar el método que devuelve la página al que quiere ir. Quedando los enlaces de menú de la siguiente manera:

	<s:link value="#{messages['menu.link.administracion.cambioClave']}\"
                   action="#{menuManagerBean.clickAndKillLastConversation('/paginas/administracion/cambioClave.xhtml')}" 
                   disabled="#{!identity.isEnabledMenu('1010_MEADCAMCLA')}"/>