Una aplicación AJAX hecha a mano

0
39178

Una aplicación AJAX hecha a mano


En este tutorial se muestra cómo hacer una página web que, usando AJAX,
accede a un servicio web SOAP. Para ello sólo se usa JavaScript, sin nada de código
en el servidor.

Por Javier Cámara (jcamara@softwareag.es)
Arquitecto de software en el BCS Competence Center de Software AG España.

Introducción

Si trabajas en algo relacionado con las aplicaciones web es muy complicado que no hayas
oído hablar de AJAX recientemente. AJAX es una forma de construir aplicaciones web
que permite que éstas sean más usables y dinámicas. El efecto se puede resumir en la frase
«páginas web con actualización parcial»: o sea, en vez de que las páginas se reciban completamente
desde el servidor cada vez que hacemos algo, hay una única página fija que recibe datos del servidor
«por debajo» con los que actualiza partes de la misma. El resultado neto es esa mayor interactividad.

En puridad AJAX significa Asynchronous JAvascript with XML, indicando que la página se
comunica con el servidor usando XML. La «asincronía» sólo sirve para que el navegador no se quede
congelado durante esa comunicación, que es lo que pasa si la comunicación es síncrona en vez
de asíncrona. Pero en realidad, como AJAX se clasifica cualquier cosa que tenga actualizaciones
parciales. Hay otras variantes que no son XML, como JSON,
que obtienen el mismo efecto.

Curiosamente esto ha sido posible desde hace varios años (yo conozco aplicaciones que podrían clasificarse
como algo parecido desde 1998 o así), y en realidad es gracias al por muchos odiado Microsoft que esto
es posible. Pero no ha sido hasta que ha existido un navegador alternativo al de Microsoft
que soportase esto por completo (Firefox), y que Google (no sólo Microsoft) haya mostrado a todo el mundo
cómo se le puede sacar partido (ej. con Google maps),
que se ha popularizado esta técnica.

En un futuro cercano, el AJAX será una forma muy común, probablemente la más extendida,
de construir muchas aplicaciones. Aunque ésto sólo
será cierto si existen herramientas que oculten la complejidad del AJAX haciéndolo lo más transparente
posible, como
Atlas,
Google web toolkit, quizás JSF,
u otra miríada de herramientas de desarrollo que insisten en que si las usas podrás crear aplicaciones
web AJAX en un plis-plas.
Aún así, en este tutorial vamos a mostrar cómo se puede hacer un poquito de AJAX a mano, o sea sin
usar ninguna herramienta especial.

Por ello, nuestra aplicación va a consistir de únicamente una página HTML con JavaScript
dentro, que se conectará a un servicio web SOAP que sacaremos de
otro tutorial
que ya hicimos previamente. Y además, esa
aplicación funcionará tanto en Internet Explorer 6 como en Firefox.

El resto de este tutorial contiene lo siguiente:

Qué debe hacer nuestra aplicación

Vamos a reutilizar los resultados de un
tutorial previo sobre construcción de servicios web a partir
de WSDL, y vamos a ponerle una cara al servicio que creamos en ese tutorial. O sea, vamos a crear una
aplicación AJAX que pida un código postal o parte de él, y luego
muestre la lista de sucursales bancarias en ese código.

El servidor

Nuestra aplicación AJAX se va a conectar a un servidor, y me temo que vas a tener que tener
instalado en tu PC ese servidor. Esto es debido a una
importante limitación de una página AJAX: no puede acceder a cualquier
recurso
. El mecanismo que usa el AJAX para comunicarse con el exterior es abrir
una conexión HTTP desde código JavaScript, y por buenas razones de seguridad los navegadores limitan
el que esa conexión se pueda abrir a una máquina diferente de la que se descargó el
JavaScript (en el caso de Firefox, hasta el puerto tiene que ser el mismo).
Por ello, la página que vamos a crear en este tutorial debe estar grabada dentro de la misma
aplicación web que implementa nuestro servicio.

La forma normal de evitar esta limitación es usar un proxy en el servidor: si una página
AJAX se quiere conectar a un recurso fuera de su máquina origen, lo que hace es conectarse
a un proceso (ej. un pequeño servlet) en su máquina origen que le hace de intermediario.
Pero nosotros no vamos a hacer esto en este tutorial, sino que vamos a poner directamente
la página AJAX en el mismo sitio donde está el servicio.

La variante JSON del AJAX no tiene esta limitación, pues al usar frames ocultos éstos pueden
apuntar a cualquier recurso. Pero claro, el servidor debe devolver JavaScript, lo cual no
es ni de lejos tan común como SOAP, y tengo mis dudas de que esté lo suficientemente estandarizado.

En nuestro tutorial, para conseguir tener el servicio web ejecutándose en tu máquina
puedes hacer, al menos, una de estas dos cosas:

  1. Realiza todo el tutorial donde se crea el servicio, instalando Eclipse, Tomcat etc; o
  2. Instálate Tomcat y luego despliega en él este archivo WAR
    que contiene la aplicación web del servicio. Ten en cuenta que luego modificaremos una página que está dentro de
    ese WAR, así que si usas un servidor distinto de Tomcat eso puede obligarte a hacer operaciones
    adicionales.

Durante el resto del tutorial se asumirá que has elegido la opción 1, y se usará Eclipse
y la aplicación sucursales del anterior tutorial para crear la página AJAX. Si has
elegido la opción 2, puedes usar cualquier editor para modificar la página, en vez de Eclipse.

Creando la interfaz HTML

La interfaz de usuario de nuestra aplicación tendrá el siguiente aspecto:

Como el objetivo de este tutorial no es aprender HTML sino AJAX, aquí tienes
esa página ya preconstruida para que te la descargues.
Además, también puedes
descargarte la aplicación final completa tal
y como quedaría si se siguen todos los pasos de este tutorial.

Como ya hemos dicho antes, esa página debe estar
dentro de la aplicación web del servicio. Si has descargado sucursales.war en vez de partir del
tutorial de creación del servicio, esto no lo tienes que hacer porque ese war ya contiene
sucursales.html . Pero si no, pues meteremos la página en Eclipse. Para ello:

  • Salva la página en alguna parte de tu disco duro
  • En Eclipse, importa la página en el directorio WebContent del proyecto Sucursales:





Tras salvar esa página, el Eclipse se ocupará de redesplegarla en el Tomcat, y si éste está
arrancado y ejecutándose en el puerto 8081, al conectarnos a
http://localhost:8081/sucursales/sucursales.html
veremos nuestra interfaz de usuario.

En el resto del tutorial nos dedicaremos a rellenar esa página con el JavaScript que
le dará el toque AJAX.

Creando el mensaje de petición

Lo primero que haremos será crear el código que invoque a nuestro servicio. Para eso primero
tenemos que averiguar cuál es exactamente el mensaje que éste espera. Si entiendes muy bien
WSDL y XML Schema, puedes intentar averiguarlo mirando sucursales.wsdl ; pero si como es normal
no es así, mejor que utilices alguna herramienta. El XML Spy
es una herramienta buenísima para esto (y de pago), pero con Eclipse lo más fácil es ver qué
documentos XML se envían y reciben cuando ejecutamos el servicio con el Web Services Explorer:



Así podemos comprobar que el mensaje de petición es así:

<?xml version=»1.0″
encoding=»UTF-8″?>


 <soapenv:Envelope xmlns:soapenvhttp://schemas.xmlsoap.org/soap/envelope/«

            xmlns:q0http://banquito.com/Sucursales/«

            xmlns:xsdhttp://www.w3.org/2001/XMLSchema«

            xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance«>

   <soapenv:Body>

     <q0:buscaSucursalesRequest>

       <q0:parteCodPostal>28</q0:parteCodPostal>

     </q0:buscaSucursalesRequest>

   </soapenv:Body>

 </soapenv:Envelope>

Y ahí aún hay algunas cosas que sobran. Vamos a modificar sucursales.html para incluir
la creación de ese mensaje:

  1. Primero, en el onclick de nuestro botón vamos a invocar a una nueva función Javascript,
    dejándolo como sigue:

        <input type=submit value="Buscar sucursales"
                onclick="buscaSucursales();return false;">
      
  2. Luego vamos a crear esa función. Por ahora sólo crearemos el mensaje XML, insertando
    dentro el código postal introducido en el formulario. Luego lo mostraremos ese mensaje,
    para ir comprobando que todo va bien:

        <head>
        <title>Banquito - búsqueda de sucursales</title>
        
        <script>
        
        function buscaSucursales()
        {
          var smsg=
            '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'+
                '<SOAP-ENV:Body>'+
                  '<bs:buscaSucursalesRequest xmlns:bs="http://banquito.com/Sucursales/">'+
                  '<parteCodPostal>'+document.formSucu.cp.value+'</parteCodPostal>'+
                  '</bs:buscaSucursalesRequest>'+
                '</SOAP-ENV:Body>'+
            '</SOAP-ENV:Envelope>';
        
         alert(smsg);
        }
        </script>
        
        </head>
    

Si accedemos a nuestra página con un navegador, por ejemplo en
http://localhost:8081/sucursales/sucursales.html
(recuerda refrescar la página para que recargue los cambios)
y le damos al botón de Buscar sucursales,
nos debería salir algo como esto:

Hemos creado el mensaje XML de la forma más obvia: como una cadena de caracteres. También
podríamos haber intentado usar DOM, pero eso a) resulta en un código mucho más complicado y
b) funciona diferente en Internet Explorer y en Firefox, con lo cual en la práctica no vale la
pena. Además, usar cadenas es la forma en que el código queda más legible, así que
en entornos «ligeros» como Javascript yo es la opción que recomiendo.

Precisamente por usar cadenas, ese código que hemos puesto tiene un fallo muy obvio, y es
que puede generar XML inválido. Por ejemplo, si como código postal metemos «casque>» el
XML que nos generará será éste:

