LoginSignup
1
1

More than 3 years have passed since last update.

続・Servlet + Apache FOP で動的に PDF を作成して Web サイトに表示してみるサンプル (+Lombok)

Last updated at Posted at 2020-11-28

前回の投稿Apache FOP を用いて、JSP/Servlet サイトで PDF を動的に生成して表示するまでを試しました。日本語の表示がちょっと厄介でしたね。

Qiita 上でイイネ!(LGTM)はゼロなのですが、知り合いからは反応があったので、もうちょっと進めてみます。コードは apache-fop-jp-sample リポジトリ にあります。

今回のネタ

さて、具体的には以下のような感じの拡張を試してみます。

  • 入力値からデータ用 xml をダイレクトに生成してるけど、DB から取ってこれますか
  • フォーマット指定がファイル読み込みだけど、これも DB から取ってこれますか
  • サンプルがシンプルすぎるけど、説明レターとか請求書的なやつもちゃんと作れそうですか
  • 説明レターなどたまに更新されるのですが、版管理とかできますかね

なかなか具体的で、すごく業務寄りの拡張でありまして。これ全部実装したら、お金貰っても良いんじゃね?レベルかもしれない。まあ、気楽に趣味の範囲で対応してみますー。

DB アクセス

Java での DB アクセスって、結局は以下の2つを設計・実装することだと思います。

  • データ構造を表現する DTO (Data Transfer Object) クラスの定義
  • データを提供する DAO (Data Access Object) クラスの定義

DTO を設計するということは、RDB のテーブルを定義すること、DDL を用意するのと同じレイヤです。また DAO を設計・実装するということは、そのテーブルへのアクセスを定義すること、SQL を用意するのと同じレイヤです。

そして今回は DTO 作成を主に試します。DAO は実際に DB アクセスせず、それをエミューレートするだけに。いわゆるスタブ、モック的な実装ですね。というのも、DB アクセス部分は古典的で、今更試すことは少ないからです。

DTO 定義に Lombok を使ってみる

DTO クラスは setter/getter ばかり並ぶコードになります。せっかくなので、前から気になっていた Lombok を使ってみましょう。アノテーション付けると setter/getter を自動的に生成してくれるやつ。

興味ない人、ここはスキップしてもokです。そのかわり setter/getter は自分で実装してください。まあ Eclipse なら自動生成できますので、そんなに大変ではないハズ。

この解説ページ を参考に最新版の Version 1.18.16 をダウンロードしたのですが、残念ながらインストールに失敗します。
image.png
Fail to install lombok-1.18.14.jar in Eclipse Version: 2020-09 (4.17.0) とか読むと、Version 1.18.12 以前を推奨しているみたいですね。なので素直に 1.18.12 を使います。
image.png
あとはプロジェクトの CLASS PATH に lombok-1.18.12.jar を追加して、念のためリビルドすればokなハズ。

【追記】後で読み返してみたら、1.18.16 でもインストール成功している!問題あるのは 1.18.14 だけみたいですね… 早とちりでしたw

以下がインストール後の様子。name というインスタンス変数を定義しただけで、対応する setName/getName メソッドが自動生成されるのがわかります。これは便利!
image.png
これ、アノテーションプロセッサで AST 変換して実現しているハズです。コンパイル時にこれら setName/getName メソッドが追加されており、生成された Java Class は通常と変わらないと思われます。

つまり、Lambok は開発環境だけにあれば良くて、生成された実行クラスは Lambok には依存しない。配布時に lambok*.jar が無くても大丈夫、なハズ。

DTO クラスを定義

さて、それっぽい DTO クラスを定義しておきましょう。まずはシンプルな顧客データ。

dto/ClientDTO.java
package dto;
import lombok.Data;

@Data
public class ClientDTO {
    public ClientDTO(long id, String name, int age) {
        super();
        this.id = id;
        this.name = name;
        this.age = age;
    }
    private long id;
    private String name;
    private int age;
}

そしてシンプルな表示用フォーマット用の DTO です。最初の定義部分と、コンストラクターは省略しました。

dto/DocumentDTO.java
public class DocumentDTO {
    private Date start;
    private String name;
    private String format;
}

DAO クラスを定義

えーっと、まずは顧客用の DAO なのですが、DB アクセスどころか… めっちゃ手抜きして、とりあえずは以下のように定義しますw

dao/ClientDAO.java
package dao;
import dto.ClientDTO;

public class ClientDAO {
    public static ClientDTO getClient(long id) {
        return new ClientDTO(id, "name of " + id, 10 + (int)id);
    }
}

そして表示用フォーマット用の DAO です。3つのフォーマットを用意します。それぞれのフォーマット用の文字列(前回の変換設定に該当する) は後で実装します。

dao/DocumentDAO.java
package dao;
import java.util.Date;
import dto.DocumentDTO;

public class DocumentDAO {
    private static DocumentDTO[] documents = {
            new DocumentDTO(new Date(0), "sample1", "<ここは後で>"), // フォーマットその1
            new DocumentDTO(new Date(0), "sample2", "<ここは後で>"), // フォーマットその2
            new DocumentDTO(new Date(), "sample2", "<ここは後で>"), // フォーマットその2の新しい版
    };
    public static DocumentDTO getDocument(Date start, String name) {
        DocumentDTO ret = null;
        for (int l=0; l < documents.length; l++) {
            DocumentDTO doc = documents[l]; 
            if (doc.getName().equals(name) && (start == null || start.compareTo(doc.getStart()) >= 0)) {
                if (ret == null || ret.getStart().compareTo(doc.getStart()) < 0) {
                    ret = doc;
                }
            }
        }
        return ret;
    }
}

ここで重要なのが start 日付で、これを 版管理 に用います。表示用フォーマットを入手する際に「name が同じで start 日付が指定より後」を探すことで、その日時用の版のフォーマットが得られるわけです。条件に合うものが複数あった場合は、最も新しい版を使用します。

はじめの2つで new Date(0) としているものは 1970年1月1日、つまりだいぶ昔を指定しています。それに対して最後の new Date() は新しい日付、実行時の日時を指定しています。

上記の Java のコードで for ループで探している部分、実際に SQL で用意すると例えば以下のような感じですかね。

SELECT format FROM Documet_table WHERE name = ? AND start >= ?;

前回のサンプルを更新しよう

まずは少し改善

前回は時間がなくて sample.xsl に無駄があったので、まずは必要そうな要素だけに整理してみます。以下のような感じでしょうか。

sample.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:fo="http://www.w3.org/1999/XSL/Format" version="1.0">
    <xsl:output encoding="UTF-8" indent="yes" method="xml"
        standalone="no" omit-xml-declaration="no" />
    <xsl:template match="users-data">
        <fo:root language="ja">
            <fo:layout-master-set>
                <fo:simple-page-master master-name="simpleA4"
                    page-height="29.7cm" page-width="21cm" margin-top="2cm"
                    margin-bottom="2cm" margin-left="2cm" margin-right="2cm">
                    <fo:region-body />
                </fo:simple-page-master>
            </fo:layout-master-set>
            <fo:page-sequence master-reference="simpleA4" font-family="Meiryo,Hiragino">
                <fo:flow flow-name="xsl-region-body">
                    <fo:block text-align="center" margin-bottom="20mm">
                        ヘッダー部分
                    </fo:block>
                    <fo:block text-align="right">
                        Date: <xsl:value-of select="date" />
                    </fo:block>
                    <fo:block>
                        <xsl:value-of select="body" />
                    </fo:block>
                </fo:flow>
            </fo:page-sequence>
        </fo:root>
    </xsl:template>
</xsl:stylesheet>

生成される PDF は以下のようになります。
image.png
また Servlet コードも PDF の属性をセットできるよう修正します。

pdfServlet.java
            FOUserAgent userAgent = fopFactory.newFOUserAgent();
            userAgent.setTitle("yamachan360's sample PDF");
            //Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, out);
            Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, userAgent, out);

これで以下のように PDF のタイトルが指定できました。
image.png
他にも userAgent を用いて PDF の作成日や作成者などを指定可能です。詳しくは英語ですが マニュアル を参照してください。

入力画面の修正

入力画面の項目を増やします。文書フォーマット、日付、顧客コードを追加しました。ボディテキスト入力も残してあります。
image.png

細かな修正としては、フォーム送信 method を GET から POST に変更しました。URL にデータ残すのも宜しくないですし。

これら変更は本質的ではないため、説明は省きますね。GitHub: index.html の html コードを参照すれば中身は理解いただけるとおもいます。

ClientDAO を利用するよう Servlet を修正

入力画面から顧客コードを得られるようになったので、Servlet 側でそれを利用するよう修正します。また作成するデータ用 xml に得られた値を埋め込みます。

pdfServlet.java
            long i_ccode = Long.parseLong(request.getParameter("i_ccode"));
            ClientDTO client = ClientDAO.getClient(i_ccode);

            String xmlData = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><?xml-stylesheet type=\"application/xml\"?>"
                    + "<users-data>"
                    + "<date>" + (new Date()).toString() + "</date>"
                    + "<cname>" + client.getName() + "</cname>"
                    + "<cage>" + client.getAge() + "</cage>"
                    + "<body>" + i_body + "</body>"
                    + "</users-data>";

DocumentDAO と Servlet の修正

これまで sample.xsl ファイルを読み込んでいた部分を、DocumentDTO 経由で得られるよう、DocumentDAO の中にフォーマット用のテキストを埋め込みます。これもコードが長いので、GitHub: DocumentDAO.java で直接参照してください。

そして Servlet 側でも、sample.xsl ファイルのかわりに DocumentDTO から得られるフォーマット用のテキストを利用するように修正します。

pdfServlet.java
            //Source xsl = new StreamSource(this.getServletContext().getRealPath("/WEB-INF/sample.xsl"));
            DocumentDTO document = DocumentDAO.getDocument(i_fdate, i_fname);
            Source xsl = new StreamSource(new StringReader(document.getFormat()));
            Transformer transformer = tFactory.newTransformer(xsl);

以上、Servlet 変更点もけっこう多いので、GitHub: pdfServlet.java でコード全体を参照してください。

実行してみる

さて、これで修正も一段落しました。

DB を模した DAO クラスがあり、そこから得られた DTO クラスを用いて PDF を自動生成することができます。フォーマットは sample1 と sample2 が用意され、更に sample2 は実行日を境に版が更新されています。

まずは sample1 フォーマットを試してみましょう。
image.png
「Submit」ボタンをクリックすると、以下の PDF が生成され表示されます。
image.png
次に sample2 フォーマットを試してみましょう。
image.png
「Submit」ボタンをクリックすると、以下の PDF が生成され表示されます。sample1 との違いは最後に追加されたテキストです。
image.png
次に sample2 フォーマットの新しい版を試してみましょう。新しい版は start が現在時刻になっているので、いまより未来を指定してみてください。
image.png
「Submit」ボタンをクリックすると、以下の PDF が生成され表示されます。旧版との違いは最後に追加されたテキストの new の部分です。
image.png
以上、用意した3つの変換フォーマットの違いが小さいので、判り辛くて済みません。ただ、フォーマットを名前と版(日付)で使い分けることができること、これで最低限試すことができたとおもいます。

生成された PDF について

今回のサンプルプログラムで生成した PDF ファイルを以下の URL に置きました。

いまのところ、以下の環境で表示の確認をしています。もし文字化けとかしちゃう環境などありましたら、コメントいただけると助かります!

  • Google Chrome on Windows 10
  • Edge on Windows 10 (IEで開いてもこちら起動する)
  • Safari on Mac
  • Safari on iPhone
  • Kindle Fire HD (いったんダウンロードしてKindleアプリで表示)

今回のコードについて

コード量が多くなったので、GitHub の apache-fop-jp-sample リポジトリ にまとめました。参考にしていただければ幸いです。

ライセンス

この投稿に含まれる私の作成した全てのコードは Creative Commons Zero ライセンスとします。権利は一切主張しませんので、商用でもなんでも、自由にお使いください。

Enjoy!

以上、Servlet を用いて Web サイトで、PDF の動的生成を試してみました。これをベースに、いろいろ機能を追加して遊んでみてください。

ではまた!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1