CLOVER🍀

That was when it all began.

JAXBと名前空間と

前回は、XML Schemaを書かずにJAXBを使用しましたが、次は入出力するXMLに名前空間を使用したいと思います。

入力するXMLをちょこっと変更します。

    val inputXml = """<?xml version="1.0" encoding="UTF-8"?>
                      |<data xmlns="http://d.hatena.ne.jp/Kazuhira/">
                      |  <title>名簿</title>
                      |  <persons>
                      |    <person id="1">
                      |      <firstName>Taro</firstName>
                      |      <lastName>Tanaka</lastName>
                      |      <age>17</age>
                      |    </person>
                      |    <person id="2">
                      |      <firstName>Hanako</firstName>
                      |      <lastName>Suzuki</lastName>
                      |      <age>15</age>
                      |    </person>
                      |  </persons>
                      |</data>""".stripMargin

xmlns付けただけですね。ところが、これだけで元のプログラムは正常に動作しなくなります。
以下は、アンマーシャル/マーシャルの結果です。

Result = [title = [null], persons = null]
Marshal XML => 
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<data/>

元々こんな感じにできていたので

Result = [title = [名簿], persons = [!!id = [1], firstName = [Taro], lastName = [Tanaka], age = [17]!!, !!id = [2], firstName = [Hanako], lastName = [Suzuki], age = [15]!!]]
Marshal XML => 
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<data>
    <title>名簿</title>
    <persons>
        <person id="1">
            <firstName>Taro</firstName>
            <lastName>Tanaka</lastName>
            <age>17</age>
        </person>
        <person id="2">
            <firstName>Hanako</firstName>
            <lastName>Suzuki</lastName>
            <age>15</age>
        </person>
    </persons>
</data>

これはまた随分と寂しい結果になりました…。まあ、マーシャルはアンマーシャルの結果をそのまま渡しているので、当然といえば当然ですが。

そうですかぁ、こういう形でXMLが解析できなかった場合は、アンマーシャルではトップレベルのオブジェクトが空で生成されちゃうんですね。では、元のクラスを名前空間に対応させてみましょう。方法は、2つが考えられます。

  • パッケージレベルで名前空間に対応させる
  • Javaクラスとそのフィールドなどをひとつひとつ対応させる

では、それぞれ見ていってみましょう。

パッケージレベルで名前空間に対応させる

これには、package-info.javaを使用します。今回は、「jaxb.entity」というパッケージにバインド対象のクラスを収めているので、このパッケージを表すディレクトリ配下に以下のような内容でpackage-info.javaを作成します。
package-info.java

@XmlSchema(
    namespace = "http://d.hatena.ne.jp/Kazuhira/",
    elementFormDefault = XmlNsForm.QUALIFIED
)
package jaxb.entity;

import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema;

@XmlSchemaアノテーションを使用します。この時、elementFormDefaultの指定がないと動作しないので、注意してください…。namespaceには、対象の名前空間を指定します。

これだけで、先ほどの実行結果が変わります。

Result = [title = [名簿], persons = [!!id = [1], firstName = [Taro], lastName = [Tanaka], age = [17]!!, !!id = [2], firstName = [Hanako], lastName = [Suzuki], age = [15]!!]]
Marshal XML => 
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<data xmlns="http://d.hatena.ne.jp/Kazuhira/">
    <title>名簿</title>
    <persons>
        <person id="1">
            <firstName>Taro</firstName>
            <lastName>Tanaka</lastName>
            <age>17</age>
        </person>
        <person id="2">
            <firstName>Hanako</firstName>
            <lastName>Suzuki</lastName>
            <age>15</age>
        </person>
    </persons>
</data>

よ〜く見ると

<data xmlns="http://d.hatena.ne.jp/Kazuhira/">

と名前空間の宣言が入っていますね。

名前空間の接頭辞をカスタマイズしたい場合は、こういう形で記述するとよいです。

@XmlSchema(
    namespace = "http://d.hatena.ne.jp/Kazuhira/",
    xmlns = {
        @XmlNs(namespaceURI = "http://d.hatena.ne.jp/Kazuhira/", prefix = "ns-custom")
    },
    elementFormDefault = XmlNsForm.QUALIFIED
)
package jaxb.entity;

import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema;

xmlns配列に@XmlNsアノテーションで名前空間と接頭辞を指定するのですが、@XmlSchemaアノテーションのnamespaceも指定しないとうまく動きませんでした…。

Javaクラスとそのフィールドなどをひとつひとつ対応させる

ちょっと面倒な方法です。この場合は、@XmlRootElementや@XmlElement、@XmlAttributeなどひとつひとつに名前空間の設定を付与していきます。

たぶん、結果を見た方が早いのでコードを載せます。
Data.java

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "data", namespace = "http://d.hatena.ne.jp/Kazuhira/")
public class Data {
    @XmlElement(name = "title", namespace = "http://d.hatena.ne.jp/Kazuhira/")
    private String title;

    @XmlElementWrapper(name = "persons", namespace = "http://d.hatena.ne.jp/Kazuhira/")
    @XmlElement(name = "person", namespace = "http://d.hatena.ne.jp/Kazuhira/")
    private List<Person> persons;

    public String getTitle() {
        return title;
    }

    public List<Person> getPersions() {
        return persons;
    }

