En la noticia sobre la publicación de ItsNat v1.3 hablábamos como en esta versión se introduce un nuevo modo stateless o sin estado en el servidor, con el fin de poder desarrollar sitios web (y aplicaciones) Single Page Interface SEO compatibles escalables a múltiples nodos sin necesidad (por parte de ItsNat) de compartir información del estado del usuario entre nodos a través de la sesión o necesitar "server affinity".
Debido a que no hay datos de la página del cliente previamente almacenados en el servidor, podemos apagar el contenedor de servlets y arrancar otro (no hay datos de sesión restaurados) y nuestra aplicación/sitio web stateless seguirá funcionando.
El modo sin estado puede ser compatible SEO puesto que los dos modos de ItsNat, stateful y stateless no tienen mucha relación con el SEO, la carga de la página inicial se puede realizar por parte de ItsNat usando el modo fast-load como de costumbre para la compatibilidad SEO, por lo que podemos generar el HTML inicial en tiempo de carga de cualquier posible estado cliente ejecutando el mismo código Java DOM que utilizamos para procesar eventos AJAX.
En este tutorial veremos un ejemplo sencillo de como gestionar la página web del usuario a través del servidor con algunas de las herramientas habituales de ItsNat: HTML puro para templates (página inicial y fragmentos) y lógica en Java usando APIs W3C DOM, es decir el mismo tipo de programación que harías en JavaScript en el cliente pero en el espacio de memoria del servidor y por tanto en el mismo espacio de memoria en donde se encuentran los datos a gestionar.
public class servletstless extends HttpServletWrapper
{
@Override
public void init(ServletConfig config) throws ServletException
{
super.init(config);
ItsNatHttpServlet itsNatServlet = getItsNatHttpServlet();
String pathPrefix = getServletContext().getRealPath("/");
pathPrefix += "/WEB-INF/pages/manual/";
ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate(
"manual.stless.example","text/html",
pathPrefix + "stless_example.html");
docTemplate.addItsNatServletRequestListener(
new StlessExampleInitialDocLoadListener());
docTemplate.setEventsEnabled(false); // Stateless
docTemplate = itsNatServlet.registerItsNatDocumentTemplate(
"manual.stless.example.eventReceiver","text/html",
pathPrefix + "stless_example_event_receiver.html");
docTemplate.addItsNatServletRequestListener(
new StatelessExampleForProcessingEventDocLoadListener());
docTemplate.setEventsEnabled(false); // Stateless
ItsNatDocFragmentTemplate docFragDesc;
docFragDesc = itsNatServlet.registerItsNatDocFragmentTemplate(
"manual.stless.example.fragment","text/html",
pathPrefix + "stless_example_fragment.html");
}
}
El siguiente fragmento de código registra la plantilla de la página inicial:
ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate(
"manual.stless.example","text/html",
pathPrefix + "stless_example.html");
docTemplate.addItsNatServletRequestListener(
new StlessExampleInitialDocLoadListener());
docTemplate.setEventsEnabled(false); // Stateless
La llamada de configuración setEventsEnabled (false) deshabilita el modo stateful en el servidor pues al no poder recibir eventos remotos no hay necesidad de guardar una copia de la página (un ItsNatDocument) , este ajuste es necesario si queremos que la página cargada sea verdaderamente stateless.
El template stless_example.html asociado:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>ItsNat Stateless Example in Manual</title>
<script>
function sendEventStateless()
{
var counterElem = document.getElementById("counterId");
var userEvt = document.getItsNatDoc().createEventStateless();
userEvt.setExtraParam('counter',counterElem.firstChild.data);
userEvt.setExtraParam('itsnat_doc_name','manual.stless.example.eventReceiver');
document.getItsNatDoc().dispatchEventStateless(userEvt,3 /*XHR_ASYNC_HOLD*/, 1000000);
}
</script>
</head>
<body>
<h3>ItsNat Stateless Example in Manual</h3>
<h4 id="presentationId" itsnat:nocache="true"></h4>
<br /><br />
<a href="javascript:sendEventStateless()">Send stateless event</a>
<br /><br />
<div>
<div>Num. Events: <b id="counterId" itsnat:nocache="true">0</b></div>
</div>
<div>
<div>
<div id="insertHereId" />
</div>
</div>
<br />
</body>
</html>
Esta plantilla de documento será la página inicial de nuestro Single Page Interface micro ejemplo sin estado, recordamos que esta plantilla se ha registrado en modo no recibir eventos remotos del cliente, pero viendo esta línea de código JavaScript:
document.getItsNatDoc().dispatchEventStateless(userEvt,3 /*XHR_ASYNC_HOLD*/, 1000000);
Vemos que hay otro nuevo tipo de evento: los eventos sin estado, con parámetros familiares tal y como el objeto evento a enviar, el modo de transporte (AJAX async hold en este caso que asegura la secuencialidad de los eventos) y un timeout generoso de respuesta.
Esta línea es interesante:
userEvt.setExtraParam('itsnat_doc_name','manual.stless.example.eventReceiver');
ItsNat reconoce este parámetro estándar de "nombre del template usado del documento a cargar" en un evento stateless, cuando recibe este evento el comportamiento en el servidor es el siguiente:
1) Un nuevo ItsNatDocument se crea en base al template especificado. Esta instancia de documento es nueva, no tiene estado previo, no importa como se ha configurado el template (setEventsEnabled (false) se recomienda sólo para mayor claridad y para evitar cargas con estado usando URLs convencionales)
2) El proceso de carga del documento es como de costumbre, con una diferencia radical: el estado final DOM cuando termina la fase de carga no se serializa como HTML y el JavaScript inicial generado si existe en esta fase, se ignora y se pierde.
3) El listener de eventos globales registrado en la fase de carga ItsNatDocument.addEventListener (EventListener) se ejecuta recibiendo el evento stateless como si fuera un evento DOM (es una extensión). Las modificaciones DOM realizadas en esta fase se envían al cliente como JavaScript en el retorno del evento de una manera similar de un evento remoto con estado normal.
Acostumbrados al modo convencional, con estado, de ItsNat, este comportamiento parece sin sentido, sin embargo lo tiene ...
El propósito principal del procesamiento de eventos sin estado es:
1) Reconstruir totalmente o parcialmente el estado DOM del cliente en el servidor: este es el propósito de la fase de carga, el estado final DOM / árbol del documento después de la carga debe coincidir total o parcialmente con el estado del cliente.
2) Modificar el estado de DOM del documento en el proceso (o fase) del evento para generar el JavaScript necesario para llevar al cliente al nuevo estado.
Como fácilmente se puede adivinar, reconstruir totalmente el estado de cliente por-evento puede ser muy costoso, el modo stateless de ItsNat en general puede ser más costoso en procesamiento en el servidor que el modo stateful, pero tiene la ventaja de la escalabilidad ilimitada, la adición de más nodos y distribución de eventos entre nodos (provinientes incluso de la misma página) sin problema de necesitar datos compartidos de sesión.
Afortunadamente, no es necesario reconstruir totalmente la página del cliente en el servidor, podemos sólo reconstruir las partes que van a ser cambiadas, concretamente el modo stateless de ItsNat es excepcional para inyectar nuevo markup a la página del cliente por lo que puede interesarnos cargar sólo los trozos en donde vamos a insertar.
Siguiendo con el ejemplo, el código Java para generar la página inicial:
public class StlessExampleInitialDocLoadListener implements ItsNatServletRequestListener
{
public StlessExampleInitialDocLoadListener()
{
}
public void processRequest(ItsNatServletRequest request,ItsNatServletResponse response)
{
ItsNatHTMLDocument itsNatDoc = (ItsNatHTMLDocument)request.getItsNatDocument();
new StlessExampleInitialDocument(itsNatDoc);
}
}
public class StlessExampleInitialDocument
{
protected ItsNatHTMLDocument itsNatDoc;
public StlessExampleInitialDocument(ItsNatHTMLDocument itsNatDoc)
{
this.itsNatDoc = itsNatDoc;
HTMLDocument doc = itsNatDoc.getHTMLDocument();
Text node = (Text)doc.createTextNode(
"This the initial stateless page (not kept in server)");
Element presentationElem = doc.getElementById("presentationId");
presentationElem.appendChild(node);
}
}
Este código es sólo una excusa para mostrar cómo la página inicial es una página ItsNat generada de forma convencional (pero sin estado), nada especial aquí.
El material interesante comienza con la plantilla stless_example_event_receiver.html registrada con el nombre manual.stless.example.eventReceiver que va a procesar eventos stateless:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>ItsNat Stateless Document For Stateless Event Processing</title>
</head>
<body>
<b id="counterId" itsnat:locById="true" itsnat:nocache="true">(num)</b>
<div id="insertHereId" itsnat:locById="true" itsnat:nocache="true" />
</body>
</html>
Antes de continuar con el código Java asociado, detengámonos para comparar el template de la página inicial:
<div>
<div>Num. Events: <b id="counterId" itsnat:nocache="true">0</b></div>
</div>
<div>
<div>
<div id="insertHereId" />
</div>
</div>
y el nuevo template:
<b id="counterId" itsnat:locById="true" itsnat:nocache="true">(num)</b>
<div id="insertHereId" itsnat:locById="true" itsnat:nocache="true" />
Los dos pares de elementos tienen el mismo id, esto no es casual, por ejemplo, el que contiene el elemento insertHereId de la plantilla de la página inicial se corresponde con el de la plantilla "procesadora de eventos", como se puede ver sólo el nombre de la etiqueta (div) y el atributo id es el mismo, notar cómo los elementos se encuentran en un lugar diferente en la página (posición relativa respecto al body), esta es la razón del atributo ItsNat locById a true (lo veremos).
El código Java asociado a esta plantilla:
public class StatelessExampleForProcessingEventDocLoadListener
implements ItsNatServletRequestListener
{
public StatelessExampleForProcessingEventDocLoadListener()
{
}
public void processRequest(ItsNatServletRequest request,ItsNatServletResponse response)
{
new StatelessExampleForProcessingEventDocument(
(ItsNatHTMLDocument)request.getItsNatDocument(),request,response);
}
}
public class StatelessExampleForProcessingEventDocument
implements Serializable,EventListener
{
protected ItsNatHTMLDocument itsNatDoc;
protected Element counterElem;
public StatelessExampleForProcessingEventDocument(ItsNatHTMLDocument itsNatDoc,
ItsNatServletRequest request, ItsNatServletResponse response)
{
this.itsNatDoc = itsNatDoc;
if (!itsNatDoc.isCreatedByStatelessEvent())
throw new RuntimeException(
"Only to test stateless, must be loaded by a stateless event");
// Counter node with same value (state) than in client:
String currCountStr = request.getServletRequest().getParameter("counter");
int counter = Integer.parseInt(currCountStr);
HTMLDocument doc = itsNatDoc.getHTMLDocument();
this.counterElem = doc.getElementById("counterId");
((Text)counterElem.getFirstChild()).setData(String.valueOf(counter));
itsNatDoc.addEventListener(this);
}
public void handleEvent(Event evt)
{
ItsNatEventDOMStateless itsNatEvt = (ItsNatEventDOMStateless)evt;
Text counterText = (Text)counterElem.getFirstChild();
String currCountStr = counterText.getData();
int counter = Integer.parseInt(currCountStr);
counter++;
counterText.setData(String.valueOf(counter));
Document doc = itsNatDoc.getDocument();
Element elemParent = doc.getElementById("insertHereId");
ScriptUtil scriptGen = itsNatDoc.getScriptUtil();
String elemRef = scriptGen.getNodeReference(elemParent);
ClientDocument clientDoc = itsNatEvt.getClientDocument();
clientDoc.addCodeToSend(elemRef + ".innerHTML = '';");
clientDoc.addCodeToSend("alert('Currently inserted fragment removed before');");
ItsNatServlet servlet = itsNatDoc.getItsNatDocumentTemplate().getItsNatServlet();
ItsNatHTMLDocFragmentTemplate docFragTemplate =
(ItsNatHTMLDocFragmentTemplate)servlet.getItsNatDocFragmentTemplate(
"manual.stless.example.fragment");
DocumentFragment docFrag = docFragTemplate.loadDocumentFragmentBody(itsNatDoc);
elemParent.appendChild(docFrag); // docFrag is empty now
// Umm we have to celebrate/highlight this insertion
Element child1 = ItsNatTreeWalker.getFirstChildElement(elemParent);
Element child2 = ItsNatTreeWalker.getNextElement(child1);
Text textChild2 = (Text)child2.getFirstChild();
Element bold = doc.createElement("i");
bold.appendChild(textChild2); // is removed from child2
child2.appendChild(bold);
child2.setAttribute("style","color:red");
// <h3 style="color:red"><i>Inserted!</i></h3>
}
}
En resumen este código actualiza el elemento counterId con el valor actual de la página e inserta un fragmento basado en el template con nombre manual.stless.example.fragment en el elemento con id insertHereId, en el documento cargado al procesar el evento stateless. Más adelante veremos que esta inserción finalmente sucede en la última página del cliente.
El markup de la plantilla registrada en manual.stless.example.fragment es:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
xmlns:itsnat="http://itsnat.org/itsnat">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Fragment</title>
</head>
<body>
<h4 style="color:green"><b>Fragment...</b></h4>
<h3>Inserted!</h3>
</body>
</html>
Como esta plantilla se inserta llamando docFragTemplate.loadDocumentFragmentBody (itsNatDoc) ignora todo salvo el contenido de <body>.
Vamos a estudiar el código:
if (!itsNatDoc.isCreatedByStatelessEvent())
throw new RuntimeException(
"Only to test stateless, must be loaded by a stateless event");
Esta verificación evita intentar cargar este documento, diseñado para el procesamiento de eventos stateless, como una página convencional especificando una dirección URL que contiene el parámetro itsnat_document con el nombre del template. El método ItsNatDocument.isCreatedByStatelessEvent () sólo devuelve true si el documento se ha cargado como parte del procesamiento de un evento stateless.
Otro de los usos más importantes de ItsNatDocument.isCreatedByStatelessEvent () es distinguir cuando el documento fue creado para cargar una página, como de costumbre, o para procesar un evento sin estado, porque como se puede deducir fácilmente, en este ejemplo muy simple el template usado para la página inicial también podría ser utilizado para el procesamiento de eventos stateless, por lo tanto este método se podría utilizar para separar el código de carga de la página inicial del código de la fase de carga del documento de procesamiento de eventos stateless.
Tenemos en cuenta que estamos recibiendo un parámetro de contador con el número entero actual en el elemento counterId del cliente:
// Counter node with same value (state) than in client:
String currCountStr = request.getServletRequest().getParameter("counter");
int counter = Integer.parseInt(currCountStr);
HTMLDocument doc = itsNatDoc.getHTMLDocument();
this.counterElem = doc.getElementById("counterId");
((Text)counterElem.getFirstChild()).setData(String.valueOf(counter));
Este código es para "reconstruir" el estado del elemento DOM counterId en el cliente en el servidor.
Finalmente, en la fase de carga:
itsNatDoc.addEventListener(this);
Registra "this" en el documento como el listener procesar el evento stateless tras la carga de este documento. Después de la fase de carga, este listener se llamará de inmediato con el evento stateless.
public void handleEvent(Event evt)
{
ItsNatEventDOMStateless itsNatEvt = (ItsNatEventDOMStateless)evt;
ItsNat extiende eventos DOM con otro tipo de eventos definidos en ItsNat , nada nuevo, en este caso los eventos stateless son un nuevo tipo de "evento DOM extendido".
ItsNatEventDOMStateless hereda de ItsNatEventStateless, un objeto que implementa la interfaz ItsNatEventStateless es "un evento sin estado".
Ahora es hora de entrar realmente en el enfoque stateless de ItsNat:
Text counterText = (Text)counterElem.getFirstChild();
String currCountStr = counterText.getData();
int counter = Integer.parseInt(currCountStr);
counter++;
counterText.setData(String.valueOf(counter));
Dijimos antes que el objetivo de la fase de carga es llevar el documento cargado al mismo estado DOM de la parte del cliente en cuestión que vamos a cambiar al procesar el evento. Como se puede ver el nodo de texto se ha actualizado con un nuevo valor entero, se generará el código JavaScript necesario y se envía al cliente para actualizar el elemento counterId en el cliente como el resultado del procesamiento del evento stateless.
ItsNatDocument itsNatDoc = itsNatEvt.getItsNatDocument();
Document doc = itsNatDoc.getDocument();
Element elemParent = doc.getElementById("insertHereId");
ScriptUtil scriptGen = itsNatDoc.getScriptUtil();
String elemRef = scriptGen.getNodeReference(elemParent);
ClientDocument clientDoc = itsNatEvt.getClientDocument();
clientDoc.addCodeToSend(elemRef + ".innerHTML = '';");
clientDoc.addCodeToSend("alert('Currently inserted fragment removed before');");
En este caso no hay nada que hacer en la fase de carga relacionada con el elemento insertHereId porque queremos que se vuelva a insertar el mismo DOM en este elemento una y otra vez y es más fácil con código JavaScript personalizado limpiar el contenido en el cliente (ItsNat en modo stateless es mucho más cliente céntrico y más código JavaScript a medida suele ser necesario).
Este código requiere más explicación, el documento que estamos modificando fue creado sobre la base del template con el nombre manual.stless.example.eventReceiver, este template es similar pero no el mismo que manual.stless.example, pero el JavaScript generado por el segundo se envía al cliente el cual se cargó a través del primero.
Vamos a repetir los elementos importantes (dinámicos):
· manual.stless.example
<div>
<div>Num. Events: <b id="counterId">0</b></div>
</div>
<div>
<div>
<div id="insertHereId" />
</div>
</div>
· manual.stless.example.eventReceiver
<b id="counterId" itsnat:locById="true" itsnat:nocache="true">(num)</b>
<div id="insertHereId" itsnat:locById="true" itsnat:nocache="true" />
Como sabemos en ItsNat la localización de un nodo se basa en "contar nodos en el árbol" desde <html> al nodo concreto, por supuesto el localizador de nodos es inteligente y existe un cacheo automático de nodos para evitar recorrer el árbol desde el nodo raíz <html> cuando sea posible. Como la posición de los elementos del primer template no es el mismo que el segundo template no podemos localizar nodos contando desde el nodo root html, esta es la razón del atributo ItsNat locById.
El atributo locById invita a ItsNat a utilizar el atributo / propiedad id en el JavaScript generado para localizar el nodo utilizando por tanto getElementById(). Al utilizar este truco puedes hacer plantillas ligeras ad-hoc con sólo el suficiente markup alineado con el estado DOM cliente.
Parte del código queda por explicar:
ItsNatServlet servlet = itsNatDoc.getItsNatDocumentTemplate().getItsNatServlet();
ItsNatHTMLDocFragmentTemplate docFragTemplate =
(ItsNatHTMLDocFragmentTemplate)servlet.getItsNatDocFragmentTemplate(
"manual.stless.example.fragment");
DocumentFragment docFrag = docFragTemplate.loadDocumentFragmentBody(itsNatDoc);
elemParent.appendChild(docFrag); // docFrag is empty now
// Umm we have to celebrate/highlight this insertion
Element child1 = ItsNatTreeWalker.getFirstChildElement(elemParent);
Element child2 = ItsNatTreeWalker.getNextElement(child1);
Text textChild2 = (Text)child2.getFirstChild();
Element bold = doc.createElement("i");
bold.appendChild(textChild2); // is removed from child2
child2.appendChild(bold);
child2.setAttribute("style","color:red");
// <h3 style="color:red"><i>Inserted!</i></h3>
Este código convencional ItsNat inserta una versión modificada del código dentro de <body> del manual.stless.example.fragment en el elemento insertHereId. El detalle interesante de este código es que la mayor parte de las modificaciones DOM suceden después de la inserción en el documento (recordar que se genera código JavaScript DOM sobre la marcha), cualquier nodo involucrado, child1, child2 y textChild2, necesita ser ubicado después de la inserción, podemos preguntarnos: ¿son localizados en el cliente recorriendo el árbol completo desde <html>?
¡No!
Como se dijo antes, el almacenamiento en caché evita recorrer totalmente el árbol desde arriba, por ejemplo ItsNat busca nodos padre ya en el árbol que estén en el caché, en este caso el nodo insertHereId es el candidato, o cualquier otro nodo hijo en caché cuando se inserta.
Estas técnicas proporcionan potentes herramientas de manipulación del DOM del cliente basado en código en el servidor, en Java y HTML puro más allá de la simple inserción de una sola vez.
Demo online (con algun ejemplo más).