Java et XML - JAXB

Chapitres traités   

Le XML est aujourd'hui un format d'échange de données très utilisé. Il possède de nombreux avantages : il est standard, simple, et surtout facile à lire. Il peut être lu par un homme, mais ce qui le plus intéressant c'est qu'il peut être lu par un ordinateur. En effet, la puissance du XML repose sur le fait qu'il peut être analysé par un programme et le contenu de ce flux est compris par la machine. La structure de ce langage permet en effet de comprendre les relations entre toutes ces données.

De ce fait, les programmeurs ont réalisé de nombreuses API permettant d'accéder aux données XML à travers leurs langages favoris (DOM et SAX par exemple en Java). Néanmoins, ces dernières ont des inconvénients. Tous d'abord, il faut étudier et apprendre une nouvelle API. Puis, ils sont trop généraux. C'est-à-dire que le programmeur doit adapter le code à son application à chaque fois qu'il veut accéder à des données XML. Ensuite, il faut qu'il crée lui-même toutes les classes permettant de gérer ces nouvelles données dans son programme, cela lui prend donc beaucoup de temps.

Pour remédier à ces inconvénients, il existe le « Data Binding » également appelé en français : association de données. En java, Sun a réalisé une API nommée JAXB (Java Architecture for XML Binding) pour simplifier les processus de transformation d’objets Java en fichier XML, et de fichier XML en objets Java.

JAXB est une spécification qui permet de faire correspondre un document XML à un ensemble de classes et vice et versa via des opérations de sérialisation/désérialisation nommée marshaling/unmarshaling.


JAXB permet aux développeurs de manipuler un document XML sans à avoir connaître XML ou la façon dont un document XML est traitée comme cela est le cas avec SAX, DOM ou STAX.

La manipulation du document XML se fait en utilisant des objets précédemment générés à partir d'une DTD pour JAXB 1.0 et d'un schéma XML du document à traiter pour JAXB 2.0.

 

Choix du chapitre Qu'est ce que le « Data Binding » ou association de données ?

Le Data Binding est une technologie permettant d'automatiser la transformation d'un modèle de données en un modèle de données objets dans un langage de programmation. Autrement dit, il permet par exemple de convertir les fichiers XML en instances de classes Java.

Pour réaliser cela, il y a trois étapes :

  1. La génération de classes.
  2. Le rassemblement des données.
  3. La redistribution des données

Le schéma suivant résume assez bien le principe : un document XML suit les règles de grammaire du « schema », ce dernier une fois compilé permet de créer une classe correspondante. Cette dernière permettra de créer une instance d'objet correspondant :

JAXB

JAXB est l'acronyme de Java Architecture for XML Binding. Le but de l'API et des spécifications JAXB est de faciliter la manipulation d'un document XML en générant un ensemble de classes qui fournissent un niveau d'abstraction plus élevé que l'utilisation de JAXP (SAX ou DOM).

Avec ces deux API, SAX et DOM, toute la logique de traitements des données contenues dans le document est à écrire.
§

JAXB au contraire fournit un outil qui analyse un schéma XML et génère à partir de ce dernier un ensemble de classes qui vont encapsuler les traitements de manipulation du document. Le grand avantage est de fournir au développeur un moyen de manipuler un document XML sans connaître XML ou les technologies d'analyse. Toutes les manipulations se font au travers d'objets java. Ces classes sont utilisées pour faire correspondre le document XML dans des instances de ces classes et vice et versa : ces opérations se nomment respectivement unmarshalling et marshalling.

 

Choix du chapitre JAXB 2.0

JAXB 2.0 a été développé sous la JSR 222 et elle est incorporée dans Java EE 5 et dans Java SE 6. Les fonctionnalités de JAXB 2.0 par rapport à JAXB 1.0 sont :

  1. Support uniquement des schémas XML (les DTD ne sont plus supportées).
  2. Mise en oeuvre des annotations.
  3. Assure la correspondance bidirectionelle entre un schéma XML et le bean correspondant.
  4. Utilisation de fonctionnalités proposées par Java 5 notamment les generiques et les énumérations.
  5. Le nombre d'entités générées est moins important : JAXB 2.0 génère une classe pour chaque complexType du schema alors que JAXB 1.0 génère une interface et une classe qui implémente cette interface. Une méthode de la classe ObjectFactory est générée pour renvoyée une instance de cette classe.

En plus de son utilité principale, JAXB 2.0 propose d'atteindre plusieurs objectifs :

  1. Être facile à utiliser pour consulter et modifier un document XML sans connaissance ni de XML ni de techniques de traitement de documents XML.
  2. Être configurable : JAXB met en oeuvre des fonctionnalités par défaut qu'il est possible de modifier par configuration pour répondre à ces propres besoins.
  3. S'assurer que la création d'un document XML à partir d'objets et retransformer ce document en objets donne le même ensemble d'objets.
  4. Pouvoir valider un document XML ou les objets qui encapsulent un document sans avoir à écrire le document correspondant.
  5. Être portable : chaque implémentation doit au minimum mettre en oeuvre les spécifications de JAXB.

L'utilisation de JAXB implique généralement deux étapes :

  1. Génération des classes et interfaces à partir du schéma XML.
  2. Utilisation des classes générées et de l'API JAXB pour transformer un document XML en graphe d'objets et vice et versa, pour manipuler les données dans le graphe d'objets et pour valider le document.

Sérialisation

La sérialisation d'un graphe d'objets Java est effectué par une opération dite de mashalling. L'opération inverse est dite d'unmashalling. Lors de ces deux opérations, le document XML peut être validé.

L'API JAXB

L'API JAXB propose un framework composé de classes regroupées dans trois packages :

  1. javax.xml.bind : Contient les interfaces principales et la classe JAXBContext.
  2. javax.xml.bind.util : Contient des utilitaires.
  3. javax.xml.bind.helper : Contient une implémentation partielle de certaines interfaces pour faciliter le développement d'une implémentation des spécifications de JAXB.
  4. javax.xml.bin.annotation : Gestion des annotations pour préciser le mapping entre les classes Java et le document XML correspondant.

Annotations

JAXB 2.0 utilise de nombreuses annotations définies dans le package javax.xml.bin.annotation essentiellement pour préciser le mode de fonctionnement lors des opérations de marshaling/unmarshaling.

Ces annotations précisent le mapping entre les classes Java et le document XML. La plupart de ces annotations ont des valeurs par défaut ce qui réduit l'obligation de leur utilisation si la valeur par défaut correspond au besoin.

Transformations

JAXB 2.0 permet aussi de réaliser dynamiquement à l'exécution une transformation d'un graphe d'objets en document XML et vice et versa. C'est cette fonctionnalité qui est largement utilisée dans les services web via l'API JAX−WS 2.0. La classe abstraite JAXBContext fournie par l'API JAXB permet de gérer la transformation d'objets Java en XML et vice et versa.

Outils de JAXB 2.0

JAXB 2.0 propose plusieurs outils pour faciliter sa mise en oeuvre :

  1. Un générateur de classes Java (schema compiler) à partir d'un schéma XML nommé xjc dans l'implémentation de référence. Ces classes générées mettent en oeuvre les annotations de JAXB.
  2. Un générateur de schéma XML (schema generator) à partir d'un graphe d'objets nommé schemagen dans l'implémentation de référence.


L'API JAXB est contenue dans la package javax.xml.bind.
§

La mise en oeuvre de JAXB 2.0

La mise en oeuvre de JAXB requiert pour un usage standard plusieurs étapes :

  1. La génération des classes en utilisant l'outil xjc de JAXB à partir d'un schéma du document XML.
  2. Ecriture de code utilisant les classes générées et l'API JAXB pour :
  3. Compilation du code généré.
  4. Exécution de l'application.

Utilisation de JAXB 2.0

L'utilisation de JAXB se fait donc en deux phases :

  1. Générer des classes à partir d'un schéma XML et utiliser ces classes dans le code de l'application.
  2. Â l'exécution de l'application, le document XML est transformé en graphe d'objets, les données de ces objets peuvent être modifiées puis le document XML peut être regénéré à partir des objets.

L'utilisation de JAXB met en oeuvre plusieurs éléments :

  1. Une ou plusieurs classes annotées qui vont encaspulées des données du document XML.
  2. Un schéma XML (optionnel) : c'est un document XML qui décrit la structure des éléments, attributs et entités d'un document XML. Le but d'un schéma XML est similaire à celui d'une DTD mais le schéma propose une description plus riche et plus fine. Ce schéma peut éventuellement être enrichi de données de configurations concernant les classes à générer.
  3. Un outil (optionnel) qui génère les classes annotées à partir d'un schéma avec éventuellement un fichier de configuration pour configurer les classes à générer.
  4. Une API utilisée à l'exécution pour transformer un document XML en un ensemble d'objets du type des classes annotées et vice et versa et permettre des validations.
  5. Un document XML qui sera lu et/ou écrit en fonction des traitements à réaliser.

