À 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éé.
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).
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 :
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...
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.
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.
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");
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
.
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
.
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.
<?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
.
<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 baliseNous 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" /> 
</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  
; 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
.
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).