S-JIS[2017-05-21] 変更履歴

Asakusa Framework XMLファイル入出力

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行分のデータを保持するデータモデルを用意する。

src/main/dmdl/models.dmdl:

"XMLファイルの1レコード"
xml_example = {

    column1 : TEXT;

    column2 : TEXT;
};

※ここでは特に「@directio.csv」等の属性は付けない。この属性は、あくまでFormatクラス等を自動生成する為のものであり、今回はそれを自作するので。


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のラムダ式で記述できる)


Importerクラスの例

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


Direct I/Oへ戻る / AsakusaFW目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま