miércoles, 22 de abril de 2009

Leyendo XML en Java sin dolor

Leer XML en Java es un verdadero pain in the ass, a diferencia de Scala, que tiene soporte nativo para XML. No estoy muy al tanto de librerías que facilitan la tarea, que las debe haber, pero quiero compartir una muy elemental que hice. Las características de su diseño que, creo, la hacen interesante son:
  • Inmutabilidad
  • Acceso a subnodos por invocaciones encadenadas, evitando NPE ante nodos inexistentes
El primer punto es una verdad a medias, porque se encapsula un org.w3c.dom.Node que podría ser modificado por afuera. Esto se solucionaría clonando el nodo. Esta "librería" es simplemente un wrapper sobre el nodo. Cada instancia es creada para un nodo específico y no se le puede asignar otro; pero el nodo en sí es mutable.
El segundo punto se basa en el patrón Null Object: la idea es prescindir de null en el API para evitar el fatídico Null Pointer Exception. Más abajo lo vemos.
Como dije antes la librería es elemental, y sus limitaciones son:
  • No sirve para "recorrer" el XML sino para obtener elementos conocidos
  • No accede a los atributos de los elementos
No sería nada complicado superar esas limitaciones. Pero no es tanto por la utilidad de la librería que escribo este post sino porque me parece un buen ejemplo de API, las ideas son aplicables a otras cosas. Aparte también ilustra la idea de tener una capa de abstracción sobre una librería existente, cuando los casos de uso son lo suficientemente simples, y se desea que el código refleje claramente la intención sin que uno deba perderse en la complejidad (o torpeza) de la librería original.


API de XmlNode

Esta es la firma de los constructores:
   
   public XmlNode();
   public XmlNode(Node node);
   public XmlNode(Document doc);
El primero es el Null Object, y si bien es un constructor público, lo normal será que sólo se utilice desde la misma clase y no desde afuera. El segundo toma un Node y el tercero un Document. Este último toma el nodo raiz del documento. La idea es que podamos partir de un documento o un nodo cualquiera, y olvidarnos de la diferenciación: lo que tenemos es un árbol de nodos.
    
    public String getName();
    public String getValue();

Con estos métodos obtenemos el nombre (tag) y el valor (contenido) del nodo. Ambos retornarán null si es un Null Object.

    public XmlNode subNode(String tagName);

Este método en cambio nunca retorna null y permite invocaciones encadenadas. El argumento debe ser un tag de un nodo hijo (inmediatamente relacionado con el actual). Veamos un ejemplo. Si creamos un XmlNode a partir de esto:

   <a> 
      <b>
         <c>hola</c>
         <d>que tal</d>
      </b>
   </a>

Podremos accederlo así:

   String nombre = nodo.getName(); // "a"
   XmlNode nodoB = nodo.subNode("b"); // subnodo <b>
   XmlNode nodoC = nodo.subNode("b").subNode("c"); // subnodo <c>
   String valor1 = nodo.subNode("b").subNode("d").getValue(); // "que tal"
   String valor2 = nodoC.getValue(); // "hola"

Y también así:

   XmlNode nodoX = nodo.subNode("z").subNode("y").subNode("x");
   String valor3 = nodoX.getValue(); // null

Ahí vemos la invocación encadenada con nodos inexistentes. En nodoX nos quedó un Null Object. Para verificar si el XmlNode "está definido" o es nulo, tenemos también el siguiente método (inspirado en el homónimo de Option en Scala):

    public boolean isDefined();

Así será más prolijo verificar si el nodo existe, en lugar de ver si getName() o getValue() retornan null.

El código

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class XmlNode
{
    protected Node node = null;
    
    public XmlNode()
    {
    }
    
   public XmlNode(Node node)
    {
        this.node = node;
    }
    
    public XmlNode(Document doc)
    {
        this.node = doc.getDocumentElement();
    }
    
    public XmlNode subNode(String tagName)
    {
        if(node != null)
        {
            NodeList nl = node.getChildNodes();
            for(int i = 0; i < nl.getLength(); i++)
            {
                Node n = nl.item(i);
                
                if(n.getNodeName().equals(tagName))
                {
                    return new XmlNode(n);
                }
            }
        }
        return new XmlNode();
    }
    
    public String getName()
    {
        return !this.isDefined()? null : node.getNodeName();
    }
    
    public String getValue()
    {
        return !this.isDefined()? null : node.getFirstChild().getNodeValue();
    }
    
    public boolean isDefined()
    {
        return this.node != null;
    }
}

2 comentarios:

  1. Muchas gracias pero no me esta funcionando con un xml con espacio de nombres ejemplo ... etc

    espero puedas echarme una mano gracias

    ResponderEliminar
    Respuestas
    1. ¿El xml pudiste cargarlo exitosamente en un org.w3c.dom.Document? ¿qué tipo de problema tuviste después con XmlNode?
      Otra limitación obvia que me olvidé de mencionar es que no se retornan colecciones de nodos. Es un wrapper muy elemental y para muchos fines prácticos se queda corto.

      Eliminar