Utilización de Commons Digester para un sistema de preferencias configurable

0
9294

Utilización de Commons Digester para un sistema de preferencias configurable

0. Índice de contenidos.

1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Mac Book Pro 17″ (2,6 Ghz Intel Core i7, 8 GB DDR3)
  • Sistema Operativo: Mac OS X Snow Leopard 10.6.4
  • Maven 2.2.1
  • Eclipse 3.6 (Helios) con M2Eclipse
  • Commons Digester 2.1

2. Introducción

Hoy quiero hablar de una librería que siempre ha estado con nosotros pero que nunca la hemos utilizado directamente. Se trata de otra de las librerías de la maravillosa serie Commons de Apache, Commons Digester. Toda la información sobre está librería la podéis encontrar en la URL: http://commons.apache.org/digester/

Nosotros vamos a utilizarla para crear un sistema de preferencias que sea configurable y extensible, es decir, que respetando la estructura de un XML podamos modificar los valores de las preferencias en función de un tipo, sin tener que tocar ni una línea de código.

3. Manos a la obra

Lo primero que tenemos que hacer es crear un proyecto con Maven e incluir la librería Commons Digester como dependencia en el pom.xml


	commons-digester
	commons-digester
	2.1

Lo siguiente es confeccionar el XML que va a almacenar las dependencias. Para nuestro ejemplo nos bastará con este:


	
		
			url-default
			driver-default
			user-default
			password-default
		
		idioma-default
	

Cada uno de los juegos de preferencia lo vamos a identificar con un atributo type, de modo que podamos añadir tantos juegos de preferencia como queramos.

Lo siguiente que tenemos que hacer es crear las clases en función del XML que tengamos. En mi caso podemos identificar las siguientes clases:

package com.autentia.preferences;

public class Datasource {
	
	private String url;
	private String driver;
	private String user;
	private String password;
	
	public String getUrl() {
		return url;
	}
	public void setUrl(String url) {
		this.url = url;
	}
	public String getDriver() {
		return driver;
	}
	public void setDriver(String driver) {
		this.driver = driver;
	}
	public String getUser() {
		return user;
	}
	public void setUser(String user) {
		this.user = user;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	
	public String toString(){
		return new StringBuilder("URL: ").append(url)
		.append(" driver: ").append(driver).append(" user: ")
		.append(user).append(" password: ").append(password).toString();
	}
	
}
package com.autentia.preferences;

public class Preference {
	
	private String type;
	private Datasource datasource;
	private String idioma;
	
	public void setType(String type) {
		this.type = type;
	}
	public String getType() {
		return type;
	}
	public Datasource getDatasource() {
		return datasource;
	}
	public void setDatasource(Datasource datasource) {
		this.datasource = datasource;
	}
	public String getIdioma() {
		return idioma;
	}
	public void setIdioma(String idioma) {
		this.idioma = idioma;
	}
	
	public String toString(){
		return new StringBuilder("Type: ").append(type)
		.append(" Datasource: ").append(datasource)
		.append(" Idioma: ").append(idioma).toString();
	}

}

Estas dos clases se corresponden con las etiquetas del XML. Ahora tenemos que tener en cuenta que vamos a tener más de un juego de preferencias por lo que la clase Preferences.java tiene que tener un vector que las almacene. Por lo tanto también vamos a necesitar un método que almacene el juego de preferencias en el vector, y otro que nos permita recuperar un juego de preferencia por su tipo. Este sería su código:

public class Preferences {
	
	private static Vector<Preference> listPreferences = new Vector<Preference>();

	public void addPreference(Preference preference){
		listPreferences.addElement(preference);
	}
	
	public static Preference getPreferenceByType(String type){
		for (Preference preference:listPreferences){
			if (preference.getType().equals(type)){
				return preference;
			}
		}
		throw new IllegalArgumentException("El tipo de preferencia no existe");
	}
}

Ahora es cuando utilizamos el poder de Commons Digester para mapear el XML con nuestros objetos. Se puede hacer de dos formas: la primera utilizando un API en Java y la segunda a través de un XML donde se definen las reglas de parseo. Para este ejemplo vamos a optar por hacerlo utilizando el API de Java. Entonces nos creamos la siguiente clase:

package com.autentia.preferences;

import java.io.IOException;

import org.apache.commons.digester.Digester;
import org.xml.sax.SAXException;

public class PreferencesDigester {
	
