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.

6 comentarios:

Eduardo dijo...

Que tal Xela?, muy interesante tu sitio, de mucho apoyo para principiantes como un servidor, siento un poco de verguenza preguntar cosas de este tipo, pero supongo que por algun lado tengo que empezar.

Tengo una aplicación que escucha por sockets, responde a ciertos comandos básicos, pero ocupo pasarle un archivo XML compacto, incluso sin cabecera xml version="1.0", la pregunta es básicamente ¿cómo puedo saber si me están enviando un XML bien formado?, es decir por mas simple que venga, que tenga apertura y cierre de todas las etiquetas.

Te agradezco de antemano. Saludos desde México!!

Xela dijo...

Hola Eduardo, me alegra saber que se me lee al otro lado del charco ;-). Algún día tengo que ir para allá. Me han hablado muy bien de vuestra tierra, tanto compatriotas tuyo como míos que han ido allí. De hecho, siempre llevo un peso en mi cartera que me dejo como recuerdo "la comadrita" de mi novia antes de volver a Monterey. :-p

En cuanto a tu duda, lo único que se me ocurre es que construyas el Document tal y como aparece en el post. El javadoc del método build dice lo siguiente:

Throws:
ValidityException - if a validity error is detected. This is only thrown if the builder has been instructed to validate.
ParsingException - if a well-formedness error is detected
IOException - if an I/O error such as a bad disk prevents the file from being read


Con lo cual, si intentando construir el Document salta una ParsingException, quiere decir que tu xml no está bien formado.

Espero que esto te sirva.

Anónimo dijo...

You might also want to look at vtd-xml, the next generation XML processing model that is far more powerful than DOM and SAX

http://vtd-xml.sf.net

carpen dijo...

Hola Xela!, he llegado hasta tu blog buscando información acerca de Java y XML. Me han parecido muy interesantes e instructivos tus post sobre SAX, JDOM y XOM. Aunque quizá ésta no sea la forma más ortodoxa de hacerlo, me gustaría pedirte consejo sobre qué utilizar en la aplicación que estoy desarrollando en Java. Básicamente se trata de una aplicación en la que los usuarios tienen que rellenar varios formularios creados con los típicos elementos de Swing: TextField, TextArea, ComboBox, RadioButton, etc. Me gustaría gestionar la información introducida en ficheros XML, para poder almacenarla, recuperarla y modificarla.
¿Qué modelo me recomiendas?
Muchas gracias.

Cher dijo...

Gracias por las aportaciones. Me han servido como punto de inicio.

FELIPE RODRIGUEZ FONTE dijo...

Hola Xela, quería hacerte una consulta ya que veo que eres un artista del xml...jeje
Te pongo en situación, quiero generar un documento xml de respaldo de una base de datos. En principio lo tengo montado en jdom, pero no me gusta demasiado porque estuve leyendo que genera todo el xml en memoria, se te ocurre algún api o algo que me sirviese para generarlo de forma secuencial sin tirar demasiado de memoria... Obviamente, en el peor caso lo genero yo mismo pero quiero evitar eso...

Graicas