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.
Para ilustrar el uso de la librería tomaremos con ejemplo una versión XML del archivo JSON que usamos en la unidad anterior:
<?xml version="1.0" encoding="UTF-8"?>
<centro>
<grupo nivel="1" etapa="2" grupo="D">
<tutor>
<nombre>Federico Arias Torres</nombre>
<especialidad>Inglés</especialidad>
</tutor>
<miembros>
<alumno nacimiento="15/05/2010">
<nombre>Matías Sánchez Aguado</nombre>
</alumno>
<alumno nacimiento="25/03/2010">
<nombre>María Bonet Periáñez</nombre>
</alumno>
</miembros>
</grupo>
<grupo nivel="2" etapa="2" grupo="A">
<tutor>
<nombre>Gertrudis Avellaneda Pérez</nombre>
<especialidad>Francés</especialidad>
</tutor>
<miembros>
<alumno nacimiento="02/10/2008">
<nombre>Marcela Venegas Pancorbo</nombre>
</alumno>
<alumno nacimiento="11/01/2009">
<nombre>Feliciano Martín Suárez</nombre>
</alumno>
</miembros>
</grupo>
</centro>
Nota
Obsérvese que directamente hemos traducido Etapa con el ordinal,
en vez de con las constantes de enumeración.
Nuestra intención es usar el modelo de datos que ya presentamos en la unidad anterior:
import java.util.Arrays;
public class Grupo {
private short nivel;
private Etapa etapa;
private char grupo;
private Tutor tutor;
private Alumno[] miembros;
public Grupo() {}
public Grupo(short nivel, String etapa, char grupo, Tutor tutor, Alumno[] miembros) {
setNivel(nivel);
setEtapa(etapa);
setGrupo(grupo);
setTutor(tutor);
setMiembros(miembros);
}
public short getNivel() {
return nivel;
}
public void setNivel(short nivel) {
this.nivel = nivel;
}
public String getEtapa() {
return etapa;
}
public void setEtapa(String etapa) {
this.etapa = etapa;
}
public char getGrupo() {
return grupo;
}
public void setGrupo(char grupo) {
this.grupo = grupo;
}
public Tutor getTutor() {
return tutor;
}
public void setTutor(Tutor tutor) {
this.tutor = tutor;
}
public Alumno[] getMiembros() {
return miembros;
}
public void setMiembros(Alumno[] miembros) {
this.miembros = miembros;
}
public String nombre() {
return String.format("%dº%s-%c", nivel, etapa, grupo);
}
@Override
public String toString() {
return String.format("%s (%s): %s", nombre(), tutor, Arrays.toString(miembros));
}
}
public class Tutor {
private String nombre;
private String especialidad;
public Tutor() {}
public Tutor(String nombre, String especialidad) {
setNombre(nombre);
setEspecialidad(especialidad);
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getEspecialidad() {
return especialidad;
}
public void setEspecialidad(String especialidad) {
this.especialidad = especialidad;
}
@Override
public String toString() {
return nombre;
}
}
public class Alumno {
private String nombre;
private LocalDate fechaNacimiento;
public Alumno() {}
public Alumno(String nombre, int edad) {
setNombre(nombre);
setEdad(edad);
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public LocalDate getFechaNacimiento() {
return fechaNacimiento
}
public int getEdad() {
return Perido.between(fechaNacimiento, LocalDate.now()).getYears();
}
public void setFechaNacimiento(LocalDate nacimiento) {
this.fechaNacimiento = nacimiento;
}
@Override
public String toString() {
return String.format("%s: %d", nombre, edad);
}
}
a los que habría que añadir el enum Etapa.
3.2.1. Preparativos#
Para lograr traducir entre el modelo de datos y XML necesitamos hacer varias consideraciones:
Al margen de lo anterior, las anotaciones suelen ser necesarias en XML porque en este formato la información se proporciona mediante nodos elementos, pero también i 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.
En XML el orden de los nodos elemento importa. Por eso es importante saber especificar en qué orden queremos que se escriban los atributos si estos generan nodos elemento. Se verá más adelante.
XML introduce otra dificultad: en JSON una secuencia o es el elemento raíz o el valor de un campo. En cambio, en XML la casuística es distinta, porque no existen las secuencias por se, lo que existen son los nodos elemento que contiene varios nodos elementos del mismo tipo. De este modo nos podemos encontrar con:
Elementos raíz sin interés que sólo existen porque necesitan contener a la secuencia de elementos, como es el caso que nos ocupa. Obsérvese que en esta version XML hemos tenido que crear un elemento raíz
<centro>, que no existía en la versión JSON.La secuencia puede estar mezclada con otro elementos. Por ejemplo:
Obsérvese que la secuencia de profesores está contenida directamente dentro de centro, a diferencia de lo que ocurriría en JSON, y además, centro contiene otros dos nodos (director y nombre).
3.2.1.1. Anotaciones#
Las particularidades de XML obligan a anotar las clases del modelo. Empecemos
por Alumno:
private static abstract class AlumnoMixin {
@JsonProperty("nacimiento") // Sólo tiene efecto en JSON.
@JacksonXmlProperty(localName = "nacimiento", isAttribute = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd/MM/yyyy")
private LocalDate fechaNacimiento;
@JsonIgnore
public abstract int getEdad();
}
En este caso debemos cambiar el nombre, pero no puede hacerse a través de la
anotación @JsonProperty (que sobra totalmente), sino de
@JacksonXmlProperty. De hecho, se ha mantenido la anotación
@JsonProperty en previsión de que se quisiera también traducir a JSON, si
no es el caso, se puede eliminar por completo. La información, además, se
muestra en forma de nodo atributo, por lo es necesario especificarlo usando
también @JacksonXmlProperty. Por otro lado, la anotación @JsonFormat no
es ninguna novedad: ya la necesitamos en JSON para definir cómo escibir la
fecha como cadena.
Pasemos a Grupo:
@JsonRootName("grupo")
@JsonPropertyOrder({"nivel", "etapa", "grupo", "tutor", "miembros"})
private static abstract class GrupoMixin {
@JacksonXmlProperty(isAttribute = true)
private short nivel;
@JacksonXmlProperty(isAttribute = true)
private Etapa etapa;
@JacksonXmlProperty(isAttribute = true)
private char grupo;
@JacksonXmlElementWrapper(localName = "miembros", useWrapping = true)
@JacksonXmlProperty(localName = "alumno")
private Alumno[] miembros;
}
Aquí tenemos varias novedades:
Nos vemos obligados a usar
@JsonRootNamepara que la etiqueta sea<grupo>y no<Grupo>.Como en XML es importante (al menos para los nodos elementos el orden), nos vemos obligados a definirlo con
@JsonPropertyOrder.Prudencia
Es probable que para
Tutornecesitemos crear una claseTutorMixinsólo para asegurarnos de que<nombre>aparezca antes que<especialidad>.Hay tres atributos que son nodos atributo, así que debemos declararlo.
El elemento
miembroscontiene elementos<alumno>. Sin anotación, la traducción sería así:@JacksonXmlElementWrapperinfluye sobre el elemento contenedor de la secuencia. En principio, la traducción es la predeterminada, pero al haber usado@JacksonXmlPropertyla hemos alterado, así que es necesaria para que miembros siga siendo miembros. Lo que sí podríamos habernos ahorrado es especificaruseWrappingya que por defecto está atruey significa que el elemento contenedor está presente. Podría no existir si hubiéramos decidido incluir en el XML la secuencia de alumnos directamente dentro de<centro>en cuyo caso deberíamos haberlo puesto afalse. Por otro lado, tenemos que cambiar el nombre de los elementos demiembrosaalumno, por lo que necesitamos usar@JacksonXmlProperty.
3.2.1.2. Envoltorio#
Otra gran diferencia respecto a la traducción a JSON es que en XML nos hemos
visto obligados a crear un elemento contenedor de la secuencia llamado
<centro>. No contiene más información que la propia secuencia, por lo que en
el modelo de datos no se traduce en una nueva clase. En Java, simplemente,
existirá un dato de tipo Grupo[] o List<Grupo>. Si intentáramos traducir
directamente estos tipos de datos a XML, la librería no nos permitiría definir
el nombre de la etiqueta para el envoltorio (queremos que sea <centro>) ni
tampoco qué etiqueta tendría cada uno de los elementos de la secuencia (utiliza
el predeterminado <item>) por lo que obtendríamos una salida semejante a
esta:
<List1> <!-- O alguna etiqueta semejante -->
<item nivel="1" etapa="2" grupo="D">
<!-- Elementos del grupo (nombrados correctamente) -->
</item>
<!-- Otro grupos con la etiqueta item -->
</List1>
Para solucionar esto tenemos dos soluciones:
Clase envoltorio auxiliar
@JsonRootName("centro")
private class GruposWrapper {
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "grupo")
public Grupo[] grupos;
public GruposWrapper(Grupo[] grupos) {
this.grupos = grupos;
}
}
Las particularidades de este código son las siguientes:
Necesitamos definir una clase auxiliar que represente al envoltorio
<centro>. Para ello hemos creado una claseGrupoWrapper.En este caso, cada elemento
<grupo>sí está directamente incluido en<centro>por lo que sí es necesaria la anotación@JacksonXmlElementWrapperconuseWrappingafalse.El nombre del elemento es
<grupo>, no<grupos>, así que es necesaria también la anotación@JacksonXmlProperty.
Una vez definida la clase, basta con que sea una instancia de ella la que se escriba (o lea):
// Escritura
mapper.writeValue(new GruposWrapper(grupos));
// Lectura
Grupo[] grupos = mapper.readValue(st, GruposWrapper.class).grupos;
Mapa envoltorio
Consiste en improvisar un mapa con una única clave que sea el nombre que queremos darle a los elementos de la secuencia:
Map<String, Object> centro = Map.of("grupo", grupos); // Se usará <grupo>, no <item>
ObjectWriter writer = mapper.writer()
.withRootName("centro"); // Indicamos que se use <centro>, no <Map1>
// Escritura
writer.writeValues(st, centro);
// Lectura (no tiene dificultad)
mapper.readValues(st, Grupo[].class);
3.2.1.3. Traductores#
En principio, como en el caso de JSON necesitamos el traductor para Etapa.
Debería ser idéntico y usarse igual, pero hay una pequeña diferencia:
public static class Deserializer extends ValueDeserializer<Etapa> {
@Override
public Etapa deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
try {
int etapa = Integer.parseInt(p.getString()); // No, p.getIntValue();
return Etapa.indexOf(etapa);
}
catch(NumberFormatException e) {
//logger.error("No se ha podido convertir {} a número",
//p.getString());
}
catch(ArrayIndexOutOfBoundsException e) {
//logger.error("No existe la etapa {}", p.getString());
}
return null;
}
}
Prudencia
Si no hemos definido una gramática XSD para el XML, todo son
cadenas, así que no podemos usar .getIntValue(). Lo cierto es que esta
versión también funciona para traducir el JSON, así que podríamos haberla
implementado directamente.
3.2.2. Escritura#
Generar la salida en el programa principal es básicamente igual que la escritura en el caso de JSON:
Path archivo = Path.of(System.getProperty("java.io.tmpdir"), "grupos.xml");
Grupo[] grupos = new Grupo[] {
new Grupo(1, Etapa.ESO, 'A', New Tutor("Florencio Vázquez Méndez", "Inglés"), {
new Alumno("María Isabel Menézdez Roldán", LocalDate.of(2010, 12, 3)),
new Alumno("Francisco Macías Bermejo", LocalDate.of(2011, 1, 16))
}),
new Grupo(2, Etapa.ESO, 'C', New Tutor("Petra Laynez Beltrán", "Francés"), {
new Alumno("Clara Ribera Paterna", LocalDate.of(2009, 12, 12)),
new Alumno("Tania Macías Cordero", LocalDate.of(2010, 3, 5))
})
};
XmlFactory factory = XmLFactory.builder()
.enable(XmlWriteFeature.WRITE_XML_DECLARATION)
.build();
MapperBuilder<?, ?> builder = XmlMapper.builder(factory)
.enable(SerializationFeature.INDENT_OUTPUT)
.addMixIn(Grupo.class, GrupoMixin.class);
ObjectMapper mapper = builder.build();
try (OutputStream st = Files.newOutputStream(archivo)) {
mapper.writeValue(st, new GruposWrapper(grupos)); // Puede generar JacksonException
} catch(JacksonException | IOException err) {
err.printStackTrace();
}
También podríamos haber generado una cadena con la salida:
try {
String contenido = mapper.writeValueAsString(new GruposWrapper(grupos));
System.out.println(contenido);
} catch(JacksonException 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.3. 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"), "grupos.xml");
MapperBuilder<?, ?> builder = XmlFactory.builder()
.addMixIn(Grupo.class, GrupoMixin.class)
ObjectMapper mapper = builder.build();
try (InputStream st = Files.newInputStream(ruta)) {
Grupos[] grupos = mapper.readValue(st, GruposWrapper.class).grupos;
System.out.println(grupos);
} catch(JacksonIOException | IOException err) {
err.printStackTrace();
}