Avantages

Les avantages d'utiliser JAXB sont nombreux :

  1. Facilite la manipulation de document XML dans une application Java.
  2. Manipulation du document XML au travers d'objets : aucune connaissance de XML ou de la manière de traiter un document n'est requise.
  3. Un document XML peut être créé en utilisant les classes générées.
  4. Les traitements de JAXB peuvent être configurés.
  5. Les ressources requises par le graphe d'objets utilisé par JAXB sont moins importantes qu'avec DOM.

Inconvénients

Si vous devez créer des classes directement à partir d'un document XML, vous devez impérativement posséder le Schéma XML correspondant (avec l'extension xsd). Si vous n'avez pas le Schéma XML, cela peut poser quelques difficultés. C'est l'inconvénient de cette technologie.

Toutefois, il existe des logiciels libres, comme Liquid XML Studio, qui savent créer un Schéma directement à partir du document XML correspondant. Ils proposent alors des valeurs par défaut que vous pouvez par la suite modifier au travers des propriétés proposées dans l'éditeur intégré.

 

Choix du chapitre Générer des classes à partir d'un schéma

Pour permettre l'utilisation et la manipulation d'un document XML, JAXB propose de générer un ensemble de classes à partir du schema XML du document.

Chaque implémentation de JAXB doit fournir un outil (binding compiler) qui permet la génération de classes et interfaces à partir d'un schema (binding a schema).

Exemple de document XML avec son Schéma XML associé

Je vous propose, pour la suite de cette étude, de prendre un exemple concret, et surtout très simple. J'aimerais pouvoir mettre en oeuvre un logiciel qui trace des formes, des carrés ou des cercles, dans la partie principale d'une application graphique fenêtrée.

Il doit être possible de récupérer l'ensemble des formes à tracer au moyen, par exemple, du document XML ci-dessous :

En utilisant le logiciel Liquid XML Studio, et en faisant les réglages nécessaires, voici le Schéma XML correspondant :

La commande xjc

Comme nous l'avons vu, l'implémentation de référence fournit l'outil xjc pour générer les classes à partir d'un schéma XML. L'utilisation la plus simple de l'outil xjc est de lui fournir simplement le fichier qui contient le schéma XML du document à utiliser.

xjc Formes.xsd

L'outil xjc possède plusieurs options dont voici les principales :

Option Rôle
-p nom_package Précise le package qui va contenir les classes générées
-d répertoire Précise le répertoire qui va contenir les classes générées
-nv Inhibe la validation du schéma
-b fichier Précise un fichier de configuration
-classpath chemin Précise le classpath

Les classes générées

Le compilateur génère des classes en correspondance avec le Schéma XML fourni à l'outil.

Deux entités sont générées dans le répertoire generated :
  1. Formes.java : classes qui encaspulent le document XML. Chaque classse qui encapsule un type complexe du schéma possède des getter et setter sur les éléments du schéma.
  2. ObjectFactory.java : fabrique qui permet d'instancier des objets utilisés lors du mapping. La fabrique permet de créer des instances de chacun des types d'objet correspondant à un type complexe du schéma. Cette fabrique est particulièrement utile lors de la création d'un nouveau document XML : le graphe d'objets est créé en ajoutant des instances des objets retournées par cette fabrique.

Les classes générées sont dépendantes de l'implémentation de JAXB utilisée : il est préférable d'utiliser les classes générées par une implémentation avec cet outil. Par défaut, JAXB utilise des règles pour définir chaque entité incluse dans le schema (element et complexType défini dans le schéma).

Formes.java (sans les commentaires)
package jaxb;  //---------------------------------------------------------------------------------------------------------- Ligne rajoutée

import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)  //------------------------------------------------------------ Element racine
@XmlType(name = "", propOrder = {"forme"})
@XmlRootElement(name = "Formes")
public class Formes { 

    @XmlElement(name = "Forme")
    protected List<Formes.Forme> forme;

    public List<Formes.Forme> getForme() {
        if (forme == null) {
            forme = new ArrayList<Formes.Forme>();
        }
        return this.forme;
    }


    @XmlAccessorType(XmlAccessType.FIELD)     //----------------------------------------------------- Element Forme
    @XmlType(name = "", propOrder = {"centre","largeur"})
    public static class Forme { 

        protected Formes.Forme.Centre centre;

        @XmlSchemaType(name = "unsignedInt")
        protected Long largeur;

        @XmlAttribute(required = true)
        protected String type;

        public Formes.Forme.Centre getCentre() { return centre; }
        public void setCentre(Formes.Forme.Centre value) { this.centre = value; }

        public Long getLargeur() {  return largeur; }
        public void setLargeur(Long value) { this.largeur = value; }

        public String getType() { return type; }
        public void setType(String value) { this.type = value; }

        @XmlAccessorType(XmlAccessType.FIELD) //--------------------------------------------------- Element Centre
        @XmlType(name = "")
        public static class Centre {

            @XmlAttribute(required = true)
            protected int x;

            @XmlAttribute(required = true)
            protected int y;

            public int getX() { return x;  }
            public void setX(int value) { this.x = value; }

            public int getY() { return y; }
            public void setY(int value) { this.y = value; }
        }
    }
}
ObjectFactory.java (sans les commentaires)
package jaxb;  //---------------------------------------------------------------------------------------------------------- Ligne rajoutée

import javax.xml.bind.annotation.XmlRegistry;

@XmlRegistry
public class ObjectFactory {

    public ObjectFactory() {}

    public Formes.Forme createFormesForme() {
        return new Formes.Forme();
    }

    public Formes createFormes() {
        return new Formes();
    }

    public Formes.Forme.Centre createFormesFormeCentre() {
        return new Formes.Forme.Centre();
    }
}

 

Choix du chapitre Utiliser l'API JAXB 2.0

JAXB fournie une API qui permet à l'exécution d'effectuer les opérations de transformation d'un document XML en un graphe d'objets utilisant les classes générées et vice et versa (unmashalling/marshalling) ainsi que des opérations de validation.

  1. L'objet principal pour les opérations de transformation est l'objet JAXBContext : il permet d'utiliser l'API JAXB. Pour obtenir une instance de cet objet, il suffit d'utiliser la méthode statique newInstance() en lui passant en argument le ou les paquetages contenant les classes générées à utiliser. Dans le cas ou plusieurs paquetages doivent être précisés, il faut les séparer par une virgule.
    JAXBContext contexte = JAXBContext.newInstance("jaxb");
  2. Une autre surdéfinition de la méthode newInstance() attend en argument la classe qui encapsule la racine du document.

Mapper un document XML à des objets (unmarshal)

L'API JAXB propose de transformer un document XML en un ensemble d'objets qui vont encapsuler les données et la hiérarchie du document. Ces objets sont des instances des classes générées à partir du schéma XML.

  1. La création des objets nécessite la création d'un objet de type JAXBContext en utilisant la méthode statique newInstance(), comme nous venons de le découvrir.
  2. Il faut ensuite instancier un objet de type Unmarshaller qui va permettre de transformer un document XML en un ensemble d'objets. Un telle instance est obtenue en utilisant la méthode createUnmarshaller() de la classe JAXBContext.
    Unmarshaller mapperXMLObjet = contexte.createUnmarshaller();
  3. La méthode unmarshal() de la classe Unmarshaller se charge de traiter un document XML et retourne un objet du type complexe qui encapsule la racine du document XML. Elle possède de nombreuses surdéfinitions à utiliser en fonction des besoins.
    Formes formes = (Formes) mapperXMLObjet.unmarshal(new File("Formes.xml"));
  4. A partir de cet objet, il est possible d'obtenir et de modifier des données encapsulées dans les différents objets créés à partir des classes générées et du contenu du document. Chacun de ces objets possèdent des getter et des setter sur leur noeud direct.

Exemple de mise en oeuvre au travers de l'application fenêtrée

Je vous propose de finaliser ces concepts au travers de l'application fenêtrée qui trace les formes stockées dans le document XML, dont voici le résultat :

Codage de l'application fenêtrée
package jaxb;

import java.awt.*;
import java.io.File;
import javax.swing.*;
import javax.xml.bind.*;

public class Principal extends JFrame {
   private Tracé dessins = new Tracé();

   public Principal() {
      super("Tracé de formes");
      add(dessins);
      setSize(400, 300);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new Principal(); }

   private class Tracé extends JComponent {
      private Formes formes;
      
