En la primera entrega del tutorial vimos que se podía personalizar la generación de código desde un esquema XSD. En este capítulo vamos a ver que también se pueden personalizar el marshalling/umarshalling que Castor XML realiza por defecto mediante el uso de ficheros XML. En concreto, vamos a personalizar el marshalling/unmarshalling del modelo de clases del segundo proyecto de ejemplo del capítulo anterior.fieldhand
Lo más sencillo será copiar ese ejemplo y denominarlo en el pom.xml como pruebasCastor-31. Asimismo, hay que cambiar en las rutas de paquetes pruebas22 por pruebas31 y crear el directorio /src/main/resources que en este capítulo sí que es necesario. Tras esto, se importaría el proyecto en Eclipse tras el correspondiente mvn eclipse:eclipse.Nota: descargar el proyecto de ejemplo aquí.
Usando nuestro modelo de clases equipo, vamos a ver un mapeo de ejemplo:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Mapping DTD Version 1.0//EN" "http://castor.org/mapping.dtd"> <mapping> <description>Mapeo de ejemplo</description> <class name="com.danielme.blog.castorxml.modeloequipo.Equipo" auto-complete="true"> <description>mapeo para la clase Equipo</description> <map-to xml="equipoXML" /> <field name="id"> <bind-xml name="identificador" node="attribute" /> </field> <field name="nombre" transient="true" /> <field name="entrenador"> <bind-xml name="entrenadorXML" /> </field> </class> <class name="com.danielme.blog.castorxml.modeloequipo.Miembro" auto-complete="true"> <description>mapeo para la clase Miembro. Para aplicarlo en las clases hijas no olvidar definir el atributo extends.</description> <field name="id"> <bind-xml name="identificador" node="attribute" /> </field> </class> <class name="com.danielme.blog.castorxml.modeloequipo.Jugador" auto-complete="true" extends="com.danielme.blog.castorxml.modeloequipo.Miembro"> </class> <class name="com.danielme.blog.castorxml.modeloequipo.Entrenador" auto-complete="true" extends="com.danielme.blog.castorxml.modeloequipo.Miembro"> </class> </mapping>
El elemento raiz del XML es mapping, y dentro tendremos:
- description: opcional, con información sobre el contenido del fichero.
- class: define el mapeo para una clase. Los atributos que admite son:
- name: nombre completo de la clase.
- extends: nombre completo de la clase padre sólo si esta clase está definida en un fichero de mapeo.
- auto-complete: si es true, se incluirán en el marshalling/unmarshalling todos los campos con getter/setter, independientemente de que esos campos se definan en el fichero de mapeo. Si es false (valor por defecto), sólo se utilizarán para el marshalling/unmarshalling los campos de la clase definidos explícitamente en el fichero de mapeo. Generalmente se define como true y si hay campos a ignorar estos se definen en el fichero de mapeo mediante el atributo transient.
Dentro de la clase podemos tener los siguientes elementos:
- description: comentario específico para la clase.
- map-to: Por defecto, CastorXML representa cada clase con un elemento cuyo nombre es el de la clase. Para modificar este comportamiento, se puede usar map-to con los siguientes atributos (todos opcionales):
- xml : nombre del elemento XML correspondiente a la clase. Obligatorio.
- ns-uri
- ns-prefix
- field: cada campo de la clase cuyo mapeo se quiera personalizar se incluye dentro de un elemento field. Los atributos más importantes para este elemento son:
- name: nombre del campo en la clase. Es el único atributo obligatorio del field.
- transient: si es true, el campo se excluye del marshalling/unmarshalling. Por defecto es false.
- handler: lo usaremos más adelante.
En un field, el único elemento que puede aparecer es un bind-xml, que a través de sus atributos name y node establece el mapeo en XML del campo de la clase. Por defecto se utiliza el nombre del campo y se incluye como element.
Teniendo en cuenta esta breve descripción de los elementos y atributos más importantes, se puede comprender fácilmente el mapeo definido en el XML puesto anteriormente como ejemplo:
- El equipo se mapeará a un elemento de nombre equipoXML.
- Se mapearán todos los campos ya que todas las clases tienen el atributo autocomplete a true, con la excepción del nombre del equipo ya que es un campo de tipo transient.
- Los campos de nombre id tendrán por nombre identificador, y en el XML serán atributos del elemento correspondiente.
- Para que el mapeo de la clase Miembro se aplique a sus clases hijas, se ha establecido para estas clases el atributo extends.
El fichero con el mapeo vamos a llamarlo mapping.xml y a ubicarlo dentro del proyecto en la ruta /src/main/resources/mappings. Para poder utilizar este fichero, es necesario retocar la clase Main que estamos reutiliando del capítulo anterior. En esta ocasión, hay que utilizar la factoría org.exolab.castor.xml.XMLContext, proporcionándole el fichero de mapeo a través de la clase org.exolab.castor.mapping.Mapping, para crear instancias de las clases org.exolab.castor.xml.Marshaller y org.exolab.castor.xml.Unmarshaller. El método main queda así:
public static void main(String[] args) { String fichero = "exportacionEquipo.xml"; SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); Equipo equipo = equipoDePrueba(); try { // 1-.Se crea un mapping a partir del fichero de mapeo InputStream inputStream = Main.class.getResourceAsStream("/mappings/mapping.xml"); Mapping mapping = new Mapping(); mapping.loadMapping(new InputSource(inputStream)); // 2-.se crea una factoría XMLContext asociada al mapeo XMLContext context = new XMLContext(); try { context.addMapping(mapping); } catch (MappingException e) { e.printStackTrace(); } // 3-.obtención del marshaller con la factoría Marshaller marshaller = context.createMarshaller(); FileWriter writer = new FileWriter(fichero); marshaller.setWriter(writer); marshaller.marshal(equipo); // 4-. obtención del unmarshaller de la factoría Unmarshaller unmarshaller = context.createUnmarshaller(); FileReader fileReader = new FileReader(fichero); Equipo unmarshal = (Equipo) unmarshaller.unmarshal(fileReader); System.out.println("EQUIPO : " + unmarshal.getNombre()); System.out.println("entrenador: " + unmarshal.getEntrenador().getNombre() + " " + unmarshal.getEntrenador().getApellidos()); System.out.println("jugadores "); for (Jugador jugador: unmarshal.getJugadores()) { StringBuffer sb = new StringBuffer(" " + jugador.getDorsal() + ": " + jugador.getNombre() + " " + jugador.getApellidos()); if (jugador.getFechaNacimiento() != null) { sb.append(" ( " + sdf.format(jugador.getFechaNacimiento().getTime()) + " ) "); } System.out.println(sb); } } catch (IOException e1) { e1.printStackTrace(); } catch (MarshalException e2) { e2.printStackTrace(); } catch (ValidationException e3) { e3.printStackTrace(); } }
El marshalling dará como resultado el siguiente fichero en el que se puede comprobar la correcta aplicación de las reglas definidas en el mapping.xml:
<?xml version="1.0" encoding="UTF-8"?> <equipoXML identificador="1"> <entrenadorXML identificador="14"> <apellidos>García</apellidos> <nombre>Joaquín</nombre> </entrenadorXML> <jugadores xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" identificador="1" xsi:type="jugador"> <apellidos>Español</apellidos> <nombre>Juan</nombre> <estatura>178</estatura> <peso>74.3</peso> <dorsal>1</dorsal> </jugadores> <jugadores xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" identificador="2" xsi:type="jugador"> <apellidos>Pérez Pérez</apellidos> <nombre>Miguel</nombre> <estatura>174</estatura> <peso>71.1</peso> <dorsal>2</dorsal> </jugadores> </equipoXML>
Como resultado del unsmarshalling, veremos en la salida:
Conversiones de tipos
En el capítulo anterior vimos que los campos de tipo Calendar eran excluídos del proceso de marshalling/umarshalling, esto se debe a que Castor XML soporta el tipo java.util.Date pero no Calendar. Afortunadamente para este tipo de situaciones no tendremos que modificar nuestras clases ya que podemos realizar una implementación de la interfaz que Castor XML utiliza a la hora de obtener o modificar los campos de una clase: org.exolab.castor.mapping.FieldHandler. En nuestro caso, vamos a heredar de la implementación abstracta GeneralizedFieldHandler para crear un conversor de Calendar genérico, ya que si implementamos FieldHandler tendremos que trabajar directamente con nuestra clase Miembro que contiene el campo a tratar.
Creamos la clase CalendarFieldHandler en un nuevo paquete com.danielme.blog.castorxml.fieldhandlers. Vamos a implementar los métodos de tal modo que en el XML los campos de tipo Calendar sean cadenas con el formato dd-MM-yyyy.
package com.danielme.blog.castorxml.fieldhandlers; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import org.exolab.castor.mapping.GeneralizedFieldHandler; /** * Implementación genérica de FildHandler para los Calendar, formato español sólo fecha. * @author danielme.com * */ public class CalendarFieldHandler extends GeneralizedFieldHandler { /** * Formato de la fecha. */ private static final String FORMAT ="dd-MM-yyyy"; /** * Formateador de fechas. */ private static final SimpleDateFormat SDF = new SimpleDateFormat(FORMAT); /** * Devuelve un Calendar como String. */ @Override public Object convertUponGet(Object value) { String fecha = null; if (value != null) { Calendar calendar = (Calendar) value; fecha= SDF.format(calendar.getTime()); } return fecha; } /** * Devuelve un String como Calendar. */ @Override public Object convertUponSet(Object value) { Calendar calendar = null; try { Date parse = SDF.parse((String) value); calendar = new GregorianCalendar(); calendar.setTime(parse); } catch (ParseException e) { e.printStackTrace(); } return calendar; } /** * Tipo de clase que convierte este FieldHandler. */ @SuppressWarnings("rawtypes") @Override public Class getFieldType() { return Calendar.class; } }
Para usar este FieldHandler, primero hay que definirlo en el fichero mapping.xml justo antes de la definición de las clases:
<mapping> <description>Mapeo de ejemplo</description> <field-handler name="CalendarFieldHandler" class="com.danielme.blog.castorxml.fieldhandlers.CalendarFieldHandler"/> <class name="com.danielme.blog.castorxml.modeloequipo.Equipo" auto-complete="true">
Ahora, hay que indicar en el field el valor del atributo handler y el tipo primitivo (string) del campo en el XML:
<class name="com.danielme.blog.castorxml.modeloequipo.Miembro" auto-complete="true"> <description>mapeo para la clase Miembro. Para aplicarlo en las clases hijas no olvidar definir el atributo extends.</description> <field name="id"> <bind-xml name="identificador" node="attribute" /> </field> <field name="fechaNacimiento" type="string" handler="CalendarFieldHandler"/> </class>
Al ejecutar el proyecto, ya tendremos en el XML las fechas al realizar el marshalling
<?xml version="1.0" encoding="UTF-8"?> <equipoXML identificador="1"> <entrenadorXML identificador="14"> <fecha-nacimiento>02-02-1964</fecha-nacimiento> <apellidos>García</apellidos> <nombre>Joaquín</nombre> </entrenadorXML> <jugadores xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" identificador="1" xsi:type="jugador"> <fecha-nacimiento>10-03-1987</fecha-nacimiento> <apellidos>Español</apellidos> <nombre>Juan</nombre> <estatura>178</estatura> <peso>74.3</peso> <dorsal>1</dorsal> </jugadores> <jugadores xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" identificador="2" xsi:type="jugador"> <fecha-nacimiento>07-04-1985</fecha-nacimiento> <apellidos>Pérez Pérez</apellidos> <nombre>Miguel</nombre> <estatura>174</estatura> <peso>71.1</peso> <dorsal>2</dorsal> </jugadores> </equipoXML>
y en la salida
WordPress: debido a las limitaciones del servicio ofrecido por WordPress.com, el código de ejemplo está comprimido en formato zip pero con la extensión «.odt», por lo que hay que cambiar la extensión a «.zip» del fichero descargado o abrirlo directamente con el software adecuado.