Sérialisation/Désérialisation XML avec Camel

Je vais présenter ici quelques moyens pour faire de la sérialisation/désérialisation en XML avec Camel, selon différents besoins :

  • mapping "simple" de et/ou vers des pojos java existants ;
  • mapping à partir d'un fichier XSD fourni, ou mapping à partir d'un modèle Java bindé.

Les sources (mavenisées) des exemples sont fournies à la fin de ce billet.

Tout d'abord, partons d'une configuration de base :

Nos pojos existants :

EssaiClinique.java :

/** Description d'un essai.
 * @author NETAPSYS
 * @date 15 mars 2011
 * @version $Revision$ $Date$
 */
public class EssaiClinique implements Serializable {
    /** Serial ID. */
    private static final long serialVersionUID = -3188027620920487407L;

    /** Nom de l'essai. */
    private String nom;

    /** Nombre de patients pour l'essai. */
    private Integer nbPatient;

    /** Date de début de l'essai. */
    private Date dateDebut;

    /** Liste des médicaments nécessaires à l'essai. */
    private List<Medicament> medicaments;

    /** {@inheritDoc} */
    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer();
        sb.append("
EssaiClinique " + this.nom + " du " + this.dateDebut);
        for (final Medicament medicament : this.medicaments) {
            sb.append(medicament);
        }
        return sb.toString();
    }

    // Getters & Setters

Medicament.java :

/** Description d'un médicament.
 * @author NETAPSYS
 * @date 15 mars 2011
 * @version $Revision$ $Date$
 */
public class Medicament implements Serializable {
    /** Serial ID. */
    private static final long serialVersionUID = 3111655483593665106L;

    /** Nom du médicament. */
    private String nom;

    /** Concentration du médicament. */
    private BigDecimal concentration;

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return "
Medicament " + this.nom + " à concentration de " + this.concentration;
    }

    // Getters & Setters

Contexte camel :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd">

	<camelContext trace="false" xmlns="http://camel.apache.org/schema/spring">
		<packageScan>
			<package>com.netapsys.routes</package>
		</packageScan>
	</camelContext>
</beans>

Classe de test permettant d'appeller les routes :

public class RoutesTest extends CamelSpringTestSupport {
    /** {@inheritDoc} */
    @Override
    public void setUp() throws Exception {
        super.setUp();
    }

    /** {@inheritDoc} */
    @Override
    protected AbstractXmlApplicationContext createApplicationContext() {
        return new ClassPathXmlApplicationContext("/config/camel-context.xml");
    }
}

Mapping Pojo/XML

Camel-xstream

Mise en place

Librairie

Si le besoin est uniquement de faire de l'export/import xml de pojos Java existant (pour faire de la sauvegarde, ou partager des données pour une application répartie par exemple), le plus simple est d'utiliser le serialiseur Xstream.
Pour cela il faut commencer par inclure camel-xstream au classpath. Avec Maven il faut rajouter la dépendance :

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-xstream</artifactId>
</dependency>

Camel

Dans la définition de notre contexte camel, il faut rajouter le format qui sera utilisé dans la route :

<dataFormats>
    <xstream id="xstream-utf8" encoding="UTF-8"/>
</dataFormats>

La route permettant la sérialisation/désérialisation :

/** Routes utilisant le marshaller xstream.
 * @author NETAPSYS
 * @date 16 mars 2011
 * @version $Revision$ $Date$
 */
public class XStreamRoutes extends RouteBuilder {
    /** @see org.apache.camel.builder.RouteBuilder#configure() */
    @Override
    public void configure() throws Exception {
        this.from("direct:xstream-marshal")
            .marshal("xstream-utf8")
            .to("file:/testCamel?fileName=essais-xstream.xml");

        this.from("direct:xstream-unmarshal")
	    .unmarshal("xstream-utf8")
            .to("log:com.netapsys.routes?level=DEBUG");
    }
}

Test

Et enfin le test de nos routes :

/** Appelle les routes pour la sérialisation/désérialisation.
 * @throws Exception Exception lors de l'exécution des routes.
 */
@Test
public void test() throws Exception {
    final EssaiClinique essai = this.getEssaiForTest();

    this.template.sendBody("direct:xstream-marshal", essai);

    Thread.sleep(2000);

    this.template.sendBody("direct:xstream-unmarshal", new File("/testCamel/essais-xstream.xml"));

    Thread.sleep(2000);
}

/** Créé un Essai pour le test.
 * @return Essai
 */
private EssaiClinique getEssaiForTest() {
    // Créé un essai de test avec 2 médicaments.
    // Pour bien gérer l'export des concentrations, ne pas oublier de paramétrer le MathContext :
    final Medicament m1 = new Medicament();
    m1.setConcentration(new BigDecimal(1.2345, MathContext.DECIMAL32));
}

Résultats

Sérialisation

En éditant le fichier XML créé (/testCamel/essais-xstream.xml), on peut voir qu'il correspond bien à nos attentes :

<?xml version='1.0' encoding='UTF-8'?>
<com.netapsys.model.EssaiClinique>
    <nom>Essai n°1</nom>
    <nbPatient>3</nbPatient>
    <dateDebut>2011-03-16 18:10:08.270 CET</dateDebut>
    <medicaments>
        <com.netapsys.model.Medicament>
            <nom>Médicament 1</nom>
            <concentration>1.234500</concentration>
        </com.netapsys.model.Medicament>
        <com.netapsys.model.Medicament>
            <nom>Médicament 2</nom>
            <concentration>2.000100</concentration>
        </com.netapsys.model.Medicament>
    </medicaments>
</com.netapsys.model.EssaiClinique>

On peut voir que par défaut XStream nomme les noeuds avec le nom complet des classes, ce qui lui permet de s'y retrouver lors de la désérialisation.

Désérialisation

En regardant les logs, on peut voir que les informations on bien été récupérées :

2011/03/16 18:29:19 DEBUG service.process(74) : Exchange[ExchangePattern:InOnly, BodyType:com.netapsys.model.EssaiClinique, Body:
EssaiClinique Essai n°1 du Wed Mar 16 18:29:16 CET 2011
Medicament Médicament 1 à concentration de 1.234500
Medicament Médicament 2 à concentration de 2.000100]

Mapping à partir d'un fichier XSD / Modèle Java bindé

Il peut être nécessaire de faire évoluer une application pour exporter des données en XML, tout en respectant un fichier XSD fourni.
Dans ce cas le sérialiseur JAXB va être utilisé, et il faudra préalablement passer par une conversion en java entre nos pojos de notre modèle métier et le modèle qui sera généré. Tout d'abord, il va falloir commencer par générer le modèle Java correspondant au XSD.

Ce type de sérialisation/désérialisation peut aussi être utilisé si nos pojos sont déjà bindés.

Camel-jaxb

1-Génération du modèle

XJC

XJC est un outil inclus dans le JDK qui va nous permettre de générer le modèle Java à partir d'un fichier XSD.
Le fichier XSD de référence, essais.xsd, est dans le projet camel-jaxb, lié à ce billet.
XJC a besoin d'un fichier de binding, qui contient principalement le chemin vers le fichier XSD, et le package où seront stockées les classes créées.

binding.xjb :

<?xml version="1.0" encoding="UTF-8"?>
<bindings schemaLocation="../essais.xsd" xmlns="http://java.sun.com/xml/ns/jaxb" version="2.1">
    <schemaBindings>
        <package name="com.netapsys.model.xml" />
    </schemaBindings>
    <globalBindings>
        <serializable uid="100" />
    </globalBindings>
</bindings>

Commande à exécuter :

"C:\Program Files\Java\jdk1.6.0_20\bin\xjc.exe" -d D:\Projet\camel-jaxb\src\main\java\ -extension D:\Projet\camel-jaxb\src\main
esources\essais.xsd -b D:\Projet\camel-xsd\src\main
esources\schema\binding.xjb

Explication succincte des paramètres :