      public Tracé() {
         try {            
            JAXBContext contexte = JAXBContext.newInstance("jaxb");
            Unmarshaller mapperXMLObjet = contexte.createUnmarshaller();
            formes = (Formes) mapperXMLObjet.unmarshal(new File("Formes.xml"));
         } 
         catch (JAXBException ex) { setTitle("Impossible de récupérer le document XML");  }
      }

      @Override
      protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         Graphics2D surface = (Graphics2D) g;
         surface.setStroke(new BasicStroke(5));
         surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
         if (formes!=null)
            for (Formes.Forme forme : formes.getForme()) {
               long largeurLong = forme.getLargeur();
               int largeur = (int) largeurLong;               
               int x = forme.getCentre().getX() - largeur/2;
               int y = forme.getCentre().getY() - largeur/2;
               String type = forme.getType();
               
               if (type.equals("Carré")) surface.drawRect(x, y, largeur, largeur);
               else surface.drawOval(x, y, largeur, largeur);
            }
      }    
   }
}

Créer un document XML à partir d'objets

JAXB permet de créer un document XML à partir d'un graphe d'objets : cette opération est nommée marshalling. Une operation de mashalling est l'opération inverse de l'opération d'unmarshalling.

Ce graphe d'objets peut être issu d'une opération de type unmarshalling (construction à partir d'un document XML existant) ou issu d'une création de toutes pièces de l'ensemble des objets. Dans le premier cas cela correspond à une modification du document et dans le second cas à une création de document.

  1. La création des objets nécessite la création d'un objet de type JAXBContext en utilisant la méthode statique newInstance(), comme précédemment.
  2. Il faut ensuite instancier un objet de type Marshaller qui va permettre de transformer un ensemble d'objets en un document XML. Un telle instance est obtenue en utilisant la méthode createMarshaller() de la classe JAXBContext.
  3. La méthode marshal() de la classe Marshaller se charge de créer un document XML à partir d'un graphe d'objets dont l'objet racine lui est fourni en paramètre. La méthode marshal() possède plusieurs surdéfinition qui permettent de préciser la forme du document XML généré :
  4. L'objet Mashaller possède des propriétés qu'il est possible de valoriser en utilisant la méthode setProperty(). Les spécifications de JAXB proposent des propriétés qui doivent être obligatoirement supportées par l'implémentation. Chaque implémentation est libre de proposer des propriétés supplémentaires.
  5. Exemple : demander le formattage du document créé :
    Marshaller mapperObjetsXML = contexte.createMarshaller();
    mapperObjetsXML.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
La création d'un document XML à partir d'objets peut se faire suivant deux approches qui seront traitées dans les chapitres qui suivent :
  1. En utilisant les classes générées à partir d'un Schéma XML.
  2. En utilisant des classes annotées sans être obligé d'utiliser un Schéma XML.


Choix du chapitre Création d'un document XML à partir d'objets suivant un Schéma XML

Comme nous l'avons découvert dans les chapitres précédents, une des classes générées à partir du schéma se nomme ObjectFactory : c'est une fabrique d'objets pour les classes générées qui encapsulent des données d'un document respectant le schéma.

Pour créer un document XML en utilisant ces classes, il faut suivre les étapes que nous avons évoqué dans la fin du chapitre précédent :

  1. La création du document nécessite la création d'un objet de type JAXBContext en utilisant la méthode statique newInstance().
  2. Il faut ensuite créer le graphe d'objets en utilisant la classe ObjectFactory pour instancier les différents objets et valoriser les données de ces objets en utilisant les setter.
    ObjectFactory fabrique = new ObjectFactory();  
    Formes formes = fabrique.createFormes(); Formes.Forme forme = fabrique.createFormesForme(); Formes.Forme.Centre centre = fabrique.createFormesFormeCentre();
  3. Il faut ensuite créer un objet de type Marshaller à partir du contexte et appeler sa méthode marshall() pour générer le document.

Exemple de mise en oeuvre au travers de l'application fenêtrée

Je vous propose de finaliser ces concepts au travers d'une application fenêtrée qui trace les formes directement sur la zone principale de la fenêtre et qui peut stocker par la suite tous les formes présentes dans le document XML correspondant :

Codage de l'application fenêtrée
package jaxb;

import java.awt.*;
import java.awt.event.*;
import java.io.File;
import javax.swing.*;
import javax.xml.bind.*;

public class Principal extends JFrame {
   private Tracé dessins = new Tracé();
   private JRadioButton cercle = new JRadioButton("Cercle", true);
   private JRadioButton carré = new JRadioButton("Carré");
   private JFormattedTextField largeur = new JFormattedTextField(50L);
   private ButtonGroup groupe = new ButtonGroup();
   private JPanel boutons = new JPanel();
   private JToolBar barre = new JToolBar();

   public Principal() {
      super("Tracé de formes");
      add(barre, BorderLayout.NORTH);
      barre.add(new AbstractAction("Nouveau", new ImageIcon("nouveau.gif")) {
         public void actionPerformed(ActionEvent e) {
            dessins.effacer();
         }
      });
      barre.add(new AbstractAction("Enregistrer", new ImageIcon("enregistrer.gif")) {
         public void actionPerformed(ActionEvent e) {
            dessins.enregistrer();
         }
      });
      dessins.addMouseListener(new MouseAdapter() {
         @Override
         public void mouseClicked(MouseEvent e) {
            if (cercle.isSelected()) dessins.ajoutForme("Cercle", e.getX(), e.getY());
            else  dessins.ajoutForme("Carré", e.getX(), e.getY());
         }
      });
      add(dessins);
      largeur.setColumns(5);
      boutons.add(cercle);
      boutons.add(carré);
      boutons.add(largeur);
      groupe.add(cercle);
      groupe.add(carré);
      add(boutons, BorderLayout.SOUTH);
      setSize(400, 300);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new Principal(); }

   private class Tracé extends JComponent {
      private Formes formes;
      private Marshaller mapperObjetsXML;
      private ObjectFactory fabrique = new ObjectFactory();
      
      public Tracé() {
         try {            
            JAXBContext contexte = JAXBContext.newInstance(Formes.class);
            mapperObjetsXML = contexte.createMarshaller();
            mapperObjetsXML.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            formes = fabrique.createFormes();
         } 
         catch (JAXBException ex) { setTitle("Impossible de créer le document XML");  }
      }

      @Override
      protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         Graphics2D surface = (Graphics2D) g;
         surface.setStroke(new BasicStroke(5));
         surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
         if (formes!=null)
            for (Formes.Forme forme : formes.getForme()) {
               long largeurLong = forme.getLargeur();
               int largeur = (int) largeurLong;               
               int x = forme.getCentre().getX() - largeur/2;
               int y = forme.getCentre().getY() - largeur/2;
               String type = forme.getType();
               
               if (type.equals("Carré")) surface.drawRect(x, y, largeur, largeur);
               else surface.drawOval(x, y, largeur, largeur);
            }
      }

      public void effacer() {
         formes = fabrique.createFormes();
         revalidate();
         repaint();
      }

      public void ajoutForme(String type, int x, int y) {
         Formes.Forme forme = fabrique.createFormesForme();
         forme.setType(type);
         forme.setLargeur((Long) largeur.getValue());
         Formes.Forme.Centre centre = fabrique.createFormesFormeCentre();
         centre.setX(x);
         centre.setY(y);
         forme.setCentre(centre);
         formes.getForme().add(forme);
         repaint();
      }

      private void enregistrer() {
         try {
            mapperObjetsXML.marshal(formes, new File("Formes.xml"));
         }
         catch (JAXBException ex) { setTitle("Impossible d'enregistrer le document XML");  }
      }
   }
}


Choix du chapitre Mapping XML/Objets en passant par les annotations

JAXB permet de mapper un document XML vers une ou plusieurs classes annotées, ou inversement, sans être obligé d'utiliser un schéma XML. Dans ce cas, le développeur a la charge d'écrire la ou les classes annotées requises.

Dans nos développement, il arrive très souvent que nous manipulions un ensemble d'informations, de différentes natures, comme un système de gestion de personnel ou encore comme le logiciel de tracé de formes. Ces informations sont toujours structurées sous forme de classes, et derrière, nous manipulons les objets correspondants.

Nous avons quelquefois besoin de pouvoir stocker ces informations dans un fichier afin de les retrouver par la suite. Se pose alors la question du format de stockage. Nous avons vu que le document XML est une bonne alternative puisqu'il est facile de le lire avec un simple éditeur de texte.