Que ni siquiera podremos mandar al servidor porque no es XML. Este tipo de cosas causan errores
inesperados más tarde y hasta problemas de seguridad, así que lo mejor es cortarlos cuanto antes.
Para eso, vamos a «escapear» los caracteres especiales de XML con una nueva función Javascript.
Como esta función no es específica de nuestra aplicación sino que sería válida para otras aplicaciones
y otros servicios, la vamos a meter en una librería de funciones común llamada ws.js
(por «web services», claro). Así pues,

  1. Creamos un nuevo fichero:



  2. Y ahí vamos a definir nuestra nueva función, ws_escapeXML:

      function ws_escapeXML(s)
      {
        var s1="";
        var i,I;
        for (i=0,I=s.length;i<I;++i)
        {
          var c=s.charAt(i);
          if (c=='>')
            s1+="&gt;";
          else if (c=='<')
            s1+="&lt;";
          else if (c=='&')
            s1+="&amp;";
          else
            s1+=c;
        }
        return s1;
      }
    
  3. Luego incluimos esa librería en nuestro sucursales.html e invocamos a la función:

    <head>
    <title>Banquito - búsqueda de sucursales</title>
    
    <script src="ws.js"></script>
    
    <script>
    
    function buscaSucursales()
    {
      var smsg=
        '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'+
            '<SOAP-ENV:Body>'+
              '<bs:buscaSucursalesRequest xmlns:bs="http://banquito.com/Sucursales/">'+
              '<parteCodPostal>'+ws_escapeXML(document.formSucu.cp.value)+'</parteCodPostal>'+
              '</bs:buscaSucursalesRequest>'+
            '</SOAP-ENV:Body>'+
        '</SOAP-ENV:Envelope>';
    
     alert(smsg);
    }
    </script>
    
    </head>
    

Después de eso, si volvemos a meter «casque>» como código postal, el
XML que nos generará ya será válido:

Llamando a nuestro servicio

Ahora que ya tenemos el mensaje creado, vamos realmente a invocar al servicio, o sea
la parte realmente AJAX del asunto. Para ello haremos lo siguiente:

  • Averiguar cuál es el URL del servicio
  • Crear una XMLHttpRequest e invocar al servicio de forma asíncrona
  • Hacer que la llamada al servicio tarde un poco
  • Recoger el resultado del servicio de forma asíncrona

Averiguar cuál es el URL del servicio

Como estamos copiando sucursales.html dentro de la aplicación web del servicio, podemos
saber con facilidad cuál es el URL del servicio independientemente del nombre de la máquina
etc. Vamos a crear una nueva función en sucursales.html que nos componga ese URL:

function getURLServicio()
{
  var url=
    document.location.protocol+"//"+document.location.host+
    primParte(document.location.pathname)+
      "/services/SucursalesSOAP";
  return url;
}


Y la función de utilidad primParte, que devuelve el primer componente de un
path (/comp1/comp2/…) la meteremos en ws.js aunque no tenga mucho que
ver con servicios web:

function primParte(s)
{
  var d=0;
  if (s.charAt(0)=='/')
    d=1;
  var p=s.indexOf('/',d);
  if (p>0)
    return s.substring(0,p);
  else
    return s;
}

Crear una XMLHttpRequest e invocar al servicio de forma asíncrona

El objeto XMLHttpRequest es la piedra angular del AJAX. Es un buen invento del
por muchos odiado Microsoft como una forma de cumplir la vieja promesa del XML de ser intercambiado
entre los navegadores y los servidores. Es muy viejo, yo recuerdo haberlo visto ya
allá por el año 2000. Y, afortunadamente, la gente
de Mozilla creyó que era una buena idea, así como el HTML dinámico (otro buen invento también
del odiado Microsoft).
Luego, los de Google se preguntaron que, ya que tanto el XMLHttpRequest como el
HTML dinámico eran multinavegador, que por qué no usarlos, y así nació el AJAX. Otros navegadores
también soportan todo esto, y el XMLHttpRequest está siendo objeto de
estandarización por parte del W3C.

Este objeto permite enviar una petición XML a un servidor por HTTP, y recibir la respuesta
XML también. Sencillo. Esta recepción de respuesta se puede hacer de forma síncrona, de modo
que el navegador se queda congelado hasta que el servidor responde (no podemos movernos por la
página, ir a otra página, usar los menús, etc); o asíncrona, de modo que el navegador sigue
procesando acciones del usuario y cuando llega la respuesta invoca a una función que le digamos.

Vamos a empezar por lo fácil y haremos una invocación síncrona:

  1. Primero editamos la función buscaSucursales de nuestro sucursales.html y
    añadimos el uso de XMLHttpRequest:

    function buscaSucursales()
    {
      var smsg=
        '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'+
            '<SOAP-ENV:Body>'+
              '<bs:buscaSucursalesRequest xmlns:bs="http://banquito.com/Sucursales/">'+
              '<parteCodPostal>'+ws_escapeXML(document.formSucu.cp.value)+'</parteCodPostal>'+
              '</bs:buscaSucursalesRequest>'+
            '</SOAP-ENV:Body>'+
        '</SOAP-ENV:Envelope>';
    
      var req=ws_newXMLHttpRequest();
    
      var endpoint=getURLServicio();
      req.open("POST",endpoint,false);
      req.setRequestHeader("SOAPAction",'"http://banquito.com/Sucursales/buscaSucursales"');
      var resp=req.send(smsg);
    
      if (!req.responseXML || !req.responseXML.documentElement)
      {
        var error;
        if (req.status !=200)
          error="Error HTTP '"+req.status+" "+req.statusText+"' accediendo al servicio en "+endpoint;
        else
          error="El servicio en "+endpoint+" no ha devuelto un XML correcto";
        alert(error);
      }
      else
        alert("Resultado:"+req.responseXML.documentElement.xml);
    }
    

    La SOAPAction es una cabecera HTTP que es uno de los mecanismos de comunicación
    de SOAP sobre HTTP. Supongo que sirve para permitir redirigir peticiones sin tener que analizar el XML.
    No estoy seguro de que Axis le haga caso, pero lo apropiado es ponerlo
    en cualquier caso. Ese valor que hemos puesto, "http://banquito.com/Sucursales/buscaSucursales",
    lo hemos averiguado consultando sucursales.wsdl:

    Y según las reglas del WS-I, es obligatorio que vaya rodeado
    de «» aunque sea vacío, y por eso lo mandamos así.

  2. En ese código se utiliza la función ws_newXMLHttpRequest, y por tanto
    hemos de crearla. Esta función se ocupa de crear un objeto XMLHttpRequest, pues aunque su uso
    es el mismo en Internet Explorer y Firefox, lamentablemente su creación no
    lo es, e incluso varía entre versiones de Internet Explorer. La meteremos en ws.js, claro:

    function ws_newXMLHttpRequest()
    {
      var req;
      try
      {
        // Esto funciona en Mozilla pero en IE lanza una excepción
        req=new XMLHttpRequest();
      }
      catch (e)
      {
        try
        {
          req=new ActiveXObject("MSXML2.XmlHttp");
        }
        catch (e)
        {
          try
          {
            req=new ActiveXObject("Microsoft.XmlHttp");
          }
          catch (e)
          {
            try
            {
              req=new ActiveXObject("MSXML.XmlHttp");
            }
            catch (e)
            {
              req=new ActiveXObject("MSXML3.XmlHttp");
            }
          }
        }
      }
      return req;
    }
    

Si desplegamos eso dentro de la aplicación web de nuestro servicio, accedemos a ella
mediante el navegador
(recuerda refrescar la página para que recargue los cambios)
y pulsamos en Buscar sucursales, deberíamos obtener algo como
esto:

Que es el XML que ha devuelto nuestro servicio, y que tiene buena pinta. Aunque aún no hemos hecho AJAX, sino como mucho, «JAX». Pero antes
de añadir la «A» de Asíncrono, vamos a demostrar para qué sirve esto de hacer las cosas
asíncronas.

Hacer que la llamada al servicio tarde un poco

Cuando el servicio responde casi inmediatamente, no se nota la diferencia entre síncrono
y asíncrono. Así que para que se note vamos a introducir un retraso en la respuesta
del servidor. Además, para hacer esto no vamos a modificar el servicio, sino que vamos a
usar una útil utilidad de Axis llamada TCPMon. Su función principal es la de
permitirnos ver el tráfico entre nuestros clientes y nuestros servidores, lo cual
es impagable cuando hay algún problema. Pero además nos permite simular una conexión
lenta, que es justo lo que queremos.

El TCPMon hace de intermediario: se pone a escuchar en un puerto, y todo lo que recibe
por él lo redirecciona a otro puerto, posiblemente de otra máquina. La idea es que
el cliente hable con el TCPMon en vez de con el servicio, y que el TCPMon sea el que
hable con el servicio, pudiendo así hacer cosas interesantes en el tráfico.
TCPMon viene con Axis, y como nuestro servicio se ejecuta con Axis, pues ya lo tenemos.
Para arrancarlo se puede usar la siguiente línea de comandos:

  java -cp "(directorio lib de nuestro servicio)\axis.jar" org.apache.axis.utils.tcpmon

Donde, claro, (directorio lib de nuestro servicio) es el directorio WEB-INF/lib de
la aplicación web de nuestro servicio. Si estás usando Eclipse, estará en tu workspace de Eclipse
y será algo como .metadata\.plugins\org.eclipse.wst.server.core\tmp0\webapps\sucursales\WEB-INF\lib.
Si estás usando tu propio Tomcat, pues ya deberías saber dónde está.

Una vez arrancado, TCPMon nos muestra esto:

Tenemos que poner un Listen port # que no esté usado, por ejemplo el 8666, que es donde
va a escuchar TCPMon y donde nuestro cliente se conectará. Y en el Listener Target Port #
ponemos el puerto donde está nuestro servicio, por ejemplo 8081. Y luego le damos al botón Add:

Pinchando en la nueva pestaña Port 8666 veremos la traza de mensajes:

Aquí conviene marcar la opción XML format, que nos muestra los mensajes XML de forma más
legible.

Para probar que TCPMon funciona, nos conectamos con nuestro navegador a sucursales.html con el
mismo URL que antes, pero ahora con el puerto 8666 para que sea TCPMon quien la cargue,
y por tanto el tráfico subsequente de XMLHttpRequest pase por él:

Si vemos la pantalla del TCPMon veremos el tráfico entre el navegador y el servidor, que en este
caso no es XML sino HTML. Como no nos interesa, le podemos dar al botón Remove All para
no liarnos. Luego, le damos a nuestro Buscar sucursales. La aplicación funciona igual que antes,
pero en TCPMon vemos que tenemos un registro de los mensajes XML intercambiados:

Esto está bien, pero lo que queríamos ver era qué pasaba cuando teníamos un servidor lento.
Para ello, volvemos al TCPMon, le damos a Close para cerrar el Port 8666 y volvemos
a definirlo, pero esta vez marcando la opción Simulate slow connection:

Y le damos a Add, claro. Si ahora volvemos a nuestra aplicación y repetimos el Buscar sucursales,
pues va a funcionar igual, sólo que bastante más lento. Y, como resulta que nuestra aplicación
hace peticiones síncronas y no asíncronas, pues el navegador entero se cuelga hasta que recibimos
la respuesta. Ni la aplicación puede mostrar ningún mensaje de estado o avance, ni el usuario puede pulsar ningún
botón de cancelar, ni cambiar de página, ni cerrar el navegador de forma normal. Después de tantos años
acostumbrados a que aunque las páginas tarden en cargarse uno puede hacer lo que quiera, pues esto resulta extraño.
Y como es fácilmente mejorable, pues vamos a hacerlo con la invocación asíncrona.

Recoger el resultado del servicio de forma asíncrona

Para ello tenemos que utilizar XMLHttpRequest de forma diferente, y procesar el resultado
no justo después de llamarlo, sino en una función Javascript separada. Así que vamos a hacer unos cuantos cambios:

  1. Primero modificamos sucursales.html para añadir una nueva variable global, AsyncReq,
    que guarde nuestra petición entre que la enviamos y la recibimos:

    <script>
    
    var AsyncReq;
    
    function buscaSucursales()
    {
    
  2. Luego cambiamos nuestra función buscaSucursales, para que inicie la petición
    pero no procese el resultado:

    function buscaSucursales()
    {
      var smsg=
        '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'+
            '<SOAP-ENV:Body>'+
              '<bs:buscaSucursalesRequest xmlns:bs="http://banquito.com/Sucursales/">'+
              '<parteCodPostal>'+ws_escapeXML(document.formSucu.cp.value)+'</parteCodPostal>'+
              '</bs:buscaSucursalesRequest>'+
            '</SOAP-ENV:Body>'+
        '</SOAP-ENV:Envelope>';
    
      AsyncReq=ws_newXMLHttpRequest();
    
      var endpoint=getURLServicio();
      AsyncReq.onreadystatechange=asyncRecv;
      AsyncReq.open("POST",endpoint,true);
      AsyncReq.setRequestHeader("SOAPAction",'"http://banquito.com/Sucursales/buscaSucursales"');
      AsyncReq.send(smsg);
    }
    
  3. Como vemos, el mayor cambio es que no procesamos el resultado, puesto que éste
    será notificado más adelante cuando haya alguno. Ese true en vez del false
    en la llamada a AsyncReq.open precisamente indica
    proceso asíncrono. Además, AsyncReq.onreadystatechange especifica cuál será la
    función Javascript que
    será invocada cuando haya cambios en el estado de la petición. En nuestro caso hemos puesto
    asyncRecv, y por tanto hemos de crear esa función en sucursales.html:

    function asyncRecv()
    {
      if (AsyncReq.readyState == 4)
      {
        var req=AsyncReq;
        AsyncReq=null;
        try
        {
          var sucursalesResponse=ws_processResponseBody(req);
          alert("Resultado:"+sucursalesResponse.xml);
        }
        catch (e)
        {
          alert("Error invocando al servicio en "+getURLServicio()+":"+e);
        }
      }
    }
    

    Los distintos valores de AsyncReq.readyState
    indican diferentes cosas; el 4 significa que se ha completado la recepción del resultado y por tanto
    es el que nos interesa.

  4. En asyncRecv se utiliza una nueva función, ws_processResponseBody,
    que procesa el resultado de la petición y, si es exitoso, devuelve el primer hijo del Body
    del mensaje SOAP recibido
    . Es ahí donde debería estar siempre la información útil, y donde
    está en nuestro caso. En caso de encontrar cualquier error, ws_processResponseBody
    lanza una excepción de Javascript, que es lo que caza el try..catch de asyncRecv.
    Esta función ws_processResponseBody la añadimos en ws.js. Podríamos hacerla más sencilla
    para empezar, pero vamos a meterle ya todo el control de errores apropiado:

    function ws_processResponseBody(req)
    {
      var error,firstBodyChild;
      if (req.responseXML && req.responseXML.documentElement)
        error=ws_getFault(req.responseXML.documentElement);
      if (!error)
      {
        if (req.status !=200)
          error="El servidor devuelve error HTTP "+req.status+" "+req.statusText;
        else if (!req.responseXML || !req.responseXML.documentElement)
          error="El servidor no ha devuelto una respuesta XML";
      }
      if (!error)
      {
        firstBodyChild=ws_getFirstChildElement(ws_getChildElement(req.responseXML.documentElement,"Body"));
        if (!firstBodyChild)
          error="El servidor no ha devuelto un mensaje SOAP correcto";
      }
      if (error)
        throw new Error(error);
      return firstBodyChild;
    }
    

    Esta función hace lo siguiente:

    1. Primero verifica si la respuesta es XML y contiene una «SOAP Fault». Las SOAP Fault
      son el mecanismo estándar en SOAP para informar de errores, así que si la hay, ws_processResponseBody
      extrae el error de ahí mediante la función adicional ws_getFault, que luego
      crearemos.

      Por cierto, a la hora de crear servicios web, por favor utiliza para informar de errores este
      mecanismo de SOAP Faults, en vez de inventarte nuevos campos donde se informe del resultado.
      Eso mejorará la interoperabilidad de tu servicio con herramientas y toolkits.
    2. Si no hay SOAP Fault (o la respuesta no es XML), entonces ws_processResponseBody
      comprueba si el servidor ha devuelto un estado HTTP distinto de 200 (éxito), o si la respuesta
      no es XML, en cuyo caso da diferentes errores.
    3. Si sigue sin haber problemas, entonces ws_processResponseBody obtiene
      el «Body» del mensaje y saca su primer hijo. Si no lo hay, genera otro error, y si lo hay,
      eso es lo que se devuelve al llamante.
  5. Como vemos, hacen falta unas cuantas funciones más a añadir a ws.js. Empecemos por
    ws_getFault:

    function ws_getFault(env)
    {
      var fault=ws_getChildElement(ws_getChildElement(env,"Body"),"Fault");
      if (fault)
      {
        var eFaultCode=ws_getChildElement(fault,"faultcode");
        var eFaultString=ws_getChildElement(fault,"faultstring");
        var faultCode=eFaultCode?ws_getElementText(eFaultCode):"";
        var faultString=eFaultString?ws_getElementText(eFaultString):"";
        return "Error devuelto por el servidor: ("+faultCode+") "+faultString;
      }
    
      return null;
    }
    

    Esta función busca un hijo «Fault» en el «Body» y si lo encuentra compone un mensaje de error
    a partir de él.

  6. Y por fin ya sólo nos quedan esas funciones que tanto ws_getFault como ws_processResponseBody
    utilizan, como ws_getChildElement
    y ws_getElementText. Ésas son parte de mi típico toolkit de mejora del API del DOM XML,
    que es muy estándar y ubicuo pero bastante engorroso de usar. Así que vamos a añadirlas a ws.js:

    function ws_getLocalName(e)
    {
      // Moz: localName, IE: baseName
      return e.localName?e.localName:e.baseName;
    }
    
    function ws_getChildElement(e,nombre)
    {
      if (!e)
        return null;
    
      var child;
      for (child=e.firstChild;child;child=child.nextSibling)
      {
        if (child.nodeType==ELEMENT && ws_getLocalName(child)==nombre)
          return child;
      }
    
      return null;
    }
    
    function ws_getFirstChildElement(e)
    {
      if (!e)
        return null;
    
      var children=e.childNodes;
      var i;
      for (i=0;i<children.length;++i)
      {
        var child=children.item(i);
        if (child.nodeType==ELEMENT)
          return child;
      }
      return null;
    }
    
    function ws_getElementText(element)
    {
      if (!element)
        return null;
    
      var text="";
      var node;
      for (node=element.firstChild;node!=null;node=node.nextSibling)
      {
        if (node.nodeType==TEXT)
          text+=node.nodeValue;
      }
      return text;
    }
    

    Y también, al principio de ws.js, estas dos declaraciones de utilidad:

    var ELEMENT=1;
    var TEXT=3;
    

    Estas funciones permiten un acceso rápido y sencillo a los hijos de elementos de un DOM, o al
    menos de una forma más sencilla que con el DOM estándar. Funcionan tanto en Internet Explorer
    como en Firefox. Es importante notar que estas funciones ignoran el namespace de los
    elementos
    . Esto es bastante práctico pues en realidad los namespaces son un incordio; pero
    también son necesarios para no confundir elementos. No obstante, al menos en aplicaciones sencillas
    como estas es práctico hacer esta simplificación. En cualquier caso, costaría poco hacer versiones
    similares de estas funciones que tuviesen en cuenta el namespace.

Si conseguimos hacer todos esos cambios correctamente
(recuerda que también puedes
descargarte la aplicación final completa),
nos conectamos a la aplicación. Para que no moleste, te aconsejo que dejes de usar TCPMon
(ej. volver a usar el puerto 8081), o que le quites el retardo; más adelante ya veremos
cómo la asíncronía mejora la aplicación para servidores lentos.

Ahora, tras introducir
un nuevo código postal y pulsar Buscar sucursales
(recuerda refrescar la página para que recargue los cambios)
nuestra aplicación AJAX nos debería
mostrar un mensaje como este:

O sea, parecido a lo anterior, pero ahora se muestra el contenido de Body en vez de todo el mensaje.
Vamos a comprobar que la gestión de errores funciona. Para ello, vamos a introducir deliberadamente
errores en sucursales.html:

  • Si se envía un mensaje XML incorrecto:

    function buscaSucursales()
    {
      var smsg=
        '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'+
            '<SOAP-ENV:Body>'+
              '<bs:buscaSucursalesRequestMAL xmlns:bs="http://banquito.com/Sucursales/">'+
              '<parteCodPostal>'+ws_escapeXML(document.formSucu.cp.value)+'</parteCodPostal>'+
              '</bs:buscaSucursalesRequestMAL>'+
            '</SOAP-ENV:Body>'+
        '</SOAP-ENV:Envelope>';
    
    

    Nuestra aplicación nos tendría que mostrar el siguiente error:

    Que sale de una SOAP Fault que nos ha devuelto AXIS.

  • Si invocamos al URL incorrecto:

    function getURLServicio()
    {
      var url=
        document.location.protocol+"//"+document.location.host+
        primParte(document.location.pathname)+
          "/MAL/services/SucursalesSOAP";
      return url;
    }
    

    Que viene del error HTTP 404 que nos ha devuelto Tomcat.

Si tu aplicación se comporta así, enhorabuena (y si no, recuerda que también puedes
descargarte la aplicación final completa).

Mostrar un mensaje de estado

Por que se note más la diferencia entre el uso síncrono y el asíncrono, vamos a
mostrar un mensaje de estado mientras estamos esperando que el servidor responda.
Podríamos usar la línea de estado del navegador, pero al menos yo nunca la miro
y así no me enteraría de lo que sale ahí, así que vamos a usar un elemento
específico que ya viene en sucursales.html:.

  <p id=progreso></p>

Así que vamos a rellenarlo apropiadamente:

  1. Editamos sucursales.html y añadimos una nueva función:

    function setProgreso(msg)
    {
      document.getElementById('progreso').innerHTML=msg?msg:"";
    }
    
  2. Y la llamamos justo tras iniciar la petición:

    function buscaSucursales()
    {
      var smsg=
        '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'+
            '<SOAP-ENV:Body>'+
              '<bs:buscaSucursalesRequest xmlns:bs="http://banquito.com/Sucursales/">'+
              '<parteCodPostal>'+ws_escapeXML(document.formSucu.cp.value)+'</parteCodPostal>'+
              '</bs:buscaSucursalesRequest>'+
            '</SOAP-ENV:Body>'+
        '</SOAP-ENV:Envelope>';
    
      AsyncReq=ws_newXMLHttpRequest();
    
      var endpoint=getURLServicio();
      AsyncReq.onreadystatechange=asyncRecv;
      AsyncReq.open("POST",endpoint,true);
      AsyncReq.setRequestHeader("SOAPAction",'"http://banquito.com/Sucursales/buscaSucursales"');
      AsyncReq.send(smsg);
    
      setProgreso("Consultando al servidor, espere...");
    }
    
  3. Y cuando finaliza:

    function asyncRecv()
    {
      if (AsyncReq.readyState == 4)
      {
        var req=AsyncReq;
        AsyncReq=null;
        try
        {
          var sucursalesResponse=ws_processResponseBody(req);
          setProgreso(null);
          alert("Resultado:"+sucursalesResponse.xml);
        }
        catch (e)
        {
          var msg="Error invocando al servicio en "+getURLServicio()+":"+e;
          setProgreso(msg);
          alert(msg);
        }
      }
    }
    

Y ahora vamos a volver a poner TCPMon con el retraso, igual que antes.
Así podemos ver cómo se comporta la aplicación cuando el servidor es lento. Cuando pinchamos
en Buscar sucursales vemos que se muestra el mensaje de estado, y seguimos pudiendo interactuar
con la aplicación:

Y cuando por fin llega la respuesta, el mensaje de estado se limpia. De hecho, la aplicación es
tan interactiva que nos permite volver a pulsar Buscar sucursales, lo cual hace que se
pierda la petición original y se inicie una nueva (lo podemos ver en la traza del TCPMon –
no tiene por qué crearse una nueva conexión HTTP, sino que puede que se reutilice alguna otra).
No obstante este aparente paralelismo de peticiones, hay que ser consciente de que
XMLHttpRequest no permite manejar a la vez más de una petición.
O sea, es el mismo comportamiento que si en una página HTML normal le damos al botón de Submit una
y otra vez. El que esto sea correcto o no depende de la semántica de nuestra aplicación, aunque
en la mayoría de casos no será aconsejable que ocurra. Aunque gestionar esto no es tan
sencillo (la forma más adecuada sería en el servicio y usando identificadores únicos de mensaje
con sentido), aquí simplemente lo vamos a evitar en el propio sucursales.html, ignorando pulsaciones
sucesivas de Buscar sucursales hasta que la primera haya finalizado:

function buscaSucursales()
{
  if (AsyncReq)
    return;

  var smsg=

Como ya estamos poniendo AsyncReq=null al procesar la respuesta, con eso debería bastar.

Mostrando los resultados en la pantalla

Para concluir nuestra aplicación AJAX ya sólo nos queda mostrar los resultados devueltos por el servicio
en la pantalla; no con un feo alert sino en el HTML. Esto al final es puro
HTML dinámico y está más trillado que el AJAX, pero sin él no hay AJAX que valga así que vamos
a ello. Por cada sucursal devuelta debemos mostrar una fila en la tabla de Sucursales encontradas.
A mí personalmente no me gusta crear HTML desde código Javascrit (ni Java), así que prefiero
trabajar con plantillas: escribo el HTML con un editor de texto, y luego lo reutilizo
desde el código. Es por eso que la tabla de sucursales.html viene con una fila-plantilla:

<p>Sucursales encontradas:</p>

<table id=ListaSucursales border=1>
  <tr>
    <th>Código postal</th>
    <th>Dirección</th>
    <th>Cód. sucursal</th>
  </tr>
  <tr>
    <td align=right></td>
    <td></td>
    <td align=right></td>
  </tr>
</table>

La primera fila (de <TH>) es la cabecera, pero la siguiente es una plantilla
que indica cómo deben mostrarse en general todas las filas de la tabla. Con esto ya podemos
modificar sucursales.html:

  1. Lo primero que hay que hacer es copiarse esa plantilla en una variable para luego poder reproducirla:

    var PlantillaLinea;
    
    function init()
    {
      PlantillaLinea=document.getElementById("ListaSucursales").rows.item(1);
      PlantillaLinea.parentNode.removeChild(PlantillaLinea);
    }
    
    </script>
    
    </head>
    
    <body onload="init()">
    
    <h1>Banquito - búsqueda de sucursales</h1>
    

    Y además la quitamos de la tabla, pues no tiene sentido que siga ahí.

  2. Luego, desde asyncRecv tenemos que causar la carga esa tabla:

    function asyncRecv()
    {
      if (AsyncReq.readyState == 4)
      {
        var req=AsyncReq;
        AsyncReq=null;
        try
        {
          var sucursalesResponse=ws_processResponseBody(req);
          var msg=processSucursales(sucursalesResponse);
          setProgreso(msg);
        }
        catch (e)
        {
          var msg="Error invocando al servicio en "+getURLServicio()+":"+e;
          setProgreso(msg);
          alert(msg);
        }
      }
    }
    
  3. Por supuesto, lo siguiente es crear esa función processSucursales:

    function processSucursales(sucursalesResponse)
    {
      limpiaSucursales();
      var msg=null;
      var eSucursales=ws_getChildElement(sucursalesResponse,"sucursalesEncontradas");
      if (!eSucursales)
        throw new Error("La respuesta del servidor no contiene el elemento 'sucursalesEncontradas'");
      else
      {
        var eInfoSucursal,ns;
        for (eInfoSucursal=eSucursales.firstChild,ns=0;
             eInfoSucursal;
             eInfoSucursal=eInfoSucursal.nextSibling,++ns)
        {
          if (eInfoSucursal.nodeType==ELEMENT)
          { // Elemento; debería ser una sucursal
            var codSucursal=ws_getElementText(ws_getChildElement(eInfoSucursal,"codSucursal"));
            var direccion=ws_getElementText(ws_getChildElement(eInfoSucursal,"direccion"));
            var codPostal=ws_getElementText(ws_getChildElement(eInfoSucursal,"codPostal"));
            muestraSucursal(codSucursal,direccion,codPostal);
          }
        }
        msg=ns+" sucursales encontradas";
      }
      return msg;
    }
    
  4. Y también las otras dos funciones que sucursalesResponse utiliza:

    function limpiaSucursales()
    {
      var ls=document.getElementById("ListaSucursales");
      while (ls.rows.length>1)
        ls.deleteRow(1);
    }
    
    function muestraSucursal(codSucursal,direccion,codPostal)
    {
      var ls=document.getElementById("ListaSucursales");
      var row=PlantillaLinea.cloneNode(true);
      ls.tBodies.item(0).appendChild(row);
      row.cells.item(0).innerHTML=codPostal;
      row.cells.item(1).innerHTML=direccion;
      row.cells.item(2).innerHTML=codSucursal;
    }
    

Con todo esto, ya nuestra aplicación AJAX debería tener un bonito comportamiento:

Y por cierto, debería funcionar perfectamente también en Firefox:

¡Ya tenemos nuestra aplicación AJAX terminada! Si hay algo que no te funcione,
puedes descargarte el código completo de sucursales.html y
ws.js
.

Conclusiones

En este tutorial hemos visto cómo se puede hacer una aplicación AJAX que acceda a un
servicio web SOAP, desde cero, utilizando únicamente funciones Javascript estándar.
Posiblemente no te parezca tan complicado como habías oído. En este caso no lo es,
pero puedes imaginar que la cosa es peor cuando en vez de una única llamada a un servicio
tienes del orden de diez, relacionadas con otros tantos pedazos de la pantalla como
combos, campos de entrada, etc, cada uno de los cuales requiere su propio tratamiento.
Además, como a menudo cosas como los combos se comportan de forma muy parecida en todas
las pantallas, esto lleva de forma natural a la creación de controles de interfaz
de usuario reutilizables
, que desde hace varios años en el mundo .Net, y por fin
parece que ahora también en el mundo Java, se considera la forma más apropiada de crear
aplicaciones web interactivas.

Si te ha gustado esta aplicación AJAX y el servicio de búsqueda de sucursales, también
puedes leer este otro tutorial
en el cual se utiliza el producto AmberPoint Express
para monitorizar su actividad… con algún resultado inesperado, por cierto.


DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad