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

Java XML StAX

JavaXMLを操作するStAXについて。


概要

StAXはStreaming API for XMLの略(なのでクラス名にXMLStreamという接頭辞が付いている)。
StAXはJava6(JDK1.6)からJavaの標準APIの仲間入りをしたらしい。(なので特別なライブラリーを入れなくても使用できる)

StAXを使ってXMLファイルを読み込む場合、StAXのXMLパーサーに対して要素を読み込むよう指示すると、要素が1つ返ってくる。
その要素の種類に応じて自分の処理を行う。
XML自体はXML要素のツリー構造をしているが、StAXはツリー構造は認識せず、ただ現れた要素を返してくるのみ。


XMLファイルを読み込む例

以下のようなXMLファイルを読み込む例。

example.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>

XmlStaxExample.java:

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
public class XmlStaxExample {

	public static void main(String... args) throws IOException {
		new XmlStaxExample().read("example.xml");
	}
	public void read(String fileName) throws IOException {
		try (InputStream is = getClass().getResourceAsStream(fileName)) {
			XMLInputFactory factory = XMLInputFactory.newInstance();
			XMLStreamReader reader = factory.createXMLStreamReader(is);
			try (Closeable c = () -> {
				try {
					reader.close();
				} catch (XMLStreamException e) {
					throw new IOException(e);
				}
			}) {
				read(reader);
			}
//			try (AutoCloseable c = () -> reader.close()) {
//				read(reader);
//			} catch (Exception e) {
//				throw new IOException(e);
//			}
		} catch (XMLStreamException e) {
			throw new IOException(e);
		}
	}

XMLInputFactoryを使ってXMLStreamReaderを生成する。

XMLStreamReaderはCloseableを実装していないので、try-with-resources構文は使えない。そのため、自分でCloseableでラップしている。
Closeableは関数型インターフェースとして使う目的のものではないが、関数型インターフェースの要件は満たしているので、ラムダ式で指定できる。ただ、XMLStreamReader.close()はIOExceptionでなくXMLStreamExceptionを投げるので、ラップする為にtry-catchを書かねばならず、さらに(ラムダ式は処理本体部分が1つの式だけであれば波括弧で囲む必要は無いのだが、try-catchは式ではないので)波括弧で囲む必要がある。
(なんでそこまでしてtry-with-resources構文を使いたいかというと、close時に発生した例外を握りつぶさず、try本体で発生した例外と統合させる為)
(CloseableでなくAutoCloseableを使う方がすっきりするかな?Exceptionでcatchしちゃうのは微妙だけど…

	private void read(XMLStreamReader reader) throws XMLStreamException {

		// 1レコード分のcolumn1,column2を保持するJavaBean
		Record record = null;

		// テキスト保持用バッファー
		StringBuilder sb = new StringBuilder();

		for (; reader.hasNext(); reader.next()) {
			int eventType = reader.getEventType();
			switch (eventType) {
			case XMLStreamConstants.START_ELEMENT: // 開始要素
				if (reader.getName().getLocalPart().equals("record")) {
					record = new Record();
				}
				sb.setLength(0);
				break;

			case XMLStreamConstants.CHARACTERS: // 文字
			case XMLStreamConstants.CDATA:
			case XMLStreamConstants.SPACE:
				sb.append(reader.getText());
				break;

			case XMLStreamConstants.END_ELEMENT: // 終了要素
				switch (reader.getName().getLocalPart()) {
				case "column1":
					record.setColumn1(sb.toString());
					break;
				case "column2":
					record.setColumn2(sb.toString());
					break;
				case "record":
					System.out.println(record);
					break;
				}
				break;

			default:
				break;
			}
		}
	}
}

XMLStreamReaderのhasNextメソッドで、要素があるかどうかを判定する。
要素がある場合はイベントタイプや値が取得できる。
次の要素を取得するにはnextメソッドを呼び出す。
(java.util.Iteratorのイテレーターパターンとは異なるので注意)

イベントタイプがSTART_ELEMENTやEND_ELEMENTの場合は、getNameメソッドでタグ名が取れる。
ただし型はQNameというクラスであり、Stringのタグ名を取得する為にはgetLocalPartメソッドを呼び出す。

XML要素のボディー部にある文字列を取得するためには、CHARACTERSやCDATA等の文字列を自分で保持しておく必要がある。(上記ソースのsb)

タグ名が被らないと分かっている場合は、終了要素のタグ名だけで処理を分岐できる。


パスを使って判別する例

上記の例ではタグ名が被らないのでEND_ELEMENTでタグ名のみを使って判定しているが、
<group1><column1>a</column1></group1>と<group2><column1>b</column1></group2>のように外側のタグ(パス)が異なる(一番内側のタグ名だけで判別できない)場合は、もう少し工夫が要る。

簡単なのは、パスを保持するスタックを用意し、START_ELEMENTが来たらプッシュし、END_ELEMENTが来たらポップすることかな?

	private void read2(XMLStreamReader reader) throws XMLStreamException {
		// パス毎のセッターメソッド
		Map<List<String>, BiConsumer<Record, String>> SETTER_MAP = new HashMap<>();
		SETTER_MAP.put(Arrays.asList("root", "record", "column1"), Record::setColumn1);
		SETTER_MAP.put(Arrays.asList("root", "record", "column2"), Record::setColumn2);

		// 処理中のパス(タグ名のスタック)
		List<String> path = new ArrayList<>();

		// 1レコード分のcolumn1,column2を保持するJavaBean
		Record record = null;

		// テキスト保持用バッファー
		StringBuilder sb = new StringBuilder();

		for (; reader.hasNext(); reader.next()) {
			int eventType = reader.getEventType();
			switch (eventType) {
			case XMLStreamConstants.START_ELEMENT: // 開始要素
				String startName = reader.getName().getLocalPart();
				path.add(startName); // push

				if (startName.equals("record")) {
					record = new Record();
				}
				sb.setLength(0);
				break;

			case XMLStreamConstants.CHARACTERS: // 文字
			case XMLStreamConstants.CDATA:
			case XMLStreamConstants.SPACE:
				sb.append(reader.getText());
				break;

			case XMLStreamConstants.END_ELEMENT: // 終了要素
				String endName = reader.getName().getLocalPart();
				if (endName.equals("record")) {
					System.out.println(record);
				} else {
					BiConsumer<Record, String> setter = SETTER_MAP.get(path);
					if (setter != null) {
						setter.accept(record, sb.toString());
					}
				}
				path.remove(path.size() - 1); // pop
				break;

			default:
				break;
			}
		}
	}

Listの具象クラスはequalsやhashCodeメソッドが実装されているので、HashMapのキーとして使うことが出来る。


クローズのタイミング

XMLStreamReaderにはcloseメソッドがあるが、このcloseメソッドは、元となったリソース(上記の例ではInputSream)のクローズを行わない。(XMLStreamReaderのcloseメソッドのJavadocを参照)
元となったリソースは別途クローズする必要がある。

ただ、実際には、nextメソッドの中で(読み込むものが無くなった時に)元のリソースをクローズするようだ。(XMLStreamReaderの具象クラスによって異なる可能性あり)


XMLファイルを出力する例

以下のようなXMLファイルを出力する例。

example.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>

XmlStaxExample.java:

import java.io.Closeable;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
public class XmlStaxExample {

	public static void main(String... args) throws IOException {
		// Recordは、column1,column2を保持するJavaBean
		List<Record> list = new ArrayList<>();
		list.add(new Record("aaa", "111"));
		list.add(new Record("bbb", "222"));

		new XmlStaxExample().write("D:/temp/example.xml", list);
	}
	public void write(String fileName, List<Record> recordList) throws IOException {
		try (FileOutputStream os = new FileOutputStream(fileName)) {
			XMLOutputFactory factory = XMLOutputFactory.newInstance();
			XMLStreamWriter writer = factory.createXMLStreamWriter(os);
			try (Closeable c = () -> {
				try {
					writer.close();
				} catch (XMLStreamException e) {
					throw new IOException(e);
				}
			}) {
				write(writer, recordList);
			}
//			try (AutoCloseable c = () -> writer.close()) {
//				write(writer, recordList);
//			} catch (Exception e) {
//				throw new IOException(e);
//			}
		} catch (XMLStreamException e) {
			throw new IOException(e);
		}
	}

XMLOutputFactoryを使ってXMLStreamWriterを生成する。
Closeable(AutoCloseable)を使っている件については、読み込みのサンプルと同様。

	private void write(XMLStreamWriter writer, List<Record> recordList) throws XMLStreamException {
		// ヘッダー
		writer.writeStartDocument("UTF-8", "1.0");
		writer.writeCharacters("\n");

		// root開始
		writer.writeStartElement("root");
		writer.writeCharacters("\n");

		for (Record record : recordList) {
			writer.writeStartElement("record");

			writer.writeStartElement("column1");
			writer.writeCharacters(record.getColumn1());
			writer.writeEndElement();

			writer.writeStartElement("column2");
			writer.writeCharacters(record.getColumn2());
			writer.writeEndElement();

			writer.writeEndElement(); // record
			writer.writeCharacters("\n");
		}

		// root終了
		writer.writeEndElement();
		writer.writeCharacters("\n");

		writer.writeEndDocument();
	}
}

XMLStreamWriterでは、一番最初にwriteStartDocumentメソッドを呼び出し、一番最後にwriteEndDocumentメソッドを呼び出す。
要素(タグ名)はwriteStartElementとwriteEndElementメソッドで出力する。
改行したい場合やインデントを入れたい場合は、自分で改行や空白を出力する必要がある。(XML的には、改行や空白もデータの一部)


クローズのタイミング

XMLStreamWriterにはcloseメソッドがあるが、このcloseメソッドは、元となったリソース(上記の例ではFileOutputStream)のクローズを行わない。(XMLStreamWriterのcloseメソッドのJavadocを参照)
元となったリソースは別途クローズする必要がある。


XMLへ戻る / 新機能へ戻る / Java目次へ戻る
メールの送信先:ひしだま