Asakusa Framework0.9.1(Direct I/O)でXMLファイルを入出力する方法のメモ。
|
|
Asakusa FrameworkでXMLファイルを読み書きするDirect I/OのFormatクラスを考えてみた。
JavaでXMLファイルを読み込むライブラリーはいくつかあるが、AsakusaFWで扱う場合はStAXが良い。
名称 | 説明 |
---|---|
DOM | XMLファイルを全部読み込んでメモリー上に保持するので、AsakusaFWで扱うような大きなファイルには向かない。 |
SAX | パースした各要素に対して「プログラマーが指定したメソッド」をコールバックする。(プッシュ型) AsakusaFWのModelInputでは、AsakusaFWから呼ばれたメソッド内で1レコード分のXMLを読まなければならないので、プッシュ型のXMLパーサーでは扱えない。 |
StAX | 要素を取り出すようXMLパーサー(StAX)に指示して要素を取得する。(プル型) AsakusaFWのModelInputでは、AsakusaFWから呼ばれたメソッド内で1レコード分のXMLを読まなければならないので、プル型のXMLパーサーは都合が良い。 |
Direct I/OでXMLファイルを読み込むImporterを作ってみる。
以下のようなXMLファイルを読み込むことを想定する。
<?xml version="1.0" encoding="UTF-8"?> <root> <record><column1>aaa</column1><column2>111</column2></record> <record><column1>bbb</column1><column2>222</column2></record> </root>
まず、1行分のデータを保持するデータモデルを用意する。
"XMLファイルの1レコード" xml_example = { column1 : TEXT; column2 : TEXT; };
※ここでは特に「@directio.csv」等の属性は付けない。この属性は、あくまでFormatクラス等を自動生成する為のものであり、今回はそれを自作するので。
次に、Formatクラスを作成する。
Direct I/Oの場合、BinaryStreamFormatを継承したクラスを作成する。
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; import com.asakusafw.runtime.directio.BinaryStreamFormat; import com.asakusafw.runtime.io.ModelInput; import com.asakusafw.runtime.io.ModelOutput; import com.example.modelgen.dmdl.model.XmlExample;
/** * XML format for {@link XmlExample}. */ public class XmlExampleXmlFormat extends BinaryStreamFormat<XmlExample> {
@Override public Class<XmlExample> getSupportedType() { return XmlExample.class; } @Override public long getPreferredFragmentSize() { return -1L; } @Override public long getMinimumFragmentSize() { // ファイル分割しない return -1L; }
XMLファイルの場合、ファイル分割はしないようにする。タグ名の途中とかで分割されると困るので。
(また、行末までちゃんと読み込むようにしたとしても、分割された途中からの部分は正しく読めなさそう(StAXではXMLファイルのヘッダーが必要そう)だし、1レコード分のデータが複数行にまたがっていて途中で分割された場合の処理も面倒)
@Override public ModelInput<XmlExample> createInput(Class<? extends XmlExample> dataType, String path, InputStream stream, long offset, long fragmentSize) throws IOException { return new Reader(stream); }
createInputメソッドで、ファイルを読み込んでデータモデルオブジェクトに書き込むModelInputを返す。
Readerクラスは自分で用意する。後述。
@Override public ModelOutput<XmlExample> createOutput(Class<? extends XmlExample> dataType, String path, OutputStream stream) throws IOException { return new Writer(stream); }
createOutputメソッドは、データモデルオブジェクトの内容をファイルに書き出すModelOutputを返す。
(XMLファイルを読むだけの場合でも、フローのテストの際にテストデータ
作成の為にXMLファイルを出力するので、ModelOutputは必要)
Writerクラスは自分で用意する。後述。
Readerクラスは、ModelInputインターフェースの実装。
private static final class Reader implements ModelInput<XmlExample> { private final InputStream stream; private final XMLStreamReader xmlReader; private final StringBuilder sb = new StringBuilder();
Reader(InputStream stream) throws IOException { this.stream = stream; try { XMLInputFactory factory = XMLInputFactory.newInstance(); this.xmlReader = factory.createXMLStreamReader(stream); } catch (XMLStreamException e) { throw new IOException(e); } }
Readerのコンストラクターで、StAXのXMLStreamReaderを生成する。
@Override public boolean readTo(XmlExample object) throws IOException { try { if (skip() == false) { return false; } object.reset(); // 各プロパティーのクリア(nullにする) for (; xmlReader.hasNext(); xmlReader.next()) { int eventType = xmlReader.getEventType(); switch (eventType) { case XMLStreamConstants.START_ELEMENT: sb.setLength(0); break; case XMLStreamConstants.CHARACTERS: case XMLStreamConstants.CDATA: case XMLStreamConstants.SPACE: sb.append(xmlReader.getText()); break; case XMLStreamConstants.END_ELEMENT: switch (xmlReader.getName().getLocalPart()) { case "column1": object.setColumn1AsString(sb.toString()); break; case "column2": object.setColumn2AsString(sb.toString()); break; case "record": // レコードの終わり xmlReader.next(); return true; default: break; } break; default: break; } } return false; } catch (XMLStreamException e) { throw new IOException(e); } } // START_ELEMENTが来るまでスキップ private boolean skip() throws XMLStreamException { for (; xmlReader.hasNext(); xmlReader.next()) { int eventType = xmlReader.getEventType(); if (eventType == XMLStreamConstants.START_ELEMENT) { return true; } } return false; }
readToメソッドでXMLファイルから1レコード分のデータを読み込み、データモデルインスタンスにセットしていく。
今回の例だとタグ名(XML要素のパス)が被っていないので単純にEND_ELEMENTで全て処理しているが、複雑なXMLだと、それに応じたコーディングをする必要がある。
@Override public void close() throws IOException { try (InputStream s = stream) { xmlReader.close(); } catch (XMLStreamException e) { throw new IOException(e); } } }
最後にXMLStreamReaderをクローズするが、XMLStreamReaderのcloseメソッドは元となったリソース(上記のInputStream)をクローズするとは限らないので、
自分でInputStreamもクローズする。(ここではtry-with-resources構文を使っている。ちなみに、Java9だと「try (stream) {
」という書き方が出来るようになるらしい)
Writerクラスは、ModelOutputインターフェースの実装。
private static class Writer implements ModelOutput<XmlExample> { private final OutputStream stream; private final XMLStreamWriter xmlWriter;
Writer(OutputStream stream) throws IOException { this.stream = stream; try { XMLOutputFactory factory = XMLOutputFactory.newInstance(); this.xmlWriter = factory.createXMLStreamWriter(stream); // ヘッダー xmlWriter.writeStartDocument("UTF-8", "1.0"); xmlWriter.writeCharacters("\n"); // root開始 xmlWriter.writeStartElement("root"); xmlWriter.writeCharacters("\n"); } catch (XMLStreamException e) { throw new IOException(e); } }
Writerのコンストラクターで、StAXのXMLStreamWriterを生成する。
また、XMLファイルのヘッダーや、一番外側の要素(今回の例ではrootタグ)の開始も出力している。
(本来、こういう出力処理はコンストラクター内に書くべきではないと思っているが、ModelOutputの場合は、他に適切な場所が無い)
@Override public void write(XmlExample object) throws IOException { try { xmlWriter.writeStartElement("record"); xmlWriter.writeStartElement("column1"); xmlWriter.writeCharacters(object.getColumn1Option().or("")); xmlWriter.writeEndElement(); xmlWriter.writeStartElement("column2"); xmlWriter.writeCharacters(object.getColumn2Option().or("")); xmlWriter.writeEndElement(); xmlWriter.writeEndElement(); // record xmlWriter.writeCharacters("\n"); } catch (XMLStreamException e) { throw new IOException(e); } }
writeメソッドで1レコード分のXMLを出力する。
@Override public void close() throws IOException { try (OutputStream s = stream; AutoCloseable c = () -> xmlWriter.close()) { // root終了 xmlWriter.writeEndElement(); xmlWriter.writeCharacters("\n"); xmlWriter.writeEndDocument(); } catch (IOException e) { throw e; } catch (Exception e) { throw new IOException(e); } } } }
closeメソッドの中で、一番外側の要素(今回の例ではrootタグ)の終了を出力する。
最後にXMLStreamWriterをクローズするが、XMLStreamWriterのcloseメソッドは元となったリソース(上記のOutputStream)をクローズしないので、
自分でOutputStreamもクローズする。
(XMLStreamWriterもtry-with-resources構文でクローズさせたいが、XMLStreamWriterはCloseableを実装していないので、AutoCloseableでラップした。なお、AutoCloseableは関数型インターフェースの要件を満たしているので、Java8のラムダ式で記述できる)
上記のFormatを使ったImporterクラスは、以下のようになる。
import com.asakusafw.runtime.directio.DataFormat; import com.asakusafw.vocabulary.directio.DirectFileInputDescription; import com.example.modelgen.dmdl.model.XmlExample;
public class XmlExampleFromXml extends DirectFileInputDescription { @Override public Class<?> getModelType() { return XmlExample.class; } @Override public Class<? extends DataFormat<?>> getFormat() { return XmlExampleXmlFormat.class; } @Override public String getBasePath() { return "input/file"; } @Override public String getResourcePattern() { return "*.xml"; } }
getModelTypeメソッドは、データモデルのクラスを返す。
getFormatメソッドで、自作したFormatクラスを返す。
getBasePathおよびgetResourcePatternメソッドは、Direct I/OのCSVファイルと同様。
→ベースパスとリソースパターンの使い分け