2. SAX

Una vez que hemos estudiado la estructura de un documento XML sabemos construir este tipo de documentos. Pero todavía nos queda algo muy importante: diseñar programas que analicen y entiendan documentos XML e incluso que los puedan modificar dinámicamente. El objetivo de SAX es posibilitar la construcción de analizadores (parsers) de documentos XML. En realidad, SAX no es más que un API para analizar los documentos (como su propio nombre, Simple API for XML, indica). Existen otros APIs, como DOM, pero con aplicaciones distintas: SAX es recomendable para extraer información del documento, mientras que DOM lo es para manipular su estructura. En este tema describiremos el funcionamiento de un analizador SAX y las tareas para las que es más adecuado.

2.1. ¿Cómo funciona SAX?

Los analizadores SAX están basados en un modelo de eventos: a medida que el parser recorre el documento éste informa de la ocurrencia de eventos, tales como el comienzo de un elemento XML o el final del documento, a un manejador de eventos (event handler). Se sigue una filosofía parecida a la forma en que se implementa el modelo de eventos en AWT: existe un objeto, una componente gráfica como un botón o un selector, que puede emitir eventos y existen objetos oyentes (listeners) que pueden manejarlos. Estos oyentes implementan una interfaz que el objeto emisor entiende y por ello deben registrarse como oyentes autorizados para ser capaz de manejar estos eventos. Cuando se produce un evento el emisor informa solo a los oyentes registrados para que estos lo traten invocando al método correspondiente de la interfaz.

Por ejemplo, veamos qué hace un analizador SAX cuando procesa un documento XML como el siguiente:

<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
<documentoXML>
<cabecera> Esto es un documento XML </cabecera>
Eso es todo amigos
</documentoXML>

Se producirían los siguientes eventos como salida:

startDocument()
startElement(): documentoXML
startElement(): cabecera
characters(): Esto es un documento XML
endElement(): cabecera
characters(): Eso es todo amigos
endElement(): documentoXML
endDocument()
Lo que demuestra que SAX realiza un análisis secuencial y no hay manera de determinar relaciones padre/hijo: es responsabilidad del programador descubrir si el elemento <cabecera> debe encontrarse justo a continuación de <documentoXML> o no.

2.2. Estructura de un analizador SAX

Existen dos versiones de SAX: la que trataremos aquí es la 2.0, que introdujo en su día diversas mejoras sobre la 1.0, como la posibilidad de manejar espacios de nombres. El API está formado por un conjunto de interfaces y clases:

2.3. Trabajar con SAX en Java

En la actualidad hay diversas implementaciones de analizadores SAX en Java. La más conocida y usada es Xerces, del proyecto Apache (no sólo implementa SAX, sino también DOM). No obstante, el problema de SAX es que deja algunos "cabos sueltos" en cuanto a su uso real. Por ejemplo, la forma de instanciar un parser SAX no está contemplada en el estándar, y es dependiente de la implementación empleada. Este problema se solucionó con la aparición del API JAXP, desarrollado por Sun, que añade una capa de abstracción sobre SAX y sobre otros estándares, y permite trabajar con XML y Java independientemente de la implementación de parser que estemos usando.

JAXP está dividido en dos partes, el API de análisis y el de transformación (para aplicar XSLT), por lo que las diferentes implementaciones existentes pueden cubrir uno, otro o ambos:

Instanciar un analizador SAX con JAXP

JAXP encapsula en la clase SAXParser una implementación de un analizador SAX. Podemos utilizar la que viene en la distribución de JAXP o bien cualquier otra, siempre que sea compatible con él. Como se muestra en el ejemplo, los objetos SAXParser se construyen a partir de la clase SAXParserFactory . Una vez instanciado el analizador, se efectúa el análisis llamando al método parse:

 

import java.io.FileReader;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;

public class EjemploSax {
public static void main(String[] args) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(args[0], new DefaultHandler());
}
}

al método parse hay que pasarle una entrada de donde obtener un documento (en el ejemplo, un nombre de fichero) y un manejador de eventos (en el ejemplo, el manejador por defecto, la clase DefaultHandler, que no hace nada salvo que haya un error en el documento). En el siguiente apartado veremos cómo escribir nuestro propio manejador de eventos.

2.4. Eventos SAX

Como hemos visto en el apartado anterior, el código principal de nuestro analizador SAX debe extender la clase DefaultHandler (o implementar los interfaces ContentHandler, ErrorHandler, EntityResolver y DTDHandler).

El siguiente ejemplo muestra un manejador de eventos que imprime un mensaje en la salida cuando se produce algún evento SAX, mostrando los parámetros del evento.

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class SAXParserHandler extends DefaultHandler {
//comienzo del documento public void startDocument() throws SAXException {
System.out.println("startDocument");
} //fin del documento
public void endDocument() throws SAXException {
System.out.println("endDocument");
}
//texto dentro de etiquetas public void characters(char[] ch, int start, int length)
throws SAXException {
String charString = new String(ch, start, length);
System.out.println("caracteres: " + charString);
}
//etiqueta de apertura, puede contener atributos public void startElement(String namespaceURI, String localName,
String qName, Attributes atts)
throws SAXException {
System.out.println("startElement: " + qName);
// Lista atributos y sus valores
for (int i=0; i<atts.getLength(); i++) {
System.out.println("Atributo: " + atts.getLocalName(i));
System.out.println("\tValor: " + atts.getValue(i));
}
}
//etiqueta de cierre
public void endElement(String nameSpaceURI, String localName,
String qName)
throws SAXException {
System.out.println("endElement: " + qName);
}

