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.
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(): cabeceraLo 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
characters(): Esto es un documento XML
endElement(): cabecera
characters(): Eso es todo amigos
endElement(): documentoXML
endDocument()
<cabecera>
debe encontrarse justo a continuación
de <documentoXML> o no.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:
ContentHandler
es la más importante desde
el punto de vista del desarrollador. Especifica los manejadores de eventos
que debemos implementar en nuestro analizador y que veremos en el siguiente
apartado. En caso de que no nos interesen todos los eventos, sino solo algunos
concretos, puede ser más sencillo extender la clase DefaultHandler
,
que ya incorpora manejadores por defecto (vacíos) para todos los eventos.ErrorHandler
, EntityResolver
y
DTDHandler
desempeñan un papel parecido a ContentHandler
,
pero para tratar errores, entidades y DTDs, respectivamente. Afortunadamente,
la clase DefaultHandler
también implementa estos interfaces.XMLReader
es el "corazón" del
API, aquí vienen los métodos que analizan el XML y generan los
eventos. No obstante, el código de estos métodos es fijo e independiente
de la aplicación, por lo que desde el punto de vista del desarrollador
nos limitaremos a usar clases ya implementadas en su totalidad.SAXException
que representan
los distintos tipos de error que se pueden producir en el análisis.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:
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.
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:
startDocument()
y endDocument()
: se producen cuando
se empieza y se termina de analizar el documento XML, respectivamente.startElement()
: Se activa en el momento en que el se encuentra
una etiqueta de apertura. Los dos primeros argumentos, namespaceURI
y localName
, se usan solamente si el parser contempla espacios
de nombres. El argumento qName
contiene el nombre del elemento.
Por otro lado, el argumento atts
contiene los atributos asociados
al elemento (consultar la interfaz org.xml.sax.Attributes
para
ver los métodos de acceso como getLocalName()
y getValue()
que en este caso se acceden via índices en lugar de por valor lo cual
es mejor para evitar dependencias del orden de aparición). endElement()
: Por cada startElement()
hay un par
endElement()
siendo los argumentos iguales pero omitiendo los
atributos en este último. Cuando tenemos un elemento vacío se
dispararán ambos eventos. characters()
: Se dispara cuando se encuentran datos tipo carácter,
incluyendo las secciones CDATA. Los argumentos incluyen un array de caracteres,
el comienzo del array y la longitud del mismo. ignorableWhitespace()
: Este método se usa para informar
de blancos irrelevantes dentro de un elemento. Recibe los mismos argumentos
que el método characters()
.startPrefixMapping()
y endPrefixMapping()
: indican
que comienza el ámbito de un espacio de nombres (definido mediante
el atributo xmlns
) o termina, respectivamente.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.
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>
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
).
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;
}
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:
fatalError()
: es un error del que el analizador no puede recuperarse.
Un ejemplo es un documento mal formado. error()
: se produce cuando el documento es incorrecto según
el estándar, pero el análisis puede proseguir. La lista de errores
fatales y no fatales está definida en el estándar
XML.warning()
: es cualquier problema no definido como error o error
fatal en el estándar XML. 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);
}