CLOVER🍀

That was when it all began.

JAXBで、ひとつのタグに複数の実装クラスを紐付けたい

とある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使い方メモ