|
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を参照)
元となったリソースは別途クローズする必要がある。