    @Override
    public String toString() {
        return String.format("title = [%s], persons = %s", title, persons);
    }
}

Person.java

@XmlAccessorType(XmlAccessType.FIELD)
public class Person {
    @XmlAttribute(name = "id", namespace = "http://d.hatena.ne.jp/Kazuhira/")
    private int id;

    @XmlElement(name = "firstName", namespace = "http://d.hatena.ne.jp/Kazuhira/")
    private String firstName;
    @XmlElement(name = "lastName", namespace = "http://d.hatena.ne.jp/Kazuhira/")
    private String lastName;
    @XmlElement(name = "age", namespace = "http://d.hatena.ne.jp/Kazuhira/")
    private int age;

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return String.format("!!id = [%d], firstName = [%s], lastName = [%s], age = [%d]!!", id, firstName, lastName, age);
    }
}

各アノテーションのnamespaceパラメータに、対象の名前空間の値を指定していきます。ひとつでも忘れると、その要素は華麗に無視されます…。

この場合、マーシャル後の結果はこういった機械的な接頭辞が付与されます。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns1:data xmlns:ns1="http://d.hatena.ne.jp/Kazuhira/">
    <ns1:title>名簿</ns1:title>
    <ns1:persons>
        <ns1:person ns1:id="0">
            <ns1:firstName>Taro</ns1:firstName>
            <ns1:lastName>Tanaka</ns1:lastName>
            <ns1:age>17</ns1:age>
        </ns1:person>
        <ns1:person ns1:id="0">
            <ns1:firstName>Hanako</ns1:firstName>
            <ns1:lastName>Suzuki</ns1:lastName>
            <ns1:age>15</ns1:age>
        </ns1:person>
    </ns1:persons>
</ns1:data>

もちろん、アンマーシャルもできています。

Result = [title = [名簿], persons = [!!id = [0], firstName = [Taro], lastName = [Tanaka], age = [17]!!, !!id = [0], firstName = [Hanako], lastName = [Suzuki], age = [15]!!]]

というわけで、名前空間の設定を行いたい場合は、できればpackage-info.javaでやってしまった方が楽ですし、出力するXML名前空間の接頭辞も指定できるので便利です。

…が、それでも@XmlElementとか@XmlAttributeアノテーションのnamespaceパラメータを使用しなければならず、なおかつ「ns1」みたいな機械的な接頭辞は変更したい場合には…とりあえず、JAXB RIを使用している場合には、なんとかなるみたいです。

割と面倒ですが。
参考)
http://stackoverflow.com/questions/3289644/define-spring-jaxb-namespaces-without-using-namespaceprefixmapper

まずは、使用している依存性解決ツールにJAXB RIの依存関係を追加します。自分は、sbtを使用しているのでbuild.sbtに書きました。Mavenなどの場合は、適宜読み替えてください。

libraryDependencies += "com.sun.xml.bind" % "jaxb-impl" % "2.2.6-b35"

バージョン指定については、Maven Centralとかで確認してくださいね。

続いて、com.sun.xml.bind.marshaller.NamespacePrefixMapperのサブクラスを用意します。
MyNamespacePrefixMapper.java

package jaxb;

import com.sun.xml.bind.marshaller.NamespacePrefixMapper;

public class MyNamespacePrefixMapper extends NamespacePrefixMapper {
    public String getPreferredPrefix(String namespaceURI, String suggestion, boolean requiredPrefix) {
        if (requiredPrefix) {
            if ("http://d.hatena.ne.jp/Kazuhira/".equals(namespaceURI)) {
                return "ns-custom";
            }
            return suggestion;
        }

        return "";
    }
}

渡ってきたnamespaceを見て、それに応じて使いたい接頭辞を返してあげればよいようです。

で、これをMarshaller#setPropertyで設定します。

    public static String marshal(Object target) throws JAXBException {
        StringWriter writer = new StringWriter();
        JAXBContext context = JAXBContext.newInstance(target.getClass());
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper());
        marshaller.marshal(target, writer);
        return writer.toString();
    }

ここです。

        marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper());

つまり、この方法はMarshallerを明示的に生成する場合でなければ使えません。

アンマーシャル/マーシャルのコードは以下のようにして…
*JaxbSupportは前回のエントリからの出典です

      val data = JaxbSupport.unmarshal(inputXml, classOf[Data])
      printf("Result = [%s]%n", data)

      printf("Marshal XML => %n%s%n", JaxbSupport.marshal(data))

実行結果は、こちら。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns-custom:data xmlns:ns-custom="http://d.hatena.ne.jp/Kazuhira/">
    <ns-custom:title>名簿</ns-custom:title>
    <ns-custom:persons>
        <ns-custom:person ns-custom:id="0">
            <ns-custom:firstName>Taro</ns-custom:firstName>
            <ns-custom:lastName>Tanaka</ns-custom:lastName>
            <ns-custom:age>17</ns-custom:age>
        </ns-custom:person>
        <ns-custom:person ns-custom:id="0">
            <ns-custom:firstName>Hanako</ns-custom:firstName>
            <ns-custom:lastName>Suzuki</ns-custom:lastName>
            <ns-custom:age>15</ns-custom:age>
        </ns-custom:person>
    </ns-custom:persons>
</ns-custom:data>

とりあえず、意図通りの出力結果にはなりました。JAXB RIに依存関係ができてしまうところが、ちょっと微妙ですけどね…。