martes, 2 de diciembre de 2008

Error típico al leer ficheros en Java

Esta vez quiero centrarme en un apartado muy común en las aplicaciones: la lectura de ficheros. El caso es que hace poco me encontré con un trozo de código que tenía un pequeño bug difícil de detectar. De hecho, el código llevaba funcionando años con el bug y ni usuarios ni programadores lo habían notado. Como esto mismo ya me dio problemas hace algún tiempo en otra aplicación, quiero compartir mi experiencia con vosotros para que no os pase. Lo mismo incluso descubrís que habéis cometido el mismo error tras leer el post. :-p

El código es muy tonto, pero es extensible a otros casos más complejos. En general, se trata de leer de un InputStream o Reader y, tras un procesado o no, escribir lo leído en un OuputStream o Writer. Veamos el ejemplo en cuestión:

BufferedInputStream in = new BufferedInputStream(new FileInputStream(archivoOrigen));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(archivoDestino));
byte buffer[] = new byte[1024];
while(in.read(buffer,0,1024)!=-1){
   out.write(buffer);
}
in.close();
out.close();

Simple, ¿verdad? Leemos los bytes de un fichero a través del stream in, de 1024 en 1024 y lo vamos escribiendo en otro fichero a través del stream out. Vamos lo que viene siendo copiar un fichero.

¿Dónde está el error? Veamos lo que dice el javadoc de la función read: devuelve el número de bytes leídos o -1 si se ha alcanzado el final del fichero. Es decir, mientras haya datos los va a leer, los va a almacenar en buffer y nos va a devolver el número de bytes leídos. Como mucho, leerá 1024 cada vez, ya que lo hemos configurado así. Nosotros estamos ignorando el valor devuelto y de ahí nuestro error.

En este ejemplo tan tonto, se pueden dar dos errores distintos:

Error #1

Lo normal es que en cada llamada al metodo read se lean 1024 bytes, pero cuando llegamos al final del fichero, es decir, en la penúltima lectura, se leerán tan sólo los bytes restantes -recuerda que en la última lectura no se lee nada, se detecta fin de fichero, se devuelve -1 y se termina el bucle while-.

Por ejemplo, supongamos que al final quedan 534 bytes por leer. Vemos el código paso a paso:

  • while(in.read(buffer,0,1024)!=-1){
La llamada a la función read almacenará los 534 bytes al principio del buffer, pero el resto del buffer quedará con los bytes de la lectura anterior. La función read devolverá el número de bytes leídos, 534, y como es distinto de -1, sigue en el bucle.

  • out.write(buffer);
Escribimos en el flujo de salida out todo el buffer, es decir, los 534 bytes de esta lectura más los 490 de la lectura anterior.

Esto provoca que el fichero copiado no sea exactamente igual que el leído, ya que el copiado tiene unos bytes de más, concretamete 1024 menos el resto de dividir el tamaño del fichero por 1024. Dicho de otra forma, el fichero generado siempre va a tener un tamaño multiplo de 1024, sea cual sea el tamaño del leído.

El error en este ejemplo sería fácil de detectar ya que se trata de copiar un fichero. Si miramos los tamaños de los ficheros y no son iguales es que falla. Sin embargo, si hacemos un procesado de los bytes, por ejemplo el redimensionamiento de una imagen, detectar el error es más complicado. Por un lado, los tamaños de los ficheros origen y destino no tienen por qué coincidir, y por otro, puesto que los bytes se añaden al final y en general serán pocos comparados con el resto, tan sólo van a suponer un cierto número de píxeles de diferencia, cosa que puede no apreciarse en la imagen o achacarse a efectos del procesado.

Error #2

Otro error que puede ocurrir es cuando en el flujo de entrada in es lento. Por ejemplo, supongamos que estuvieramos leyendo a través de internet, mediante http, un fichero. Podría ocurrir que en una iteración el flujo in no tuviera datos que leer. En este caso, no almacenaría nada en el buffer y devolvería 0 como resultado. Sin embargo, nosotros sí que escribiríamos el buffer, que contiene los datos de la lectura anterior, en el flujo out, repitiendo de esta forma el conjunto de bytes en el fichero de destino. Es decir, estaríamos escribiendo dos o más veces los mismos bytes.

Esto me ocurrió en un pequeño programita que leía los bytes de una página web y los enviaba por correo electrónico. Al repetirse ciertas secuencias de bytes, el html leído no era para nada correcto: a veces había dos etiquetas <html><head>.... , otras veces había dos <body>, y lo peor es que era aleatorio. Unas veces ocurría una cosa, otras veces otra distinta y a veces leía bien. Por supuesto, se debía a que a veces la conexión era lenta y se quedaba atascado en un sitio, otras veces en otro y otras iba bien. Cuando se quedaba atascado, el bucle iba repitiendo el último trozo leído. Un verdadero quebradero de cabeza, os lo aseguro.

Solución

La solución a esto es bien sencilla. Simplemente basta con almacenar el número de bytes leídos y escribir solamente este número usando la función write con 3 parámetros en lugar de 1:

BufferedInputStream in = new BufferedInputStream(new FileInputStream(archivoOrigen));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(archivoDestino));
byte buffer[] = new byte[1024];
int leidos;
while((leidos=in.read(buffer,0,1024))!=-1){
   out.write(buffer,0,leidos);
}
in.close();
out.close();

Por último, alguno pensará que por qué no se leen y escriben todos los bytes del tirón en lugar de hacerlo de 1024 en 1024. La respuesta es que se debe a cuestiones de rendimiento: se hace para evitar tener alamacenado en memoria todo el fichero. De esta forma, aunque leas un fichero de cientos de megas únicamente tendrás a la vez en memoria 1024 bytes del fichero. Por supuesto, el tamaño del buffer cada uno lo puede ajustar a sus necesidades. Si hay que procesar el fichero, puede que no haya más remedio que mantenerlo todo en memoria. La cuestión es evitarlo en la medida de lo posible.

Espero que os haya servido.

domingo, 23 de noviembre de 2008

Java y XML: XOM

Este es el último post de la serie de artículos sobre Java y XML. Esta vez le toca el turno a XOM.

XOM es, al igual que JDOM, una API desarrollada específicamente para Java que da soporte al tratamiento de XML: parseo, búsquedas, modificación, generación y serialización. Los principios básicos de XOM son corrección, simplicidad y rendimiento.

Corrección en cuanto al modelado XML, ya que XOM se ajusta al modelo XML al 100%. Simplicidad porque cada método hace exactamente lo que se supone que debe hacer en base a su nombre, y nada más. Rendimiento porque es rápido y ligero en memoria. Un document XOM puede llegar a ocupar 3 veces menos en memoria que un document JDOM o DOM.

Todo esto lo comentan detalladamente en sus principios de diseño. Es interesante que le eches un vistazo a estos principios, ya que al usar la API puede haber ciertos comportamientos un poco chocantes al principio, sobre todo si has usado antes JDOM, y es aquí donde explican las razones de los mismos.

El modelo objetos de XOM es muy similar al de JDOM. Todos son clases y la clase principal es Node. De node heradan el resto de tipos de nodos: Element, Document, Attribute, Comment, Namespace, Text. En XOM Text incluye tanto a texto simple como secciones CDATA. Aquí no te tienes que preocupar si algo va en CDATA o no. XOM lo añade cuando es necesario. Puedes saber más acerca del API de XOM en su javadoc.

Para incluir XOM en tu proyecto únicamente tienes que descargarte el jar de la última versión e incluirlo en tu proyecto. Así de fácil.

Pasemos ya a lo interesante. Para ilustrar ejemplos de uso de XOM simplemente he adaptado el ejemplo de JDOM a XOM. Como siempre partiremos del siguiente XML:

