3. DOM

DOM (Document Object Model) es una especificación de un API, independiente del lenguaje, para acceder y modificar documentos XML (y también HTML). Dicho API representa documentos XML y HTML como objetos que pueden ser accedidos por aplicaciones tales como navegadores Web, buscadores de documentos, y lenguajes de scripts. Mediante el uso de DOM, dichos programas pueden crear, navegar, manipular, y modificar los documentos XML (y HTML).

3.1. Motivación y orígenes de DOM

Como hemos visto, SAX permite realizar programas (analizadores) que reaccionen ante los datos contenidos en un documento XML. Sin embargo si queremos acceder a esta información de forma no-secuencial (no siguiendo el orden marcado por el analizador) o queremos alterar o manipular esta información (por ejemplo mover una sección o parte del documento) entonces necesitamos algo más flexible.

La idea es proporcionar algún mecanismo para crear un modelo de un documento XML como un conjunto de objetos. A partir de este modelo ya podemos navegar por la estructura del documento y añadir, quitar o modificar elementos y su contenido. Dicho modelo se llama DOM (Document Object Model).

Otro objetivo importante de DOM es el proporcionar una interfaz de programación estándar de forma que pueda usarse en una amplia variedad de entornos y aplicaciones. DOM se ha diseñado para poder ser usado con cualquier lenguaje de programación.

DOM está siendo definido por el W3C (World Wide Web Consortium) en varias etapas o "niveles" (levels). Las especificaciones de DOM se pueden consultar en http://www.w3c.org.

La especificación DOM Level 1 comenzó a finales del año 1997 (http://www.w3.org/TR/REC-DOM-LEVEL-1) siendo la actualización más reciente de 2000. Este primer nivel se limita a aquellos métodos necesarios para representar y manipular la estructura del documento y su contenido. DOM Level 2 incluye facilidades relacionadas con eventos, así como para la manipulación de hojas de estilo. La actualización más reciente de DOM Level 2 es de Noviembre de 2000 (http://www.w3.org/TR/REC-DOM-Level-2-Core). Actualmente se dispone de un borrador de la especificación del nivel 3 (de Octubre de 2002, (http://www.w3.org/TR/REC-DOM-Level-3-Val), que proporciona facilidades para mover nodos entre documentos, ordenación de nodos, así como un API para cargar ficheros XML y convertirlos a una representación DOM y al revés.

Las aplicaciones que utilizan DOM típicamente comienzan analizando algún documento XML y obteniendo la representación DOM asociada (objeto Document que representa al documento). Cómo se realiza esto no está especificado en el nivel 1, y el nivel 2 proporciona solamente mejoras limitadas: existe una clase que proporciona acceso a los métodos para crear objetos Document, pero no hay forma de acceder a un constructor de lectura/análisis de un documento XML de forma independiente de la implementación.

Una vez que se tiene el modelo proporcionado por DOM, se puede acceder a las distintas partes del documento XML asociado a través de las propiedades y métodos del objeto Document correspondiente. Dichas propiedades y métodos se definen en la especificación DOM. La especificación proporcionada por el W3C define el API de DOM para Java, ECMAScript, y OMG IDL.

3.2. Representación DOM de un documento XML

DOM especifica un conjunto de interfaces para manejar documentos XML o HTML. El objetivo es obtener un modelo del documento en el que se represente su estructura.

Tanto los documentos XML, como HTML, tienen una estructura jerárquica, por eso el modelo lógico resultante obtenido por DOM de dichos documentos se asemeja a un árbol (o bosque). Sin embargo, es importante tener en cuenta que la especificación de DOM no indica que los docuementos obtenidos deben implementarse como un árbol (o bosque), ni especifica las relaciones entre los objetos a implementar. DOM es un modelo lógico, que puede implementarse de la manera que se crea más conveniente. Otra propiedad importante de los modelos obtenidos por DOM es su isomorfismo estructural: si se usa dos implementaciones distintas de DOM para obtener una representación del mismo documento, se creará el mismo modelo de estructura, con los mismos objetos y relaciones entre ellos.

Supongamos el siguiente texto, extraído de un documento XML:

<zoo>
   <mamiferos>
      <animal> Gato </animal>
      <animal> Perro </animal>
   </mamiferos>
   <reptiles>
      <animal> Boa </animal>
      <animal> Lagarto </animal>
   </reptiles>
</zoo>

una implementación de DOM obtendría el siguiente modelo (ver Figura 3.1):

Figura 3.1. Modelo DOM asociado a un documento XML.

Los documentos se modelan usando objetos, y el modelo obtenido incluye no sólo la estructura de un documento, sino también el comportamiento de los objetos por los que está formado dicho documento. En otras palabras, los nodos en el diagrama anterior no representan una estructura de datos, sino objetos, los cuales tienen unas propiedades y un comportamiento. DOM identifica:

Las interfaces que pueden usarse para gestionar documentos XML (y HTML) son abstracciones (similares a las clases abstractas de Java), es decir, constituyen la manera de especificar la forma de acceder y manipular una representación de un documento de una aplicación. Las interfaces no implican una implementación concreta particular. Cada aplicación DOM es libre de mantener cualquier representación para los documentos que considere conveniente.

DOM está formado por dos partes: DOM Core (conjunto de interfaces para acceder a documentos XML) y el DOM HTML (extensión con interfaces específicas para acceder a documentos HTML).

3.3. DOM (Core) Level 1

DOM presenta los documentos como una jerarquía de objetos Node, que también implementan otras interfaces más especializadas. La interfaz Node es el tipo de datos primario de un documento DOM, representando un nodo del árbol. Algunos tipos de nodos pueden tener nodos hijo de varios tipos, y otros son nodos hoja, que representan el nivel más inferior en la estructura del documento. Los tipos de nodo, y qué tipos de nodos pueden tener como hijos son los siguientes:

Tipos Node Qué representa Posibles nodos hijo Atributos - valor
Document Representa todo el documento XML o HTML Element (<=1), ProcessingInstruction, Comment, DocumentType

name - #document

value - null

DocumentFragment Representa una porción de un documento Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference

name -
#document fragment

value - null

DocumentType Cada documento tiene un atributo "doctype" cuyo valor es nulo o bien un objeto DocumentType
--

name - nombre del tipo de documento

value - null

EntityReference Referencias a entidades Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference

name - nombre de la entidad referenciada

value - null

Element Representa un elemento Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference

name - nombre
etiqueta

value - null

Attr Atributo de un objeto Element Text, EntityReference

name - nombre del atributo

value - valor atributo

ProcessingInstruction Instrucción de procesamiento
--

name - destino

value - contenido
instrucción

Comment Comentarios
--

name - #comment

value - contenido del comentario

Text Texto
--

name - #text

value - texto del nodo

CDATASection Secciones de datos
--

name -
#cdata-section

value - texto sección

Entity Entidad de un documento XML o HTML Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference

name -
nombre entidad

value - null

Notation Notación declarada en el DTD
--

name-
nombre notación

value - null

 

DOM especifica una interfaz NodeList para manejar listas ordenadas de elementos Node (como por ejemplo hijos de un objeto Node, o los elementos devueltos por el método Element.getElementsByTagName).

Otra interfaz importante es NamedNodeMap, utilizada para manejar conjuntos de nodos no ordenados, referenciados por su atributo name (como por ejemplo los atributos de un objeto Element).

Tanto NodeList como NamedNodeMap reflejan los cambios en la estructura del documento subyacente, de forma que si un usuario utiliza un objeto NodeList que contiene el hijo de un objeto Element, y posteriormente el usuario añade más hijos a dicho elemento (o los elimina o modifica), dichos cambios se reflejan automáticamente en el objeto NodeList, sin que el usuario tenga que hacerlo explícitamente. De igual forma, los cambios en un elemento Node de un árbol se reflejan en todas las referencias a ese Node en las NodeLists y NamedNodeMaps.

Otras interfaces consideradas fundamentales (y por lo tanto deben ser implementadas en cualquier implementación de DOM), son las siguientes:

3.4. DOM (Core) Level 2

DOM Level 2 (DOM2) está dividido en catorce módulos distribuídos en ocho paquetes diferentes (DOM1 se corresponde con los módulos Core y XML):

3.5. Analizadores (parsers) DOM para Java

DOM está definido casi en su totalidad por interfaces, en lugar de clases. Diferentes analizadores proporcionan su propia implementación de las interfaces estándar. DOM no está tan ampliamente soportado como SAX, pero la mayoría de los analizadores Java lo proporcionan, incluyendo Crimson, Serces, XML for Java, Oracle XML Parser for Java, y GNU JAXP.

DOM no está completo en sí mismo. Casi todos los programas DOM necesitan usar algunas clases específicas del analizador. No resulta demasiado difícil cambiar un analizador por otro, pero normalmente se requiere recompilar el programa. DOM2 no especifica como analizar un documento, crear un nuevo documento, o serializarlo en un fichero o flujo de datos. Estas importantes funciones son realizadas por clases específicas del analizador correspondiente.

JAXP, el API de Java para procesamiento de XML, palía algo esta deficiencia de DOM proporcionando formas estándar (independientes del analizador) para analizar documentos existentes, crear nuevos documentos y serializar árboles DOM (en memoria) en ficheros XML.

Si el analizador utilizado implementa JAXP, entonces en lugar de usar clases específicas del analizador, se pueden usar las clases DocumentBuilderFactory y DocumentBuilder, del paquete javax.xml.parsers, para analizar los documentos. Los pasos a seguir son:

El proceso anterior se ilustra en la Figura 3.2:

Figura 3.2. Esquema del proceso de análisis de un documento XML.

Ejemplo 1. Lectura de un documento XML y obtención de un documento DOM

public class DomEcho
{  static Document document;
   
   public static void main(String argv[])
   {
     if (argv.length != 1) {
       ... 
     DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();
     }
     try {
       DocumentBuilder builder = factory.newDocumentBuilder();
       document = builder.parse( new File(argv[0]));
     } catch (...) { 
       ... //captura de excepciones
     }

  }
}

Ejemplo 2. Manipulación de un documento DOM

Una vez que el documento ha sido analizado (mediante el método parse) podemos olvidar las diferencias existentes entre los distintos analizadores y trabajar con las interfaces estándar DOM.

Para navegar por la estructura del árbol, la interfaz org.w3c.dom.Node define varios métodos para navegar por la estructura de un documento DOM. Las operaciones getFirstChild, getLastChild, getNextSibling, getPreviousSibling, y getParentNode son suficientes para recorrer cualquier elemento del árbol.

La clase Restructurer reordena los nodos de un árbol de forma que los que representan instrucciones de procesamiento queden por encima del nodo raiz, y los comentarios aparezcan debajo del nodo raiz

public class Restructurer {

   
   public static void processNode(Node current) throws DOMException
   {
     Node nextSibling = current.getNextSibling();
     int nodeType = current.getNodeType();


     if (nodeType == Node.COMMENT_NODE ||
         nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
        
       Node document = current.getOwnerDocument();
       Node root = document.getFirstChild();
       while (!(root.getNodeType() == Node.ELEMENT_NODE)) {
         root = root.getNextSibling();
       }
       Node parent = current.getParentNode();
       parent.removeChild(current);
       if (nodeType == Node.COMMENT_NODE) {
          document.appendChild(current);
       }
       else if (nodetype == Node.PROCESSING_INSTRUCTION_NODE) {
          document.insertBefore(current,root);
       }
     }
     else if (current.hasChildNodes()) {
        Node firstChild = current.getFirstChild();
        processNode(firstChild);
     }

     if (nextSibling != null) {
        processNode(nextSibling);
     }

  }
}

La clase RestructureDriver construye un documento DOM a partir de un documento XML desde una URL, lo modifica y muestra el documento a través de System.out

public class RestructureDriver {

   
   public static void main(String[] args) {
     
     if (args.lenght <=0) {
       System.out.println("Usage: java RestructureDriver URL");
       return;
     }
     String url = args[0];
     try {
       DocumentBuilderFactory factory =
          DocumentBuilderFactory.newInstance();
       factory.setNamespaceAware(true);
       DocumentBuilder parser = factory.newDocumentBuilder();
       //lectura del documento
       Document document = parser.parse(url);
       //modificación del documento
       Restructurer.processNode(document);
       //escritura del documento resultante
       TransformerFactory xformFactory =
            TransformerFactory.newInstance();
       Transformer idTransform = xformFactory.newTransformer();
       Source input = new DOMSource(document);
       Result output = new StreamResult(System.out);
       idTransform.transform(input,output);
     }
     catch (...) {
        //captura de excepciones
     }

  }
}

El paquete javax.xml.transform permite serializar un documento DOM. Los pasos a seguir son:

Ejemplo 3. Creación de un documento DOM

Podemos crear un documento DOM vacío, en lugar de analizar un documento XML existente, al que añadimos un nodo raiz y varios nodos hijo.

La interfaz abstracta que crea nuevos objetos Document se llama DomImplementation, dichos objetos, una vez creados, pueden ser serializados en un stream o un fichero. Para crear un documento se utiliza el método:

public Document createDocument (String rootElementNamespaceURI, 
                                String rootElementQualifiedName, 
								DocumentType doctype);

Si el documento simplemente es un documento bien formado, puede utilizarse null como argumento doctype. Si el elemento raíz del documento no está registrado en un espacio de nombres, también puede utilizarse el valor null como primer argumento. Los objetos Document creados carecen de elementos por debajo del nodo raíz, de forma que deben usarse los métodos de la interfaz Document para crear nuevos nodos, y métodos de la interfaz Node para añadir dichos nodos al árbol.

La mayoría de programas que crean nuevos documentos siguen la siguiente estructura:

public class FibonacciDOM 
{

  public static void main(String[] args) {

    try {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setNamespaceAware(true);
      DocumentBuilder builder = factory.newDocumentBuilder();
      DOMImplementation impl = builder.getDOMImplementation();
      
      Document doc = impl.createDocument(null, "Fibonacci_Numbers", null);
       
      BigInteger low  = BigInteger.ONE;
      BigInteger high = BigInteger.ONE;

      Element root = doc.getDocumentElement();

      for (int i = 0; i < 10; i++) {
        Text space = doc.createTextNode("\n  ");
root.appendChild(space); Element number = doc.createElement("fibonacci"); Text text = doc.createTextNode(low.toString()); number.appendChild(text); root.appendChild(number); BigInteger temp = high; high = high.add(low); low = temp; } Text lineBreak = doc.createTextNode("\n");
root.appendChild(lineBreak); // Serialización del documento TransformerFactory xformFactory = TransformerFactory.newInstance(); Transformer idTransform = xformFactory.newTransformer(); Source input = new DOMSource(doc); Result output = new StreamResult(System.out); idTransform.transform(input, output); } catch (...) { //captura de excepciones } } }
Este programa produce documentos como el siguiente:
<?xml version="1.0" encoding="utf-8"?><Fibonacci_Numbers>
<fibonacci>1</fibonacci> <fibonacci>1</fibonacci> <fibonacci>2</fibonacci> <fibonacci>3</fibonacci> <fibonacci>5</fibonacci>
<fibonacci>8</fibonacci> <fibonacci>13</fibonacci> <fibonacci>21</fibonacci> <fibonacci>34</fibonacci> <fibonacci>55</fibonacci>
</Fibonacci_Numbers>

Ejemplo 4. Búsqueda de un nodo

Los métodos getFirstChild, getLastChild, getNextSibling, getPreviousSibling, y getParentNode son suficientes para recorrer cualquier elemento del árbol.

public Node findSubNode(String name, Node node) 
{
  if (node.getNodeType() != Node.ELEMENT_NODE) {
    System.err.println("Error: El nodo buscado no es de ese tipo");
    System.exit(22);
  }

  if (! node.hasChildNodes()) return null;

  NodeList list = node.getChildNodes();
  for (int i=0; i < list.getLength(); i++) {
    Node subnode = list.item(i);
    if (subnode.getNodeType() == Node.ELEMENT_NODE) {
      if (subnode.getNodeName() == name) return subnode;
    }
  }
  return null;
}

Ejemplo 5. Obtención del contenido de un nodo

Cuando se quiere obtener el texto que contiene un nodo, es conveniente navegar por la lista de nodos hijo, ignorando aquellos nodos que no contienen texto.

public String getText(Node node) 
{
  StringBuffer result = new StringBuffer();
  if (! node.hasChildNodes()) return "";

  NodeList list = node.getChildNodes();
  for (int i=0; i < list.getLength(); i++) {
    Node subnode = list.item(i);
    if (subnode.getNodeType() == Node.TEXT_NODE) {
      result.append(subnode.getNodeValue());
    }
    else if (subnode.getNodeType() ==
        Node.CDATA_SECTION_NODE) 
    {
      result.append(subnode.getNodeValue());
    }
    else if (subnode.getNodeType() ==
        Node.ENTITY_REFERENCE_NODE) 
    {
      result.append(getText(subnode));
    }
  }
  return result.toString();
}