Dans ce contexte, il serait judicieux de pouvoir fabriquer ces documents XML directement par l'intermédiaire des classes déjà existantes (mapping Objets/XML), sans passer par un Schéma XML (puisqu'encore aucun document n'existe). C'est dans ce cadre là que nous pouvons rajouter des annotations spécifiques directement sur les classes concernées.

Il n'est donc plus nécessaire d'utiliser des classes générées par le compilateur de schéma mais dans ce cas, la ou les classes doivent être créées manuellement en utilisant les annotations adéquates. La classe qui encapsule la racine du document doit être annotée avec l'annotation @XmlRootElement. Une exception de type javax.xml.bind.MarshalException est levée par JAXB si la classe racine ne possède pas cette annotation.

JAXB utilise des comportements par défaut qui ne nécessite de la part du développeur que de définir des comportements particuliers si la valeur par défaut ne convient pas. Ainsi, nous pouvons limiter le nombre d'annotations requises.

Communication entre le document XML et les objets correspondants

Comme nous l'avons découvert dans les chapitres précédents, le mapping entre un document XML et les objets correspondants, se feront par l'intermédiaires des classes JAXBContext, Marshaller et Unmarshaller.

  1. La création ou la connexion avec un document XML nécessite la création d'un objet de type JAXBContext en utilisant la méthode statique newInstance().
  2. Il faut ensuite créer un objet de type Marshaller à partir du contexte et d'appeler sa méthode marshall() pour générer le document XML à partir des objets déjà créés.
  3. Ou bien créer un objet de type Unmarshaller à partir du contexte et d'appeler sa méthode unmarshall() pour générer les objets correspondant au document XML.

Annotation des classes

La configuration de la transformation d'un document XML en objets Java est réalisée grâce à l'utilisation d'annotations dédiées dans les classes Java. De nombreuses annotations sont définies par JAXB 2.0. Toutes ces annotations sont définies dans le package javax.xml.bind.annotation.

Annotation Description
XmlAccessorOrder Contrôler l'ordre des attributs et des propriétés dans la classe.
XmlAccessorType Contrôler si l'attribut ou la propriété de la classe est sérialisé par défaut.
XmlAnyAttribute Convertir une propriété de la classe en un ensemble d'attributs de type jocker dans le document XML.
XmlAnyElement Convertir une propriété de la classe en une description representant l'élément JAXB dans le document XML.
XmlAttachmentRef Marquer un attribut ou une propriété de la classe comme une URI que fait référence à une type MIME particulier.
XmlAttribute Convertir une propriété de la classe en un attribut dans le document XML.
XmlElement Convertir une propriété de la classe en un élément dans le document XML.
XmlElementDecl Associer une fabrique à un element XML.
XmlElementRef Convertir une propriété de la classe en un élément dans le document XML qui est associé à un nom de propriété particulier.
XmlElementRefs Marquer une propriété de la classe qui fait référence aux classes qui possèdente XmlElement ou JAXBElement.
XmlElements Créer un conteneur pour de multiples annotations XmlElement, qui précise ainsi le choix possible parmi celles qui sont proposées.
XmlElementWrapper Créer un élément père dans le document XML pour des collections d'éléments.
XmlEnum Convertir une énumération en une représentation équivalente dans le document XML.
XmlEnumValue Convertir un énumérateur en une représentation équivalente dans le document XML.
XmlID Convertir une propriété de la classe en un ID XML.
XmlIDREF Convertir une propriété de la classe en un IDREF XML.
XmlInlineBinaryData Encoder en base64 dans le document XML.
XmlList Utilisé pour convertir une propriété de la classe vers une liste de type simple.
XmlMimeType Associer un type MIME qui contrôle la représentation XML en rapport avec la propriété de la classe.
XmlMixed Annoter une propriété de la classe qui peut supporter plusieurs types de valeur avec un contenu mixte.
XmlNs Associer un prefix d'un espace de nommage à une URI.
XmlRegistry Marquer une classe comme possédant une ou des méthodes annotées avec XmlElementDecl.
XmlRootElement Associer une classe ou une énumération à un element XML. Très souvent utilisé pour spécifier la racine du document XML.
XmlSchema Associer un espace de nommage à un paquetage.
XmlSchemaType Associer un type Java ou une énumeration à un type défini dans un schéma.
XmlSchemaTypes Conteneur pour plusieurs propriétés annotées par XmlSchemaType.
XmlTransient Marquer une entité pour qu'elle ne soit pas mappée dans le document XML.
XmlType Convertir une classe ou une énumération vers le type spécifié dans Shéma XML correspondant.
XmlValue Mapper une classe vers le type complexe dans le Schéma XML ou vers le type simple suivant le cas.

Premier exemple de mise en oeuvre - pas d'attributs dans les balises

Nous allons toute de suite élaborer un premier exemple qui permet de faire du tracé de formes, comme précédemment, avec la possiblité de sauvegarder le tracé réalisé sur l'IHM dans un document XML et de pouvoir ensuite le restituer à loisir. Tout ceci se fait sans document XML préalable, et à plus forte raison, sans Schéma XML équivalent.

Dans ce premier exemple, nous ne proprosons pas d'attributs dans les balises. Du coup, il devient nécessaire de prévoir des balises spécifiques pour les coordonnées. Remarquez au passage que notre document Formes, représenté par l'élément racine <formes> peut aussi bien accueillir des éléments <cercle> que des éléments <carré>, ce qui offre une structure un petit peu plus compliquée que précédemment.


Mise en place du source relatif aux formes
package jaxb;

import java.awt.Graphics2D;
import java.util.ArrayList;
import javax.xml.bind.annotation.*;

// Définition de l'élément racine du document et de ses sous-éléments
@XmlRootElement
public class Formes {
   @XmlElements({
        @XmlElement(name = "carré", type = Carré.class),
        @XmlElement(name = "cercle", type = Cercle.class)
    })
   private ArrayList<Forme> formes = new ArrayList<Forme>();

   public ArrayList<Forme> getFormes() { return formes; }
   public void ajoutForme(Forme forme) { formes.add(forme); }
   public void supprimerFormes() { formes.clear(); }
}

// hiérarchie des classes représentant l'ensemble des formes possibles
abstract class Forme {
   protected int x, y;
   public Forme() {}
   public Forme(int x, int y) { this.x = x; this.y=y; }
   public int getX() { return x; }
   public void setX(int x) { this.x = x; }
   public int getY() { return y; }
   public void setY(int y) { this.y = y; }
   abstract void dessine(Graphics2D surface);
}

class Cercle extends Forme {
   private int rayon = 50;
   public Cercle() {}
   public Cercle(int x, int y, int rayon) { super(x, y); this.rayon = rayon; }
   public int getRayon() { return rayon; }
   public void setRayon(int rayon) { this.rayon = rayon; }
   @Override
   void dessine(Graphics2D surface) { surface.drawOval(x-rayon, y-rayon, 2*rayon, 2*rayon); }
}

class Carré extends Forme {
   private int côté = 100;
   public Carré() {}
   public Carré(int x, int y, int côté) { super(x, y); this.côté = côté; }
   public int getCôté() { return côté; }
   public void setCôté(int côté) { this.côté = côté; }
   @Override
   void dessine(Graphics2D surface) { surface.drawRect(x-côté/2, y-côté/2, côté, côté);  }
}

Dans ce source, nous découvrons deux parties essentielles :

  1. La hiérarchie des classes qui représente l'ensemble des formes, avec même une classe de base abstraite. Ce sont ces classes que nous utilisons directement dans l'IHM. Remarquez, qu'elles ne possèdent aucune annotation particulière.
  2. La première partie du code est cette fois-ci plus spécifique à l'élaboration du document XML. Dans ce cadre là, une classe Formes est spécialement créée pour représenter l'élément racine du document qui est spécifié au travers de l'annotation @XmlRootElement.
  3. Cette classe Formes possède un seul attribut : formes qui représente l'ensemble des formes susseptibles d'être enregistrées. Comme il existe plusieurs formes possibles, nous utilisons une annotation particulière, juste au-dessus de l'attribut, qui va servir à préciser quels sont les éléments autorisés à intégrer l'ensemble des formes. Il s'agit de l'annotation @XmlElements à l'intérieur de laquelle nous allons positionner des annotations de type @XmlElement qui vont permettre de choisir précisément la classe à prendre en compte avec le nom de la balise associée. Ainsi, lorsque nous placerons une forme, le système proposera la balise correspondante parmi celles qui font partie de l'ensemble défini par l'annotation @XmlElements.
  4. Et c'est tout ... Finalement, nous utilisons très très peu d'annotations.
Codage de l'application principale
package jaxb;

import java.awt.*;
import java.awt.event.*;
import java.io.File;
import javax.swing.*;
import javax.xml.bind.*;
import jaxb.Formes.*;

public class Principal extends JFrame {
   private Tracé dessins = new Tracé();
   private JRadioButton cercle = new JRadioButton("Cercle", true);
   private JRadioButton carré = new JRadioButton("Carré");
   private JFormattedTextField largeur = new JFormattedTextField(50);
   private ButtonGroup groupe = new ButtonGroup();
   private JPanel boutons = new JPanel();
   private JToolBar barre = new JToolBar();

   public Principal() {
      super("Tracé de formes");
      add(barre, BorderLayout.NORTH);
      barre.add(new AbstractAction("Nouveau", new ImageIcon("nouveau.gif")) {
         public void actionPerformed(ActionEvent e) {
            dessins.effacer();
         }
      });
      barre.add(new AbstractAction("Ouvrir", new ImageIcon("ouvrir.gif")) {
         public void actionPerformed(ActionEvent e) {
            dessins.ouvrir();
         }
      });
      barre.add(new AbstractAction("Enregistrer", new ImageIcon("enregistrer.gif")) {
         public void actionPerformed(ActionEvent e) {
            dessins.enregistrer();
         }
      });
      dessins.addMouseListener(new MouseAdapter() {
         @Override
         public void mouseClicked(MouseEvent e) {
            int larg = (Integer)largeur.getValue();
            if (cercle.isSelected()) dessins.ajoutForme(new Cercle(e.getX(), e.getY(), larg/2));
            else  dessins.ajoutForme(new Carré(e.getX(), e.getY(), larg));
         }
      });
      add(dessins);
      largeur.setColumns(3);
      boutons.add(cercle);
      boutons.add(carré);
      boutons.add(largeur);
      groupe.add(cercle);
      groupe.add(carré);
      add(boutons, BorderLayout.SOUTH);
      setSize(400, 300);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new Principal(); }

   private class Tracé extends JComponent {
      private Formes formes = new Formes();
      private Marshaller mapperObjetsXML;
      private Unmarshaller mapperXMLObjets;
      
      public Tracé() {
         try {            
            JAXBContext contexte = JAXBContext.newInstance(Formes.class);
            mapperObjetsXML = contexte.createMarshaller();
            mapperObjetsXML.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            mapperXMLObjets = contexte.createUnmarshaller();
         } 
         catch (JAXBException ex) { setTitle("Impossible de créer le document XML");  }
      }

      @Override
      protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         Graphics2D surface = (Graphics2D) g;
         surface.setStroke(new BasicStroke(5));
         surface.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
         if (formes!=null)
            for (Forme forme : formes.getFormes()) forme.dessine(surface);
      }

      public void effacer() {
         formes.supprimerFormes();
         revalidate();
         repaint();
      }

      public void ajoutForme(Forme forme) {
         formes.ajoutForme(forme);
         repaint();
      }

      private void enregistrer() {
         try {
            mapperObjetsXML.marshal(formes, new File("Formes.xml"));
         }
         catch (JAXBException ex) { setTitle("Impossible d'enregistrer le document XML");  }
      }

      private void ouvrir() {
         try {
            formes = (Formes) mapperXMLObjets.unmarshal(new File("Formes.xml"));
            repaint();
         }
         catch (JAXBException ex) { setTitle("Impossible d'enregistrer le document XML");  }
      }
   }
}

Vous remarquez ici, par rapport aux chapitres précédents, que la gestion des formes est beaucoup plus simple, puisqu'il s'agit de classes des plus normales sans annotations particulières. Le fait d'avoir d'ailleurs une classe abstraite commune permet de simplifier encore plus le problème.

L'utilisation des annotations sans passer par une Schéma XML me paraît finalement plus facile à mettre en oeuvre et surtout plus intuitif. L'intérêt du Schéma XML est surtout intéressant lorsque nous devons faire une application qui tient compte de documents XML déjà existants.

Quelques considérations techniques sur certaines annotations

Le tableau ci-dessous reprend quelques-unes des annotations pour bien préciser leur particularité et leur utilité.

Annotation Considérations
XmlRootElement Peut être utilisée sur une classe pour préciser que cette classe sera l'élément racine du document XML. Chaque attribut de la classe devient ainsi un élément fils dans le document XML. L'attribut namespace de l'annotation @XmlRootElement permet de préciser l'espace de nommage.

Codage de la classe Personne qui va servir de référence
import java.util.Date;  
import javax.xml.bind.annotation.*;  
  
@XmlRootElement  
public class Personne {  
  private String nom;  
  private String prenom;  
  private int taille;  
  private Date dateNaissance;  
    
  public Personne() {  
  }  
  
  public Personne(String nom, String prenom, int taille, Date dateNaissance) {  
    super();  
    this.nom = nom;  
    this.prenom = prenom;  
    this.taille = taille;  
    this.dateNaissance = dateNaissance;  
  }  
  
  public Date getDateNaissance() {  
    return dateNaissance;  
  }  
  
  public void setDateNaissance(Date dateNaissance) {  
    this.dateNaissance = dateNaissance;  
  }  
  
  public String getNom() {  
    return nom;  
  }  
  
  public void setNom(String nom) {  
    this.nom = nom;  
  }  
  
  public String getPrenom() {  
    return prenom;  
  }  
  
  public void setPrenom(String prenom) {  
    this.prenom = prenom;  
  }  
  
  public int getTaille() {  
    return taille;  
  }  
  
  public void setTaille(int taille) {  
    this.taille = taille;  
  }  
}
  
Résultat
<?xml version="1.0"  encoding="UTF-8" standalone="yes"?>  
<personne>  
     <nom>REMY</nom>  
     <prenom>Emmanuel</prenom>  
     <taille>171</taille>  
     <dateNaissance>2007-01-16T17:03:31.213+01:00</dateNaissance>
</personne>
XmlTransient Permet d'ignorer une propriété/entité dans la mapping entre l'objet et l'élément XML correspondant :

Exemple
import java.util.Date;  
import javax.xml.bind.annotation.*;  
  
@XmlRootElement  
public class Personne {  
...
  @XmlTransient
  public String getNom() {  
    return nom;  
  }  
 ...
}
Résultat
<?xml version="1.0"  encoding="UTF-8" standalone="yes"?>  
<personne>
     <prenom>Emmanuel</prenom>  
     <taille>171</taille>  
     <dateNaissance>2007-01-16T17:03:31.213+01:00</dateNaissance>
</personne>
XmlAttribute Permet de mapper une propriété sous la forme d'un attribut et fournir des précisions sur la configuration de cet attribut

Exemple
import java.util.Date;  
import javax.xml.bind.annotation.*;  
  
@XmlRootElement  
public class Personne {  
...
  @XmlAttribute
  public String getNom() {  
    return nom;  
  }  
 ...
}
Résultat
<?xml version="1.0"  encoding="UTF-8" standalone="yes"?>  
<personne nom="REMY">
     <prenom>Emmanuel</prenom>  
     <taille>171</taille>  
     <dateNaissance>2007-01-16T17:03:31.213+01:00</dateNaissance>
</personne>
XmlElementWrapper Pour les collections, il est possible d'utiliser l'annotation @XmlElementWrapper pour définir un élement père qui encapsule les occurrences de la collection et d'utiliser l'annotation @XmlElement pour préciser le nom de chaque élément de la collection.

Exemple
import java.util.Date;  
import javax.xml.bind.annotation.*;  
  
@XmlRootElement  
public class Personne {
  private String nom;  
  private String prenom;  
  private int taille;  
  private Date dateNaissance;  
  @XmlElementWrapper(name = "Residence") 
@XmlElement(name = "Adresse")
protected List<Adresse> adresses = new ArrayList<Adresse>(); ... } class Adresse { private String rue; private int codePostal; private String ville; private String pays; ... }
Résultat
<?xml version="1.0"  encoding="UTF-8" standalone="yes"?>  
<personne>  
     <nom>REMY</nom>  
     <prenom>Emmanuel</prenom>  
     <taille>171</taille>  
     <dateNaissance>2007-01-16T17:03:31.213+01:00</dateNaissance>
     <Residence>
        <Adresse>
          <rue>Nom de la rue</rue>
          <codePostal>78895</codePostal>
          <ville>Une ville</ville>
          <pays>France</pays>
        </Adresse>
     </Residence>
</personne>
XmlType Il est possible de configurer l'ordre des éléments au travers de cette annotation

Exemple
import java.util.Date;  
import javax.xml.bind.annotation.*;  
  
@XmlRootElement  
@XmlType(propOrder = {"dateNaissance", "adresses", "nom", "prenom", "taille"}) 
public class Personne {
  private String nom;  
  private String prenom;  
  private int taille;  
  private Date dateNaissance;  
  @XmlElementWrapper(name = "Residence") 
@XmlElement(name = "Adresse")
protected List<Adresse> adresses = new ArrayList<Adresse>(); ... } class Adresse { private String rue; private int codePostal; private String ville; private String pays; ... }
Résultat
<?xml version="1.0"  encoding="UTF-8" standalone="yes"?>  
<personne>  
     <dateNaissance>2007-01-16T17:03:31.213+01:00</dateNaissance>  
     <Residence>
        <Adresse>
          <rue>Nom de la rue</rue>
          <codePostal>78895</codePostal>
          <ville>Une ville</ville>
          <pays>France</pays>
        </Adresse>
     </Residence>
     <nom>REMY</nom>  
     <prenom>Emmanuel</prenom>  
     <taille>171</taille>  
</personne>
XmlJavaTypeAdapter Il est possible de définir des classes de type Adapter qui permettent de personnaliser la façon dont un objet est serialisé/desérialisé dans le document XML. Ces classes Adapter héritent de la classe javax.xml.bind.annotation.adapters.XmlAdapter. Il suffit de redéfinir les méthodes marshal() et unmashal() afin de permettre un formatage de l'information adapté au visuel que nous désirons faire apparaître côté document XML. Ces méthodes seront automatiquement utilisées à l'exécution par l'API JAXB lors des transformations. L'annotation @XmlJavaTypeAdapter permet de préciser la classe de type Adapter qui sera à utiliser plutôt que d'utiliser la conversion par défaut

Exemple
import java.util.Date; 
import java.text.*; 
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.*; 
  
@XmlRootElement  
@XmlType(propOrder = {"dateNaissance", "adresses", "nom", "prenom", "taille"}) 
public class Personne {
  private String nom;  
  private String prenom;  
  private int taille;  
  @XmlJavaTypeAdapter(DateAdapter.class) 
  private Date dateNaissance;  
  @XmlElementWrapper(name = "Residence") 
@XmlElement(name = "Adresse")
protected List<Adresse> adresses = new ArrayList<Adresse>(); ... } class Adresse { private String rue; private int codePostal; private String ville; private String pays; ... } class DateAdapter extends XmlAdapter<String, Date> {
private DateFormat miseEnForme = DateFormat.getDateInstance(DateFormat.LONG);
public Date unmarshal(String date) throws Exception {
return miseEnForme.parse(date);
}
public String marshal(Date date) throws Exception {
return miseEnForme.format(date); }
}
Résultat
<?xml version="1.0"  encoding="UTF-8" standalone="yes"?>  
<personne>  
     <dateNaissance>1 octobre 1959</dateNaissance>  
     <Residence>
        <Adresse>
          <rue>Nom de la rue</rue>
          <codePostal>78895</codePostal>
          <ville>Une ville</ville>
          <pays>France</pays>
        </Adresse>
     </Residence>
     <nom>REMY</nom>  
     <prenom>Emmanuel</prenom>  
     <taille>171</taille>  
</personne>

Deuxième exemple de mise en oeuvre - Mise en place d'attributs dans les balises XML

Nous allons reprendre l'application précédente qui permet de sauvegarder, et donc de restituer un ensemble de formes, dans un document XML. Ce document XML est légèrement différent que le précédent. Pour chacune des formes, nous envisageons, cette fois-ci, de prendre une nouvelle balise nommée <centre> à l'intérieur de laquelle nous stipulons les coordonnées du centre de la forme.

Cette fois-ci nous devons proposer des attributs, respectivement x et y, qui vont représenter les coordonnées dela balise <centre>.


Mise en place du source relatif aux formes
package jaxb;

import java.awt.Graphics2D;
import java.util.ArrayList;
import javax.xml.bind.annotation.*;

// Définition de l'élément racine du document et de ses sous-éléments
@XmlRootElement
public class Formes {
   @XmlElements({
        @XmlElement(name = "carré", type = Carré.class),
        @XmlElement(name = "cercle", type = Cercle.class)
    })
   private ArrayList<Forme> formes = new ArrayList<Forme>();

   public ArrayList<Forme> getFormes() { return formes; }
   public void ajoutForme(Forme forme) { formes.add(forme); }
   public void supprimerFormes() { formes.clear(); }
}

// La classe Forme est modifiée pour accueillir un centre à la place des coordonnées x et y
abstract class Forme {
   protected Centre centre;
   public Forme() { centre = new Centre(); }
   public Forme(int x, int y) { centre = new Centre(x, y); }
   public Centre getCentre() { return centre;  }
   public void setCentre(Centre centre) { this.centre = centre; }
   abstract void dessine(Graphics2D surface);
}

// Nouvel élément représenant le <centre> qui possède deux attributs x et y
class Centre {
   @XmlAttribute
   private int y;
   @XmlAttribute
   private int x;

   public Centre() {}
   public Centre(int x, int y) { this.x = x; this.y = y; }
   public int getX() { return x; }
   public int getY() { return y; }   
}

class Cercle extends Forme {
   private int rayon = 50;
   public Cercle() {}
   public Cercle(int x, int y, int rayon) { super(x, y); this.rayon = rayon; }
   public int getRayon() { return rayon; }
   public void setRayon(int rayon) { this.rayon = rayon; }
   @Override
   void dessine(Graphics2D surface) {
      int x = centre.getX();
      int y = centre.getY();
      surface.drawOval(x-rayon, y-rayon, 2*rayon, 2*rayon);
   }
}

class Carré extends Forme {
   private int côté = 100;
   public Carré() {}
   public Carré(int x, int y, int côté) { super(x, y); this.côté = côté; }
   public int getCôté() { return côté; }
   public void setCôté(int côté) { this.côté = côté; }
   @Override
   void dessine(Graphics2D surface) {
      int x = centre.getX();
      int y = centre.getY();
      surface.drawRect(x-côté/2, y-côté/2, côté, côté);
   }
}

Dans ce source, nous pouvons faire un certain nombre de remarques :

  1. La plus grosse partie du code n'est pas modifiée.
  2. Nous devons mettre en oeuvre une classe Centre qui, dès lors, possède les coordonnées des formes souhaitées. Les coordonnées deviennent alors des attributs XML de cette classe Centre.
  3. Dans la classe Forme, nous proposons un nouvel attribut centre, de type Centre, qui remplace les attributs x et y. Les constructeurs doivent prendre en compte la création de ces différents centres.
  4. Il faut revoir également les méthodes redéfinies dessine() puisque nous ne pouvons plus accéder directement aux coordonnées.
  5. Encore une fois, malgrès un changement notable sur le document XML, nous utilisons très peu d'annotations.

Répertoire téléphonique avec une MAP

Cette fois-ci, nous changeons de sujet. Nous proposons une application qui permet de stocker un petit répertoire téléphonique de portable associé juste à quelques prénoms connus. Ce répertoire devra toutefois être dans l'ordre alphabétique des prénoms.

Source du répertoire téléphonique
package carte;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.text.ParseException;
import javax.swing.*;
import java.util.*;
import javax.swing.text.MaskFormatter;
import javax.xml.bind.*;
import javax.xml.bind.annotation.*;

public class GestionRépertoire extends JFrame {
   private JTextField prénom = new JTextField("Nouveau prénom", 18);
   private JFormattedTextField téléphone;
   private JPanel panneau = new JPanel();
   private JToolBar barre = new JToolBar();
   private Marshaller mapperObjetsXML;
   private Unmarshaller mapperXMLObjets;
   private Répertoire répertoire = new Répertoire();
   private JComboBox liste = new JComboBox();

   public GestionRépertoire() throws ParseException {     
      super("Répertoire téléphonique");
      documentXML();
      liste.setForeground(Color.RED);
      téléphone = new JFormattedTextField(new MaskFormatter("##-##-##-##-##"));
      téléphone.setValue("06-00-00-00-00");
      barre.add(new AbstractAction("Ajout") {
         public void actionPerformed(ActionEvent e) {
            répertoire.ajout(prénom.getText(), (String) téléphone.getValue());
            répertoire.miseAJour(liste);
         }
      });
      barre.add(new AbstractAction("Suppression") {
         public void actionPerformed(ActionEvent e) {
            répertoire.suppression(prénom.getText());
            répertoire.miseAJour(liste);
         }
      });
      barre.add(new AbstractAction("Ouvrir", new ImageIcon("ouvrir.gif")) {
         public void actionPerformed(ActionEvent e) {
         try {
            répertoire = (Répertoire) mapperXMLObjets.unmarshal(new File("Téléphones.xml"));
            répertoire.miseAJour(liste);
         }
         catch (JAXBException ex) { setTitle("Impossible d'enregistrer le document XML");  }
         }
      });
      barre.add(new AbstractAction("Enregistrer", new ImageIcon("enregistrer.gif")) {
         public void actionPerformed(ActionEvent e) {
            try {
               mapperObjetsXML.marshal(répertoire, new File("Téléphones.xml"));
            }
            catch (JAXBException ex) { setTitle("Impossible d'enregistrer le document XML");  }
         }
      });
      add(barre, BorderLayout.NORTH);
      panneau.add(new JLabel("Prénom : "));
      panneau.add(prénom);
      panneau.add(new JLabel("Téléphone : "));
      panneau.add(téléphone);
      add(panneau);
      add(liste, BorderLayout.SOUTH);
      pack();
      setLocationRelativeTo(null);
      setResizable(false);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private void documentXML() {
      try {
         JAXBContext contexte = JAXBContext.newInstance(Répertoire.class);
         mapperObjetsXML = contexte.createMarshaller();
         mapperObjetsXML.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
         mapperXMLObjets = contexte.createUnmarshaller();
      }
      catch (JAXBException ex) { setTitle("Impossible de créer le document XML");  }
   }
   
   public static void main(String[] args) throws ParseException { new GestionRépertoire(); }
}

@XmlRootElement
class Répertoire {
   private TreeMap<String, String> répertoire = new TreeMap<String, String>();

   public TreeMap<String, String> getRépertoire() { return répertoire; }
   public void setRépertoire(TreeMap<String, String> répertoire) { this.répertoire = répertoire; }

   public void ajout(String prénom, String numéro) { répertoire.put(prénom, numéro); }
   public void suppression(String prénom) { répertoire.remove(prénom); }
   
   public void miseAJour(JComboBox liste) {
       liste.removeAllItems();
       for (Map.Entry<String, String> élément : répertoire.entrySet()) liste.addItem(élément);
   }
}

Faisons un certain nombre de remarques :

  1. La classe utilisée pour stocker l'ensemble des numéros est la classe TreeMap qui permet de gérer dans l'ordre ce que nous appelons des cartes.
  2. Nous plaçons juste une annotation @XmlRootElement qui permet de gérer l'ensemble des numéros au travers de ce Map.
  3. Tout ce fait automatiquement.
  4. L'inconvénient toutefois, c'est que le nom des balises, dans le document XML, est alors imposé. Nous trouvons donc des intitulés correspondant à la terminologie utilisée par l'interface Map, c'est-à-dire : <entry>, <key> et <value>.
  5. Il sera éventuellement nécessaire de passer par une classe de type Adapter pour contrôler le nom des balises du document XML.

Répertoire téléphonique avec une MAP et une classe d'adaptation pour le document XML

Nous reprenons le même sujet que précédemment. L'objectif ici est de proposer une meilleure mise en forme du document XML pour une lecture plus adaptée à la situation.

Partie du code sur la mise en forme du document XML uniquement
...

@XmlRootElement
class Répertoire {
   @XmlElement(name="téléphones")
   @XmlJavaTypeAdapter(MapAdapter.class)
   private TreeMap<String, String> répertoire = new TreeMap<String, String>();

   public TreeMap<String, String> getRépertoire() { return répertoire; }
   public void ajout(String prénom, String numéro) { répertoire.put(prénom, numéro); }
   public void suppression(String prénom) { répertoire.remove(prénom); }
   
   public void miseAJour(JComboBox liste) {
       liste.removeAllItems();
       for (Map.Entry<String, String> élément : répertoire.entrySet()) liste.addItem(élément);
   }
}

class MapAdapter extends XmlAdapter<Téléphones, TreeMap<String, String>> {
   @Override
   public TreeMap<String, String> unmarshal(Téléphones téléphones) throws Exception {
      TreeMap<String, String> cartes = new TreeMap<String, String>();
      for (Téléphone téléphone : téléphones.getTéléphones())
         cartes.put(téléphone.getPrénom(), téléphone.getNuméro());
      return cartes;
   }
   @Override
   public Téléphones marshal(TreeMap<String, String> valeurs) throws Exception {
      Téléphones téléphones = new Téléphones();
      for (Map.Entry<String, String> carte : valeurs.entrySet())        
         téléphones.getTéléphones().add(new Téléphone(carte.getKey(), carte.getValue()));
      return téléphones;
   }
}


class Téléphones {
   @XmlElement(name="téléphone")
   private ArrayList<Téléphone> téléphones = new ArrayList<Téléphone>();
   public ArrayList<Téléphone> getTéléphones() { return téléphones; }
}

class Téléphone {   
   @XmlAttribute
   private String numéro;
   @XmlAttribute
   private String prénom;

   public String getNuméro() { return numéro; }
   public String getPrénom() { return prénom; }

   public Téléphone(String prénom, String numéro) {
      this.prénom = prénom;
      this.numéro = numéro;
   }

   public Téléphone() {}
}

Faisons un certain nombre de remarques :

  1. Il n'existe pas d'annotation spécifiques à un conteneur de type Map. Nous devons donc tout construire pour avoir un document XML suivant notre souhait.
  2. Dans notre document XML, pour nommer un élément <téléphone>, nous devons utiliser spécifiquement l'annotation @XmlElement. Or, cette annotation ne se place qu'au dessus d'une propriété de classe. La seule solution consiste donc à créer une classe conteneur qui possède alors une liste d'éléments. Ainsi chaque élément sera bien nommé <téléphone>, si le choix de l'attribut de la classe porte le même nom où si nous spécifions le nom au travers de l'attribut name de l'annotation @XmlElement.
  3. Pour avoir des attributs sur cette balise <téléphone>, nous devons créer également une classe dont chacune des propriétés aura comme annotation @XmlAttribute. Ici aussi, les attributs d'une balise correspondent toujours aux attributs d'une classe. La création de cette classe devient donc obligatoire. Finalement la liste précédente sera une liste de cette classe.
  4. Pour finir, il faut faire le lien entre la Map (liaison avec le programme principal) et la liste (liaison avec le document UML). La seule solution est de proposer une classe Adapter qui fait le marshalling et l'unmarshalling de façon adéquat.

Répertoire téléphonique pouvant intégrer plusieurs téléphones

Nous reprenons de nouveau la même application. Chaque utilisateur peut cette fois-ci avoir plusieurs téléphones : le mobile, le téléphone du domicile ou le téléphone du travail.

Source complet
package carte;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.text.ParseException;
import javax.swing.*;
import java.util.*;
import javax.swing.text.MaskFormatter;
import javax.xml.bind.*;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.*;

public class GestionRépertoire extends JFrame {
   private JTextField prénom = new JTextField("Nouveau prénom", 18);
   private JFormattedTextField téléphone;
   private JPanel panneau = new JPanel();
   private JToolBar barre = new JToolBar();
   private Marshaller mapperObjetsXML;
   private Unmarshaller mapperXMLObjets;
   private Répertoire répertoire = new Répertoire();
   private JComboBox typeTéléphone = new JComboBox(TypeTéléphone.values());
   private JComboBox liste = new JComboBox();

   public GestionRépertoire() throws ParseException {
      super("Répertoire téléphonique");
      documentXML();
      liste.setForeground(Color.RED);
      téléphone = new JFormattedTextField(new MaskFormatter("##-##-##-##-##"));
      téléphone.setValue("06-00-00-00-00");
      barre.add(new AbstractAction("Ajout") {
         public void actionPerformed(ActionEvent e) {
            répertoire.ajout(prénom.getText(), typeTéléphone.getSelectedItem(), (String) téléphone.getValue());
            répertoire.miseAJour(liste);
         }
      });
      barre.add(new AbstractAction("Suppression") {
         public void actionPerformed(ActionEvent e) {
            répertoire.suppression(prénom.getText());
            répertoire.miseAJour(liste);
         }
      });
      barre.add(new AbstractAction("Ouvrir", new ImageIcon("ouvrir.gif")) {
         public void actionPerformed(ActionEvent e) {
         try {
            répertoire = (Répertoire) mapperXMLObjets.unmarshal(new File("Téléphones.xml"));
            répertoire.miseAJour(liste);
         }
         catch (JAXBException ex) { setTitle("Impossible d'enregistrer le document XML");  }
         }
      });
      barre.add(new AbstractAction("Enregistrer", new ImageIcon("enregistrer.gif")) {
         public void actionPerformed(ActionEvent e) {
            try {
               mapperObjetsXML.marshal(répertoire, new File("Téléphones.xml"));
            }
            catch (JAXBException ex) { setTitle("Impossible d'enregistrer le document XML");  }
         }
      });
      add(barre, BorderLayout.NORTH);
      panneau.add(new JLabel("Prénom : "));
      panneau.add(prénom);
      panneau.add(new JLabel("Téléphone : "));
      panneau.add(typeTéléphone);
      panneau.add(téléphone);
      add(panneau);
      add(liste, BorderLayout.SOUTH);
      pack();
      setLocationRelativeTo(null);
      setResizable(false);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private void documentXML() {
      try {
         JAXBContext contexte = JAXBContext.newInstance(Répertoire.class);
         mapperObjetsXML = contexte.createMarshaller();
         mapperObjetsXML.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
         mapperXMLObjets = contexte.createUnmarshaller();
      }
      catch (JAXBException ex) { setTitle("Impossible de créer le document XML"); }
   }

   public static void main(String[] args) throws ParseException { new GestionRépertoire(); }
}

enum TypeTéléphone {Mobile, Domicile, Travail};

@XmlRootElement
class Répertoire {
   @XmlElement(name="téléphones")
   @XmlJavaTypeAdapter(MapAdapter.class)
   private TreeMap<String, EnumMap<TypeTéléphone, String>> répertoire = new TreeMap<String, EnumMap<TypeTéléphone, String>>();