	public Preferences digesterPreferences(String fileNamePreferences) throws IOException, SAXException{
		
		Digester digester = new Digester();
		digester.setValidating(false);
		
		digester.addObjectCreate("preferences/preference/datasource", Datasource.class);
		digester.addBeanPropertySetter("preferences/preference/datasource/url");
		digester.addBeanPropertySetter("preferences/preference/datasource/driver");
		digester.addBeanPropertySetter("preferences/preference/datasource/user");
		digester.addBeanPropertySetter("preferences/preference/datasource/password");
		
		digester.addObjectCreate("preferences/preference", Preference.class);
		digester.addSetProperties("preferences/preference");
		digester.addBeanPropertySetter("preferences/preference/idioma");
		digester.addSetNext("preferences/preference/datasource", "setDatasource");
		
		digester.addObjectCreate("preferences", Preferences.class);
		digester.addSetNext("preferences/preference", "addPreference");

		Preferences preferences = (Preferences)digester.parse(ClassLoader.getSystemResource(fileNamePreferences));
		
		return preferences;
		
	}

}

Como véis básicamente utilizamos cuatro funciones del API:

  • addObjectCreate(patrón, clase): nos permite crear una instancia de nuestra clase a partir del patrón que le indiquemos y que tiene que coincidir con la posición en el XML de la etiqueta que va a representar la clase.
  • addBeanPropertySetter(patrón): nos permite establecer el valor de la etiqueta que indica el patrón, al atributo de la clase. En caso de que el nombre de la etiqueta no coincidiera exactamente con el nombre del atributo, podríamos pasarle un segundo parámetro con el nombre del atributo donde va a establecer el valor de la etiqueta.
  • addSetProperties(patrón): nos permite mapear los atributos de la etiqueta, por ejemplo, type=»default», a los atributos de la clase automáticamente, por lo tanto no es necesario hacerlo explícitamente uno por uno.
  • addSetNext(patrón, método): nos permite indicar que método se va encargar de establecer el valor en el atributo de la clase

Una vez especificado el método de mapeo, nos basta con invocar a la función parse a la que le pasamos el fichero XML con las preferencias, para que aplique el mapeo y nos devuelva los objetos rellenos con los valores de las preferencias.

Para probar esto, no vamos a hacer una clase Main, lo que vamos a hacer es un test (realmente yo he ido aplicando TDD por lo que lo primero que he hecho ha sido el test). El código del test quedaría de esta forma:

package com.autentia.preferences;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.junit.Test;

public class PreferencesDigesterTest {

	@Test
	public void testDigesterPreferences() {
		try {
			new PreferencesDigester().digesterPreferences("preferences.xml");
			Preference preference = Preferences.getPreferenceByType("default");
			assertEquals("url-default", preference.getDatasource().getUrl());
			assertEquals("idioma-default", preference.getIdioma());
			assertEquals("default", preference.getType());
		} catch (Exception e) {
			fail(e.getMessage());
		}
	}

}

Decimos que es un sistema de preferencias configurable y extensible porque ahora sin tocar una sola línea de código Java, yo puedo añadir tantos juegos de preferencias como necesite, siempre respetando la estructura del XML.

De esta forma, si ahora ya añado el siguiente juego de preferencias:


	
		
			url-default
			driver-default
			user-default
			password-default
		
		idioma-default
	
	
		
			url-preproduccion
			driver-preproduccion
			user-preproduccion
			password-preproduccion
		
		idioma-preproduccion
	

Puedo modificar la clase de test para comprobar que efectivamente está recogiendo los dos juegos de preferencias.

package com.autentia.preferences;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.junit.Test;

public class PreferencesDigesterTest {

	@Test
	public void testDigesterPreferences() {
		try {
			new PreferencesDigester().digesterPreferences("preferences.xml");
			Preference preferenceDefault = Preferences.getPreferenceByType("default");
			assertEquals("url-default", preferenceDefault.getDatasource().getUrl());
			assertEquals("idioma-default", preferenceDefault.getIdioma());
			assertEquals("default", preferenceDefault.getType());
			
			Preference preferencePreproduccion = Preferences.getPreferenceByType("preproduccion");
			assertEquals("url-preproduccion", preferencePreproduccion.getDatasource().getUrl());
			assertEquals("idioma-preproduccion", preferencePreproduccion.getIdioma());
			assertEquals("preproduccion", preferencePreproduccion.getType());
			
		} catch (Exception e) {
			fail(e.getMessage());
		}
	}

}

4. Conclusiones

No me extraña que sea una librería tan utilizada por otras, la verdad es que hace bastante fácil el mapeo de XML a clases de Java. Una pena que no haya forma (o yo no la he encontrado, si es así me ponéis un comentario) de que se puedan mapear automáticamente las etiquetas que están dentro de otras, como en el caso del datasource, de igual forma que sí lo hace con las propiedades de las etiquetas.

Saludos.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad