3.2. Jackson#
La otra estrategia para manipular el formato XML es traducirlo al modelo de objetos de Java, estrategia que ya seguimos con JSON y con YAML y que, como en el caso de estos otros dos formatos, también puede llevarse a cabo con Jackson, para lo cual necesitaremos jackson-dataformat-xml.
En este caso, usaremos también como ejemplo ilustrativo el XML sobre un
claustro de profesores. En principio, podemos modelar las dos entidades
existentes (claustro y profesor) con esta clase Claustro
:
public class Claustro {
private String centro;
private Profesor[] plantilla;
public Claustro() {}
public Claustro(String centro, Profesor[] plantilla) {
setCentro(centro);
setPlantilla(plantilla);
}
public String getCentro() { return centro; }
public void setCentro(String centro) { this.centro = centro; }
public Profesor[] getPlantilla() { return plantilla; }
public void setPlantilla(Profesor[] plantilla) { this.plantilla = plantilla; }
public String toString() {
return String.format("%s -- %s", centro, Arrays.toString(plantilla));
}
}
y esta otra Profesor
:
public class Profesor {
private String id;
private String sustituye;
private String casillero;
private String apelativo;
private String nombre;
private String apellidos;
private String departamento;
public Profesor() {}
// Deberíamos tratar "casillero" y "sustituye"...
public Profesor(String id, String apelativo, String nombre, String apellidos, String departamento, String nacimiento) {
setId(id);
setApelativo(apelativo);
setNombre(nombre);
setApellidos(apellidos);
setDepartamento(departamento);
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getSustituye() { return sustituye; }
public void setSustituye(String sustituye) { this.sustituye = sustituye; }
public String getCasillero() { return casillero; }
public void setCasillero(String casillero) { this.casillero = casillero; }
public String getApelativo() { return apelativo; }
public void setApelativo(String apelativo) { this.apelativo = apelativo; }
public String getNombre() { return nombre; }
public void setNombre(String nombre) { this.nombre = nombre; }
public String getApellidos() { return apellidos; }
public void setApellidos(String apellidos) { this.apellidos = apellidos; }
public String getDepartamento() { return departamento; }
public void setDepartamento(String departamento) { this.departamento = departamento; }
public String toString() {
return String.format("%s, %s [%s] (%s)", apellidos, nombre, id, departamento);
}
}
Prudencia
Se ha definido el departamento como una cadena por simplificar el código. En puridad, el departamento debería ser un Enum y, en ese caso, habría que hacer un traductor.
3.2.1. Escritura#
Antes de comenzar a exponerla repasemos algunas diferencias significativas respecto a JSON:
En XML la información se proporciona mediante nodos elementos, pero también mediante nodos atributo. En cambio, en el modelo de objetos sólo existen los atributos del objeto. Por tanto, debe existir un modo de indicar cuándo quiere mapearse un atributo de objeto a elemento y cuando a atributo XML.
La conversión de una campo que fuera una secuencia JSON al modelo de objetos era evidente: un atributo del objeto que sea una lista o un array. En cambio, en XML esto no es tan claro y el ejemplo lo ilustra. Los profesores del claustro son conceptualmente una secuencia, pero no están contenidos dentro todos ellos dentro de un elemento hijo de claustro, sino que son todos hijos inmediatos de claustro. Dicho de otra forma, no nos encontramos con esto:
sino con esto otro:
En XML el orden de los nodos elemento importa y podría ser que nos interesara predefinirlo.
Teniendo presente esto, podemos alterar las clases anteriores del siguiente modo:
@JacksonXmlRootElement(localName = "claustro")
public class Claustro {
@JacksonXmlProperty(isAttribute = true)
private String centro;
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "profesor")
private Profesor[] plantilla;
public Claustro() {}
public Claustro(String centro, Profesor[] plantilla) {
setCentro(centro);
setPlantilla(plantilla);
}
public String getCentro() { return centro; }
public void setCentro(String centro) { this.centro = centro; }
public Profesor[] getPlantilla() { return plantilla; }
public void setPlantilla(Profesor[] plantilla) { this.plantilla = plantilla; }
public String toString() {
return String.format("%s -- %s", centro, Arrays.toString(plantilla));
}
}
public class Profesor {
@JacksonXmlProperty(isAttribute = true)
private String id;
@JacksonXmlProperty(isAttribute = true)
private String sustituye;
@JacksonXmlProperty(isAttribute = true)
private String casillero;
private String apelativo;
private String nombre;
private String apellidos;
private String departamento;
public Profesor() {}
// Deberíamos tratar "casillero" y "sustituye"...
public Profesor(String id, String apelativo, String nombre, String apellidos, String departamento, String nacimiento) {
setId(id);
setApelativo(apelativo);
setNombre(nombre);
setApellidos(apellidos);
setDepartamento(departamento);
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getSustituye() { return sustituye; }
public void setSustituye(String sustituye) { this.sustituye = sustituye; }
public String getCasillero() { return casillero; }
public void setCasillero(String casillero) { this.casillero = casillero; }
public String getApelativo() { return apelativo; }
public void setApelativo(String apelativo) { this.apelativo = apelativo; }
public String getNombre() { return nombre; }
public void setNombre(String nombre) { this.nombre = nombre; }
public String getApellidos() { return apellidos; }
public void setApellidos(String apellidos) { this.apellidos = apellidos; }
public String getDepartamento() { return departamento; }
public void setDepartamento(String departamento) { this.departamento = departamento; }
public String toString() {
return String.format("%s, %s [%s] (%s)", apellidos, nombre, id, departamento);
}
}
Hemos utilizados anotaciones sobre las clases para:
Que la etiqueta raíz sea claustro y no Claustro.
Que los atributos centro e id se transcriban como atributos XML, no como un nodos elemento.
Que la etiqueta sea profesor y no plantilla.
Que la lista de profesores este directamente incluida dentro de claustro y no dentro de un hijo de claustro (repásese la segunda diferencia significativa que se acaba de enumerar).
Nota
En realidad, cuando no se prescinde del envoltorio, el elemento en que se incluyen todos los componentes de la secuencia tiene el mismo nombre que los propios componentes (
profesor
en este caso). Si quisiéramos cambiarlos, podríamos haber escrito:@JacksonXmlElementWrapper(localName = "profesores") @JacksonXmlProperty(localName = "profesor") private Profesor[] plantilla;
Truco
Si quisiéramos modificar el orden en que se vuelcan en el XML los
atributos de una clase, podríamos usar @JsonPropertyOrder
:
Por supuesto, la anotación también es válida cuando se genera el formato JSON, pero no suele tener importancia.
Por último queda generar la salida en el programa principal, que es básicamente igual que la escritura en el caso de JSON:
Path archivo = Path.of(System.getProperty("java.io.tmpdir"), "claustro.xml");
Claustro claustro = new Claustro(
"IES Castillo de Luna",
new Profesor[] {
new Profesor("p1", "Paco", "Francisco", "Calderón Márquez", "Inglés"),
new Profesor("p2", "Loli", "Dolores", "Fuertes de Barriga", "Francés")
}
);
ObjectMapper mapper = new XmlMapper()
.configure(ToXmlGenerator.Feature.WRITE_XML_DECLARATION, true)
.enable(SerializationFeature.INDENT_OUTPUT); // Salida "bonita".
try (
OutputStream st = Files.newOutputStream(archivo);
OutputStreamWriter sw = new OutputStreamWriter(st);
)
{
mapper.writeValue(sw, claustro);
}
catch(IOException err) {
err.printStackTrace();
}
También podríamos haber generado una cadena con la salida:
try {
String contenido = mapper.writeValueAsString(claustro);
System.out.println(contenido);
}
catch(IOException err) {
err.printStackTrace();
}
Nota
Si plantilla fuera un ArrayList
en vez de un array, el codigo
encargado de serializar en formato XML sería el mismo. Lo mismo puede
afirmarse en la lectura, que se verá a continuación.
Para traducir tipos no primitivos, basta hacer exactamente lo mismo que con JSON.
3.2.2. Lectura#
Habiendo definido las clases con anotaciones como en el apartado anterior, la lectura del formato es prácticamente la misma que para JSON:
Path archivo = Path.of(System.getProperty("user.home"), "claustro.xml");
ObjectMapper mapper = new XmlMapper();
try (
InputStream st = Files.newInputStream(ruta);
InputStreamReader sr = new InputStreamReader(st);
) {
Claustro claustro = mapper.readValue(sr, Claustro.class);
System.out.println(claustro);
}
catch(IOException err) {
err.printStackTrace();
}