とあるXMLをパース、生成しようと思った時に、件名のような課題にぶつかりまして。
たとえば、以下のようなXMLを考えた時に
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <zoo> <animals> <cat>cat-1</cat> <dog>dog-1</dog> <mouse>mouse-1</mouse> </animals> </zoo>
cat、dog、mouseタグをそれぞれCat、Dog、Mouseクラスで表現したとして、これらをanimalsタグ配下にまとめようと思った時に各クラスに親クラスやインターフェースでまとめたくなるケースがあると思います。
こういう時に、JAXBでどうするか調べてみたので、それをまとめてみたいと思います。
お題としては、そのまま上記のXMLを表現するJAXBに関するクラスを実装します。
Maven依存関係
テストコードで確認するので、Maven依存関係としては以下を利用します。
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.3.0</version> <scope>test</scope> </dependency>
お題を表現するJAXB関連のクラス
それでは、お題を表現するためのクラスを実装していきます。
動物関係のクラスを作成するので、Animalインターフェースを用意します。
src/main/java/org/littlewings/jaxb/Animal.java
package org.littlewings.jaxb; public interface Animal { String getName(); void setName(String name); }
名前の設定と取得ができるものとします。
このAnimalインターフェースに対して、実装クラスを作成していきます。
src/main/java/org/littlewings/jaxb/Cat.java
package org.littlewings.jaxb; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlValue; @XmlAccessorType(XmlAccessType.FIELD) public class Cat implements Animal { @XmlValue private String name; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } }
src/main/java/org/littlewings/jaxb/Dog.java
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlValue; @XmlAccessorType(XmlAccessType.FIELD) public class Dog implements Animal { @XmlValue private String name; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } }
src/main/java/org/littlewings/jaxb/Mouse.java
package org.littlewings.jaxb; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlValue; @XmlAccessorType(XmlAccessType.FIELD) public class Mouse implements Animal { @XmlValue private String name; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } }
Animalインターフェースを実装しているくらいで、特になんの変哲もないクラスです。
では、これらをまとめるためのルート要素であるZooクラスを作成します。
src/main/java/org/littlewings/jaxb/Zoo.java
package org.littlewings.jaxb; import java.util.ArrayList; import java.util.List; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlElements; import javax.xml.bind.annotation.XmlRootElement; @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "zoo") public class Zoo { @XmlElementWrapper(name = "animals") @XmlElements({ @XmlElement(name = "dog", type = Dog.class), @XmlElement(name = "cat", type = Cat.class), @XmlElement(name = "mouse", type = Mouse.class) }) private List<Animal> animals = new ArrayList<>(); public List<Animal> getAnimals() { return animals; } public void setAnimals(List<Animal> animals) { this.animals = animals; } public void addAnimal(Animal animal) { animals.add(animal); } }
Listで子要素を持つことにしているのでXmlElementWrapperが入っていますが、
@XmlElementWrapper(name = "animals")
ポイントはXmlElementsとXmlElementでの名前と型指定ですね。
@XmlElements({ @XmlElement(name = "dog", type = Dog.class), @XmlElement(name = "cat", type = Cat.class), @XmlElement(name = "mouse", type = Mouse.class) }) private List<Animal> animals = new ArrayList<>();
この設定により、animalsタグ配下にどの名前のタグが出現するかで、割り当てられる実装クラスが変化することになります。
確認
それでは、動作確認してみます。
テストコードとして、以下のクラスを用意。
src/test/java/org/littlewings/jaxb/JaxbTypeDispatchTest.java
package org.littlewings.jaxb; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.io.StringWriter; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.List; import javax.xml.bind.JAXB; import javax.xml.transform.stream.StreamSource; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class JaxbTypeDispatchTest { @Test public void marshall() { Zoo zoo = new Zoo(); Cat cat = new Cat(); cat.setName("cat-1"); Dog dog = new Dog(); dog.setName("dog-1"); Mouse mouse = new Mouse(); mouse.setName("mouse-1"); zoo.addAnimal(cat); zoo.addAnimal(dog); zoo.addAnimal(mouse); StringWriter writer = new StringWriter(); JAXB.marshal(zoo, writer); assertThat(writer.toString()) .isEqualTo(readXml("zoo.xml")); } @Test public void unmarshall() { Zoo zoo = JAXB.unmarshal(new StreamSource(new StringReader(readXml("zoo.xml"))), Zoo.class); List<Animal> animals = zoo.getAnimals(); assertThat(animals.get(0).getName()) .isEqualTo("cat-1"); assertThat(animals.get(1).getName()) .isEqualTo("dog-1"); assertThat(animals.get(2).getName()) .isEqualTo("mouse-1"); } protected String readXml(String path) { try (InputStream is = getClass().getClassLoader().getResourceAsStream(path); InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { StringBuilder builder = new StringBuilder(); int c; while ((c = reader.read()) != -1) { builder.append((char) c); } return builder.toString(); } catch (IOException e) { throw new UncheckedIOException(e); } } }
マーシャル、アンマーシャルそれぞれのケースを用意。
クラスパス上には、マーシャル時の答え合わせ、アンマーシャル時のパース用のXMLを用意。
src/test/resources/zoo.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <zoo> <animals> <cat>cat-1</cat> <dog>dog-1</dog> <mouse>mouse-1</mouse> </animals> </zoo>
このファイルを、readXmlメソッドで読み込んでいます。まあ、最初に載せたXMLと同じものですね。
参考)
JAXB使い方メモ