//espacio en blanco que se puede ignorar public void ignorableWhitespace(char[] ch, int start, int length)
throws SAXException {
System.out.println(length + " caracteres en blanco ignorables");
}

//comienzo de espacio de nombres public void startPrefixMapping(String prefix, String uri)
throws SAXException {
System.out.println("Comienza prefijo de namespace: " + prefix);
}
//fin de espacio de nombres public void endPrefixMapping(String prefix)
throws SAXException {
System.out.println("Termina prefijo de namespace: " + prefix);
}
//instrucción de procesamiento public void processingInstruction(String instruction, String data)
throws SAXException {
System.out.println("Instrucción: " +
instruction + ", datos: " + data);
}
//entidad que no se desea resolver, por lo que se ignora public void skippedEntity(String name) throws SAXException {
System.out.println("Entidad saltada: " + name);
}
}

Como puede verse en el listado anterior, un manejador de eventos SAX puede gestionar los siguientes eventos:

2.4.1. Ignorar los espacios en blanco

Si se ejecuta el manejador de eventos anterior sobre un fichero de ejemplo como el siguiente:

<?xml version="1.0"?>
<prueba>
<uno>Una prueba simple</uno>
</prueba>

Se generará la salida:

startDocument
startElement: prueba
caracteres: startElement: uno
caracteres: Una prueba simple
endElement: uno
caracteres: endElement: prueba
endDocument

Obsérvese que cuando el analizador llega a los espacios en blanco que hay entre etiquetas (entre <prueba> y <uno>, por ejemplo) por defecto no los considera como espacios ignorables, sino significativos. Esto es así porque en ausencia de información externa (un DTD, por ejemplo) el analizador no puede decidir qué espacios pueden ignorarse. Por ello, en este caso el evento ignorableWhitespace no se produce. Para que el analizador distinga entre espacios significativos y no significativos, es necesario definir una gramática y decirle que valide el documento con ella.

2.4.2. Validar los documentos

Se puede hacer que el analizador verifique la gramática del documento empleando el método setValidating de la clase SAXParserFactory. Modificando el ejemplo anterior, donde se instanciaba un analizador SAX:

//modificación a la clase EjemploSax
...
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(true); 
SAXParser parser = factory.newSAXParser(); ...

Si definimos una gramática (mediante un DTD, por ejemplo) y la referenciamos desde el documento XML, el analizador SAX verificará que el fichero la cumpla y además detectará los espacios en blanco que puede ignorar.

//fichero "prueba.dtd"
<!ELEMENT prueba (uno)>
<!ELEMENT uno (#PCDATA)>
//modificación al documento XML
<?xml version="1.0"?>
<!DOCTYPE prueba SYSTEM "prueba.dtd">
<prueba>
<uno>Una prueba simple</uno>
</prueba>

2.4.3. Trabajar con espacios de nombres

En SAX 2.0 se puede trabajar con espacios de nombres. Para habilitar el soporte de espacios de nombres en JAXP, se emplea el método setNamespaceAware de la clase SAXParserFactory.

//modificación a la clase EjemploSax
...
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true); 
SAXParser parser = factory.newSAXParser();
... 

A partir de este momento, se disparará el evento startPrefixMapping cuando se defina un espacio de nombres y endPrefixMapping cuando acabe el ámbito del espacio definido (la etiqueta que lo define mediante un atributo xmlns).

2.4.4. Localización de los eventos

Mediante los manejadores de eventos se puede saber cuándo se ha producido un evento, pero en principio no en qué línea del documento ha sido. Esta información es accesible a través de la clase Locator. Esta clase tiene dos métodos, getLineNumber() y getColumnNumber(), que sirven para saber por qué línea y columna del documento va el análisis. Para utilizar Locator es necesario definir un objeto de esta clase y hacer que el parser lo utilice mediante el método setDocumentLocator. Por ejemplo:

//Añadido al manejador de eventos (clase SAXParserHandler)
import org.xml.sax.Locator;
...
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}

2.5. Gestión de errores

Para gestionar los errores que se puedan producir durante el análisis es necesario utilizar una clase que implemente el interfaz ErrorHandler. Afortunadamente, la clase del manejador por defecto, DefaultHandler, también implementa los gestores de error. Por ello bastará con ampliar nuestra clase de gestión de eventos con los métodos apropiados, que son tres:

Los tres métodos tienen como argumento un objeto de la clase SAXParseException, que representa el error o warning que se ha producido. Por ejemplo, para gestionar errores fatales se incluiría el siguiente código

//Añadido al manejador de eventos (clase SAXParserHandler)
import org.xml.sax.SAXParseException;
... public void fatalError(SAXParseException e) throws SAXException {
System.out.println("Error fatal " + e);
}