1.はじめに
Apache FOPを利用するにあたり、Apache FOPのTIFF出力の方法 のブログで紹介したTIFF出力とPDF出力に使用したものと同じXSL-FOドキュメントを例に、実際のシステム開発で役立つ、Apache FOPの使用方法を説明したいと思います。Apache FOPを使用するために、帳票出力などに出力するデータは、XML形式のデータでなくてはなりません。しかし実際にXML形式で出力するとなると、DOMなどのXMLを扱うプログラムを利用しなければなりません。そのためのコストも余分にかかります。しかし、JAXBを利用することで、XMLを簡単に作成することができます。一般的な帳票の出力データ形式は、そんなに複雑なものではないはずです。ここでは、単一項目と、繰返し項目のデータを扱います。図 2. fax.xsdがそのためのXMLスキーマです。XMLスキーマは、XMLマスターベーシック試験より少し難しい程度で、この後にでてくるXSLTの部分に関しても、XMLマスターベーシック試験を取得している人なら難しくないはずです。
(SyntaxHighlighterを使用して、XMLスキーマ、XSLT、Java、XMLを掲載しています。ブラウザー上で、各々の図の右上のアイコンをマウスでクリックすることで、ソースを表示したり、クリップボードにコピーしたり、印刷したりできます。注意点として、「さらに記事を読み込む」のリンクで、この記事を読み込んだ場合に、SyntaxHighlighterによる表示が不正になる場合がありますので、恐れ入りますが、タイトルをクリックして再表示して下さい。)
2.帳票出力するまでの準備作業の流れ
- XSL-FO文書の実サンプルの作成。
- XMLの様式とXMLスキーマの作成。
- XSLTの作成。
- JAXBによるクラス生成。
- XMLとXSLTから、XSL-FOドキュメントを作成して、帳票を作成するJavaプログラムの作成。
XSL-FO文書の実サンプルを作成します。データのXMLの様式とXMLスキーマの作成をします。その後、XSL-FO文書の動的に変わる部分を、XSLTを使用して書き直します。XMLスキーマを元に、JAXBを使用してXMLとJavaオブジェクトをマッピングするクラスを作成します。生成されたクラスを使用してXMLデータを作成します。ここで示した流れ以外にもいろいろあると思いますが、これが一番わかりやすいと思います。
3.実際の出力結果
図 1. PDF出力(クリックするとPDFを表示します。)
4.帳票出力項目について
帳票の出力項目は、単一出力では、「発行日」、「送信者会社名」、「送信者TEL」、「送信者FAX」、「宛先名」、「宛先TEL」、「宛先FAX」、「宛先郵便番号」、「合計金額」、「消費税」と「合計金額(税込)」の11項目。複数出力では、「商品名」、「価格」、「単価」と「金額」の4項目で、合計15項目です。
5.fax.xsdのXMLスキーマについて
送信者と宛先の内容は共通化できるので、使用するXMLの型は共通となっています。ただし、送信者の方には、郵便番号がないので、29行目で、minOccurs=”0″としています。複数出力の項目は、複数出力されるように、maxOccurs=”unbounded”としています。
図 2. fax.xsd
<?xml version="1.0" encoding="UTF-8"?> <xs:schema version="1.0" targetNamespace="https://okulejp.wordpress.com/fax" xmlns:tns="https://okulejp.wordpress.com/fax" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"> <xs:element name="fax" type="tns:faxType"/> <xs:complexType name="faxType"> <xs:sequence> <xs:element ref="tns:issueDate"/> <xs:element ref="tns:destination"/> <xs:element ref="tns:sender"/> <xs:element ref="tns:items"/> <xs:element ref="tns:sum"/> </xs:sequence> </xs:complexType> <xs:element name="issueDate" type="xs:string"/> <xs:element name="destination" type="tns:company"/> <xs:element name="sender" type="tns:company"/> <xs:element name="items" type="tns:itemsType"/> <xs:element name="sum" type="tns:sumType"/> <xs:complexType name="company"> <xs:sequence> <xs:element name="name" type="xs:string"/> <xs:element name="tel" type="xs:string"/> <xs:element name="fax" type="xs:string"/> <xs:element name="postCode" type="xs:string" minOccurs="0"/> </xs:sequence> </xs:complexType> <xs:complexType name="itemsType"> <xs:sequence maxOccurs="unbounded"> <xs:element ref="tns:item"/> </xs:sequence> </xs:complexType> <xs:element name="item" type="tns:itemType"/> <xs:complexType name="itemType"> <xs:sequence> <xs:element name="productName" type="xs:string"/> <xs:element name="number" type="xs:int"/> <xs:element name="unitPrice" type="xs:int"/> <xs:element name="price" type="xs:int"/> </xs:sequence> </xs:complexType> <xs:complexType name="sumType"> <xs:all> <xs:element name="taxAmount" type="xs:int"/> <xs:element name="consumptionTax" type="xs:int"/> <xs:element name="tax-includedPrice" type="xs:int"/> </xs:all> </xs:complexType> </xs:schema>
6.fax.xslのXSLTについて
XSLTにおける関数使用について、XSLTで使用できる関数は、バージョンが1.0なら、「XML Path Language (XPath) Version 1.0」の4 Core Function Libraryにあるもの、2.0なら「XQuery 1.0 and XPath 2.0 Functions and Operators(Second Edition)」にあるものが使用できます。
XSLTでXpathの関数を用いて、金額から5%かけて消費税を求めても良かったのですが、消費税が変わったときに、XSLTも変更しないといけないので、XMLデータから消費税を取得しました。XMLデータから消費税率を取得して、XSLTで消費税を計算する方法もあると思います。XMLデータで取得したものを出力するか、XSLTで計算したものを出力するかは、ケースバイケースだと思います。
47行目で、fo:table要素に、table-omit-footer-at-break=”true”を指定すると、フッターは繰り返されません。
81行目で、format-number(fax:sum/fax:consumptionTax, ‘#,###’)の’#,###’は、3桁ごとにカンマを表示するという意味です。
107行目で、fo:table-cell要素に、keep-together.within-column属性を追加して、その属性値にalwaysを指定しています。行が複数ページにまたがるのを防ぐためです。ない場合は、項番18の内容が複数ページに分割されます。
119行目で、ループが最終行の場合、fo:table-cell要素に、属性値が、last-pageのid属性を追加しています。これは、23行目のfo:page-number-citation要素が参照していて、最終ページ番号を取得しています。(Apache FOPが、実際の出力時に、商品詳細の最後の部分がどのページ番号になるのかを計算してくれます。)
市販の帳票作成ツールでは、表のソート機能がありますが、XSLTでも、xsl:sort要素を使用することでソートできます。102行目のxsl:for-eachの後に、xsl:sort要素を追加します。またxsl:sort要素を複数追加した場合は、ソートキーは複数になります。
図 3. fax.xsl
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns:fax="https://okulejp.wordpress.com/fax"> <xsl:template match="/"> <xsl:apply-templates select="fax:fax" /> </xsl:template> <xsl:template match="fax:fax"> <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format" xml:lang="ja"> <fo:layout-master-set> <fo:simple-page-master page-height="297mm" page-width="210mm" margin-top="0mm" margin-left="0mm" margin-right="0mm" margin-bottom="0mm" master-name="PageMaster"> <fo:region-body margin-top="10mm" margin-left="10mm" margin-right="10mm" margin-bottom="20mm" /> <fo:region-before extent="10mm" /> <fo:region-after extent="10mm" /> <fo:region-start extent="10mm" /> <fo:region-end extent="10mm" /> </fo:simple-page-master> </fo:layout-master-set> <fo:page-sequence master-reference="PageMaster" font-family="MS ゴシック"> <fo:static-content flow-name="xsl-region-after"> <fo:block text-align="center" font-size="15pt"> ( <fo:page-number />/<fo:page-number-citation ref-id="last-page" /> ) </fo:block> </fo:static-content> <fo:flow font-family="MS ゴシック" flow-name="xsl-region-body"> <fo:block-container absolute-position="absolute" top="0mm" left="130mm"> <fo:block font-size="11pt">発行日 <xsl:value-of select="fax:issueDate" /></fo:block> </fo:block-container> <fo:block text-align="center" space-before="2mm" border-after-style="solid" border-after-width="thin" font-size="xx-large">御請求書</fo:block> <fo:block-container absolute-position="absolute" top="20mm" left="00mm"> <fo:block font-size="15pt">会社名 <xsl:value-of select="fax:destination/fax:name" /></fo:block> <fo:block font-size="13pt">TEL <xsl:value-of select="fax:destination/fax:tel" /></fo:block> <fo:block font-size="13pt">FAX <xsl:value-of select="fax:destination/fax:fax" /></fo:block> <fo:block></fo:block> </fo:block-container> <fo:block-container absolute-position="absolute" top="25mm" left="150mm"> <fo:block font-size="14pt"><xsl:value-of select="fax:sendern/fax:name" /></fo:block> <fo:block font-size="12pt">〒 <xsl:value-of select="fax:sender/fax:postCode" /></fo:block> <fo:block font-size="12pt">TEL <xsl:value-of select="fax:sender/fax:tel" /></fo:block> <fo:block font-size="12pt">FAX <xsl:value-of select="fax:sender/fax:fax" /></fo:block> <fo:block></fo:block> </fo:block-container> <fo:block-container id="test" absolute-position="absolute" top="50mm" left="00mm"> <fo:block font-size="12pt">下記の通り、ご請求申し上げます。</fo:block> </fo:block-container> <fo:table table-layout="fixed" font-family="MS ゴシック" top="100%" width="20%" font-size="14pt" space-before="47mm" table-omit-footer-at-break="true"> <fo:table-column column-number="1" column-width="15mm" /> <fo:table-column column-number="2" column-width="80mm" /> <fo:table-column column-number="3" column-width="15mm" /> <fo:table-column column-number="4" column-width="40mm" /> <fo:table-column column-number="5" column-width="40mm" /> <fo:table-header background-color="gray"> <fo:table-row> <fo:table-cell border-style="solid"> <fo:block text-align="center" margin-top="0.2em" margin-bottom="0.2em">項番</fo:block> </fo:table-cell> <fo:table-cell border-style="solid"> <fo:block text-align="center" margin-top="0.2em" margin-bottom="0.2em">商品名</fo:block> </fo:table-cell> <fo:table-cell border-style="solid"> <fo:block text-align="center" margin-top="0.2em" margin-bottom="0.2em">数量</fo:block> </fo:table-cell> <fo:table-cell border-style="solid"> <fo:block text-align="center" margin-top="0.2em" margin-bottom="0.2em">単価</fo:block> </fo:table-cell> <fo:table-cell border-style="solid"> <fo:block text-align="center" margin-top="0.2em" margin-bottom="0.2em">金額</fo:block> </fo:table-cell> </fo:table-row> </fo:table-header> <fo:table-footer> <fo:table-row> <fo:table-cell number-columns-spanned="3" number-rows-spanned="3"> <fo:block>備考</fo:block> </fo:table-cell> <fo:table-cell background-color="gray" padding="4pt" border-style="solid"> <fo:block font-weight="bold" text-align="center">合計</fo:block> </fo:table-cell> <fo:table-cell padding="4pt" border-style="solid"> <fo:block font-weight="bold" text-align="right"><xsl:value-of select="format-number(fax:sum/fax:taxAmount, '#,###')" /></fo:block> </fo:table-cell> </fo:table-row> <fo:table-row> <fo:table-cell background-color="gray" padding="4pt" border-style="solid"> <fo:block font-weight="bold" text-align="center">消費税</fo:block> </fo:table-cell> <fo:table-cell padding="4pt" border-style="solid"> <fo:block font-weight="bold" text-align="right"><xsl:value-of select="format-number(fax:sum/fax:consumptionTax, '#,###')" /></fo:block> </fo:table-cell> </fo:table-row> <fo:table-row> <fo:table-cell background-color="gray" padding="4pt" border-style="solid"> <fo:block font-weight="bold" text-align="center" font-family="MS Pゴシック">合計(税込)</fo:block> </fo:table-cell> <fo:table-cell padding="4pt" border-style="solid"> <fo:block font-weight="bold" text-align="right"><xsl:value-of select="format-number(fax:sum/fax:tax-includedPrice, '#,###')" /></fo:block> </fo:table-cell> </fo:table-row> </fo:table-footer> <fo:table-body> <xsl:for-each select="fax:items/fax:item"> <fo:table-row> <fo:table-cell border-style="solid" padding="0.4em"> <fo:block text-align="center"><xsl:value-of select="position()" /></fo:block> </fo:table-cell> <fo:table-cell keep-together.within-column="always" border-style="solid" padding="0.4em"> <fo:block> <xsl:value-of select="fax:productName" /> </fo:block> </fo:table-cell> <fo:table-cell border-style="solid" padding="0.4em"> <fo:block text-align="right"><xsl:value-of select="fax:number" /></fo:block> </fo:table-cell> <fo:table-cell border-style="solid" padding="0.4em"> <fo:block text-align="right"><xsl:value-of select="format-number(fax:unitPrice, '#,###')" /></fo:block> </fo:table-cell> <fo:table-cell border-style="solid" padding="0.4em"> <xsl:if test="position() = last()"> <xsl:attribute name="id">last-page</xsl:attribute> </xsl:if> <fo:block text-align="right"><xsl:value-of select="format-number(fax:price, '#,###')" /></fo:block> </fo:table-cell> </fo:table-row> </xsl:for-each> </fo:table-body> </fo:table> </fo:flow> </fo:page-sequence> </fo:root> </xsl:template> </xsl:stylesheet>
7.JAXBの使用方法
fax.xsdのスキーマを元に、バインディングコンパイラxjcを用いてJavaのクラスに生成します。コマンドラインから実行する方法と、Eclipseから実行する方法を示します。
●コマンドイランから実行する方法。スキーマファイル名を引数にしてxjcを呼び出すだけです。バインディングコンパイラxjcは、JDK 6以上に含まれています。
カレントディレクトリのcom/wordpress/okulejp/faxフォルダに7つのファイルが生成されました。
C:\Users\okule>"C:\Program Files (x86)\Java\jdk1.6.0_30\bin"\xjc C:\temp\jaxb\fax.xsd parsing a schema... compiling a schema... com\wordpress\okulejp\fax\Company.java com\wordpress\okulejp\fax\FaxType.java com\wordpress\okulejp\fax\ItemType.java com\wordpress\okulejp\fax\ItemsType.java com\wordpress\okulejp\fax\ObjectFactory.java com\wordpress\okulejp\fax\SumType.java com\wordpress\okulejp\fax\package-info.java C:\Users\okule>
●Eclipseから実行する方法。
図 4. fax.xsdを右クリックして、コンテキストメニューを表示させ、「Generate」→「JAXB Classes…」を選択します。
図 5. New JAXB Classes from schema設定画面が表示されたら、「Finish」ボタンを押下します。
8.Fax.javaについて
XMLの要素名とJavaの型名が対応しているので、DOMに比べてXMLを作成するのが簡単で、それでいてXMLスキーマ違反になりにくいです。
74から77行目で、XMLスキーマ検証を行っています。
クラスパスにXSLTプロセッサのプロバイダが、クラスパス上に複数ある場合に、使用するXSLTプロセッサを指定できます。88と90行目で、Xalanを使う場合と、Saxonを使う場合が示されています。詳細は、TransformerFactory.newInstance(String factoryClassName, ClassLoader classLoader) に載っています。
図 6. Fax.java
package com.wordpress.okulejp.fax; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.StringWriter; import java.io.StringReader; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.Marshaller; import javax.xml.transform.Result; import javax.xml.transform.sax.SAXResult; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.OutputKeys; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.XMLConstants; import org.apache.fop.apps.FOUserAgent; import org.apache.fop.apps.Fop; import org.apache.fop.apps.FopFactory; import org.apache.fop.apps.MimeConstants; public class Fax { public static void main(String[] args) { OutputStream out = null; try { JAXBContext context = JAXBContext.newInstance("com.wordpress.okulejp.fax"); ObjectFactory ojb = new ObjectFactory(); //fax要素の作成 FaxType faxType = ojb.createFaxType(); JAXBElement<FaxType> fax = ojb.createFax(faxType); //発行日 faxType.setIssueDate("平成24年08月12日"); //宛先 Company company = ojb.createCompany(); company.setName("株式会社A総務部御中"); company.setTel("03-3333-3333"); company.setFax("03-3333-3334"); faxType.setDestination(company); //送信者 company = ojb.createCompany(); company.setName("株式会社B"); company.setTel("05-5555-5555"); company.setFax("05-5555-5556"); company.setPostCode("999-9999"); faxType.setSender(company); //商品詳細 ItemType itemType = null; ItemsType itemsType = ojb.createItemsType(); for(int i=0; i<22; i++) { itemType = ojb.createItemType(); itemType.setProductName("ブルーレイレコーダー"); itemType.setNumber(1); itemType.setUnitPrice(120000); itemType.setPrice(120000); itemsType.getItem().add(itemType); } faxType.setItems(itemsType); itemsType.getItem().get(3).setProductName("ブルーレイレコーダー12345678901234567890"); itemsType.getItem().get(17).setProductName("ブルーレイレコーダー1234567890123456789012345678901234567890"); //金額合計 SumType sumType = ojb.createSumType(); sumType.setTaxAmount(2640000); sumType.setConsumptionTax(132000); sumType.setTaxIncludedPrice(2772000); faxType.setSum(sumType); // XMLスキーマ検証 Marshaller marshaller = context.createMarshaller(); SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); Schema schema = schemaFactory.newSchema(new File("src/com/wordpress/okulejp/fax/fax.xsd")); marshaller.setSchema(schema); // マーシャリング StringWriter marshallerStringWriter = new StringWriter(); marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.marshal(fax, marshallerStringWriter); System.out.println(marshallerStringWriter.toString()); //TransformerFactoryクラスの実装を選択する。(明示しない場合は、プラットホームのデフォルトのものが使用される。) //Xalanを使用する場合。 //TransformerFactory trf = TransformerFactory.newInstance("org.apache.xalan.processor.TransformerFactoryImpl", null); //Saxonを使用する場合。Saxonは、XSLT2.0に対応。 //TransformerFactory trf = TransformerFactory.newInstance("net.sf.saxon.TransformerFactoryImpl", null); //入力XMLとXSLTから、XSL-FOドキュメントを作成する。 TransformerFactory trf = TransformerFactory.newInstance(); Transformer transformer = trf.newTransformer(new StreamSource("src/com/wordpress/okulejp/fax/fax.xsl")); java.util.Properties xmlProps = new java.util.Properties(); xmlProps.setProperty(OutputKeys.VERSION, "1.0"); xmlProps.setProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperties(xmlProps); StringReader stringReader = new StringReader(marshallerStringWriter.toString()); StreamSource streamSource = new StreamSource(stringReader); StringWriter foStringWriter = new StringWriter(); StreamResult streamResult = new StreamResult(foStringWriter); transformer.transform(streamSource, streamResult); System.out.println(foStringWriter.toString()); //XSL-FOドキュメントから、PDFを作成する。 FopFactory fopFactory = FopFactory.newInstance(); FOUserAgent foUserAgent = fopFactory.newFOUserAgent(); fopFactory.setUserConfig(new File("fop.xconf")); out = new FileOutputStream("src/com/wordpress/okulejp/fax/fax.pdf"); out = new BufferedOutputStream(out); Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out); Result result = new SAXResult(fop.getDefaultHandler()); stringReader = new StringReader(foStringWriter.toString()); streamSource = new StreamSource(stringReader); transformer = trf.newTransformer(); transformer.transform(streamSource, result); } catch (Exception e) { e.printStackTrace(); System.exit(1); } finally { try { if(out!=null) { out.close(); } } catch(IOException io) { io.printStackTrace(); System.exit(1); } } } }
図 7. fax.xml(Fax.javaで作成されたXML。)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <fax xmlns="https://okulejp.wordpress.com/fax"> <issueDate>平成24年08月12日</issueDate> <destination> <name>株式会社A総務部御中</name> <tel>03-3333-3333</tel> <fax>03-3333-3334</fax> </destination> <sender> <name>株式会社B</name> <tel>05-5555-5555</tel> <fax>05-5555-5556</fax> <postCode>999-9999</postCode> </sender> <items> <item> <productName>ブルーレイレコーダー</productName> <number>1</number> <unitPrice>120000</unitPrice> <price>120000</price> </item> <item> <productName>ブルーレイレコーダー</productName> <number>1</number> <unitPrice>120000</unitPrice> <price>120000</price> </item> : : 省略 : : <item> <productName>ブルーレイレコーダー</productName> <number>1</number> <unitPrice>120000</unitPrice> <price>120000</price> </item> </items> <sum> <taxAmount>2640000</taxAmount> <consumptionTax>132000</consumptionTax> <tax-includedPrice>2772000</tax-includedPrice> </sum> </fax>
9.総評
fax.xsdから、JAXB Classesを生成すれば、XMLを意識することなく、Javaのsetterとgetterメソッドを用いて、XMLデータを作成するのがとても簡単なのがわかります。特に、DOMによるXMLを作成したことがある人なら、すごく簡単にXMLを作成できることが実感できると思います。固定様式の帳票を作成するときの入力XMLデータの様式は、一定または、ほぼ一定であると思うので、XMLスキーマファイルを難なく作成できると思います。JAXBをEclipse上で簡単に利用できるので、XMLバインドツールの敷居が低くなったと思います。またEclipseでは、XMLスキーマ、XSLT2.0のコード補完機能が利用できるので開発効率が上がります。それに加えて、XMLスキーマを選択して、マウスで右クリックしてコンテキストメニューを表示し、XMLファイルを生成することができます。またこのブログで掲載したとおり、fop.xsdを利用することで、Apache FOPのコード補完機能も利用できます。
10.参照
「Java SE 6完全攻略」第73回 JAXB その1
上記のサイトは、JAXBをより深く学ぶのに非常に役立ちます。いろいろなサイトを検索して探しましたが、一番わかりやすくて体系づけられて説明されています。連載は、「第80回 JAXB その8」まであります。
©中條勝徳 and okulejp.com, 2012.