<?xml version="1.0" encoding="UTF-8"?>
<etiquetaPrincipal xmlns:xela="http://www.latascadexela.es" >
   <etiquetaHija id="1" atributo1="valorAtributo1" atributo2="valorAtributo2">
      Texto dentro de la etiqueta hija
   </etiquetaHija>
   <!-- Comentario -->
   <xela:etiquetaConNamespace descripcion="etiqueta con un namespace"/>   
</etiquetaPrincipal>

Parseando un XML

Al igual que JDOM, XOM también usa SAX internamente para parsear los XML. Como vemos SAX es usado ampliamente por las APIs para el parseo de XML. ¿Por qué será? ;-)

// Creamos el builder XOM
Builder builder = new Builder();
// Construimos el arbol DOM a partir del fichero xml
Document doc = builder.build(new FileInputStream("/ruta_a_fichero/fichero.xml"));

Recorriendo el árbol de nodos

Una vez obtenido el objeto Document ya podemos recorrer el árbol de nodos como gustemos. Una de las particularidades de XOM es que al obtener los hijos de un elemento no devuelve un List, sino una implementación propia de elementos(Elements) o nodos(Nodes). Siguiendo el mismo ejemplo que en JDOM:

// Obtenemos la etiqueta raíz
Element raiz = doc.getRootElement();
// Recorremos los hijos de la etiqueta raíz
Elements hijosRaiz = raiz.getChildElements();
for(int i=0;i<hijosRaiz.size();i++){
   Element hijo = hijosRaiz.get(i);
   
   // Obtenemos el nombre y su contenido de tipo texto
   String nombre = hijo.getLocalName();
   String texto = hijo.getValue();
   
   System.out.println("\nEtiqueta: "+nombre+". Texto: "+texto);
   
   // Obtenemos el atributo id si lo hubiera
   String id = hijo.getAttributeValue("id");
   if(id!=null){
      System.out.println("\tId: "+id);
   }
}

También podemos obtener una etiqueta en concreto de entre las etiquetas hijas de otra.

// Obtenemos una etiqueta hija del raiz por nombre
Element etiquetaHija = raiz.getFirstChildElement("etiquetaHija");
System.out.println(etiquetaHija.getLocalName());
// Podemos obtener directamente el texto de la etiqueta hija
String texto = etiquetaHija.getValue();
System.out.println(texto);

y con namespaces

// Obtenemos una etiqueta hija del raiz por nombre con Namespaces
Element etiquetaNamespace = raiz.getFirstChildElement("etiquetaConNamespace", "http://www.latascadexela.es");
System.out.println(etiquetaNamespace.getLocalName());

Buscando con XPath

También podemos usar XPath con XOM para buscar etiquetas dentro del XML. Si quieres saber más acerca de XPath, te remito al post de DOM.

Desde el punto de vista del código hacer búsquedas XPath con XOM es mucho más sencillo que con JDOM ya que el propio objeto Node, y por ende todas sus clases hijas, ya dispone de un método para ello: query.

// Buscamos una etiqueta mediante XPath
Nodes resultadosXP = doc.query("/etiquetaPrincipal/etiquetaHija");
Element etiquetaHijaXP = (Element)resultadosXP.get(0);
System.out.println(etiquetaHijaXP.getLocalName());

y con namespaces

// Buscamos una etiqueta con namespace mediante XPath
// Primero definimos los namespaces que vamos a usar
XPathContext context = new XPathContext("xela","http://www.latascadexela.es");
// Hacemos la búsqueda pasando el XPathContext
Nodes resultadosNamespaceXP = raiz.query("/etiquetaPrincipal/xela:etiquetaConNamespace",context);
Element etiquetaNamespaceXP = (Element)resultadosNamespaceXP.get(0);
System.out.println(etiquetaNamespaceXP.getLocalName());

Modificando el XML

Podemos modificar el árbol de nodos del XML parseado. Añadamos una nueva etiqueta.

// Creamos una nueva etiqueta
Element etiquetaNueva = new Element("etiquetaNueva");
// Añadimos un atributo
etiquetaNueva.addAttribute(new Attribute("atributoNuevo", "Es un nuevo atributo"));
// Añadimos contenido
etiquetaNueva.appendChild("Contenido dentro de la nueva etiqueta");
// La añadimos como hija a una etiqueta ya existente
etiquetaHija.appendChild(etiquetaNueva);

Creando XML desde cero

Con XOM es fácil crear un XML desde cero. Para crear cada objeto del árbol de nodos, tan solo tenemos que usar el operador new al igual que ocurría en JDOM.

// Vamos a crear un XML desde cero
// Vamos a generar la etiqueta raiz
Element eRaiz = new Element("raiz");
// y la asociamos al document
Document docNuevo = new Document(eRaiz);

Copiando nodos de un árbol a otro

Con XOM es muy fácil pasar elementos de un árbol a otro. El concepto es el mismo que en JDOM. Todos los objetos son susceptibles de ser añadido a cualquier árbol. Lo único que hay que tener en cuenta es que cada objeto sólo puede tener un padre y por tanto si lo que queremos es moverlo de un sitio a otro, habrá que desasociarlo antes del padre. Para ello, la clase Node proporciona el método detach. Es decir, si hacemos detach en un Element, lo que habremos hecho es dejar huérfano ese Element, es decir, ya no tiene padre, por lo que se puede añadir como hijo a cualquier otro nodo. A diferencia de JDOM, en XOM no se puede hacer detach del elemento raíz.

Si lo que queremos es copiar nodos, usaremos el método copy para generar una copia del mismo y esta copia la podremos añadir a cualquier nodo ya que se genera huérfana.

// Vamos a copiar la etiquetaHija del primer document a este
// Lo primero es crear una copia de etiquetaHija
Element copiaEtiquetaHija = (Element)etiquetaHija.copy();
// Después la colocamos como hija de la etiqueta raiz
eRaiz.appendChild(copiaEtiquetaHija);

// Vamos a mover la etiquetaConNamespace a este document
// Primero la desasociamos de su actual padre
etiquetaNamespace.detach();
// Una vez que ya es huerfana la podemos colocar donde queramos
// Por ejemplo, bajo la etiqueta raiz
eRaiz.appendChild(etiquetaNamespace);

Una vez ejecutado el ejemplo y si obtenemos la representación en forma de String del XML obtendremos el siguiente resultado:

XML parseado:
<?xml version="1.0"?>
<etiquetaPrincipal xmlns:xela="http://www.latascadexela.es">
   <etiquetaHija id="1" atributo1="valorAtributo1" atributo2="valorAtributo2">
      Texto dentro de la etiqueta hija
   <etiquetaNueva atributoNuevo="Es un nuevo atributo">Contenido dentro de la nueva etiqueta</etiquetaNueva>
   </etiquetaHija>
   <!-- Comentario -->
      
</etiquetaPrincipal>

XML nuevo:
<?xml version="1.0"?>
<raiz><etiquetaHija id="1" atributo1="valorAtributo1" atributo2="valorAtributo2">
      Texto dentro de la etiqueta hija
   <etiquetaNueva atributoNuevo="Es un nuevo atributo">Contenido dentro de la nueva etiqueta</etiquetaNueva></etiquetaHija>
   <xela:etiquetaConNamespace xmlns:xela="http://www.latascadexela.es" descripcion="etiqueta con un namespace" />
</raiz>

Como podemos observar la etiquetaConNamespace se ha eliminado del XML original ya que usamos el método detach para llevarla de un árbol a otro. Esto no ocurre con etiquetaHija ya que previamente fue copiada.

Serializando el XML

La serialización (paso de árbol de nodos a String o bytes) en XOM es muy sencilla. La clase Node proporciona un método para el paso de cualquier elemento y todos sus hijos a String. Es el método toXML.

// Vamos a serializar el XML a String
String docStr = doc.toXML();
String docNuevoStr = docNuevo.toXML();

System.out.println("XML parseado:\n"+docStr);
System.out.println("XML nuevo:\n"+docNuevoStr);

Si lo que queremos es serializar a bytes, XOM proporciona la clase Serializer, que escribe cualquier elemento del árbol en un OutputStream.

// Si lo que quieres es serializar a bytes,
// XOM ofrece el objeto Serializer
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Serializer serializer = new Serializer(baos);
serializer.write(doc);

byte[] bytes = baos.toByteArray();
System.out.println("Bytes: "+bytes);

A continuación dejo el código completo de la clase que he creado como ejemplo:

package es.latascadexela.xml.xom;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import nu.xom.Attribute;
import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Elements;
import nu.xom.Nodes;
import nu.xom.ParsingException;
import nu.xom.Serializer;
import nu.xom.ValidityException;
import nu.xom.XPathContext;


/**
* Clase de ejemplo de procesado de XML mediante XOM.
*
* @author Xela
*
*/
public class ProcesaXML {

   public static void main(String[] args) {

      try {   
         // Creamos el builder XOM
         Builder builder = new Builder();
         // Construimos el arbol DOM a partir del fichero xml
         Document doc = builder.build(new FileInputStream("/ruta_a_fichero/fichero.xml"));

         // Obtenemos la etiqueta raíz
         Element raiz = doc.getRootElement();
         // Recorremos los hijos de la etiqueta raíz
         Elements hijosRaiz = raiz.getChildElements();
         for(int i=0;i<hijosRaiz.size();i++){
            Element hijo = hijosRaiz.get(i);
            
            // Obtenemos el nombre y su contenido de tipo texto
            String nombre = hijo.getLocalName();
            String texto = hijo.getValue();
            
            System.out.println("\nEtiqueta: "+nombre+". Texto: "+texto);
            
            // Obtenemos el atributo id si lo hubiera
            String id = hijo.getAttributeValue("id");
            if(id!=null){
               System.out.println("\tId: "+id);
            }
         }
         
         // Obtenemos una etiqueta hija del raiz por nombre
         Element etiquetaHija = raiz.getFirstChildElement("etiquetaHija");
         System.out.println(etiquetaHija.getLocalName());
         // Podemos obtener directamente el texto de la etiqueta hija
         String texto = etiquetaHija.getValue();
         System.out.println(texto);
                  
         // Obtenemos una etiqueta hija del raiz por nombre con Namespaces
         Element etiquetaNamespace = raiz.getFirstChildElement("etiquetaConNamespace", "http://www.latascadexela.es");
         System.out.println(etiquetaNamespace.getLocalName());
         
         // Buscamos una etiqueta mediante XPath
         Nodes resultadosXP = doc.query("/etiquetaPrincipal/etiquetaHija");
         Element etiquetaHijaXP = (Element)resultadosXP.get(0);
         System.out.println(etiquetaHijaXP.getLocalName());
         
         // Buscamos una etiqueta con namespace mediante XPath
         // Primero definimos los namespaces que vamos a usar
         XPathContext context = new XPathContext("xela","http://www.latascadexela.es");
         // Hacemos la búsqueda pasando el XPathContext
         Nodes resultadosNamespaceXP = raiz.query("/etiquetaPrincipal/xela:etiquetaConNamespace",context);
         Element etiquetaNamespaceXP = (Element)resultadosNamespaceXP.get(0);
         System.out.println(etiquetaNamespaceXP.getLocalName());
         
         // Creamos una nueva etiqueta
         Element etiquetaNueva = new Element("etiquetaNueva");
         // Añadimos un atributo
         etiquetaNueva.addAttribute(new Attribute("atributoNuevo", "Es un nuevo atributo"));
         // Añadimos contenido
         etiquetaNueva.appendChild("Contenido dentro de la nueva etiqueta");
         // La añadimos como hija a una etiqueta ya existente
         etiquetaHija.appendChild(etiquetaNueva);
         
         // Vamos a crear un XML desde cero
         // Vamos a generar la etiqueta raiz
         Element eRaiz = new Element("raiz");
         // y la asociamos al document
         Document docNuevo = new Document(eRaiz);
         
         // Vamos a copiar la etiquetaHija del primer document a este
         // Lo primero es crear una copia de etiquetaHija
         Element copiaEtiquetaHija = (Element)etiquetaHija.copy();
         // Después la colocamos como hija de la etiqueta raiz
         eRaiz.appendChild(copiaEtiquetaHija);
         
         // Vamos a mover la etiquetaConNamespace a este document
         // Primero la desasociamos de su actual padre
         etiquetaNamespace.detach();
         // Una vez que ya es huerfana la podemos colocar donde queramos
         // Por ejemplo, bajo la etiqueta raiz
         eRaiz.appendChild(etiquetaNamespace);
         
         
         // Vamos a serializar el XML a String
         String docStr = doc.toXML();
         String docNuevoStr = docNuevo.toXML();
         
         System.out.println("XML parseado:\n"+docStr);
         System.out.println("XML nuevo:\n"+docNuevoStr);
         
         // Si lo que quieres es serializar a bytes,
         // XOM ofrece el objeto Serializer
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         Serializer serializer = new Serializer(baos);
         serializer.write(doc);
         
         byte[] bytes = baos.toByteArray();
         System.out.println("Bytes: "+bytes);
         
      } catch (FileNotFoundException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      } catch (ValidityException e) {
         e.printStackTrace();
      } catch (ParsingException e) {
         e.printStackTrace();
      }

   }

}

Con esto hemos visto lo más importante en cuanto al uso de XOM. Como ya he dicho ha sido simplemente pasar el ejemplo de JDOM a XOM. Puedes comparar las dos clases de ejemplo y observar las diferencias. Ya verás que son muy similares, aunque cada una tiene sus peculiaridades.

Te comento lo mismo que hice en el post de JDOM. Si comparas estos ejemplos con DOM podrás hacerte una idea de lo fácil e intuitivo que es usar XOM frente a DOM. Si aún usas SAX o DOM en tus proyectos, te animo a adentrarte en el mundo de XOM. Y si no ves ninguna ventaja, pues nada, sigue con lo que estabas. Desde mi punto de vista las ventajas son muchas y los inconvenientes muy pocos.

En cuanto a si usar JDOM o XOM, yo me decantaría por XOM por sus principios básicos: corrección, simplicidad y rendimiento. En cualquier caso si te decantas por el uso de JDOM, tampoco lo considero tan mala opción. Una cosa es lo que digan los de XOM en su página oficial y otra cosa lo que realmente sea. Habría que hacer pruebas de rendimiento entre ambas para comprobar cuál es realmente mejor.

Aquí termina la serie de post dedicados a Java y XML. Con estos artículos he intentado dar una visión de las distintas posibilidades que conozco en cuanto al tratamiento de XML con Java. La idea era ayudar a aquellos que se inician en el mundo del XML por un lado, y por otro, a aquellos que andan anclados en SAX y DOM, mostrales que hay algo más allá de estas dos APIs. Espero haber logrado mi objetivo.

miércoles, 8 de octubre de 2008

Ajustar el timezone de Java en Linux


Si en alguna de tus aplicaciones te ha ocurrido que la api de Java (Date o GregorianCalendar) no te da la hora correctamente, aún siendo correcta la del sistema operativo, probablemente se deba a que Java no está interpretando la zona horaria correctamente.

Me sucedió en mi Debian, y lo primero que se me ocurrió fue ajustar el timezone del sistema operativo con el comando tzconfig. Efectivamente, la zona horaria no estaba configurada correctamente, pero aún corrigiendo esto la hora de Java seguía sin ser la correcta.

En concreto mi zona horaria era GTM+2, correspondiente a España en verano, mientras que la JVM me daba GTM+1. En ese momento lo consideré una chorrada. Además el cambio horario estaba cerca y el problema desaparecería al pasar al horario de invierno (GTM+1) :-p. Ha sido hoy cuando la chorrada ha pasado a ser un problema y por ello no he tenido más remedio que buscar la solución.

La solución es bien sencilla

Probablemente ya la sepas, pero soy de los que piensa que más vale encontrar algo útil un millón de veces a no encontrarlo nunca.

