Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !


Les applications web, c'est bien pratique, avec toutes ces pages qui agrègent texte, image et autre avec force HTML et CSS. Seulement, les utilisateurs ne sont pas toujours en ligne. Et puis, ils préfèrent parfois obtenir un document unique qu'ils pourront emmener sur une clé USB. C'est même parfois nécessaire: parce que votre utilisateur peut souhaiter partager ce document (sur papier ou sous forme numérique). Ou encore pour en garder une trace dans ses archives. Et pour tous ces usages, HTML n'est pas la panacée. Le standard de facto pour les documents électroniques portables est PDF. Dans cet article, nous allons donc voir comment écrire une servlet pour générer un document PDF avec iText.

Le point de départ

Pour cet article, nous allons nous imaginer que vous développez une application pour la société Comp'Il. Celle-ci travaille dans le domaine de l'édition musicale et est spécialisée dans la réalisation de compilations thématiques.

L'application qui nous intéresse ici va être chargée de générer un rapport sur l'état du stock. En situation réelle, les données devraient être extraites d'une base de données ou d'une autre source externe. Mais pour ce tutoriel, nous nous contenterons de quelque-chose de plus élémentaire:

package fr.chicoree.itext.servlet;
 
public class CompactDisc {
    public CompactDisc(String reference, String designation) {
        this.reference = reference;
        this.title = designation;
    }
 
    public String getTitle() {
        return title;
    }
 
    public String getReference() {
        return reference;
    }
 
    private String reference;
    private String title;
}
package fr.chicoree.itext.servlet;
 
import java.util.Arrays;
import java.util.List;
 
public class StockDAO {
    public List<CompactDisc> getStock() {
        /* Real world application should query the DB here ! */
        CompactDisc[] stock = {
                new CompactDisc("CT2009", "Best-of Country 2009", 617),
                new CompactDisc("CE2009", "Best-of Musique Celtique 2009", 1203),
                new CompactDisc("MA0101", "Musique du monde - Asie vol.1", 259),
                new CompactDisc("MA0102", "Musique du monde - Asie vol.2", 275),
                new CompactDisc("BO0060", "Le meilleur des années 60'", 762),
                new CompactDisc("BO0070", "Le meilleur des années 70'", 866),
                new CompactDisc("BO0080", "Top 80", 814)
        };
 
        return Arrays.asList(stock);
    }
}

HTML: à la main

Si l'on voulait écrire rapidement (sans utiliser de framework spécialisé) une servlet pour produire notre rapport au format HTML, voici ce que cela pourrait donner:

public class HTMLReportServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        StockDAO dao = new StockDAO();
        List<CompactDisc> stock = dao.getStock();
 
        resp.setContentType("text/html; charset=utf-8");
        PrintWriter os = new PrintWriter(resp.getOutputStream());
 
        os.println("<html>");
        os.println("<head><title>Comp'Il Stock Report</title></head>");
        os.println("<body>");
 
        os.println("<h1>Comp'Il Stock Report</h1>");
        os.println("<p>" + new Date() + "</p>");
 
        os.println("<table>");
        os.println("<tr><th>Ref</th><th>Title</th><th>Qty</th></tr>");
        for (CompactDisc cd : stock) {
            String line = String.format(
                    "<tr><td>%s</td><td>%s</td><td align='right'>%d</td></tr>",
                    cd.getReference(), cd.getTitle(), cd.getStock());
 
            os.println(line);
        }
        os.println("</table>");
 
        os.println("</body>");
        os.println("</html>");
 
        os.flush();
    }
}

D'accord, ce code est tout sauf un joyau du point de vue du génie logiciel! Mais j'ai voulu le garder le plus évident possible. Son intérêt principal étant de vous permettre d'avoir un point de comparaison avec la version PDF que nous allons voir juste après. Juste avant, examinons tout de même le résultat produit:

<html>
<head><title>Comp'Il Stock Report</title></head>
<body>
<h1>Comp'Il Stock Report</h1>
<p>Thu Dec 17 18:21:49 CET 2009</p>
<table>
<tr><th>Ref</th><th>Title</th><th>Qty</th></tr>
<tr><td>CT2009</td><td>Best-of Country 2009</td><td align='right'>617</td></tr>
 
<tr><td>CE2009</td><td>Best-of Musique Celtique 2009</td><td align='right'>1203</td></tr>
<tr><td>MA0101</td><td>Musique du monde - Asie vol.1</td><td align='right'>259</td></tr>
<tr><td>MA0102</td><td>Musique du monde - Asie vol.2</td><td align='right'>275</td></tr>
<tr><td>BO0060</td><td>Le meilleur des années 60'</td><td align='right'>762</td></tr>
<tr><td>BO0070</td><td>Le meilleur des années 70'</td><td align='right'>866</td></tr>
 
<tr><td>BO0080</td><td>Top 80</td><td align='right'>814</td></tr>
</table>
</body>
</html>

PDF: iText entre en jeu

Pour générer la version PDF de notre document, nous allons utiliser iText. Sur le fond, rien de spécial ne distingue l'utilisation d'iText dans le contexte d'une servlet plutôt que dans celui d'une application autonome. Par contre, il faut prendre garde à utiliser convenablement les en-tête http – et respecter les spécifications des servlets. Ainsi, examinons ces deux lignes:

/* ... */
            resp.setContentType("application/pdf");
            resp.setHeader("Content-Disposition", " inline; filename=report.pdf");

Celles-ci permettent d'indiquer dans l'en-tête http renvoyé par la servlet que nous allons servir un document PDF. C'est à dire de type mime application/pdf.

Par ailleurs, j'indique aussi que le document doit être vu (si possible) en ligne, c'est à dire directement dans le navigateur ... s'il supporte cette fonctionnalité! Le cas échéant, l'en-tête Content-Disposition permet également de préciser le nom sous lequel télécharger le fichier.

.pdf

Par ailleurs, l'indication du nom de fichier, et surtout le fait qu'il se termine avec l'extension .pdf sert aussi d'indice à certains navigateurs capricieux pour déterminer le type de document qu'ils reçoivent.

Oui, vous avez raison, normalement le type mime sert à ça. Mais il faut croire que les développeurs des anciennes versions d'IE n'étaient pas au courant...

Autre petite chose à noter, les méthodes d'iText que nous allons utiliser sont susceptibles de générer des exceptions de la classe com.lowagie.text.DocumentException. Or une servlet ne peut laisser passer que des exceptions ServletException ou IOException. Donc deux options ici:

C'est ce second choix que nous allons faire ici, puisque je ne vois pas de traitement local raisonnable en cas d'anomalie lors de génération du PDF. Ce qui donne cette structure pour le code:

public class PDFReportServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        /* ... */
        Document document = new Document();
        try {
            /* Setup http header */
            resp.setContentType("application/pdf");
            resp.setHeader("Content-Disposition", " inline; filename=report.pdf");
 
            /* generate PDF document */
            /* 
               ... 
            */
        } catch (DocumentException de) {
            // Wrap inside a ServletException
            throw new ServletException(de);
        }
 
        document.close();
    }
}

En ce qui concerne la génération du document à proprement parler, celle-ci n'utilise que du code iText classique. Sans entrer trop dans les détails, j'ajoute pour commencer le titre du document sous la forme de méta-information. Ce titre apparaîtra à l'utilisateur final à la fois dans la barre de titre, ainsi que dans les propriétés du document:

/* ... */
            document.addTitle("Comp'Il Stock Report");

Ensuite j'ajoute quelques paragraphes (com.lowagie.text.Paragraph):

/* ... */
            Paragraph p = new Paragraph("Comp'Il Stock Report",new Font(Font.HELVETICA, 24));
            p.setSpacingAfter(8);
            document.add(p);
 
            p = new Paragraph(new Date().toString(), new Font(Font.HELVETICA, 10));
            p.setSpacingAfter(8);
            document.add(p);

Puis finalement, l'essentiel du document sous la forme d'une table (com.lowagie.text.pdf.PdfPTable):

/* ... */
            /* New table - 3 columns of *relative* width 2/4/1 */
            PdfPTable   table = new PdfPTable(new float[] { 2f, 4f, 1f });
 
            /* Add the three headings */
            Font    headingStyle = new Font(Font.HELVETICA, Font.DEFAULTSIZE, Font.BOLD);
            table.addCell(new Phrase("Ref", headingStyle));
            table.addCell(new Phrase("Title", headingStyle));
            table.addCell(new Phrase("Qty", headingStyle));
 
            /*
               ...
            */
 
            document.add(table);

PdfPTable ou PdfTable

La classe PdfPTable (notez bien le P au milieu!) est la classe recommandée par Bruno Lowagie (l'auteur de iText) pour générer des tables. Elle succède et remplace PdfTable (sans P) qui n'est plus maintenue. L'inconvénient (tout relatif) étant que PdfPTable ne peut être utilisée que pour générer un document PDF. Pas si vous voulez utiliser iText pour générer un document HTML ou RTF.

A titre de référence, voici le code complet de cette servlet:

public class PDFReportServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        StockDAO dao = StockDAO.getInstance();
        List<CompactDisc> stock = dao.getStock();
 
        Document document = new Document();
        try {
            /* setup http header */
            resp.setContentType("application/pdf");
            resp.setHeader("Content-Disposition", " inline; filename=report.pdf");
 
            /* generate PDF document */
            PdfWriter.getInstance(document, resp.getOutputStream());
            document.open();
 
            document.addTitle("Comp'Il Stock Report");
 
            Paragraph p = new Paragraph("Comp'Il Stock Report",new Font(Font.HELVETICA, 24));
            p.setSpacingAfter(8);
            document.add(p);
 
            p = new Paragraph(new Date().toString(), new Font(Font.HELVETICA, 10));
            p.setSpacingAfter(8);
            document.add(p);
 
            /* New table - 3 columns of *relative* width 2/4/1 */
            PdfPTable   table = new PdfPTable(new float[] { 2f, 4f, 1f });
 
            /* Add the three headings */
            Font    headingStyle = new Font(Font.HELVETICA, Font.DEFAULTSIZE, Font.BOLD);
            table.addCell(new Phrase("Ref", headingStyle));
            table.addCell(new Phrase("Title", headingStyle));
            table.addCell(new Phrase("Qty", headingStyle));
 
            for(CompactDisc cd: stock) {
                table.addCell(cd.getReference());
                table.addCell(cd.getTitle());
 
                PdfPCell qty = new PdfPCell(new Phrase(Integer.toString(cd.getStock())));
                qty.setHorizontalAlignment(Element.ALIGN_RIGHT);
                table.addCell(qty);
            }
            document.add(table);
        } catch (DocumentException de) {
            // Wrap inside a ServletException
            throw new ServletException(de);
        }
 
        document.close();
    }
}

Lors de l'accès à cette servlet, mon client web (firefox) présente la boîte de dialogue suivante:

Firefox demande à l'utilisateur s'il préfère ouvrir ou télécharger le document PDF. Notez que le nom proposé pour le fichier est celui indiqué dans l'en-tête Content-Disposition.
Le document produit. Le titre de la fenêtre est le titre indiqué dans les méta-informations du document.

web.xml

Avant de terminer, un mot du fichier web.xml. Si vous êtes habitué du codage de servlet, vous savez que ce fichier sert notamment à indiquer au container de servlet à quelle URL votre servlet va répondre.

Pourquoi en parler? Et bien parce que j'utilise ici un petit truc:

<web-app version="2.5">
    <servlet>
        <servlet-name>iText HTMLServlet</servlet-name>
        <servlet-class>fr.chicoree.itext.servlet.HTMLReportServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>iText HTMLServlet</servlet-name>
        <url-pattern>/report.html</url-pattern>
    </servlet-mapping>
 
    <servlet>
        <servlet-name>iText PDFServlet</servlet-name>
        <servlet-class>fr.chicoree.itext.servlet.PDFReportServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>iText PDFServlet</servlet-name>
        <url-pattern>/report.pdf</url-pattern>
    </servlet-mapping>
</web-app>

Remarquez surtout ces lignes:

<servlet-name>iText HTMLServlet</servlet-name>
<url-pattern>/report.html</url-pattern>
<!-- ... -->
<servlet-name>iText PDFServlet</servlet-name>
<url-pattern>/report.pdf</url-pattern>

En fait, je fais répondre la version HTML de la servlet aux requêtes pour report.html et la version PDF aux requêtes report.pdf. Je trouve que c'est à la fois simple et user friendly, puisque l'utilisateur voit la même URL avec les extensions qu'il connaît.

Note:

Si vous connaissez bien http, vous savez qu'un client peut spécifier sous quelle représentation il préférerait recevoir une ressource grâce à l'en-tête standard http Accept. Puisqu'il est fait spécialement pour ça, il serait donc tout à fait légitime d'utiliser cet en-tête pour choisir entre la génération de PDF ou de HTML.

Néanmoins, puisqu'il n'est pas possible de spécifier la valeur d'un en-tête dans une adresse web, il reste souvent nécessaire de pouvoir spécifier la représention directement dans l'URI. Comme je le fais ici via l'extension .html ou .pdf.

Conclusion

L'objectif de cet article était de vous montrer qu'il n'est pas très compliqué de produire un document PDF à partir d'une servlet. Ici, je suis parti du postulat que vous alliez écrire une servlet rien que pour cela. Mais peut-être que la génération de PDF ne devrait être qu'une fonctionnalité d'une application plus ambitieuse écrite avec un framework comme JBoss Seam ou Apache Wicket.

Néanmoins, j'espère que les informations présentées ici vous ont convaincu qu'à condition de prêter attention aux quelques petits détails que j'ai soulignés (type mime, content-type, exceptions), cela n'a rien d'insurmontable. Et le cas échéant vous devriez donc être en mesure d'intégrer iText à votre framework préféré!

Ressources