前回の投稿 は 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 をダウンロードしたのですが、残念ながらインストールに失敗します。
Fail to install lombok-1.18.14.jar in Eclipse Version: 2020-09 (4.17.0) とか読むと、Version 1.18.12 以前を推奨しているみたいですね。なので素直に 1.18.12 を使います。
あとはプロジェクトの CLASS PATH に lombok-1.18.12.jar を追加して、念のためリビルドすればokなハズ。
【追記】後で読み返してみたら、1.18.16 でもインストール成功している!問題あるのは 1.18.14 だけみたいですね… 早とちりでしたw
以下がインストール後の様子。name というインスタンス変数を定義しただけで、対応する setName/getName メソッドが自動生成されるのがわかります。これは便利!
これ、アノテーションプロセッサで AST 変換して実現しているハズです。コンパイル時にこれら setName/getName メソッドが追加されており、生成された Java Class は通常と変わらないと思われます。
つまり、Lambok は開発環境だけにあれば良くて、生成された実行クラスは Lambok には依存しない。配布時に lambok*.jar が無くても大丈夫、なハズ。
DTO クラスを定義
さて、それっぽい DTO クラスを定義しておきましょう。まずはシンプルな顧客データ。
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 です。最初の定義部分と、コンストラクターは省略しました。
public class DocumentDTO {
private Date start;
private String name;
private String format;
}
DAO クラスを定義
えーっと、まずは顧客用の DAO なのですが、DB アクセスどころか… めっちゃ手抜きして、とりあえずは以下のように定義しますw
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つのフォーマットを用意します。それぞれのフォーマット用の文字列(前回の変換設定に該当する) は後で実装します。
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 に無駄があったので、まずは必要そうな要素だけに整理してみます。以下のような感じでしょうか。
<?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 は以下のようになります。
また Servlet コードも PDF の属性をセットできるよう修正します。
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 のタイトルが指定できました。
他にも userAgent
を用いて PDF の作成日や作成者などを指定可能です。詳しくは英語ですが マニュアル を参照してください。
入力画面の修正
入力画面の項目を増やします。文書フォーマット、日付、顧客コードを追加しました。ボディテキスト入力も残してあります。
細かな修正としては、フォーム送信 method を GET から POST に変更しました。URL にデータ残すのも宜しくないですし。
これら変更は本質的ではないため、説明は省きますね。GitHub: index.html の html コードを参照すれば中身は理解いただけるとおもいます。
ClientDAO を利用するよう Servlet を修正
入力画面から顧客コードを得られるようになったので、Servlet 側でそれを利用するよう修正します。また作成するデータ用 xml に得られた値を埋め込みます。
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 から得られるフォーマット用のテキストを利用するように修正します。
//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 フォーマットを試してみましょう。
「Submit」ボタンをクリックすると、以下の PDF が生成され表示されます。
次に sample2 フォーマットを試してみましょう。
「Submit」ボタンをクリックすると、以下の PDF が生成され表示されます。sample1 との違いは最後に追加されたテキストです。
次に sample2 フォーマットの新しい版を試してみましょう。新しい版は start が現在時刻になっているので、いまより未来を指定してみてください。
「Submit」ボタンをクリックすると、以下の PDF が生成され表示されます。旧版との違いは最後に追加されたテキストの new の部分です。
以上、用意した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 の動的生成を試してみました。これをベースに、いろいろ機能を追加して遊んでみてください。
ではまた!