Hay tres soluciones posibles:
  1. Ajustar la variable de entorno TZ.

    bash$ export TZ="Europe/Madrid"

  2. Incluir el parámetro -Duser.timezone en la ejecución del comando java.

    bash$ java -Duser.timezone=Europe/Madrid ...."

  3. Ajustar el fichero /etc/localtime para que apunte a la zona horaria correcta. Todas las zonas horarias disponibles se encuentran en /usr/share/zoneinfo.

    bash$ ln -s /usr/share/zoneinfo/Europe/Madrid /etc/localtime


Puedes tomar la solución que veas más conveniente, pero si no eres el administrador de la máquina, probablemente la tercera solución te sea imposible.

Espero que te sea útil.

martes, 30 de septiembre de 2008

Evitar caché de los navegadores al actualizar una aplicación web

Los navegadores web suelen cachear algunos ficheros que se importan desde el html (imágenes, css , js, ...). De una forma simplista, se puede decir que cuando un usuario entra por primera vez en una página web el navegador lo descarga todo, incluido imágenes, css, js, ... En las sucesivas visitas a la página únicamente descarga el html y usa los css, js e imágenes que previamente había cacheado.

El resultado es una carga más rápida de la página y un problema para el desarrollador web a la hora de actualizar la página. Si alguna vez te has preguntado cómo "limpiar" la caché de los navegadores de los usuarios visitantes, este es tu post.


Planteamiento del problema

Cuando se actualiza una aplicación web, de cara al usuario final pueden existir cambios tanto en el html de la página como en los ficheros que la complementan, típicamente css, js e imágenes. Puesto que los navegadores suelen cachear este tipo de ficheros, puede ocurrir lo siguiente:

El usuario Pepito navega tranquilamente por la web A. Su navegador cachea los css, js e imágenes. Poco después, los desarrolladores de la web A deciden realizar una actualización que implica tanto cambios en el html final como cambios en los css, js e imágenes. Pepito vuelve a entrar en la web A y su navegador descarga únicamente el html, tomando el resto de ficheros de su cache. Por lo tanto, no actualiza los css, js ni imágenes. Pepito estaría viendo el html nuevo junto con los css, js e imágenes antiguos. Probablemente esta mezcla de versiones haría que Pepito no visualizara correctamente la web.


Puedes pensar que es un caso bastante extraño. Sin embargo, ¿qué ocurre si la web tiene un sistema de cacheo por delante? Por ejemplo, un squid. Dependiendo de la aplicación web, esto puede ser desde una simple anécdota a un problema bastante grande.

En el caso del squid y siempre que lo administremos nosotros, siempre podemos limpiar la cache del mismo. ¿Pero qué ocurre con la caché de los navegadores de todos los ordenadores del mundo mundial? Podíamos pensar en usar javascript para eliminar la cache de cada usuario que entre en la página (no sé si se puede hacer :-p), pero la solución es mucho más simple que eso.

Solución al problema

La solución que propongo es cambiar la url con la que se piden los css, js e imágenes de una actualización a otra. Ni siquiera es necesario cambiar el nombre de los ficheros. Basta con añadir un parámetro a la url que cambie con cada actualización.

Por ejemplo, en lugar de pedir los ficheros así:

<link type="text/css" rel="stylesheet" href="/css/mi_css.css"/>
<script type="text/javascript" src="/js/mi_javascript.js"/>
<img src="/img/mi_imagen.gif"/>

habría que solicitarlos de la siguiente manera:

<link type="text/css" rel="stylesheet" href="/css/mi_css.css?1.0.0"/>
<script type="text/javascript" src="/js/mi_javascript.js?1.0.0"/>
<img src="/img/mi_imagen.gif?1.0.0"/>

y tras una actualización, así:

<link type="text/css" rel="stylesheet" href="/css/mi_css.css?1.1.0"/>
<script type="text/javascript" src="/js/mi_javascript.js?1.1.0"/>
<img src="/img/mi_imagen.gif?1.1.0"/>

Como podrás observar, simplemente he añadido un parámetro a las urls. Puedes poner cualquier cosa siempre y cuando cambie entre una actualización y otra. En mi caso se trata de la versión de la aplicación. En el primer caso era la versión 1.0.0 y tras actualizar la aplicación web a la versión 1.1.0, el html quedaría como el último ejemplo.

Con Maven mucho más fácil

Si usas Maven esta solución no te llevará más de unos minutos. Simplemente tienes que añadir en tus jsp's, plantillas freemarker, hojas de estilo xslt o donde sea que generes el código html, la variable pom.version y filtrar los ficheros mediante maven.

Por ejemplo, supongamos que usamos freemarker. Colocaríamos lo siguiente en nuestros ficheros:

<link type="text/css" rel="stylesheet" href="/css/mi_css.css?${pom.version}"/>
<script type="text/javascript" src="/js/mi_javascript.js?${pom.version}"/>
<img src="/img/mi_imagen.gif?${pom.version}"/>


Cada vez que generemos el war de nuestra aplicación, Maven sustituirá ${pom.version} por la versión de nuestra aplicación.

Filtrar los ficheros con Maven

Si usas Maven, lo normal es que tengas tu aplicación bajo el directorio src/main/webapp. Puedes filtrar toda esa carpeta si así lo deseas, pero no es aconsejable ya que Maven incluso aplica el filtro a las imágenes, y créeme, aplicar un filtro de texto a un fichero binario normalmente no es lo que se desea ¿verdad, Fran? ;-)

Por tanto, te recomiendo que muevas los ficheros que desees filtrar a otra carpeta. Por ejemplo, src/main/webresources. Si te has perdido y no tienes claro qué es lo que "quieres" filtrar, te recuerdo que debes filtrar todos los ficheros donde hayas puesto ${pom.version}, ya sean .ftl, .xsl, .css, .jsp, ....

Después añade la etiqueta webResources y todo su contenido en el maven-war-plugin de tu pom.xml:

[...]

<groupId>es.latascadexela</groupId>
<artifactId>blog</artifactId>
<version>1.0.0</version>

[...]

<build>

[...]

   <plugins>
      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-war-plugin</artifactId>
         <configuration>
            <webResources>
               <resource>
                  <filtering>true</filtering>
                  <directory>src/main/webresources</directory>
               </resource>
            </webResources>
         </configuration>
      </plugin>

[...]

Si quieres filtrar algún fichero situado en src/main/resources, tan sólo tendrás que poner el filtering a true en la etiqueta resources de tu pom.xml

[...]

<groupId>es.latascadexela</groupId>
<artifactId>blog</artifactId>
<version>1.0.0</version>

[...]

<build>
   <resources>
      <resource>
         <filtering>true</filtering>
         <directory>src/main/resources</directory>
      </resource>
   </resources>

[...]

Cuando se genere una nueva versión de la aplicación, en todas las urls aparecerá la versión de la misma. Al actualizar la aplicación web, los navegadores tendrán cacheados los ficheros con la url que conteniene la versión anterior. Al tratarse de una url nueva, ya que habrás subido la versión y por tanto es distinta, se descargarán los nuevos ficheros css, js e imágenes, obtienendo un html nuevo y sus correspondientes nuevas versiones de los ficheros css, js, e imágenes.

¡¡¡Problema resuelto!!!

sábado, 13 de septiembre de 2008

Configurar Eclipse para trabajar con Maven

Trabajo desarrollando aplicaciones web en Java y uso Eclipse para ello. Afortunadamente usamos Maven 2 en nuestros proyectos. En este post expongo la versión de Eclipse que uso y los plugins que tengo instalado para facilitarme el trabajo.

Llevo usando Eclipse desde la versión 2.x (x porque no me acuerdo exactamente cual era) y muchos han sido los cambios desde aquella versión hasta la actual: 3.4, también conocida como Ganymede. De entre todas las versiones que he probado, Ganymede es sin duda la mejor, más estable y más rápida. Esto puede parecer una perogrullada, pero te aseguro que no siempre la versión más nueva es mejor que las anteriores. Al menos no tiene por qué ser más estable.

