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ファイルと同様。
→ベースパスとリソースパターンの使い分け