|
StAXはStreaming API for XMLの略(なのでクラス名にXMLStreamという接頭辞が付いている)。
StAXはJava6(JDK1.6)からJavaの標準APIの仲間入りをしたらしい。(なので特別なライブラリーを入れなくても使用できる)
StAXを使ってXMLファイルを読み込む場合、StAXのXMLパーサーに対して要素を読み込むよう指示すると、要素が1つ返ってくる。
その要素の種類に応じて自分の処理を行う。
XML自体はXML要素のツリー構造をしているが、StAXはツリー構造は認識せず、ただ現れた要素を返してくるのみ。
以下のような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>
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 version="1.0" encoding="UTF-8"?> <root> <record><column1>aaa</column1><column2>111</column2></record> <record><column1>bbb</column1><column2>222</column2></record> </root>
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を参照)
元となったリソースは別途クローズする必要がある。