以前の連載、「Officeのリッチクライアントアプリケーション」で、我々は、基準としてOffice2007プラットフォームを使い、様々な異なる方法でJavaテクノロジーと相互運用するリッチクライアントアプリケーションをどこから構築するかについて議論しました。その際にカバーされなかったOfficeとJavaの相互運用性の1領域は恐らく最も古びた方法になっているでしょう。なぜならOfficeとJavaは、JavaアプリケーションはOffice文書を操作し、文書を作成し、編集し、文書中からデータを抽出して取得する等等、といったように相互に連携するのですから。
Office文書(主にWord, Excel, PowerPoint)がバイナリ形式で保存されることが歴史的に常に何かと問題となってきましたが、これは世界中のCOM開発者には構造化された保存形式として知られており、本来、COMインタフェースを使ってアクセスする階層化されたバイナリ形式なのです。COM開発者(その他、Visual Basic、Delphi、C++/ATLのようにCOMを意識した言語開発者)にとって大変有用なアプローチではあるものの、ファイルの内容が、COMをサポートしていない言語では作成したファイルにアクセスすることは不可能でした。これらのファイルの内容にJavaからアクセスできる回避方法は、アプリケーションからアプリケーションへと大胆に変化していきました。例えば、Excelはカンマで区切られた値(CSV)のファイルを読むことができるのはよく知られたことですから、Excelが使える形式にデータを抽出したいJavaアプリケーションは、データをCSV形式(間違いなく見苦しいフォーマットなのですが)に抽出するのです。Wordにしても、勿論のことリッチテキスト形式(RTF)ファイルを読むことができ、RTFの仕様は公開されており、幾分はきちんと文書化されていました。Officeのその後のバージョン、すなわちOffice2003は、それ自身ユニークな(WordMLのように)新しいXML形式を発表しました。Java開発者はそれを使ってOffice文書を読み書きできるのですが、その形式はきちんと文書化されていなかったので、Java開発者は頻繁にトライアル・アンド・エラーの開発により、自分たちでWordML形式を学ぶこととなりました。様々なオープンソースプロジェクトが状況を軽減しようと試みました。ApacheからのPOIフレームワークのように、Excel文書を読み書きするために、また、様々なJava-COMソリューションが、Javaの開発者は、Office自身が使用するのと同様の構造化ストレージAPIを使って、Officeファイルを読み書きできると示唆しました。しかしそれはとても十分ではありませんでした。以来、開発者はOffice 文書の形式の内部構造を解明しなければなりませんでした。それはかなり複雑な形式で、勿論、全く文書化されていませんでした。
全般的に、Java/Officeストーリーは、控えめに言っても、極めて不快な状態でした。Javaの開発者はじっと耐えるか、「Officeは最低だ。とにかく、なぜ、誰かがそれを使いたがるんだい?」と言うことで、イソップ物語の1つを大いに連想するやり方で自分自身を慰めるか、単純にOfficeを使っているユーザーに、Microsoft とSun の訴訟問題のせいで、JavaはOfficeを理解することができないのだ、と言ってみるかしかありませんでした。
Office2007で、Microsoftは疑いもなく、これらの問題の重要な部分を「解決」しました。ネイティブJDKそのもの以上に複雑なものは何もなく、言い換えれば、サードパーティのライブラリは必要なく、Javaアプリケーションは今やOffice2007のいかなる文書をも読み書きできるのです。なぜなら、Office2007の文書は今やXML文書のZIPファイル以外の何物でもないからです。オープンXMLの仕様は、「Open XML」の仕様として知られており、C#言語とCLIランタイムの仕様を収めている欧州コンピュータ製造工業会(ECMA)に提出されています。オープンXMLの仕様は、ECMA(サイト・英語)で誰でもフリーでダウンロードできるようになっています。Office2007のインストレーション(検証やいくばくかのテストのため)や標準Java6JDKインストレーションといったこれらを装備し、Javaは今やOffice 2007の文書を自在にオープンすることができ、うまみのある中心をかきだし、コンテンツを操作し、データを再保存することができます。
この記事では、以前の記事とは異なり、シンプルなアプリケーションを構築するというよりも、コードはStuart Halloway氏により最初に開発された、探査テストと呼ばれるテクニックを使うことになるでしょう。探査テストでは、開発者は、自分たちがAPIを探査できるようにユニットテストを書きます。それは、従来のユニットテスト世界のテストアサーションの意味を使って結果を検証するものです。探査テストの副次的な利点は、この例のように、APIの新しいバージョンが利用可能になり、新しいバージョンを検証するため新しいバージョンのOfficeがテストを実行可能な際、APIの利用モデルにおいて何も変更する必要が無いということです。
まずはじめに、Office2007文書を概観してみましょう。シンプルなWord2007文書になり、このように、まさにプレーンなテキストを含んでいます。
保存する際は、Word2007は、過去の文書との互換性を持つ形式、すなわちOffice2003のWordMLか更に古いWord97以降のバイナリ構造のストレージ形式かを指定しない限り、このように”Hello.docx”として保存します。”.docx”形式はオープンXMLフォーマットであり、そしてMicrosoft独自の文書化によれば、XML文書のZipファイルであり、以前のバージョンのOfficeでバイナリ構造で格納するAPIがデータを保存していたのと類似した方法でデータを保持し、文書をフォーマットしています。これが正しければ、ZIP、TRA形式を認識するJava の"jar"ユーティリティはファイルのコンテンツを表示することができるはずです。実際、表示は可能です。
Word2007の文書形式の基本構造は既に結果出力を見ることにより極めて明確です。(そしてjarユーティリティがこれを理解するという事実はそれ自体、興奮に値することなのです。その意味するところは、java.util.jarとjava.util.zipもしくはjava.util.jar かjava.util.zipパッケージが極めて簡単にコンテンツにアクセスできる、ということなのです。)いかなる仕様書をクラックしてオープンすることも無く、コアとなる文書のコンテンツはdocument.xmlに保存され、XMLファイルの残りの部分は様々な補足部分になっています。例えば文書内で使われるフォントはfontTable.xmlに、Officeの主題はtheme.xml 、theme1.xml・・・といったぐあいに。
今回は、幾つかの探査テストを書いてみます。(興味ある読者は、テキストエディタかIDEを作動し、JUnit 4テストクラスにそれらを追加し、イマジネーションがそれらを受け取るようにテストを拡張することをお勧めします。)JUnit4を使った最初のテストは、単純にファイルが期待される位置に存在することを確認します。(これは、残りのテストを実行する際には明白な要件となります。):
@Test public void verifyFileIsThere() {
assertTrue(new File("hello.docx").exists());
assertTrue(new File("hello.docx").canRead());
assertTrue(new File("hello.docx").canWrite());
}
次のテストは、Javaライブラリの最も明白な候補であるjava.util.zip.ZipFileを使い、ファイルが開けることを単純に検証するものです。:
@Test public void openFile()
throws IOException, ZipException
{
ZipFile docxFile =
new ZipFile(new File("hello.docx"));
assertEquals(docxFile.getName(), "hello.docx");
}
今までのところ、うまくいっています。JavaのZipFileは、ファイルが実際にzipファイルであることは認識します。不運に見舞われなければ、コンテンツを繰り返しみて、内部のデータを確認することができるでしょう。さあ、コンテンツの最初から最後まで、”document.xml”というファイルを繰り返し探す、クイックテストを書いてみましょう。:
@Test public void listContents()
throws IOException, ZipException
{
boolean documentFound = false;
ZipFile docxFile =
new ZipFile(new File("hello.docx"));
Enumeration entriesIter =
docxFile.entries();
while (entriesIter.hasMoreElements())
{
ZipEntry entry = entriesIter.nextElement();
if (entry.getName().equals("document.xml"))
documentFound = true;
}
assertTrue(documentFound);
}
しかし、奇妙なことに、実行時、このテストは次のようなテストフェイルを出力します。”document.xml”は見つからないようです。なぜなら、ZipFile或いはZipEntryを使うと、APIはアーカイブの内部にマッチすべき完全なディレクトリ名、ファイル名を要求するからです。上記テストは”word/document.xml”に変更すれば、合格です。
文書を探すことが適正であれば、次に、それをオープンし、内部のXMLを見てみましょう。これは極めて簡単です。ZipFileはZipEntryそのものを名前で返してくれるAPIを持っているからです。:
@Test public void getDocument()
throws IOException, ZipException
{
ZipFile docxFile =
new ZipFile(new File("hello.docx"));
ZipEntry documentXML =
docxFile.getEntry("word/document.xml");
assertNotNull(documentXML);
}
驚くことではありませんが、getInputStream()コールがInputStreamを返すのを経由して、ZipFileコードはそれ自身が持っているエントリーの内容を返すことができます。InputStreamをDOMファクトリーノードの中に送り込むと、文書それ自体のDOMを作成します。:
@Test public void fromDocumentIntoDOM()
throws IOException, ZipException, SAXException,
ParserConfigurationException
{
ZipFile docxFile =
new ZipFile(new File("hello.docx"));
ZipEntry documentXML =
docxFile.getEntry("word/document.xml");
InputStream documentXMLIS =
docxFile.getInputStream(documentXML);
DocumentBuilderFactory dbf =
DocumentBuilderFactory.newInstance();
Document doc =
dbf.newDocumentBuilder().parse(documentXMLIS);
assertEquals("[w:document: null]",
doc.getDocumentElement().toString());
}
実際に、document.xmlコンテンツ(下記のように、名前空間宣言は明確にするために削除されます)は、Wordがサポートする必要のある、広範囲の種類のフォーマッティングをサポートする他のXML文書形式に比べ、極めて単調に見えます。:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document ...>
<w:body>
<w:p w:rsidR="00DE36E5" w:rsidRDefault="00DE36E5">
<w:r>
<w:t>Hello, from Office 2007!</w:t>
</w:r>
</w:p>
<w:sectPr w:rsidR="00DE36E5">
<w:pgSz w:w="12240" w:h="15840"/>
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>
<w:cols w:space="720"/>
<w:docGrid w:linePitch="360"/>
</w:sectPr>
</w:body>
</w:document>
これらの各要素が何を意味するのかについての十分な詳細については、この記事の範囲を越えているので、読者は、完全なリファレンスについては、オープンXMLドキュメンテーションセットを参照してください。しかし、極めて明白なことは、文書のコアとなる部分は““エレメントであり、“p”(パラグラフ)エレメントを含み、順に、“t”(テキスト)エレメントを構成する“r”(テキスト範囲)エレメントを含み、hello.docx文書そのもののボディをその中に見つけ、このケースでは、"Hello, from Office 2007!"という1つのセンテンスを見つけることになります。
ファイルの内容を読み、今やそれらの内容を変更でき、ファイルに書き戻し、Word2007でオープンすることもできるということは素晴らしいことに違いないでしょう。しかし、ZipFile とZipEntryのAPIを概観すると、ある問題を示しています。これらのクラスはzipファイルを読むのに使うことができますがzipファイルを書くための機能を持っていないのです。
この特別な欠陥を是正するために様々な仕組みが用意されています。1つのアプローチは、単純にXMLテキストを文字列に書き出し、その文字列をdocument.xmlファイルに保存し、ZipOutputStreamクラスを使ってコンテンツ全体を再度zipすることです。もう1つのアプローチはzipコンテンツを適切に編集できるサードパーティのツールを使う(或いは構築する)ことかもしれません。しかし、これはコアJDKそれ自体では無いので、この記事では、ZipOutputStreamを取り上げることにしましょう。
これら全てが起こるためには、2、3のことが必須になります。第一に、JavaアプリケーションはDOM階層構造を検索し、"t"ノードを発見し、そのテキストコンテンツをWord文書に送り返すべきメッセージで置き換えなければなりません。("Hello, Office 2007, from Java6!"はともかくも妥当であるように思われます。)タスクはJava XML APIを使って実行することが簡単ではないので、結果として生じるDOMインスタンスはディスクに保存されなければならなりません。(一言で言えば、開発者はjavax.xml.transformパッケージからTransformerを作成しなければなりません。そして、ByteArrayOutputStreamを巻きつけられたStreamResult向けにXML identity-transformを実行しなければなりません。
ひとたび全てが為されれば、今回はZipOutputStreamを使って、コードは新しいZIPファイルを生成しなければなりません。しかし、ファイルのコンテンツが変更される必要があるがために、スタイルやフォント、フォーマットではなく、その他のオリジナルファイルのコンポーネントが再度元からコピーされなければなりません。単純なループは、ソースファイルからZipEntriesをひとつずつ繰り返し処理し、コンテンツ全体(コンテンツが変換されたバイト列である"word/document.xml"は例外とします)を新しいZipEntryにコピーして、そのエントリーに書きだすという単純なループ処理で十分でしょう。全ての処理が終わると、コードは下記のように見えます。:
@Test public void modifyDocumentAndSave()
throws IOException, ZipException, SAXException,
ParserConfigurationException,
TransformerException,
TransformerConfigurationException
{
ZipFile docxFile =
new ZipFile(new File("hello.docx"));
ZipEntry documentXML =
docxFile.getEntry("word/document.xml");
InputStream documentXMLIS =
docxFile.getInputStream(documentXML);
DocumentBuilderFactory dbf =
DocumentBuilderFactory.newInstance();
Document doc =
dbf.newDocumentBuilder().parse(documentXMLIS);
Element docElement = doc.getDocumentElement();
assertEquals("w:document", docElement.getTagName());
Element bodyElement = (Element)
docElement.getElementsByTagName("w:body").item(0);
assertEquals("w:body", bodyElement.getTagName());
Element pElement = (Element)
bodyElement.getElementsByTagName("w:p").item(0);
assertEquals("w:p", pElement.getTagName());
Element rElement = (Element)
pElement.getElementsByTagName("w:r").item(0);
assertEquals("w:r", rElement.getTagName());
Element tElement = (Element)
rElement.getElementsByTagName("w:t").item(0);
assertEquals("w:t", tElement.getTagName());
assertEquals("Hello, from Office 2007!",
tElement.getTextContent());
tElement.setTextContent(
"Hello, Office 2007, from Java6!");
Transformer t =
TransformerFactory.newInstance().newTransformer();
ByteArrayOutputStream baos =
new ByteArrayOutputStream();
t.transform(new DOMSource(doc),
new StreamResult(baos));
ZipOutputStream docxOutFile = new ZipOutputStream(
new FileOutputStream("response.docx"));
Enumeration entriesIter =
docxFile.entries();
while (entriesIter.hasMoreElements())
{
ZipEntry entry = entriesIter.nextElement();
if (entry.getName().equals("word/document.xml"))
{
byte[] data = baos.toByteArray();
docxOutFile.putNextEntry(
new ZipEntry(entry.getName()));
docxOutFile.write(data, 0, data.length);
docxOutFile.closeEntry();
}
else
{
InputStream incoming =
docxFile.getInputStream(entry);
byte[] data = new byte[1024 * 16];
int readCount =
incoming.read(data, 0, data.length);
docxOutFile.putNextEntry(
new ZipEntry(entry.getName()));
docxOutFile.write(data, 0, readCount);
docxOutFile.closeEntry();
}
}
docxOutFile.close();
}
ここに表示されたコードの量についてお詫びしなければなりません。実際、これがJavaの他言語やライブラリに比較して最も弱い領域の1つなのです。幸いにも、結果は、結果として生じる文書がそう見える以外のところに代償を払います。
このシナリオを改善するのにいくつかの対策があることは明らかです。
まず、より良いXML操作ライブラリ、それはすぐに使えるXPathをサポートし、最適化されて順番にXML DOM構造をディスクに戻す、そのようなXML操作ライブラリはここにあるコードの量を削減するのに大きな助けとなるでしょう。オープンソースのJava/XMLライブラリであるJDOM(jdom.orgで利用可能である)は当然の選択です。ApacheからのXMLBeansも同様でしょう。OpenXML形式を記述するスキーマ文書を取得し、それらを使ってOpenXML文書形式をより綿密に映すjavaクラスのセットを生成することは当然の結果です。そして、開発者は"Document"クラスや"Element"クラスよりもネイティブなJavaクラスを使って仕事をすることが可能になるのではないでしょうか。
第二に、これらのアプローチのどちらでも、よりOffice特有のAPI、実際のXMLのストレージから離れ、Word(或いはExcelかPowerPoint)文書と連携する抽象化レイヤーを高め、代わりにこれらはパラグラフ、フォント、等々を有する文書であるという事実に集中するといったAPIに結合することができるかもしれません。基本的に、POIのようなライブラリは新しいOfficeのXML形式を反映した更新が可能ですし、またそのような更新をすべきであり、また、理想的には、新しいOpenXML形式と同様に、旧式のバイナリ構造ストレージ形式もサポートするように記述されるようになることが望ましいのです。
三番目に、再度、サードパーティのライブラリを使って実現も可能だったのですが、JavaはZIPファイルのサポートを、わずかな修正で利用することができました。
全ての厄介なAPIコールにも関らず、それでも、Javaプログラマにとって、どうやってOfficeプラットフォームを開こうかと考えることは、わくわくして元気付けられることなのです。Officeアプリケーション内でJavaを使うこととJavaアプリケーション内でOfficeを使うこと、そしてJavaからOfficeファイル形式を読み書きできること、これらの相互運用性の間にあって、OfficeプラットフォームはかつてないほどにJavaプログラミングコミュニティにとって、よりオープンになっています。
本記事内に付随するサンプルコードは「ここ」(source)でダウンロード可能です。
原文はこちらです:http://www.infoq.com/articles/cracking-office-2007-with-java(このArticleは2007年6月4日に原文が掲載されました)