Introducción a los Servicios Web en Java (II): XML y Java

XML es el lenguaje de los Servicios Web. Cuando invocamos un servicio u obtenemos una respuesta desde el mismo, toda la información que estamos enviando/recibiendo es en formato XML. Tanto la estructura de datos de un servicio como su interface se definen con XML. XML está presente en todas partes cuando hablamos de Servicios Web.

En este segundo artículo del tutorial de Servicios Web en Java vamos a ver las bases de XML desde una perspectiva teórica, para a continuación adentrarnos en la parte práctica a través de las API's StAX y XPath.

2.1 XML: lenguaje de marcado

XML (eXtensible Markup Language, Lenguaje de Marcado Extensible) es como su nombre indica, un lenguaje de marcado: un lenguaje para codificar un documento mediante marcas (etiquetas), las cuales contienen información acerca de la estructura, contenido y/o presentación de dicho documento. HTML es otro lenguaje de marcado, aunque éste fue diseñado con la intención de presentar información (mientras que la misión de XML es la de estructurar, transportar y almacenar información).

2.2 XML: estructura y sintaxis

Un documento XML se compone como mínimo de una declaración, un elemento raíz (también llamado elemento padre) y uno o más sub-elementos (también llamados elementos hijo), que a su vez pueden anidar otros sub-elementos:



	
		DDD-1111
		Toyota
		Prius
	
	
		HHH-4444
		Audi
		A5
	

	

La primera línea es la declaración XML, y en ella se define tanto la versión de XML como la codificación que vamos a usar en el documento. En los próximos ejemplos se omitirá para simplificar el código, pero en el mundo real debes usarla siempre.

El resto del documento contiene la información en si. A la estructura de un documento XML se la llama árbol XML ya que podemos considerar el elemento padre <parking> como la raíz, cada uno de los elementos hijo <plaza> como una rama, y cada uno de los elementos hijo <matrícula>, <marca> y <modelo> como una rama que nace de la rama anterior.

Exceptuando la declaración XML, todos los elementos deben ser cerrados mediante su etiqueta de cierre (</parking>, </plaza>, etc). El nombre de todas las etiquetas (ya sean de apertura o cierre) es sensible a mayúsculas, por lo que <matricula> es distinto de <Matricula>. El orden de apertura-cierre-anidación debe ser el correcto, o el documento estará mal formado y no será válido.

XML permite incluir comentarios que, como en otros lenguajes, son omitidos durante el proceso de parseado. Pueden ir en cualquier parte del documento y pueden ser multilínea:

Por último, existen cinco caracteres que requieren un tratamiento especial cuando queremos usarlos de forma literal. Dicho tratamiento consiste en sustituirlos por su entidad de referencia asociada:

Carácter especial Entidad de referencia
& &amp;
< &lt;
> &gt;
" &quot;
' &apos;

Usados de forma literal el parseador puede tratarlos de forma incorrecta, provocando que se malforme el documento y se produzca un más que probable error. Veamos un ejemplo:

<!-- XML inválido -->
<apunte lenguaje="java">
	<regla>Usamos el carácter > para comparar si un valor numérico primitivo es mayor que otro.</regla> 
	<excepcion>Cuando trabajamos con objetos wrapper (Integer, Float) es preferible usar el método compare()</excepcion>
<apunte>
	

El contenido de la etiqueta <regla> contiene el carácter >, y el parseador puede interpretarlo como un carácter de marcado (carácter de cierre de etiqueta) en lugar de como un carácter de contenido (símbolo mayor qué). La solución es usar la entidad de referencia asociada al carácter conflictivo:

<!-- XML válido -->
<apunte lenguaje="java">
	<regla>Usamos el carácter &gt; para comparar si un valor numérico primitivo es mayor que otro.</regla> 
	<excepcion>Cuando trabajamos con objetos wrapper (Integer, Float) es preferible usar el método compare()</excepcion>
<apunte>
	

2.3 XML: elementos en detalle

Puesto que prácticamente todo en un documento XML está formado por elementos, vamos a ver en detalle este componente.

Un elemento puede contener lo siguiente:

  • Otros elementos
  • Texto (hasta ahora lo hemos llamado contenido)
  • Atributos
  • Una combinación de todos los anteriores

El único punto que no hemos visto aún es el de Atributos, aunque su nombre deja muy clara su función:


	Juan
	Gómez
	107

	

Básicamente, el atributo departamento ofrece información adicional sobre el elemento <empleado>. Los atributos ser usados para proveer información que es irrelevante para el uso que queremos hacer de los datos, pero puede ser útil para, por ejemplo, su tratamiento (como realizar un filtrado). Ten en cuenta que podríamos sustituir el atributo por un elemento hijo, y estaríamos proveyendo la misma información; usa el estilo que más te convenga, aunque cuando no tengas clara la relevancia real es aconsejable, por legibilidad, usar un elemento hijo:


	
	IT

	

El nombre de un elemento puede ser, como hemos visto hasta ahora, cualquiera que se te ocurra (incluyendo letras, números y caracteres especiales), aunque existen ciertas restricciones que debemos cumplir:

  • No puede comenzar con un número o un carácter de puntuación
  • No puede comenzar con la palabra xml ni cualquier combinación de ésta que combine mayúsculas y minúsculas
  • No puede contener espacios en blanco

Además, existen ciertas convenciones que es útil seguir a la hora de elegir el nombre de un elemento:

  • Usa nombres descriptivos, cortos y simples (mejor <matricula> que <numero_de_matricula>
  • Evita usar el carácter guión, ya que el software que haga uso de los datos podría intentar realizar una sustracción. En su lugar usa el carácter barra baja
  • Evita usar el carácter punto, ya que el software que haga uso de los datos podría creer que estamos usando notación por puntos (como objeto.propiedad). En su lugar usa el carácter barra baja
  • Evita usar el carácter dos puntos, ya que está reservado para el uso de namespaces (los veremos en detalle en este mismo artículo). En su lugar usa el carácter barra baja
  • Evita usar caracteres no-ASCII (como las vocales acentuadas), ya que el software que haga uso de los datos podría no soportar codificaciones distintas
  • Indenta adecuadamente el código, de otra manera será ilegible cuando alcance cierta longitud

2.4 XML: extensible

Como su nombre indica, XML es extensible. ¿Y que significa esto? Ni más ni menos que podemos extender un documento (añadir información) sin romper su estructura:


	Juan
	Gómez
	107
	IT
	Senior
	
		
			Java
			Alto
		
		
			HTML5
			Alto
		
	

	        

Todo el software que procese información sobre la versión anterior de la etiqueta <empleado> seguirá funcionando después de la extensión, puesto que la estructura original sigue presente (los elementos afectados mantienen el nombre y nivel de anidamiento).

2.5 XML: namespaces

Los namespaces (nombres de espacio) proveen un método para la organización jerárquica de la información contenida en distintos documentos XML, y así evitar conflictos entre elementos con un mismo nombre. Veamos un ejemplo:


	Soporte
	Planta 3
	1180



	Esther Artiz
	
		Cliente C1, Proyecto P1
		Cliente C1, Proyecto P2
	

	        

En el ejemplo anterior tenemos un elemento con el mismo nombre que está siendo usado en dos representaciones de información distintas: <nombre>. A la hora de hacer referencia a este elemento nos vamos a encontrar con un problema de ambigüedad que va a derivar en un conflicto de nombres.

La forma de solucionar este problema es haciendo uso de prefijos:


	Soporte
	Planta 3
	1180



	Esther Artiz
	
		Cliente C1, Proyecto P1
		Cliente C1, Proyecto P2
	

	        

En el ejemplo anterior hemos definido todos los elementos mediante los prefijos d y e para constatar a que namespace corresponde cada uno de ellos. Ahora sólo nos falta declarar los namespaces correspondientes:


	

				

	

	        

Como puedes ver en el ejemplo anterior, usamos el atributo xmlns (XML NameSpace) para definir una URI que identifica y asocia el prefijo usado con nuestro namespace. La URI no tiene porqué ser accesible en internet (de hecho no lo es), su única misión es identificar un namespace (imagínalo como la sentencia package en Java).

Otra forma de declarar un namespace es haciéndolo en el elemento raíz del documento:


	
		
	

	
		
	

	        

La aplicación y uso de namespaces es algo más profunda que lo expuesto hasta ahora, permitiendo definir comportamientos como redefinición y sobreescritura, por ejemplo. Sin embargo características tan avanzadas (que realmente no lo son) no son necesarias para seguir este tutorial.

Con esto ya tenemos la base de conocimientos sobre XML necesaria para comenzar a jugar con el lenguaje. Ahora es el momento de introducir la API StAX.

2.6 StAX: conceptos básicos

Asumiendo que XML está en todas partes cuando hablamos de Servicios Web, tarde o temprano se hace necesario manipular documentos XML a través de nuestro querido lenguaje Java. Aunque existen mecanismos para poner en marcha Servicios Web mediante tecnologías como RMI o EJB, éstas sólo representan una capa de abstracción sobre el código XML subyacente.

StAX (Streaming API for XML, API para la transmisión de XML) es una API para la manipulación de documentos XML que surgió para cubrir las carencias que sufrían las APIs SAX y DOM, situándose en un punto intermedio a nivel de procesamiento entre estas dos, convirtiéndola en una API más eficiente. Mientras que en DOM accedemos al árbol completo del documento XML (lo que nos permite un acceso aleatorio a cualquier parte de este pero a un coste elevado), en SAX se registran eventos asociados con cada elemento, de manera que cuando necesitamos acceder a uno de ellos debemos empujar la información de dicho elemento hacia nuestra aplicación (más eficiente).

En StaX resolvemos el problema del acceso a los datos de un documento XML desde una perspectiva intermedia: usando un cursor nos movemos a través del árbol según sea necesario, y sólo cuando estamos en el nodo adecuado tiramos de la información. Es un modelo más cercano a SAX (eventos) que a DOM (árbol), pero al contrario que con el primero no se mantiene un registro actualizado de todos los eventos, si no que gracias al cursor vamos descubriendo los elementos y sus eventos asociados en tiempo real. De igual forma, mientras nos movemos vamos olvidando elementos/eventos pasados.

De manera adicional, StAX permite escribir documentos XML (SAX por ejemplo sólo permite lectura), de manera que además de un tratamiento de datos más efectivo, reducimos el número de APIs que son necesarias cuando se requiere una gestión integral. Por todos estos motivos, StAX va a ser nuestra API de referencia (salvo honrosas excepciones) cuando manejemos documentos XML desde Java.

2.7 StAX: leyendo XML

Una de las más sencillas pero profundas frases que Carl Sagan nos dejó fue la siguiente:

Si quieres hacer un pastel de manzana desde el principio, primero debes crear el Universo.

En nuestro caso, para poder parsear XML mediante StAX lo primero que necesitamos es, un fichero XML:


	
		AAA-1111
		Juan Sánchez
		111-88-88
		04-12-2014
	
	
		BBB-2222
		Antonio Santos
		111-64-64
		21-03-2014
	
	
		CCC-3333
		Almudena Gea
		111-22-22
		01-10-2014
	

		

Ahora que tenemos nuestro universo, ya podemos ponernos el delantal y comenzar a parsear XML mediante StAX. Como puedes observar, el documento XML contiene información sobre un párking con plazas (de aparcamiento) que contienen información sobre un vehículo y su propietario.

Lo primero que vamos a hacer es obtener e imprimir por consola el número de plazas:

package es.davidmarco.soa.stax;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

public class Lector {
	// La ruta del documento XML, al ser una ruta relativa suele ser el directorio
	// principal del proyecto (p.e. /home/usuario/eclipse/workspace/proyecto)
	public static final String DOCUMENTO_XML = "parking.xml";
	public static final String ELEMENTO_PLAZA = "plaza";
	
	public int numeroDePlazas() {	
		int numeroPlazas = 0;
		
		XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
		InputStream inputStream = null;
		XMLStreamReader xmlStreamReader = null;
		
		try {
			inputStream = new FileInputStream(DOCUMENTO_XML);
			xmlStreamReader = xmlInputFactory.createXMLStreamReader(inputStream);
			
			while (xmlStreamReader.hasNext()) {
				xmlStreamReader.next();

				if (xmlStreamReader.isStartElement() && ELEMENTO_PLAZA.equals(xmlStreamReader.getLocalName())) {
					numeroPlazas++;
				}				
			}
		} catch (XMLStreamException ex) {
			ex.printStackTrace();
		} catch (FileNotFoundException ex) {
			ex.printStackTrace();
		} finally {
			try {
				xmlStreamReader.close();
				inputStream.close();
			} catch (XMLStreamException ex) {
				ex.printStackTrace();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		}
		
		return numeroPlazas;
	}
	
	public static void main(String[] args) {
		Lector lector = new Lector();
		System.out.println("Número de plazas: " + lector.numeroDePlazas());
	}
}
		

No es necesario explicar con detalle cada función de cada método de cada API, eso es algo que está totalmente fuera de lugar; toda esa información puedes encontrarla en la documentación oficial.

Lo que realmente nos interesa es el código donde realizamos el tratamiento del documento XML. Una vez que hemos creado el objeto XMLStreamReader llamamos a su método hasNext() para comprobar si existe un evento en la siguiente posición del cursor. En caso afirmativo, nos movemos a dicha posición invocando al método next(). En este momento, y mientras no movamos de nuevo el cursor, todas las operaciones que realicemos a través de la instancia xmlStreamReader se realizarán sobre el evento actual. En nuestro caso hemos realizado dos operaciones: isStartElement() para comprobar si el evento actual es una apertura de etiqueta XML y getLocalName() para comprobar si su nombre es plaza. En caso afirmativo incrementamos en uno nuestro contador de plazas. Al finalizar la ejecución obtenemos el siguiente resultado por consola:

Número de plazas: 3
		

Todos los eventos, independientemente de su tipo, tienen unas propiedades genéricas que podemos invocar:

xmlStreamReader.isStartElement()
		

Sin embargo, en ciertas ocasiones no necesitamos conocer las propiedades del evento, si no el tipo mismo del evento:

while (xmlStreamReader.hasNext()) {
	xmlStreamReader.next();
				
	int tipoEvento = xmlStreamReader.getEventType();				
	if ((tipoEvento == XMLEvent.START_ELEMENT) && (ELEMENTO_PLAZA.equals(xmlStreamReader.getLocalName()))) {
		numeroPlazas++;
	}				
}		
		

Obtener el tipo de evento es tremendamente útil cuando queremos realizar distintas acciones dependiendo del tipo de evento:

switch (tipoEvento) {
	case XMLEvent.START_ELEMENT:
		// acción 1
		break;
	case XMLEvent.END_ELEMENT:
		// acción 2
		break;
	case XMLEvent.NAMESPACE:
		// acción 3
		break;
	default:
		// acción por defecto
		break;
}
		

Para finalizar con la lectura de información vamos a escribir un sencillo ejemplo para mostrar por consola todas las matrículas y su identificador de plaza asociado:

package es.davidmarco.soa.stax;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

public class Lector {
	public static final String DOCUMENTO_XML = "parking.xml";
	public static final String ELEMENTO_PLAZA = "plaza";
	public static final String ELEMENTO_MATRICULA = "matricula";
	public static final String ATRIBUTO_ID = "id";

	public List<String> matriculasConId() {
		List<String> matriculasConIdList = new ArrayList<String>();
		
		XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); 
		InputStream inputStream = null;
		XMLStreamReader xmlStreamReader = null;
		
		try {
			inputStream = new FileInputStream(DOCUMENTO_XML);
			xmlStreamReader = xmlInputFactory.createXMLStreamReader(inputStream);

			String tmpMatriculaId = null;
			while (xmlStreamReader.hasNext()) {
				xmlStreamReader.next();

				if (xmlStreamReader.isStartElement() && ELEMENTO_PLAZA.equals(xmlStreamReader.getLocalName())) {
					// Obtenemos el valor del primer atributo del elemento (id) (numeración basada en indice-cero)
					tmpMatriculaId = xmlStreamReader.getAttributeValue(0);

					// En esta iteración es imposible que la siguiente condición se cumpla, por lo que saltamos a la siguiente
					continue;
				}
								
				if (xmlStreamReader.isStartElement() && ELEMENTO_MATRICULA.equals(xmlStreamReader.getLocalName())) {
					// Obtenemos el texto contenido dentro del elemento (entre las etiquetas de apertura y cierre)
					tmpMatriculaId += ":" + xmlStreamReader.getElementText();
					matriculasConIdList.add(tmpMatriculaId);
					
					// Reseteamos la cadena temporal de id:matricula
					tmpMatriculaId = null;
				}			
			}
		} catch (XMLStreamException ex) {
			ex.printStackTrace();
		} catch (FileNotFoundException ex) {
			ex.printStackTrace();
		} finally {
			try {
				xmlStreamReader.close();
				inputStream.close();
			} catch (XMLStreamException ex) {
				ex.printStackTrace();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		}
		
		return matriculasConIdList;
	}
	
	public static void main(String[] args) {
		Lector lector = new Lector();
		System.out.println("Matrículas con id: " + lector.matriculasConId());
	}
}
		

Puesto que StAX trabaja con un modelo de cursor, es necesario usar una variable temporal cuando necesitamos gestionar múltiples eventos como si fueran uno solo. Para resolver este problema hubiera sido más conveniente usar DOM, pero uno de los objetivos de este artículo es mostrar StAX y por tanto hemos resuelto nuestro problema mediante éste. Sin embargo, en el mundo real nunca debes pensar en términos de preferencia, si no de conveniencia.

Antes de finalizar con esta sección te lanzo dos retos:

  • ¿Eres capaz de escribir el mismo programa sustituyendo los dos bloques if por un único bloque switch?
  • ¿Y de obtener el valor del atributo id por nombre en lugar de por posición?

2.8 StAX: escribiendo XML

Ahora que ya sabemos como leer un documento XML, es el momento de crearlos desde cero. Y de nuevo, StAX al rescate:

package es.davidmarco.soa.stax;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

public class Escritor {	
	public void generarParkingXML() {
		XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory();
		FileOutputStream fileOutputStream = null;
		XMLStreamWriter xmlStreamWriter = null;
		
		try {
			fileOutputStream = new FileOutputStream("/parking.xml");
			xmlStreamWriter = xmlOutputFactory.createXMLStreamWriter(fileOutputStream);
			
			// Decorador para la indentación del documento
			xmlStreamWriter = new IndentingXMLStreamWriter(xmlStreamWriter);

			// Encoding y versión de XML
			xmlStreamWriter.writeStartDocument("UTF-8", "1.0");
			
			// Comentario XML
			xmlStreamWriter.writeComment(" Generado mediante StAX ");

			// Abrimos <parking> (elemento root)
			xmlStreamWriter.writeStartElement("parking");

			// Abrimos <plaza>
			xmlStreamWriter.writeStartElement("plaza");			
			xmlStreamWriter.writeAttribute("id", "1");
			
			xmlStreamWriter.writeStartElement("matricula");
			xmlStreamWriter.writeCharacters("AAA-1111");
			xmlStreamWriter.writeEndElement();
			
			xmlStreamWriter.writeStartElement("propietario");
			xmlStreamWriter.writeCharacters("Juan Sánchez");
			xmlStreamWriter.writeEndElement();
			
			xmlStreamWriter.writeStartElement("telefono");
			xmlStreamWriter.writeCharacters("111-88-88");
			xmlStreamWriter.writeEndElement();
			
			xmlStreamWriter.writeStartElement("vencimiento");
			xmlStreamWriter.writeCharacters("04-12-2014");
			xmlStreamWriter.writeEndElement();
			
			// Cerramos <plaza>
			xmlStreamWriter.writeEndElement();
			
			// Cerramos <parking> (elemento root)
			xmlStreamWriter.writeEndDocument();
		} catch (FileNotFoundException ex) {
			ex.printStackTrace();
		} catch (XMLStreamException ex) {
			ex.printStackTrace();
		} finally {		
			try {
				xmlStreamWriter.flush();
				xmlStreamWriter.close();
				fileOutputStream.close();
			} catch (XMLStreamException ex) {
				ex.printStackTrace();
			} catch (IOException ex) {
				ex.printStackTrace();
			}			
		}
	}

	public static void main(String[] args) {
		Escritor escritor = new Escritor();
		escritor.generarParkingXML();		
	}
}
		

El código anterior va a generar un documento similar al que leíamos en la sección anterior (aunque para mantener el código lo más reducido posible hemos generado solamente el primer elemento <plaza>). El resultado es el siguiente:




	
		AAA-1111
		Juan Sánchez
		111-88-88
		04-12-2014
	

		

El ejemplo anterior no requiere apenas explicación: las clases de lectura InputXxx y XxxReader han sido sustituidas por clases OutputXxx y XxxWriter, y la construcción de los elementos XML es realmente sencilla e intuitiva. El único punto de interés proviene del uso de IndentingXMLStreamWriter, una clase que actúa como decorador de XMLStreamWriter y que indenta automáticamente todo el código generado.

2.9 XPath: conceptos básicos

XPath (XML Path, Rutas XML) es una librería incluida en Java para la evaluación de expresiones sobre documentos XML, y al mismo tiempo es el lenguaje que define dichas expresiones y que depende del W3C Consortium (y por tanto está asociado a la propia red Intenet y no a ningún lenguaje de programación en concreto). A efectos prácticos, XPath es ambas cosas cuando trabajamos con Java.

2.10 XPath: construyendo expresiones

En XPath se evalúa un documento XML como si fuera una estructura de tipo árbol. Cada elemento XML se denomina nodo (cuando existe anidamiento hablamos de subnodos, pero no dejan de ser nodos en si mismos), y el camino que debemos seguir para alcanzar un nodo en concreto se denomina ruta (de ahí el nombre de la librería):

/raiz/nodo
		

Extrapolándolo a nuestro documento XML de plazas de párking:

/parking/plaza
		

La expresión anterior va a alcanzar a todos los elementos <plaza>. Ahora imaginemos que queremos ser más selectivos y alcanzar sólo las matrículas:

/parking/plaza/matricula
		

Pero XPath es aún más potente: con la siguiente expresión podemos filtrar todas las plazas cuya fecha de vencimiento cumpla en una fecha concreta:

/parking/plaza[vencimiento = '04-12-2014']
		

Si queremos filtrar aún más la información obtenida, podemos indicar que una vez realizado el filtro por fecha de vencimiento sólo nos devuelva el nombre de los propietarios:

/parking/plaza[vencimiento = '04-12-2014']/propietario
		

También podemos seleccionar un nodo por índice, esto es, la posición dentro del árbol XML (ten presente que se tratan de indices basados en uno, al contrario que cuando trabajamos con arrays e indices basados en cero):

/parking/plaza[1]
		

Incluso podemos subir a través de los nodos padre una vez que hemos realizado el filtrado. ¿Recuerdas el fragmento XML de la sección 2.4? Con la siguiente expresión alcanzamos el número de empleado de todos aquellos con formación en tecnología Java:

/empleado/tecnologias/tecnologia[nombre = 'Java']/../../numero
		

Y con la siguiente expresión obtenemos la información completa de los empleados que, además de tener formación en tecnología Java, tienen un nivel de conocimientos alto:

/empleado/tecnologias/tecnologia[nombre = 'Java' and nivel = 'Alto']
		

XPath define toda una serie que selectores (/, .., @, etc) y operadores (and, or, >, , <, , etc) para realizar estas y otras operaciones. Te recomiento que visites el manual de referencia oficial o su versión traducida al castellano para profundizar en la sintaxis del lenguaje. Estoy seguro que pronto serás el nuevo gurú de XPath =)

2.11 XPath: ejecutando expresiones

Ahora que sabemos como construir expresiones es el momento de ejecutarlas:

package es.davidmarco.soa.xpath;

import java.io.IOException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class Evaluador {
	public NodeList filtrarNodos(String expresion, String rutaDocumentoXML) {		
		NodeList nodosResultado = null;
		
		try {
			DocumentBuilderFactory dBuilderFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder dBuilder = dBuilderFactory.newDocumentBuilder();
			Document documentoXML = dBuilder.parse(rutaDocumentoXML);
			
			XPath xpath = XPathFactory.newInstance().newXPath();
			
			nodosResultado = (NodeList) xpath.evaluate(expresion, documentoXML, XPathConstants.NODESET);			
		} catch (ParserConfigurationException ex) {
			ex.printStackTrace();
		} catch (SAXException ex) {
			ex.printStackTrace();
		} catch (IOException ex) {
			ex.printStackTrace();
		} catch (XPathExpressionException ex) {
			ex.printStackTrace();
		}
	
		return nodosResultado;
	}

	public static void main(String[] args) {
		Evaluador evaluador = new Evaluador();
		NodeList nodos = evaluador.filtrarNodos("/parking/plaza", "parking.xml");
		
		for (int i = 0; i < nodos.getLength(); i++) {
			System.out.println(nodos.item(i).getTextContent());
		}
	}
}
		

En el ejemplo anterior hemos usado DOM para recrear el árbol XML y XPath para evaluar una expresión. El resultado final es una lista de nodos (NodeList) que hemos iterado para mostrar por consola el contenido de cada uno de ellos. Realmente simple, ¿verdad?

2.12 XPath: validación de expresiones

Al igual que cuando trabajamos con SQL, las expresiones XPath deben ser validadas para evitar ataques de XPath Injection. Imagina el siguiente método para obtener los datos de una plaza por número de matrícula:

public NodeList obtenerPlazaPorMatricula(String matricula) {
	// ...
	
	String expresion = "/parking/plaza[matricula='" + matricula + "' ", "parking.xml";
	return (NodeList) xpath.evaluate(expresion, documentoXML, XPathConstants.NODESET);
}
		

Si llamamos a nuestro flamante método pasándole como parámetro una matrícula existente, nos devolverá la información deseada. Si la matrícula no existe, devolverá una lista vacía. ¿Pero que pasa si el parámetro matricula contiene un valor malicioso?

' or 1=1 or '
		

El resultado devuelto al pasar la cadena anterior como valor del parámetro matricula será la información de todas las plazas, lo que evidentemente es una fuga de información:

AAA-1111
Juan Sánchez
111-88-88
04-12-2014
	
BBB-2222
Antonio Santos
111-64-64
21-03-2014
	
CCC-3333
Almudena Gea
111-22-22
01-10-2014
		

¿Qué ha ocurrido? Muy sencillo: al no validar la expresión, nuestro no tan flamante método nos da la posibilidad de construir expresiones complejas. En nuestro caso hemos construido una expresión condicional con tres condiciones separadas por dos operadores or, incluyendo el comillado apropiado para que la expresión sea válida y el compilador no lance una excepción de sintaxis:

/parking/plaza[matricula='' or 1=1 or '']
		

La primera y tercera condiciones devuelven false, pero la segunda condición devuelve true y por tanto el resultado global de la expresión es también true para el valor de todos los elementos <matricula>. Por tanto, la información de todas las plazas es devuelta.

La solución requiere validar la entrada de datos:

public static final String ESPACIO_EN_BLANCO = " ";
public static final int INDICE_CERO = 0;

public NodeList obtenerPlazaPorMatricula(String matricula) {
	// ...
	
	// Hacemos split del array y seleccionamos el primer valor
	String matriculaValidada = matricula.split(ESPACIO_EN_BLANCO)[INDICE_CERO];
	
	String expresion = "/parking/plaza[matricula='" + matriculaValidada + "' ", "parking.xml";
	return (NodeList) xpath.evaluate(expresion, documentoXML, XPathConstants.NODESET);
}
		

Una solución más acorde con el mundo real sería validar la matrícula contra una expresión regular apropiada, y sólo en caso de validación positiva ejecutar la llamada. En el caso de parámetros numéricos la solución es mucho más sencilla, ya que una llamada a Integer.parseInt() o Float.parseFloat() hará saltar una excepción en caso de no recibir uno de estos tipos.

Resumen

En este artículo del tutorial de Servicios Web en Java hemos visto tanto el lenguaje XML como algunas herramientas Java para trabajar con él. Apenas hemos empezado con XML, y a lo largo de todo el tutorial haremos un uso intensivo de este lenguaje, además de presentar más herramientas Java para la manipulación de XML (como JAXB).

En el próximo artículo veremos XML Schema, el lenguaje para describir la estructura, constricciones y restricciones de un documento XML. También veremos algunos patrones de diseño y buenas prácticas para diseñar tipos de datos listos para ser usados en nuestros Servicios Web.