  • -d : répertoire où se situe le package spécifié dans le fichier xjb ;
  • -extension : chemin vers le fichier xsd (attention, le fait de mettre un chemin relatif peut être source de bug lors de l'exécution de la commande) ;
  • -b : emplacement du fichier xjb (même remarque que ci-dessus).

Modèle

Les traces dans les logs montrent les classes créées, qui sont visibles, dans le package spécifié dans le fichier de binding. Pour plus d'informations sur les annotations utilisées, je vous invite à consulter ce post.
Pour notre exemple, ces classes générées correspondront à un modèle métier déjà bindé.

2-Mise en place

Librairie

Pour effectuer la sérialisation/désérialisation, il faut inclure la librairie camel-jaxb au classpath. Avec maven, il faut rajouter la dépendance :

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-jaxb</artifactId>
</dependency>

Camel

Il n'y a pas de modification à apporter au contexte Camel.
Écrivons la route que nous appellerons pour effectuer la sérialisation/désérialisation :

/** @see org.apache.camel.builder.RouteBuilder#configure() */
@Override
public void configure() throws Exception {
    final DataFormat dataFormat = new JaxbDataFormat("com.netapsys.model.xml");

    this.from("direct:jaxb-marshal")
          .marshal(dataFormat)
          .to("file:/testCamel?fileName=essais-jaxb.xml");

    this.from("direct:jaxb-unmarshal").unmarshal(dataFormat).to("log:logger.service?level=DEBUG");
}

Cette route ressemble beaucoup à la première, à la différence de l'utilisation d'une implémentation de DataFormat spécifique à JAXB, qui connaît l'emplacement du modèle à utiliser.

Test

Là aussi le test ressemble beaucoup au précédent :

/** Test de sérialisation/désérialisation.
 * @throws Exception Exception lors de l'exécution des routes.
 */
@Test
public void test() throws Exception {
    final XMLEssaiClinique essai1 = this.getEssaiForTest();

    this.template.sendBody("direct:jaxb-marshal", essai1);

    Thread.sleep(2000);

    this.template.sendBody("direct:jaxb-unmarshal", new File("/testCamel/essais-jaxb.xml"));

    Thread.sleep(2000);
}

/** Créé un Essai pour le test.
 * @return Bean du modèle XML pour le test.
 * @throws DatatypeConfigurationException Erreur lors de la conversion des données.
 */
private XMLEssaiClinique getEssaiForTest() throws DatatypeConfigurationException {
    // Attention à la gestion de la date :
    final XMLEssaiClinique essai1 = new XMLEssaiClinique();
    essai1.setDateDebut(DatatypeFactory.newInstance()
                                          .newXMLGregorianCalendar(new GregorianCalendar()));
    // Valorisation de l'essai avec deux médicaments.
}

La principale différence ici est le type des objets passés à la route. Il faut en effet passer des objets que JAXB connaît. Dans une application existante, cela implique la mise en place d'un mapping java entre les pojos existants (EssaiClinique.java et Medicament.java dans notre cas), vers les pojos du modèle XML générés par XJC.

3-Résultats

Sérialisation

Ici encore, le fichier XML correspond à nos attentes :

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<XMLEssaiClinique>
    <nom>Essai n°1</nom>
    <nbPatient>3</nbPatient>
    <dateDebut>2011-03-17+01:00</dateDebut>
    <medicaments>
        <XMLMedicament>
            <nom>Médicament 1</nom>
            <concentration>1.234500</concentration>
        </XMLMedicament>
        <XMLMedicament>
            <nom>Médicament 2</nom>
            <concentration>2.000100</concentration>
        </XMLMedicament>
    </medicaments>
</XMLEssaiClinique>

Désérialisation

Toujours en étudiant les logs, on peut voir que les objets ont bien été créés. Pour les besoins du test, j'ai surchargé la méthode toString() dans les classes du modèle XML générées par xjc :

XMLEssaiClinique Essai n°1 du 2011-03-17+01:00
XMLMedicament Médicament 1 à concentration de 1.234500
XMLMedicament Médicament 2 à concentration de 2.000100

Conclusion

Nous avons présenté deux méthodes pour faire de la sérialisation/désérialisation d'objets en XML, chacune des méthodes répondant à un besoin différent. Les deux premiers avantages que je vois à l'utilisation de Camel sont la simplicité de la mise en place du système, tout en gardant de nombreuses possibilités de personnalisation grâce aux différentes annotations disponibles.

Liens :

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.