En Eclipse dentro de una misma versión te puedes descargar distintas configuraciones: Eclipse for Java Developers, Eclipse for Java EE Developers, Eclipse Classic, ... La estabilidad de Eclipse varía mucho de una versión a otra. Me ha pasado varias veces que me he descargado la de Java o Java EE e iban muy mal y otra configuración, la Classic, iba perfectamente. Esto ocurre también en Ganymede, así que te recomiendo que te descargues e instales la versión Eclipse Classic. Lo puedes hacer en http://www.eclipse.org/downloads/.



Si trabajas con Maven, existe un plugin para Eclipse: m2eclipse. La versión estable de este plugin es paradójicamente más inestable que la de desarrollo. Al menos mi experiencia ha sido así. Por tanto, te recomiendo que te instales la versión de desarrollo de este plugin (0.9.6). Para ello basta con añadir en la lista de repositorios de Eclipse el siguiente repositorio: http://m2eclipse.sonatype.org/update-dev/ . Observa que acaba en update-dev en lugar de update. Si no sabes como se hace pulsa en Help -> Software Updates... -> Add Site... y pega la url en la ventana que se muestra.

Al ser versión de desarrollo, también tenéis que actualizar el repositorio denominado Ganymede. Borra el que ya hay y añade el este http://download.eclipse.org/releases/ganymede/staging/. Edición: Esto ya no es necesario. Ya lo han pasado a la rama principal.

Aparte de ser más estable, la versión de desarrollo incorpora el POM Editor. Con esta funcionalidad se puede editar el XML del POM (versiones, dependencias, plugins, ...) como si fuera un simple formulario. Además, te informa de todas las dependencias de tu proyectos, tanto directas como indirectas. Para poner la guinda al pastel, comentar que también es capaz de general el árbol de dependencias del proyecto. Se trata de un gráfico en el que cada artefacto maven se representa como un nodo y cada uno enlaza a los artefactos de los que depende. Dejaré unas capturas de ejemplo en cuanto pueda. La siguiente imagen es un ejemplo del dependency graph de un proyecto en el que trabajo.



Otra funcionalidad que incluye es la de Update dependencies. En versiones anteriores había que recurrir a trucos como editar el POM o deshabilitar y volver a habilitar el Gestor de Dependencias de Maven (Disable/Enable Dependency Management). Si has trabajado con Maven y Eclipse sabrás de lo que hablo. Ahora se soluciona fácilmente con esa opción.

También me gustaría comentar los otros plugins que tengo:
  • Subclipse (http://subclipse.tigris.org/update_1.4.x). Para integración con subversion. Imprescindible para proyectos en los que trabaje más de una persona. :-p
  • Freemarker (http://www.freemarker.org/eclipse/update)(http://sourceforge.net/projects/freemarker-ide/). Lo usamos como motor de plantillas.
  • SpringIDE (http://springide.org/updatesite/). Muy util si usas Spring, ya que te ayuda en la creación y configuración de ficheros XML.
  • WebTools (http://download.eclipse.org/webtools/updates/). Imprescindible para el desarrollo web.
  • Sysdeo Eclipse Tomcat Launcher plugin. Para poder arrancar un Tomcat local desde Eclipse y depurar en él. La instalación de este plugin ha de hacerse manualmente. Es decir, descomprimiendo el fichero que te bajes en la carpeta plugins de Eclipse.

Como digo, la versión Classic de Eclipse Ganymede con estos plugins me funciona bastante bien. Es rápida, estable y con muchas funcionalidades muy útiles en mi día a día. Esto no quiere decir que no falle de vez en cuando, pero sí que falla bastante menos que las anteriores versiones. Si usas versiones antiguas de Eclipse, merece la pena y mucho actualizarse a Ganymede.

jueves, 4 de septiembre de 2008

Depuración remota en Tomcat con Eclipse

La depuración es muy importante en los desarrollos software. Según recuerdo, había una teoría que decía que el 50% del tiempo de desarrollo se dedica en tareas de depuración.

Si trabajas desarrollando aplicaciones web en Java y usas Tomcat como servidor de aplicaciones, este post puede ser bastante útil para tí.

Uso Eclipse para desarrollar mis aplicaciones. Normalmente aplicaciones webs. Para desarrollar y depurar suelo tener un tomcat en mi equipo (local) y mediante el plugin para Eclipse de Sysdeo puedo arrancar el tomcat desde Eclipse y depurar el código.

Sin embargo, hay ocasiones en las que por circustancias de la vida no tienes más remedio que depurar tu aplicación corriendo en algún tomcat remoto (instalado en otro equipo). En este caso tienes dos opciones:
  1. Llenar tu aplicación de mensajes de log que saquen información suficiente para poder detectar los fallos.
  2. Seguir los pasos que expongo a continuación para depurar desde Eclipse exactamente igual que si el tomcat estuviese corriendo en local.

1. Configurar Tomcat para permitir depuración remota

Para ello tan sólo basta con añadir las siguientes opciones en la variable JAVA_OPTS de tomcat. Esta variable se define en el fichero TOMCAT_HOME/bin/catalina.sh. También se puede definier en el script de inicio/parada de tomcat /etc/init.d/tomcat.

JAVA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n"

El puerto puedes poner el que quieras. Por defecto suele ser el 8000.

Por supuesto, después de cambiar la configuración hay que reiniciar tomcat.

Una vez hecho esto, tomcat tendrá abierto el puerto 8000 para que las aplicaciones externas (como Eclipse) se puedan conectar a él e intercambiar información para poder depurar el código de forma remota.

2. Depurar de manera remota con Eclipse

Abrimos Eclipse y nos vamos a Run -> Open Debug Dialog...

Hacemos click derecho en Remote Java Application y seleccionamos New... Aparecerá una ventana como la siguiente.

Debug DialogAquí tan sólo tenemos que rellenar lo siguiente:
  • Name: es el nombre de la configuración. Puedes poner lo que quieras
  • Project: pincha en el botón Browser y selecciona el proyecto que deseas depurar. Por supuesto, esa misma aplicación tiene que estar corriendo en el tomcat remoto. De aquí únicamente se tomarán los fuentes.
  • Host: ip o nombre del servidor donde está corriendo el tomcat.
  • Port: puerto que configuraste en el paso anterior.

Una vez hecho esto pulsamos en Apply y Debug. Acto seguido Eclipse conectará con el tomcat y ya podemos poner nuestro breakpoint.


Para posteriores veces podemos conectar Eclipse con nuestro tomcat tan sólo pulsando en el botón de debug y en el nombre de nuestra configuración.



Eso es todo. Simple pero muy útil. Por cierto, que no me entere que te dejas esta configuración en algún tomcat de producción, eh? Es únicamente para desarrollo y depuración. ;-)

sábado, 9 de agosto de 2008

Rumore, rumore

Vamos a cambiar un poco la temática. Esta vez escribo sobre páginas curiosas en internet. ¿Nunca os ha llegado un correo alertando sobre un peligro y os habéis preguntado si realmente será cierto o no? La verdad es que yo soy bastante reticente a creer ese tipo cosas ya que en la mayoría de los casos lo que se pretende es crear una cadena de correos. Hoy sin embargo, me he topado con una página donde se coleccionan rumores y leyendas urbanas de todo tipo. Para mi sorpresa había algún rumor que yo tenía como falso y parece ser cierto. En este post expongo algunos rumores conocidos o que me han impactado.

Probablemente ya conozcas esta página pues por lo que he visto es bastante conocida, sobre todo en Estados Unidos. www.snopes.com es una página dedicada a los rumores y leyendas urbanas. Están clasificadas por temática y en la mayoría especifican si es verdadera o falsa. En otras no es posible, pero siempre dan una amplia explicación acerca del rumor.

El hombre usa tan sólo el 10% de su cerebro

Seguro que has escuchado esto infinidad de veces. Al parecer es totalmente falso. Parece ser que los medios de comunicación y sobre todo la parapsicología han engrandecido este mito para su autopromoción. Me hizo bastante gracia una frase que dice el artículo: Mientras que para la gente que repite el mito, es probablemente cierto, afortunadamente el resto de nosotros usamos todo nuestro cerebro.

El agua calentada en un microondas puede explotar repentinamente

Hace tiempo me llego un correo, de esos que forman una cadena, alertándome de esto. Lo cierto es que no me lo creí mucho, pero poco tiempo después le ocurrió a alguien cercano. Según snopes, es cierto pero ocurre muy de vez en cuando. Yo puedo dar fe de ello. Así que cuidado con calentar demasiado el agua en el microondas.

El virus de la antorcha olímpica

Este me hizo mucha gracia porque he recibido el correo varias veces. Para el que no lo haya recibido nunca (dudo que haya muchos), se trata de un correo que te advierte sobre otro correo que te podrá llegar en varios días con el título Invitation, Merry Chirstmas o Olympic Torch (hay varias variantes). Según cuenta el correo se trata de un virus que es una antorcha olímpica y te quema todo el disco duro, :-p jaja

Yo entiendo que haya gente que no entienda mucho de informática y pueda creer que esto vaya a ocurrir, pero es que lo he recibido de gente que ES informática. Por dios, si nada más ver como está redactado se ve que es de coña. Que vale que pueda ser un virus, pero ¡que te queme el disco duro con una antorcha! .... en fin. Hay gente muy precavida que reenvía.

Por supuesto, es falso.

Un estudio de la OMS concluye que el rubio natural se extingirá en unos 200 años

Este fue el mito que me hizo descubrir esta página. Además no fue en internet, sino que estaba leyendo un libro y al terminar tenía una bibliografía y entre ella venía este artículo. El libro es Next, de Michael Crichton y lo recomiendo a todo el que le guste la lectura.

El rumor es falso. Al parecer la OMS nunca realizo tal estudio, simplemente se lo atribuyeron por la cara.

Regreso al Futuro II predice el ganador de la World Series de 1997 de Baseball

¿Os acordáis de Regreso al Futuro? Me encanta esa trilogía. Biff roba el almanaque deportivo en el futuro y vuelve al pasado para hacerce rico. Corre el rumor de que en la película se dice que Florida gana la World Series en 1997, como ocurrió en realidad. Recordad que la película es de 1987. En 1987 Florida ni siquiera tiene equipo de baseball. Doble hazaña: adivinar el campeón y además el equipo campeón no existe en el momento de hacer la profecía.

Nada más lejos de la realidad, pues aunque Florida ganó en 1997, en la película se dice que fue Miami quién gana en 1997. Así que es falso.

Tu información personal puede ser obtenida en Zabasearch.com

Pues al parecer es cierto. Aunque sólo para ciudadanos estadounidenses. A ver si eres capaz de encontrar al famoso John Smith.


Walt Disney fue criogenizado tras su muerte para ser resucitado en el futuro

Es uno de los mitos más repetidos. Al parecer, falso.

Un tornado arrasa un autocine de Canada mientras estaban proyectando la película Twister

Parece coña, ¿verdad? Pues es cierto.

En el Titanic en el momento de chocar contra el iceberg, se estaba proyectando una versión muda de la película The Poseidon Adventure

Cierto. Un tanto irónico, ¿verdad?

El Rey Juan Carlos asesina accidentalmente a su hermano

Me sorprendió ver esto en una página americana. Desconocía este suceso, pero al parecer el hermano del Rey Juan Carlos, Don Alfonso, murió mientras jugaba con su hermano mayor, Juan Carlos, tras dispararse accidentalmente el arma del Rey. Tenían 14 y 18 años. Según snopes, sólo el Rey Juan Carlos sabrá lo que sucedió entonces. Lo califican como indeterminado.

El daño causado al pentágono el 11-S no fue causado por el secuestrado Boeing 757

Según la versión oficial, fue un Boeing 757 secuestrado por los terroristas el que se estrelló contra el pentágono causando los daños que todos hemos visto alguna vez. Hay teorías que mantienen que no fue un Boeing lo que se estrelló, pues si ves las imágenes no hay rastros del avión por ningún lado. Según snopes, este mito es falso, lo que otorga validez a la versión oficial.

Sin embargo, yo tengo mis dudas sobre ello. Sobre todo después de ver ciertos documentales como Zeitgeist. Si navegas un poco por internet puedes sacar tus propias conclusiones. Aquí te dejo un par de enlaces, pero hay muchos:

Realmente no me quedo ni con una cosa ni con la otra, pues igual que por una parte pienso que los intereses económicos son los que desgraciadamente manejan el mundo de hoy, también creo que es fácil crear rumores y mucho más si se trata de una teoría conspiratoria de esta magnitud. Lo que sí pienso es que no se ha contado toda la verdad y además creo que desgraciadamente, ésta nunca la sabremos. Hay muchas lagunas sobre este tema.

Recuerda, no creas todo lo que ves. La manipulación está hoy a la ordén del día.

viernes, 8 de agosto de 2008

Java y XML: JDOM

Este es otro post de la serie de artículos sobre Java y XML. Tras haber visto cómo trabajar con SAX y DOM, ahora toca el turno de hablar un poco de JDOM.

JDOM es una API desarrollada específicamente para Java y da soporte al tratamiento de XML: parseo, búsquedas, modificación, generación y serialización. Es un modelo similar a DOM, pero no está creado ni modelado sobre DOM. Se trata de un modelo alternativo. La principal diferencia es que DOM fue desarrollado para que fuera independiente del lenguaje, mientras que JDOM está creado y optimizado específicamente para Java. Esto imprime a JDOM las ventajas inherentes a Java, lo que lo hacen que sea una API más eficiente y más natural de usar para el desarrollador Java y por tanto requerie un menor coste de aprendizaje.

Al igual que DOM, JDOM genera un árbol de nodos al parsear un documento XML. En cuanto a lo tipos de nodos, son similares a los de DOM, aunque algunos cambia el nombre ligeramente. La jerarquía de clases también se ve algo modificada.

Mientras que en DOM todo hereda de la clase Node, en JDOM casi todo hereda de la clase Content, que se encuentran en el paquete org.jdom. Digo casi todo porque ni Document ni Attribute (Attr en DOM) heredan de Content. La razon es que Document no se considera un contenido, sino más bien el continente y Attribute tampoco se considera un nodo, sino una propiedad de los nodos tipo Element.

Heredan de Content: Element, Comment y Text. De este último, Text, hereda CDATA (CDATASection en DOM). Por supuesto, hay más clases que heredan de Content y que forman el árbol de nodos, pero no las veremos en estos ejemplos ya que su uso es menos frecuente: DocType, EntityRef y ProccessingInstruction. Estas o sus equivalentes también existen en DOM.

Otra diferencia entre DOM y JDOM es que mientras en DOM todos estos tipos de nodos eran interfaces, en JDOM son clases y por tanto para crear un objeto de cualquier tipo basta con usar el operador new. Por ejemplo,

Element etiqueta = new Element("nombre_etiqueta");
CDATA cdata = new CDATA("Contenido del CDATA");
Comment comentario = new Comment("Texto del comentario");

Antes de pasar a los ejemplos, voy a comentar qué hay que hacer para incluir JDOM en tu proyecto. Lo primero es descargar la última versión de JDOM, por ahora la 1.1. Descomprime el fichero y sigue las intrucciones de uso. Si eres de los seguidores de la ley del mínimo esfuerzo, simplemente tienes que copiar los siguientes ficheros al directorio lib de tu proyecto:
  • jdom.jar, el cual se encuentra en la carpeta build.
  • jaxen-core.jar, jaxen-jdom.jar y saxpath.jar, que se encuentran en la carpeta lib.
  • Es posible que necesites algunos otros jars que se encuentran en la carpeta lib. Al menos para los ejemplos publicados aquí no son necesarios.

Ahora sí, pasemos a lo interesante. Como siempre vamos a partir del siguiente XML:

<?xml version="1.0" encoding="UTF-8"?>
<etiquetaPrincipal xmlns:xela="http://www.latascadexela.es" >
<etiquetaHija id="1" atributo1="valorAtributo1" atributo2="valorAtributo2">
Texto dentro de la etiqueta hija
</etiquetaHija>
<!-- Comentario -->
<xela:etiquetaConNamespace descripcion="etiqueta con un namespace"/>
</etiquetaPrincipal>

Si tenéis cualquier duda podéis acudir al JavaDoc de JDOM. También podéis preguntarla aquí. Estaré encantado de ayudar en lo que pueda.

Parseando un XML

Una de las ventajas de SAX (o la única) es que procesa los ficheros XML por trozos y por tanto no necesita tener todo el XML en memoria. Esa ventaja la han aprovechado los de JDOM y para parsear el XML a partir de un fichero se usa el SAXBuilder. Internamente usará un SaxHandler que generará el árbol de nodos a partir del XML.

// Creamos el builder basado en SAX
SAXBuilder builder = new SAXBuilder();
// Construimos el arbol DOM a partir del fichero xml
Document documentJDOM = builder.build(new FileInputStream("/ruta_a_fichero/fichero.xml"));

Si lo que tenemos es un árbol de nodos DOM, es decir, que lo hemos generado con DOM, también podemos construir su equivalente JDOM usando la clase DOMBuilder.

DOMBuilder dombuilder = new DOMBuilder();
Document doc = dombuilder.build(documentDOM);

Recorriendo el árbol de nodos

Una vez obtenido el objeto Document ya podemos recorrer el árbol de nodos como gustemos. Un ejemplo de como recorrer las etiquetas hijas de la etiqueta raíz:

// Obtenemos la etiqueta raíz
Element raiz = doc.getRootElement();
// Recorremos los hijos de la etiqueta raíz
List<Element> hijosRaiz = raiz.getChildren();
for(Element hijo: hijosRaiz){
   // Obtenemos el nombre y su contenido de tipo texto
   String nombre = hijo.getName();
   String texto = hijo.getValue();
   
   System.out.println("\nEtiqueta: "+nombre+". Texto: "+texto);
   
   // Obtenemos el atributo id si lo hubiera
   String id = hijo.getAttributeValue("id");
   if(id!=null){
      System.out.println("\tId: "+id);
   }
}

También podemos obtener una etiqueta en concreto de entre las etiquetas hijas de otra.

// Obtenemos una etiqueta hija del raiz por nombre
Element etiquetaHija = raiz.getChild("etiquetaHija");

// Incluso podemos obtener directamente el texto de una etiqueta hija
String texto = raiz.getChildText("etiquetaHija");

y con namespaces

// Obtenemos una etiqueta hija del raiz por nombre con Namespaces
// Primero creamos el objeto Namespace
Namespace nsXela = Namespace.getNamespace("xela", "http://www.latascadexela.es");
// Ahora obtenemos el hijo
Element etiquetaNamespace = raiz.getChild("etiquetaConNamespace", nsXela);

Buscando con XPath

También podemos usar XPath con JDOM para buscar etiquetas dentro del XML. Si quieres saber más acerca de XPath, te remito al post de DOM.

En JDOM existe un objeto llamado XPath, el cuál ofrece dos métodos estáticos: selectSingleNode y selectNodes. Uno para evaluar expresiones que devuelvan un único resultado y otro para expresiones que devuelvan una lista de resultados. El primero es un caso particular del segundo, que es el caso genérico.

// Buscamos una etiqueta mediante XPath
Element etiquetaHijaXP = (Element)XPath.selectSingleNode(doc, "/etiquetaPrincipal/etiquetaHija");

y con namespaces

// Buscamos una etiqueta con namespace mediante XPath
Element etiquetaNamespaceXP = (Element)XPath.selectSingleNode(doc, "/etiquetaPrincipal/xela:etiquetaConNamespace");

Si vamos a evaluar una misma expresión XPath sobre varios objetos del XML, como por ejemplo varios Document, es más eficiente construirnos un objeto XPath con esa expresión y aplicarlo tantas veces como deseemos. Para ello usamos el método estático newInstance de XPath.

// Si hacemos uso muchas veces del mismo XPath sobre varios document
// es más eficiente crear un objeto XPath y usarlo varias veces
XPath xpathEtiquetaHija= XPath.newInstance("/etiquetaPrincipal/etiquetaHija");
Element etiqueta1 = (Element)xpathEtiquetaHija.selectSingleNode(doc1);
Element etiqueta2 = (Element)xpathEtiquetaHija.selectSingleNode(doc2);
...
Element etiquetaN = (Element)xpathEtiquetaHija.selectSingleNode(docN);

Modificando el XML

Podemos modificar el árbol de nodos del XML parseado. Por ejemplo, vamos a ver lo fácil que es añadir una nueva etiqueta.

// Creamos una nueva etiqueta
Element etiquetaNueva = new Element("etiquetaNueva");
// Añadimos un atributo
etiquetaNueva.setAttribute("atributoNuevo", "Es un nuevo atributo");
// Añadimos contenido
etiquetaNueva.setText("Contenido dentro de la nueva etiqueta");
// La añadimos como hija a una etiqueta ya existente
etiquetaHija.addContent(etiquetaNueva);

Creando XML desde cero

Con JDOM es fácil crear un XML desde cero. Para crear cada objeto del árbol de nodos, tan solo tenemos que usar el operador new.

// Vamos a crear un XML desde cero
// Lo primero es crear el Document
Document docNuevo = new Document();
// Vamos a generar la etiqueta raiz
Element eRaiz = new Element("raiz");
// y la asociamos al document
docNuevo.addContent(eRaiz);

Copiando nodos de un árbol a otro

Con JDOM es muy fácil pasar elementos de un árbol a otro. Aquí no existe el concepto de ámbito de un document como ocurría en DOM. Todos los objetos son susceptibles de ser añadido a cualquier árbol. Lo único que hay que tener en cuenta es que cada objeto sólo puede tener un padre y por tanto si lo que queremos es moverlo de un sitio a otro, habrá que desasociarlo antes del padre. Para ello, la clase Content proporciona el método detach. Es decir, si hacemos detach en un Element, lo que habremos hecho es dejar huérfano ese Element, es decir, ya no tiene padre, por lo que se puede añadir como hijo a cualquier otro nodo.

Si lo que queremos es copiar nodos, usaremos el método clone para generar una copia del mismo y esta copia la podremos añadir a cualquier nodo ya que se genera huérfana.

// Vamos a copiar la etiquetaHija del primer document a este
// Lo primero es crear una copia de etiquetaHija
Element copiaEtiquetaHija = (Element)etiquetaHija.clone();
// Después la colocamos como hija de la etiqueta raiz
eRaiz.addContent(copiaEtiquetaHija);

// Vamos a mover la etiquetaConNamespace a este document
// Primero la desasociamos de su actual padre
etiquetaNamespace.detach();
// Una vez que ya es huerfana la podemos colocar donde queramos
// Por ejemplo, bajo la etiqueta raiz
eRaiz.addContent(etiquetaNamespace);

Una vez ejecutado el ejemplo y si obtenemos la representación en forma de String del XML obtendremos el siguiente resultado:

XML parseado:
<?xml version="1.0" encoding="UTF-8"?>
<etiquetaPrincipal xmlns:xela="http://www.latascadexela.es">
   <etiquetaHija id="1" atributo1="valorAtributo1" atributo2="valorAtributo2">
      Texto dentro de la etiqueta hija
      <etiquetaNueva atributoNuevo="Es un nuevo atributo">Contenido dentro de la nueva etiqueta</etiquetaNueva>
   </etiquetaHija>
   <!-- Comentario -->
</etiquetaPrincipal>


XML nuevo:
<?xml version="1.0" encoding="UTF-8"?>
<raiz>
   <etiquetaHija id="1" atributo1="valorAtributo1" atributo2="valorAtributo2">
      Texto dentro de la etiqueta hija
      <etiquetaNueva atributoNuevo="Es un nuevo atributo">Contenido dentro de la nueva etiqueta</etiquetaNueva>
   </etiquetaHija>
   <xela:etiquetaConNamespace xmlns:xela="http://www.latascadexela.es" descripcion="etiqueta con un namespace" />
</raiz>

Como podemos observar la etiquetaConNamespace se ha eliminado del XML original ya que usamos el método detach para llevarla de un árbol a otro. Esto no ocurre con etiquetaHija ya que previamente fue clonada.

Serializando el XML

La serialización (paso de árbol de nodos a String o bytes) en JDOM es muy similar a la que se puede realizar con DOM. Lo primero que hacemos es especificar el formato de salida del XML, es decir, si queremos identación entre etiquetas, que las etiquetas sin texto tengan etiqueta de cierre o no, que haya un retorno de carro entre etiquetas, que aparezca o no la declaración xml, ....

JDOM ya ofrece tres formatos predefinidos. Son PrettyFormat, RawFormat y CompactFormat. Sus nombres son bastante intiuitivos: bonito, plano y compacto. Pero os remito a la documentación para que sepáis exactamente en qué consiste cada uno.

Una vez especificado el formato, usamos el XMLOutputter y ya podemos serializar tanto Document como Element, como cualquier nodo. Incluso podemos serializar un listado de nodos. La serialización la podemos hacer a String, a OuputStream o a Writer, lo que cubre una amplia gama de posibilidades.

// Vamos a serializar el XML
// Lo primero es obtener el formato de salida
// Partimos del "Formato bonito", aunque también existe el plano y el compacto
Format format = Format.getPrettyFormat();
// Creamos el serializador con el formato deseado
XMLOutputter xmloutputter = new XMLOutputter(format);
// Serializamos el document parseado
String docStr = xmloutputter.outputString(doc);

A continuación dejo el código completo de la clase que he creado como ejemplo:

package es.latascadexela.xml.jdom;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.jdom.xpath.XPath;

/**
* Clase de ejemplo de procesado de XML mediante JDOM.
*
* @author Xela
*
*/
public class ProcesaXML {

   public static void main(String[] args) {

      try {   
         // Creamos el builder basado en SAX
         SAXBuilder builder = new SAXBuilder();
         // Construimos el arbol DOM a partir del fichero xml
         Document doc = builder.build(new FileInputStream("/ruta_a_fichero/fichero.xml"));

         // Obtenemos la etiqueta raíz
         Element raiz = doc.getRootElement();
         // Recorremos los hijos de la etiqueta raíz
         List hijosRaiz = raiz.getChildren();
         for(Element hijo: hijosRaiz){
            // Obtenemos el nombre y su contenido de tipo texto
            String nombre = hijo.getName();
            String texto = hijo.getValue();
            
            System.out.println("\nEtiqueta: "+nombre+". Texto: "+texto);
            
            // Obtenemos el atributo id si lo hubiera
            String id = hijo.getAttributeValue("id");
            if(id!=null){
               System.out.println("\tId: "+id);
            }
         }
         
         // Obtenemos una etiqueta hija del raiz por nombre
         Element etiquetaHija = raiz.getChild("etiquetaHija");
         System.out.println(etiquetaHija.getName());
         // Incluso podemos obtener directamente el texto de una etiqueta hija
         String texto = raiz.getChildText("etiquetaHija");
         System.out.println(texto);
                  
         // Obtenemos una etiqueta hija del raiz por nombre con Namespaces
         // Primero creamos el objeto Namespace
         Namespace nsXela = Namespace.getNamespace("xela", "http://www.latascadexela.es");
         // Ahora obtenemos el hijo
         Element etiquetaNamespace = raiz.getChild("etiquetaConNamespace", nsXela);
         System.out.println(etiquetaNamespace.getName());
         
         // Buscamos una etiqueta mediante XPath
         Element etiquetaHijaXP = (Element)XPath.selectSingleNode(doc, "/etiquetaPrincipal/etiquetaHija");
         System.out.println(etiquetaHijaXP.getName());
         
         // Buscamos una etiqueta con namespace mediante XPath
         Element etiquetaNamespaceXP = (Element)XPath.selectSingleNode(doc, "/etiquetaPrincipal/xela:etiquetaConNamespace");
         System.out.println(etiquetaNamespaceXP.getName());
         
         // Si hacemos uso muchas veces del mismo XPath sobre varios document
         // es más eficiente crear un objeto XPath y usarlo varias veces
         XPath xpathEtiquetaHija= XPath.newInstance("/etiquetaPrincipal/etiquetaHija");
         Element etiqueta = (Element)xpathEtiquetaHija.selectSingleNode(doc);
         System.out.println(etiqueta.getName());

         // Creamos una nueva etiqueta
         Element etiquetaNueva = new Element("etiquetaNueva");
         // Añadimos un atributo
         etiquetaNueva.setAttribute("atributoNuevo", "Es un nuevo atributo");
         // Añadimos contenido
         etiquetaNueva.setText("Contenido dentro de la nueva etiqueta");
         // La añadimos como hija a una etiqueta ya existente
         etiquetaHija.addContent(etiquetaNueva);
         
         // Vamos a crear un XML desde cero
         // Lo primero es crear el Document
         Document docNuevo = new Document();
         // Vamos a generar la etiqueta raiz
         Element eRaiz = new Element("raiz");
         // y la asociamos al document
         docNuevo.addContent(eRaiz);
         
         // Vamos a copiar la etiquetaHija del primer document a este
         // Lo primero es crear una copia de etiquetaHija
         Element copiaEtiquetaHija = (Element)etiquetaHija.clone();
         // Después la colocamos como hija de la etiqueta raiz
         eRaiz.addContent(copiaEtiquetaHija);
         
         // Vamos a mover la etiquetaConNamespace a este document
         // Primero la desasociamos de su actual padre
         etiquetaNamespace.detach();
         // Una vez que ya es huerfana la podemos colocar donde queramos
         // Por ejemplo, bajo la etiqueta raiz
         eRaiz.addContent(etiquetaNamespace);
         
         
         // Vamos a serializar el XML
         // Lo primero es obtener el formato de salida
         // Partimos del "Formato bonito", aunque también existe el plano y el compacto
         Format format = Format.getPrettyFormat();
         // Creamos el serializador con el formato deseado
         XMLOutputter xmloutputter = new XMLOutputter(format);
         // Serializamos el document parseado
         String docStr = xmloutputter.outputString(doc);
         // Serializamos nuestro nuevo document
         String docNuevoStr = xmloutputter.outputString(docNuevo);
         
         System.out.println("XML parseado:\n"+docStr);
         System.out.println("XML nuevo:\n"+docNuevoStr);
         
      } catch (FileNotFoundException e) {
         e.printStackTrace();
      } catch (JDOMException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

   }

}

Con esto hemos visto lo más importante en cuanto al uso de JDOM. Por supuesto, hay más cosas que no he mencionado y que pueden resultar útil. Simplemente he tratado de dar unas nociones básicas de uso de JDOM para aquellas personas que nunca lo han visto.

Si comparas estos ejemplos con DOM podrás hacerte una idea de lo fácil e intuitivo que es usar JDOM frente a DOM. Si aún usas SAX o DOM en tus proyectos, te animo a adentrarte en el mundo de JDOM. Y si no ves ninguna ventaja, pues nada, sigue con lo que estabas. Desde mi punto de vista las ventajas son muchas y los inconvenientes muy pocos.

En el próximo post veremos ejemplos de uso de XOM, una API parecida a JDOM, que también puede resultar interesante como alternativa a SAX y DOM.