   public TreeMap<String, EnumMap<TypeTéléphone, String>> getRépertoire() { return répertoire; }
   public void suppression(String prénom) { répertoire.remove(prénom); }

   public void ajout(String prénom, Object type, String numéro) {
      EnumMap<TypeTéléphone, String> téléphones;
      if (répertoire.containsKey(prénom)) {
         téléphones = répertoire.get(prénom);
         téléphones.put((TypeTéléphone)type, numéro);
      }
      else {
         téléphones = new EnumMap<TypeTéléphone, String>(TypeTéléphone.class);
         téléphones.put((TypeTéléphone)type, numéro);
         répertoire.put(prénom, téléphones);
      }
   }

   public void miseAJour(JComboBox liste) {
       liste.removeAllItems();
       for (Map.Entry<String, EnumMap<TypeTéléphone, String>> carte : répertoire.entrySet())
          liste.addItem(carte);
   }
}

class MapAdapter extends XmlAdapter<Téléphones, TreeMap<String, EnumMap<TypeTéléphone, String>>> {
   @Override
   public TreeMap<String, EnumMap<TypeTéléphone, String>> unmarshal(Téléphones téléphones) throws Exception {
      TreeMap<String, EnumMap<TypeTéléphone, String>> cartes = new TreeMap<String, EnumMap<TypeTéléphone, String>>();
      for (Téléphone téléphone : téléphones.getTéléphones()) {
         EnumMap<TypeTéléphone, String> liste= new EnumMap<TypeTéléphone, String>(TypeTéléphone.class);
         for (NuméroParType npt : téléphone.getTéléphone()) liste.put(npt.getType(), npt.getNuméro());
         cartes.put(téléphone.getPrénom(), liste);
      }
      return cartes;
   }
   @Override
   public Téléphones marshal(TreeMap<String, EnumMap<TypeTéléphone, String>> valeurs) throws Exception {
      Téléphones téléphones = new Téléphones();
      for (Map.Entry<String, EnumMap<TypeTéléphone, String>> carte : valeurs.entrySet()) {
         Téléphone personne = new Téléphone(carte.getKey());
         for (Map.Entry<TypeTéléphone, String> téléphone : carte.getValue().entrySet())
            personne.ajoutTéléphone(téléphone.getKey(), téléphone.getValue());
         téléphones.getTéléphones().add(personne);
      }         
      return téléphones;
   }
}

class Téléphones {
   @XmlElement(name="personne")
   private ArrayList<Téléphone> téléphones = new ArrayList<Téléphone>();
   public ArrayList<Téléphone> getTéléphones() { return téléphones; }
}

class Téléphone {
   @XmlAttribute
   private String prénom;
   @XmlElement
   private ArrayList<NuméroParType> téléphone = new ArrayList<NuméroParType>();

   public Téléphone(String prénom) { this.prénom = prénom; }
   public Téléphone() {}

   public String getPrénom() { return prénom; }
   public ArrayList<NuméroParType> getTéléphone() { return téléphone; }

   public void ajoutTéléphone(TypeTéléphone type, String numéro) {
      téléphone.add(new NuméroParType(type, numéro));
   }
}

class NuméroParType {
   @XmlAttribute
   private String numéro;
   @XmlAttribute
   private TypeTéléphone type;


   public NuméroParType() {}
   public NuméroParType(TypeTéléphone type, String numéro) {
      this.type = type;
      this.numéro = numéro;
   }

   public String getNuméro() { return numéro; }
   public TypeTéléphone getType() { return type; }
}

Il existe une annotation @XmlEnum, mais vous remarquez qu'il n'a pas été nécessaire de la mettre en oeuvre. JAXB se débrouille très bien sans et propose les énumérateurs sous forme de chaînes de caractères, ceci automatiquement.

 

Choix du chapitre Générer un schéma à partir de classes compilées

L'outil schemagen fourni avec l'implémentation par défaut permet de générer un schéma XML à partir de classes annotées compilées.

Exemple d'application qui va servir de base pour la création d'un schéma XML

Nous allons reprendre l'application du tracé de formes qui permet de sauvegarder, et donc de restituer un ensemble de formes, dans un document XML.

Cette fois-ci nous devons proposer des attributs, respectivement x et y, qui vont représenter les coordonnées dela balise <centre>.


Mise en place du source relatif aux formes
package jaxb;

import java.awt.Graphics2D;
import java.util.ArrayList;
import javax.xml.bind.annotation.*;

// Définition de l'élément racine du document et de ses sous-éléments
@XmlRootElement
public class Formes {
   @XmlElements({
        @XmlElement(name = "carré", type = Carré.class),
        @XmlElement(name = "cercle", type = Cercle.class)
    })
   private ArrayList<Forme> formes = new ArrayList<Forme>();

   public ArrayList<Forme> getFormes() { return formes; }
   public void ajoutForme(Forme forme) { formes.add(forme); }
   public void supprimerFormes() { formes.clear(); }
}

// La classe Forme est modifiée pour accueillir un centre à la place des coordonnées x et y
abstract class Forme {
   protected Centre centre;
   public Forme() { centre = new Centre(); }
   public Forme(int x, int y) { centre = new Centre(x, y); }
   public Centre getCentre() { return centre;  }
   public void setCentre(Centre centre) { this.centre = centre; }
   abstract void dessine(Graphics2D surface);
}

// Nouvel élément représenant le <centre> qui possède deux attributs x et y
class Centre {
   @XmlAttribute
   private int y;
   @XmlAttribute
   private int x;

   public Centre() {}
   public Centre(int x, int y) { this.x = x; this.y = y; }
   public int getX() { return x; }
   public int getY() { return y; }   
}

class Cercle extends Forme {
   private int rayon = 50;
   public Cercle() {}
   public Cercle(int x, int y, int rayon) { super(x, y); this.rayon = rayon; }
   public int getRayon() { return rayon; }
   public void setRayon(int rayon) { this.rayon = rayon; }
   @Override
   void dessine(Graphics2D surface) {
      int x = centre.getX();
      int y = centre.getY();
      surface.drawOval(x-rayon, y-rayon, 2*rayon, 2*rayon);
   }
}

class Carré extends Forme {
   private int côté = 100;
   public Carré() {}
   public Carré(int x, int y, int côté) { super(x, y); this.côté = côté; }
   public int getCôté() { return côté; }
   public void setCôté(int côté) { this.côté = côté; }
   @Override
   void dessine(Graphics2D surface) {
      int x = centre.getX();
      int y = centre.getY();
      surface.drawRect(x-côté/2, y-côté/2, côté, côté);
   }
}

Utilisation de l'utilitaire schemagen

Schema1.xsd


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="formes" type="formes"/>

  <xs:complexType name="formes">
    <xs:sequence>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="carr&#233;" type="carr&#233;"/>
        <xs:element name="cercle" type="cercle"/>
      </xs:choice>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="carr&#233;">
    <xs:complexContent>
      <xs:extension base="forme">
        <xs:sequence>
          <xs:element name="c&#244;t&#233;" type="xs:int"/>
        </xs:sequence>
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>

  <xs:complexType name="forme" abstract="true">
    <xs:sequence>
      <xs:element name="centre" type="centre" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="centre">
    <xs:sequence/>
    <xs:attribute name="x" type="xs:int" use="required"/>
    <xs:attribute name="y" type="xs:int" use="required"/>
  </xs:complexType>

  <xs:complexType name="cercle">
    <xs:complexContent>
      <xs:extension base="forme">
        <xs:sequence>
          <xs:element name="rayon" type="xs:int"/>
        </xs:sequence>
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>
</xs:schema>