理系学生日記

おまえはいつまで学生気分なのか

タグ名が可変の XML 文書を JAXB で構築する

タイトルから既に異常な感じがしますが、要するに以下のような XML 文書を作りたいというときにどうすれば良いのかという話です。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<whole>
    <aaa>value aaa</aaa>
    <bbb>value bbb</bbb>
    <!-- 
       ここからが可変。タグ名は "part" + 連番。タグ数の上限値は不明
       なお、タグの子要素は同じ構造を取るあ
    -->
    <part01>
        <part_a>part value A1</part_a>
        <part_b>part value B1</part_b>
    </part01>
    <part02>
        <part_a>part value A2</part_a>
        <part_b>part value B2</part_b>
    </part02>
    <!-- 可変項目ここまで -->
</whole>

まず、ぼくの知る限りにおいて、このような可変かつ動的に変化し得る XML Schema を厳格に(xsd:complexType などで)定義するすべはありません。xsd:all も xsd:choice も使えない。方法あったら教えてほしい。わりとマジで。

方針

そもそも XML Schema に喧嘩を売るような XML 設計になってしまっているので、直接的かつシンプルな解法というものがどうしても見つかりませんでした。
そういうわけですので、汚ない方法だよなとは思いながら、本件の対処として

  1. 実行時に動的に part の部分の XML の DOM を生成
  2. whole の XML の DOM を生成
  3. part の DOM と whole の DOM を DOM 操作で接合させる
  4. この DOM を unmarshall → marshall して、最終的な XML とする

という方針を取ることにします。DOM 生成や marshall/unmarshall には JAXB (ref: GitHub - javaee/jaxb-v2) を使用しました。JAXB、いつのまにやら Java 6 では標準なんですね。
最後の unmarshall → marshall の流れは実際には不要だったりしますが、marshaller のハンドラで諸々していたりするので、そのあたりの実装上の問題があってこんなムダなことをしています。

XML Schema

上記文書では、whole と part* という 2 種類の要素が混ざっていると考えることができるので、ここではこの 2 つの要素に対して XML Schema を定義します。
part* については、とりあえず要素名としては "part" という名前にすると、以下のようにシンプルに書き下すことができます。これを part.xsd として保存しました。

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="part">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:element name="part_a" type="xsd:string"/>
        <xsd:element name="part_b" type="xsd:string"/>
      </xsd:sequence>
    </xsd:complexType>
  </xsd:element>
</xsd:schema>

一方で、whole については、「何が "bbb" タグの次に来るのか」が実行時まで分からないので、ここは仕方なく xsd:any を使うことにします。これを whole.xsd として保存しました。

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="whole">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:element name="aaa" type="xsd:string"/>
        <xsd:element name="bbb" type="xsd:string"/>
        <xsd:any processContents="lax" maxOccurs="unbounded"/>
      </xsd:sequence>
    </xsd:complexType>
  </xsd:element>
</xsd:schema>

あとは、JAXB の xjc が上記 XML Schema からよしなに Java コードを生成してくれるので、ここは xjc にお任せしました。

$ xjc -p kiririmode.test.jaxb.schema.part  part.xsd
$ xjc -p kiririmode.test.jaxb.schema.whole whole.xsd

生成されたコードを、ソーツツリーに組み込んで実行してみます。コードは既述の方針を書き下してみただけです。

package kiririmode.test.jaxb.main;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import kiririmode.test.jaxb.schema.part.Part;
import kiririmode.test.jaxb.schema.whole.ObjectFactory;
import kiririmode.test.jaxb.schema.whole.Whole;

public class JAXBTest {
    
    private static final String JAXB_PACKAGE_WHOLE = "kiririmode.test.jaxb.schema.whole";
    private static final String JAXB_PACKAGE_PART  = "kiririmode.test.jaxb.schema.part";

    public static void main(String[] args) {
        try {
            JAXBContext jc = JAXBContext.newInstance(JAXB_PACKAGE_WHOLE);
            Marshaller marshaller = jc.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            
            ObjectFactory of = new ObjectFactory();
            Whole whole = of.createWhole();
            whole.setAaa("value aaa");
            whole.setBbb("value bbb");
            
            // whole の XML DOM を生成
            Document wholeDoc = marshall2DOM(whole, marshaller);
            
            // part の XML DOM を生成し、whole の XML DOM に継ぎ木する
            for ( int i = 1; i <= 3; i++ ) {
                graftNodeToDoc(
                    createPartDOM("part value A", "part value B", "part0" + i ).getDocumentElement(),
                    wholeDoc
                );
            }
            
            // unmarshall → marshall で最終的な XML を生成
            Unmarshaller unmarshaller = jc.createUnmarshaller();
            marshaller.marshal(
                    unmarshaller.unmarshal(wholeDoc.getDocumentElement()),
                    System.out
            );
        
        } catch (Exception e) {
            e.printStackTrace();
        }    
    }
    
    public static void graftNodeToDoc(Node node, Document doc) {
        Node addedNode = doc.importNode(node, true);
        
        Element root = doc.getDocumentElement();
        root.appendChild(addedNode);
    }
    
    public static Document marshall2DOM(Object obj, Marshaller marshaller) throws JAXBException, ParserConfigurationException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        Document document = dbf.newDocumentBuilder().newDocument();
        
        marshaller.marshal(obj, document);
        return document;
    }
    
    public static Document createPartDOM(String a, String b, String tagname) throws JAXBException, ParserConfigurationException {
        kiririmode.test.jaxb.schema.part.ObjectFactory of = new kiririmode.test.jaxb.schema.part.ObjectFactory();
        Part part = of.createPart();
        
        part.setPartA(a);
        part.setPartB(b);
        
        JAXBElement<Part> elem = new JAXBElement<Part>(new QName(tagname), Part.class, part);
    
        JAXBContext jc = JAXBContext.newInstance(JAXB_PACKAGE_PART);
        Marshaller marshaller = jc.createMarshaller();
        return marshall2DOM(elem, marshaller);
    }
}

これを実行すると、想定通りの XML が生成されていることが確認できます。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<whole>
    <aaa>value aaa</aaa>
    <bbb>value bbb</bbb>
    <part01>
        <part_a>part value A</part_a>
        <part_b>part value B</part_b>
    </part01>
    <part02>
        <part_a>part value A</part_a>
        <part_b>part value B</part_b>
    </part02>
    <part03>
        <part_a>part value A</part_a>
        <part_b>part value B</part_b>
    </part03>
</whole>