Générer un pdf depuis hibernate avec xstream et fop

Objectif

À partir d'une liste d'enregistrements issus d'une table de l'application, créer un fichier PDF récapitulatif. Les tables sont gérées dans l'application avec Hibernate.

Pour arriver à créer notre fichier PDF, nous allons donc partir de nos instances de classe Hibernate. Avec XSTREAM, nous en réaliserons un export XML. Puis nous créerons un fichier XSL, qui contiendra les commandes de mise en forme. Enfin, nous transformerons le fichier XML avec la bibliothèque FOP, qui prendra en paramètre le fichier XSL préalablement créé.

Bibliothèques nécessaires

Voici un extrait du contenu du fichier pom.xml (fichier de configuration de Maven) :

      <dependency>
          <groupId>com.thoughtworks.xstream</groupId>
          <artifactId>xstream-hibernate</artifactId>
          <version>1.4.8</version>
      </dependency>
      <dependency>
          <groupId>org.apache.xmlgraphics</groupId>
          <artifactId>fop</artifactId>
          <version>1.1</version>
      </dependency>
     
        <dependency>
          <groupId>org.apache.avalon.framework</groupId>
          <artifactId>avalon-framework-api</artifactId>
          <version>4.2.0</version>
        </dependency>
       <dependency>
          <groupId>org.apache.avalon.framework</groupId>
          <artifactId>avalon-framework-impl</artifactId>
          <version>4.2.0</version>

xmlgraphics contient à la librairie FOP. Celle-ci a besoin d'avalon-framework. Toutefois, pour des questions de versions, vous devrez télécharger les classes d'Avalon directement depuis ces liens (cf. https://kevynmeganck.wordpress.com/2014/12/02/avalon-framework-4-2-0-missing-2/) :

http://mvnrepository.com/artifact/avalon-framework/avalon-framework-api/4.2.0

http://mvnrepository.com/artifact/avalon-framework/avalon-framework-impl/4.2.0

et les copier dans votre dossier .m2 (dossier contenant les classes gérées par Maven - caché sous Linux dans votre dossier home, et dans Mes Documents dans Windows).

Préparer le fichier XML à partir de XSTREAM

XSTREAM s'interface très facilement avec Hibernate. Toutefois, pour obtenir un fichier XML lisible, vous devrez le paramétrer.

Hibernate transforme les lignes de vos tables en classes. Cela induit plusieurs problèmes possibles :

  • par défaut, les tables liées apparaissent comme des classes. Lors du traitement initial, vous récupérerez les informations concernant la classe dans votre fichier XML, et pas leur contenu.
  • Les classes liées à votre classe principale contiennent, en principe, la référence à votre classe principale. Il en résulte un risque de générer des appels réflexifs (la classe appelle une sous-classe, qui appelle elle-même la classe principale, qui rappelle la classe liée, etc.). Par défaut, XSTREAM crée une référence relative pour les classes qui ont déjà été intégrées dans le fichier XML. Cette référence n'est pas exploitable facilement lors de la transformation XSLT : il faut donc supprimer la création des références relatives, et éviter la réflexivité.

Transformer les classes liées en données

Pour chaque classe liée dans votre objet Hibernate, vous devez créer une classe qui implémente Converter :

    public class NageoireConverter implements Converter {
        @Override
        public boolean canConvert(Class clazz) {
            if (clazz == null)
                return false;
            return clazz.getSimpleName().startsWith("Nageoire");
        }
        @Override
        public void marshal(Object value, HierarchicalStreamWriter writer,
                MarshallingContext arg2) {
            Nageoire objet = (Nageoire) value;
            writer.setValue(objet.getFinName());
        }
        @Override
        public Object unmarshal(HierarchicalStreamReader arg0,
                UnmarshallingContext arg1) {
            return null;
        }
    }

Dans cet exemple, quand XSTREAM va appeler la classe Nageoire, il va en fait récupérer la valeur fournie par la fonction getFinName(), qui retourne ici le nom de la nageoire (fin est la traduction de nageoire en anglais). La classe est analysée à partir de son nom (fonction canConvert).

Le code principal utilisé pour générer votre fichier XML contiendra alors le code suivant :

    public String exportToXml() {
        XStream xstream = new XStream(new DomDriver());
        xstream.registerConverter(new NageoireConverter());

Vous rajouterez autant de registerConverter que nécessaire...

Donner un nouveau nom à vos classes

Pour faciliter la gestion de vos nœuds dans le fichier XML, vous avez tout intérêt à renommer vos classes dans XSTREAM, avec l'instruction :

xstream.alias("peche", Peche.class);

Ainsi, la classe Peche sera connue par la balise <peche>, et vous pourrez vous référer plus loin dans le code à votre classe Peche en utilisant la chaîne peche.

Supprimer les appels récursifs

Indiquez d'abord à XSTREAM de ne pas travailler en mode référence :

xstream.setMode(XStream.NO_REFERENCES);

Ensuite, pour chaque classe liée, indiquez-lui qu'elle ne doit en aucun cas traiter l'attribut qui correspond à la classe parente :

xstream.omitField(Echantillon.class, "peche");

Vous devez le faire pour chaque classe liée, y compris pour les classes liées intégrées à des classes liées.

Gérer les collections

Votre classe principale peut contenir des collections, pour référencer les tables liées de type père-fils. Vous devez indiquer à XSTREAM de les gérer comme des collections, et donc de parcourir tous les fils correspondants.

Pour cela, vous devrez déclarer les instructions suivantes :

       xstream.addImplicitCollection(Peche.class, "echantillons",
                "echantillon", Echantillon.class);

Ici, la valeur echantillons de la classe Peche contient une liste d'échantillons. L'instruction addImplicitCollection va créer dans le fichier XML, pour chaque échantillon, une entrée <echantillon>, référencée par la classe Echantillon.

Vous devrez également supprimer la référence à la classe parente dans la classe fille pour éviter les appels récursifs :

      xstream.omitField(Echantillon.class, "peche");

Supprimer les données que vous ne voulez pas voir apparaître dans le fichier XML

Il suffit d'indiquer quel champ ne doit pas être traité avec l'instruction :

xstream.omitField(PecheDebris.class, "peche");

Vous ne traiterez pas la classe PecheDebris dans chaque instance de la classe Peche.

Générer le fichier XML

Une fois toutes les configurations réalisées, il vous reste à générer votre fichier XML. Ici, nous traitons des enregistrements qui proviennent de la table trait :

        String xml = "<?xml version=\"1.0\"?>\n<peches>";
        try {
            FileWriter xmlFile = new FileWriter("fichier-export.xml");
            for (Peche peche : result) {
                xml = xml + xstream.toXML(peche);
            }
            xml = xml + "\n</peches>";
            xmlFile.write(xml);
            xmlFile.close();
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
            System.err.println(ex.getStackTrace());
        }

result est un ArrayList contenant le résultat de la requête d'interrogation de Hibernate. Vous pouvez également traiter en une seule opération (sans réaliser la boucle) la liste result.

Préparer le fichier XSL

Les fichiers XSL servent de base aux transformations XML. Leur syntaxe est assez particulière, mais s'appréhende relativement aisément si vous connaissez les principes généraux des fichiers XML.

Voici quelques extraits du fichier utilisé, qui reprend l'ensemble des commandes employées.

L'entête du fichier - définir les traducteurs et le format de la page

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <xsl:output method="xml" indent="yes"/>
  <xsl:template match="peches">
    <fo:root>
      <fo:layout-master-set>
        <fo:simple-page-master master-name="A4-portrait"
              page-height="29.7cm" page-width="21.0cm" margin="1cm" margin-bottom="1cm">
              <fo:region-body margin-top="1cm"/>
              <fo:region-before extent="1cm"/>
              <fo:region-after extent="0cm"/>    
        </fo:simple-page-master>
      </fo:layout-master-set>
     
      <fo:page-sequence master-reference="A4-portrait">
         <fo:static-content flow-name="xsl-region-after">
     <fo:block text-align="center"> - <fo:page-number/> -  </fo:block>
    </fo:static-content>
    <fo:static-content flow-name="xsl-region-before">
    <fo:block text-align="center">Récapitulatif des pêches</fo:block>
    </fo:static-content>
        <fo:flow flow-name="xsl-region-body">       
          <fo:block>
          <xsl:apply-templates select="peche" />
          </fo:block>
        </fo:flow>
      </fo:page-sequence>
    </fo:root>
   </xsl:template>
(...)
</xsl:stylesheet>

Quelques explications...

Le début du code permet de créer des alias XML pour la classe Transform, qui s'appellera xls, et la classe Format, qui s'appelera fo dans le reste du document.

La ligne <xsl:template match="peches"> permet de définir ce qu'il se passe quand le programme rencontre la balise peches dans le fichier XML : comme il s'agit du nœud principal de notre ficher, nous en profitons pour indiquer le format, les marges, l'entête, le pied de page (le numéro de la page).

La ligne <xsl:apply-templates select="peche" /> demande au programme de rechercher un modèle qui permettrait de traiter la balise quand elle sera rencontrée. Pour rappel, notre fichier XML contient un nombre quelconque de nœuds peche.

Le traitement de chaque nœud

  <xsl:template match="peche">
  <fo:block border="thin" space-before.optimum="1em"/>
  <fo:block>
  <fo:inline font-weight="bold" > Pêche n° <xsl:value-of select="pecheId"/> </fo:inline>
  Date/heure du début : <fo:inline font-weight="bold" ><xsl:value-of select="pecheStart"/></fo:inline>
  Date/heure de fin : <fo:inline font-weight="bold" ><xsl:value-of select="pecheEnd"/></fo:inline>
  </fo:block>
(...)
</xsl:template>

Nous retrouvons ici les principales fonctions d'affichage :

  • fo:block correspond à un bloc de texte, qui peut être formaté, encadré...
  • fo:inline permet de définir une zone particulière dans un bloc, par exemple, ici, pour indiquer qu'une partie du texte doit être en gras (font-weight="bold")
  • xsl:value-of select="pecheId" permet d'afficher la valeur de la balise

Créer une table et itérer des valeurs

Nous allons maintenant utiliser les dernières commandes les plus utiles pour créer un tableau.

<xsl:if test="./echantillon">
<fo:table table-layout="fixed" border-collapse="collapse" width="100%" border-style="solid"  space-before.optimum="1em">
  <fo:table-column column-width="40%"/>
  <fo:table-column column-width="10%" />
  <fo:table-column column-width="10%" />
  <fo:table-column column-width="40%" />
      <fo:table-header border-style="solid" >
          <fo:table-cell><fo:block text-align="center" border-left-style="solid">Espèces capturées</fo:block></fo:table-cell>
          <fo:table-cell><fo:block text-align="center"  border-left-style="solid" >Nombre total</fo:block></fo:table-cell>
          <fo:table-cell><fo:block text-align="center"  border-left-style="solid" border-right-style="solid" >Nombre mesuré</fo:block></fo:table-cell>
         <fo:table-cell><fo:block text-align="center"  border-right-style="solid">Longueurs</fo:block></fo:table-cell>
       </fo:table-header>
     <fo:table-body >
      <xsl:for-each select="echantillon">
          <fo:table-row>
          <fo:table-cell border-left-style="solid" >
              <fo:block>
              <xsl:value-of select="./espece"/>
              </fo:block>
          </fo:table-cell>
          <fo:table-cell  border-left-style="solid" >
              <fo:block text-align="center">
              <xsl:value-of select="./totalNumber"/>
              </fo:block>
          </fo:table-cell>
           <fo:table-cell   border-left-style="solid" >
              <fo:block text-align="center">
              <xsl:value-of select="./totalMeasuredNumber"/>
              </fo:block>
          </fo:table-cell>
        <fo:table-cell >
        <fo:block  border-left-style="solid" border-right-style="solid">
            <xsl:for-each select="individu">
            <xsl:value-of select="./forkLength" />&#160;
            </xsl:for-each>
        </fo:block>
        </fo:table-cell>       
          </fo:table-row>
      </xsl:for-each>
         
      </fo:table-body>
  </fo:table>
</xsl:if>

Nous commençons par rechercher s'il existe au moins une balise appelée echantillon avec la commande <xsl:if test="echantillon">. Ici, un échantillon correspond à un nombre de poissons capturés pour une espèce donnée. Un individu est un poisson mesuré pour l'espèce (donc, dans l'échantillon).

S"il existe un échantillon, nous créons notre entête de tableau. Chaque taille de colonne est indiquée ici en valeur relative, ce qui permettra d'avoir des tableaux tous identiques dans le document. L'entête est décrite dans le nœud <fo:table-header> : en cas de changement de page, l'entête du tableau sera reproduite dans la page suivante.

Nous allons maintenant créer notre corps de tableau, avec la balise <fo:table-body>. Le programme va créer une ligne pour chaque échantillon, avec les balises <xsl:for-each select="echantillon">, pour traiter chaque nœud, et <fo:table-row> pour créer une nouvelle ligne.

Remarquez la dernière colonne : nous avons traité chaque longueur mesurée d'individu pour l'afficher l'une après l'autre. Le symbole &#160; permet d'indiquer un espace.

Dernier point : dans une boucle, pour accéder aux différents nœuds imbriqués, utilisez la notation ./nom_du_nœud.

Transformer le fichier XML en PDF

    public void exportToPdf() {
        FopFactory fopFactory = FopFactory.newInstance();
        FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
        File xsltfile = new File("pdf.xsl");
        File xmlfile = new File("fichier-export.xml");
        File pdffile = new File ("export.pdf");
        OutputStream out;
        try {
            out = new BufferedOutputStream(new FileOutputStream(pdffile));
            try {
                TransformerFactory factory = TransformerFactory.newInstance();
                Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF,
                        foUserAgent, out);
               
                Source src = new StreamSource(xmlfile);
                Transformer transformer = factory
                        .newTransformer(new StreamSource(xsltfile));
                Result res = new SAXResult(fop.getDefaultHandler());
                transformer.transform(src, res);
            } finally {
                out.close();

Desktop.getDesktop().open(pdffile);

            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

Le code est largement inspiré de la documentation fournie par Apache-FOP (cf. http://xmlgraphics.apache.org/fop/1.1/embedding.html). Il n'est pas vraiment optimisé, mais il fonctionne...

La commande Desktop.getDesktop().open(pdffile); permet de lancer l'affichage du fichier à l'écran, si le poste de travail le supporte (lecteur installé, association de fichier opérationnelle).