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

[前回の投稿](https://qiita.com/yamachan360/items/c028911b3eab49d0a281) は [Apache FOP](https://xmlgraphics.apache.org/fop/) を用いて、JSP/Servlet サイトで PDF を動的に生成して表示するまでを試しました。日本語の表示がちょっと厄介でしたね。

Qiita 上でイイネ!(LGTM)はゼロなのですが、知り合いからは反応があったので、もうちょっと進めてみます。コードは [apache-fop-jp-sample リポジトリ](https://github.com/yamachan/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](https://projectlombok.org/) を使ってみましょう。アノテーション付けると setter/getter を自動的に生成してくれるやつ。

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

[この解説ページ](https://qiita.com/yyoshikaw/items/32a96332cc12854ca7a3) を参考に最新版の Version 1.18.16 をダウンロードしたのですが、残念ながらインストールに失敗します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/0d87f27e-2d18-d099-8a74-770edb39fa36.png)
[Fail to install lombok-1.18.14.jar in Eclipse Version: 2020-09 (4.17.0)](https://github.com/rzwitserloot/lombok/issues/2599) とか読むと、Version 1.18.12 以前を推奨しているみたいですね。なので素直に 1.18.12 を使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/d11e0fa8-3a8b-31e5-ad8d-5bba53a7b8b9.png)
あとはプロジェクトの CLASS PATH に lombok-1.18.12.jar を追加して、念のためリビルドすればokなハズ。

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

以下がインストール後の様子。name というインスタンス変数を定義しただけで、対応する setName/getName メソッドが自動生成されるのがわかります。これは便利！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/dacbc033-7fc8-dbd4-1d7e-dcf4bf7c966b.png)
これ、アノテーションプロセッサで AST 変換して実現しているハズです。コンパイル時にこれら setName/getName メソッドが追加されており、生成された Java Class は通常と変わらないと思われます。

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

## DTO クラスを定義

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

```java: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 です。最初の定義部分と、コンストラクターは省略しました。

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

## DAO クラスを定義

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

```java: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つのフォーマットを用意します。それぞれのフォーマット用の文字列(前回の[変換設定](https://qiita.com/yamachan360/items/c028911b3eab49d0a281#%E5%A4%89%E6%8F%9B%E8%A8%AD%E5%AE%9A)に該当する) は後で実装します。

```java: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 で用意すると例えば以下のような感じですかね。

```sql
SELECT format FROM Documet_table WHERE name = ? AND start >= ?;
```
# 前回のサンプルを更新しよう

## まずは少し改善

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

```xml: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/0478eb90-6b61-9d6a-aae5-01b36f7827be.png)
また Servlet コードも PDF の属性をセットできるよう修正します。

```java: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/606e6f03-390b-8734-51e8-b261463195d8.png)
他にも ``userAgent`` を用いて PDF の作成日や作成者などを指定可能です。詳しくは英語ですが [マニュアル](https://xmlgraphics.apache.org/fop/0.95/embedding.html#user-agent) を参照してください。

## 入力画面の修正

入力画面の項目を増やします。文書フォーマット、日付、顧客コードを追加しました。ボディテキスト入力も残してあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/8b46e829-b071-f7e6-3584-b8e40d14cd15.png)

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

これら変更は本質的ではないため、説明は省きますね。[GitHub: index.html](https://github.com/yamachan/apache-fop-jp-sample/blob/main/WebContent/index.html) の html コードを参照すれば中身は理解いただけるとおもいます。

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

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

```java: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](https://github.com/yamachan/apache-fop-jp-sample/blob/main/src/dao/DocumentDAO.java) で直接参照してください。

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

```java: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](https://github.com/yamachan/apache-fop-jp-sample/blob/main/src/pdfServlet.java) でコード全体を参照してください。

# 実行してみる

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

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

まずは sample1 フォーマットを試してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/33b84305-3506-8b78-39bf-1687c25c6961.png)
「Submit」ボタンをクリックすると、以下の PDF が生成され表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/a8e477ee-7323-c524-dd51-a46950d13874.png)
次に sample2 フォーマットを試してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/9d5984a1-a423-3f5f-47a9-a13f33d1bb24.png)
「Submit」ボタンをクリックすると、以下の PDF が生成され表示されます。sample1 との違いは最後に追加されたテキストです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/6aadc1f3-535c-afcb-dc63-c7a38f8af77f.png)
次に sample2 フォーマットの新しい版を試してみましょう。新しい版は start が現在時刻になっているので、いまより未来を指定してみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/3dd02481-a83d-5c76-3cd9-2e409f8ef113.png)
「Submit」ボタンをクリックすると、以下の PDF が生成され表示されます。旧版との違いは最後に追加されたテキストの new の部分です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/eacff39c-522f-64b2-0141-b28f0428fc77.png)
以上、用意した3つの変換フォーマットの違いが小さいので、判り辛くて済みません。ただ、フォーマットを名前と版(日付)で使い分けることができること、これで最低限試すことができたとおもいます。

# 生成された PDF について

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

* [http://rinco.jp/test/fop1.pdf](http://rinco.jp/test/fop1.pdf)

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

* 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 リポジトリ](https://github.com/yamachan/apache-fop-jp-sample) にまとめました。参考にしていただければ幸いです。

# ライセンス

この投稿に含まれる私の作成した全てのコードは [Creative Commons Zero](https://creativecommons.org/publicdomain/zero/1.0/deed.ja) ライセンスとします。権利は一切主張しませんので、商用でもなんでも、自由にお使いください。

# Enjoy!

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

ではまた！