3.1. JAXP#
La librería viene incluida en el JDK y tiene soporte para XPath 1.0 y XSLT 1.0. Para XQuery no tiene soporte alguno.
3.1.1. Lectura#
Comencemos por conocer cómo abrir un documento XML y acceder a sus nodos:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// La siguiente línea puede provocar un ParseConfigurationException
DocumentBuilder builder = factory.newDocumentBuilder();
try (InputStream st = getClass().getResourceAsStream("/claustro.xml")) {
// La línea posterior también puede generar una excepción.
Document xml = builder.parse(st);
// Accedemos a la información del documento a través de su DOM.
// ...
}
Prudencia
Con este código se producirá un error si el XML no es bien formado, pero no se realizará con un DTD ninguna comprobación sobre su validez. Sin embargo, si hay una declaración de tipo de documento:
<!DOCTYPE claustro SYSTEM "claustro.dtd">
y el DTD no existe, se generará un error por no encontrar la referencia. Evite incluir esta cláusula hasta que estudiemos la validación.
Una vez tenemos disponible el objeto Document ya podremos acceder a los nodos del documento. En principio, los nodos se modelan mediante la clase Node sobre la que construyen subsclases que modelan los distintos tipos de nodos (Element, Text, Attr, etc.). Cada uno de estos tipos de nodos está asociado a un número entero al que se da nombre mediante un atributo estático de Node. Por ejemplo:
Element root = xml.getDocumentElement(); // Obtiene el nodo raíz.
root.getNodeType() == Node.ELEMENT_NODE // true.
El propio documento (la variable xml) es un tipo de nodo:
xml.getNodeType() == Node.DOCUMENT_NODE // true
A partir de un nodo (y el propio documento se considera como tal) se pueden hacer diversas pesquisas:
hasChildNodes()devuelve verdadero si el nodo tiene algún hijo.
hasAttributes()devuelve verdadero si el nodo tiene algún atributo.
getNodeName()devuelve el nombre del nodo (si es un elemento, el nombre de la etiqueta).
getNodeValue()devuelve el valor del nodo. Dependiendo del tipo de nodo, así será su valor (al comienzo de la explicación sobre Node hay una tabla que indica qué valor devuelve cada tipo).
getTextContent()devuelve el valor de texto del elemento[1], que es la concatenación de los nodos de texto del propio elemento y de todos sus descendientes. Obsérvese que para un nodo así:
<apelativo>Paco</apelativo>
resulta que:
// apelativo es una variable que contiene el elemento apelativo antedicho apelativo.getTextContent().equals(apelativo.firstChild().getNodeValue());
getFirstChild()ogetLastChild()Devuelve el primero o el último nodo hijo de aquel. Se ha remarcado nodo, porque el nodo no tiene que ser forzosamente un elemento (téngase, pues, cuidado con los documentos indentados)
getParentNode()ogetPreviousSibling()ogetFollowingSibling()devuelve el nodo padre o el hermano previo o el hermano siguiente.
getChildNodes()devuelve un objeto NodeList con los nodos hijos. A pesar de su naturaleza no implementa la interfaz Iterable y sólo presenta un método (
getLength()) que devuelve la cantidad de hijos y un métodoitem(int index)que devuelve el i-ésimo nodo de la lista. Por tanto, si se quiere quiere recorrer la colección, habrá que usar un for clásico:NodeList hijos = root.getChildNodes(); for(int i=0; i<hijos.getLength(); i++) { Node nodo = hijos.item(i); switch(nodo.getNodeType()) { case Node.ELEMENT_NODE: System.out.println(nodo.getNodeName()); break; default: break; } }
Truco
Pese a que, a priori, sólo podemos utilizar el for tradicional para recorrer NodeList, podemos buscarnos las vueltas para convertirlo en un flujo y poder usar con él técnicas de Java funcional:
NodeList hijos = root.getChildNodes(); Stream<Node> streamNode = IntStream.range(0, hijos.getLength()).mapToObj(hijos::item);
getAttributes()devuelve los atributos del elemento en forma de NamedNodeMap que como NodeList tampoco implemente la interfaz Iterable. Además de
item(int index)ygetLength(), dispone también degetNamedItem(String name)que permite obtener el nodo atributo a partir de su nombre:NodeList profesores = xml.getElementsByTagName("profesores"); Element p4 = (Element) profesores.item(3); NamedNodeMap attrs = p4.getAttributes(); for(int i=0; i< attrs.getLength(); i++) { Attr attr = (Attr) attrs.item(i); System.out.printf("%s: %s\n", attr.getNodeName(), attr.getNodeValue()); }
Por otra parte, los tipos de nodos tiene también sus métodos específicos. Por ejemplo, Element:
getElementsByTagName(String name)devuelve los elementos descendientes cuyo nombre de etiqueta es el suministrado en el argumento. El método también existe para los nodo Document:
NodeList profesores = xml.getElementsByTagName("profesores");
getElementById(String id)devuelve el elemento con el identificador suministrado como argumento. Evidentemente el procesador debe conocer cuáles son los atributos de tipo identificador, por lo que es necesario que el documento se haya validado.
Element p17 = xml.getElementById("p17"); // null, porque no estamos usando el DTD
getAttributeNode(String name)ogetAttribute(String name)devuelve el atributo (Attr) o el valor del atributo cuyo nombre se proporciona en el argumento.
3.1.2. Escritura#
Bajo el epígrafe anterior hemos tratado únicamente cómo acceder a información, pero no cómo crear nueva información en formato XML. Para ello debemos construir primero un DOM y luego escribirlo a un archivo.
3.1.2.1. Generación del DOM#
Podemos tomar uno ya existente resultado de haber leído un archivo previo o crearlo ex novo:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document xml = builder.newDocument();
Element root = xml.createElement("claustro");
root.setAttribute("centro", "IES Castillo de Luna");
Element profesor = xml.createElement("profesor");
profesor.setAttribute("id", "p1");
profesor.setIdAttribute("id", true); // Es un identificador
Text texto = xml.createTextNode("Me he hartado de crear elementos");
profesor.appendChild(texto);
root.appendChild(profesor);
xml.appendChild(root);
3.1.2.2. Escritura a archivo#
Partamos de que ya tenemos un Document construido como queremos que quede (véase el apartado anterior) y queremos ahora generar el XML correspondiente:
DOMSource source = new DOMSource(xml);
TransformerFactory tfactory = TransformerFactory.newInstance();
Transformer transformer = tfactory.newTransformer(); // Puede provocar excepción.
Path ruta = Path.of(System.getProperty("java.io.tmpdir"), "claustro.xml");
try (
OutputStream st = Files.newOutputStream(ruta);
OutputStreamWriter sw = new OutputStreamWriter(st);
) {
StreamResult result = new StreamResult(sw);
transformer.transform(source, result);
}
catch(IOException | TransformerException err) {
err.printStackTrace();
}
En cambio, si simplemente quisiéramos volcar el XML como una cadena:
StringWriter sw = new StringWriter();
StreamResult result = new StreamResult(sw);
transformer.transform(source, result);
String contenido = sw.toString();
Podemos, además, manipular cómo se escribe el XML resultante y qué declaraciones incluirá su cabecera:
transformer.setOutputProperty(OutputKeys.INDENT, "yes"); // Salida bonita.
transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "claustro.dtd"); // Añadimos DOCTYPE
Ver también
Para otras propiedades, consúltese OutputKeys.
3.1.3. XPath#
Hemos visto una lectura bastante torpe en que el único
criterio para acceder de forma selectivas a los nodos es mediante un
identificador (getElementById) o mediante el nombre de la etiqueta
(getElementsByTagName). Sin embargo, si conocemos XPath,
podemos seleccionar nodos usando esta tecnología:
XPath xPath = XPathFactory.newInstance().newXPath();
XPathExpression expresion = xpath.compile("//profesor[@casillero]");
// xml es el Document del ejemplo anterior.
NodeList profesores = (NodeList) expresion.evaluate(xml, XPathConstants.NODESET);
// ... Consultamos la lista de profesores con atributo casillero
No puede ser más fácil… si se conoce XPath y se sabe cómo construir la expresión pertinente. También podemos usar una expresión sin compilarla primero, si nuestra intención es usarla una sola vez:
Element profesor = (Element) xPath.evaluate("//profesor[@id='p81']", xml, XPathConstants.NODE); // Mejor que NODESET, porque sabemos que es único
System.out.println(profesor.getTagName()); // profesor
String apelativo = (String) xPath.evaluate("//profesor[@id='p81']/apelativo", xml, XPathConstants.STRING);
System.out.println(apelativo); // Verónica
int cantidad = ((Double) xPath.evaluate("count(//profesor)", xml, XPathConstants.NUMBER)).intValue();
System.out.println(cantidad);
3.1.4. Validación#
Hemos evitado validar el documento hasta ahora. Sin embargo, la validación puede ser interesante o incluso, puede ser preciso, intentar que ni siquiera se haga ninguna comprobación en caso de que la declaración exista, pero el DTD no.
En primer lugar, si queremos que la validación se lleve a cabo debemos añadir:
factory.setValidating(true);
Otro aspecto importante es el de la manipulación de la validación, que se lleva a cabo definiendo un resolutor de entidades:
public class MiEntityResolver implements EntityResolver2 {
@Override
public InputSource getExternalSubset(String name, String baseURI)
throws SAXException, IOException {
return null;
}
@Override
public InputSource resolveEntity(String publicId, String systemId)
throws SAXException, IOException {
return resolveEntity(null, publicId, null, systemId);
}
@Override
public InputSource resolveEntity(String name, String publicId, String baseURI, String systemId)
throws SAXException, IOException {
if (systemId == null) return null;
try {
systemId = resolvePath(baseURI, systemId);
} catch(URISyntaxException e) {
return null;
}
return new InputSource(systemId);
}
private String resolvePath(String base, String path) throws URISyntaxException {
if(new URI(path).isAbsolute()) return path;
if(base == null) throw new IllegalArgumentException("No puede calcularse una ruta relativa si la base es nula.");
if(base.contains("!")) {
// Nos quedamos con la parte del path dentro del jar
Path basePath = Path.of(base.substring(base.indexOf("!") + 1));
// Resolvemos el path relativo y devolvemos el recurso.
URL resource = getClass().getResource(basePath.resolveSibling(path).toString());
return resource != null ? resource.toString() : null;
}
return new URI(base).resolve(path).toString();
}
}
Debemos fijarnos en el último método. Cuando devuelve null, es como, si no
hubiéramos definido nada, y el procesador obrará como lo hace habitualmente para
llevar a cabo la validación. En cambio, si devolvemos un new
InputSource(cadena) utilizará el DTD que indique esa cadena (puede ser una
URL o un archivo local) con independencia de lo que expresase la declaración
original.
Definido, podremos usarlo en el builder antes de procesar el documento:
builder.setEntityResolver(new MiEntityResolver());
Document xml = builder.parse(st);
Advertencia
Este código da por hecho que se conoce la ruta del XML (baseURI). Esto,
sin embargo, puede ser cierto o no. Todo este procesamiento lo desencadena
builder.parse(), el cual permite proporcionar los datos de distinto
modo. Puede pasarse un String o un File en cuyo caso
sí se sabrá baseURI. En cambio, si se pasa un InputStream (que es
precisamente la forma en la que hemos ilustrado el uso) no hay modo de
conocer su valor y, en consencuencia, será null. Eso sí, en este último
caso, se admite un segundo argumento con una cadena que defina baseURI.
¿Qué hace exactamente el código que hemos propuesto? En principio, si no hay definido ningun DTD, no hace nada. En cambio, si hay definido uno, comprueba si el DTD se proporciona con ruta absoluta. Si es así, respeta el valor y la validación se hará con el valor expresado en el archivo. En cambio, si la ruta es relativa, hace una distinción:
Si
baseURIcontiene el carácter!se ha pasado como XML un archivo de recursos y en consecuencia el DTD también lo será y hay que buscarlo dentro del paquete JAR.Si no es así, se resuelve de forma normal la ruta relativa usando como base el XML (
baseURI).
Truco
El argumento del constructor de InputSource también puede ser un Reader, así que si nuestra intención es que el programa no escupa nunca un error (incluso aunque el DTD de la declaración no se encuentre), podemos hacer hacer lo siguiente:
@Override
public InputSource resolveEntity(String name, String publicID, String baseURI, String systemID)
throws SAXException, IOException {
return new InputSource(new StringReader(""));
}
Por hacer
¿Cómo forzar una validación, aunque no haya declaración DOCTYPE?
Notas al pie