CLOVER🍀

That was when it all began.

JavaでXMLを書き出す

昨日、こちらののブログで見かけた、こんなエントリ。

JDOM2でXMLファイルを出力してみる
http://kikutaro777.hatenablog.com/entry/2013/09/04/215134

JavaでXMLを出力する方法を探されていたようなのですが、使われていたライブラリがJDOMだったので、思わずTwitterでいろいろつぶやいてしまいました。

JDOMは、JavaにおけるDOMの代替ですが、自分はJDOMがまだβ版の頃から使っていた、なおかつJavaのライブラリで初めて扱ったものだったので非常に感慨深く…ハイ。

で、せっかくなので「JavaでXMLを出力する」という方法について、普段自分が使う方法を3つほど挙げたいと思います。

今回は、元のブログで目標とされている、

<?xml version="1.0" encoding="UTF-8"?>
<Product>
  <Option Id="Opt">
    <Name>
      <JPN>オプション品</JPN>
      <ENG>Option Item</ENG>
    </Name>
  </Option>
  <!-- Optionタグの繰り返し -->
</Product>

という感じのXMLを出力するものとします。OptionのIdやJPN、ENGの値には実際には連番を含めるようです。

DOMツリーを使うライブラリを使用する

JDOMは、このパターンにあたります。

JDOM
http://www.jdom.org/

JDOM2 A Primer
https://github.com/hunterhacker/jdom/wiki/JDOM2-A-Primer

最新版は、JDOM2。リリースを知らなかったです…。

類似のものとしては、Java標準だとDOM、外部ライブラリとしてはdom4jがあります。

dom4j
http://dom4j.sourceforge.net/

では、JDOMの例を。

Maven依存関係。

    <dependency>
      <groupId>org.jdom</groupId>
      <artifactId>jdom2</artifactId>
      <version>2.0.5</version>
    </dependency>

コード例。

import java.io.IOException;

import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.output.DOMOutputter;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

public class JdomExample {
    public static void main(String[] args) {
        Element product = new Element("Product");
        Document document = new Document(product);

        for (int i = 0; i < 5; i++) {
            Element option =
                new Element("Option")
                .setAttribute("Id", "Opt" + i);
            product.addContent(option);

            Element name = new Element("Name");
            option.addContent(name);

            name.addContent(new Element("JPN").setText("オプション品" + i));
            name.addContent(new Element("ENG").setText("Option Item" + i));
        }

        try {
            XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
            outputter.output(document, System.out);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            DOMOutputter domOutputter = new DOMOutputter();
            org.w3c.dom.Document dom = domOutputter.output(document);
        } catch (JDOMException e) {
            e.printStackTrace();
        }
    }
}

元のブログのコードと、大差ありません。

最後に、DOMに変換するコードを加えています。

            DOMOutputter domOutputter = new DOMOutputter();
            org.w3c.dom.Document dom = domOutputter.output(document);

まあ、DOMと相互運用できますよってことで。

DOMに比べての優位性は、

  • メモリ使用量が少ない
  • JavaのCollectionが使える(NodeListなどは、Collectionではない)
  • DOMのように、Nodeインターフェースからのダウンキャストを(あまり)求められない

というところでしょうか。

不利なところはやはり標準APIではないので、DOMに変換可能かどうかという点はちょっと気になるところです。

よく比較されるdom4jの方が高機能なのですが、個人的にはJDOMをよく使います。JDOMは2になってジェネリクスをサポートするようになっているので、この点も嬉しいところです。

今回挙げる方法では、もっともプリミティブな手段になるので、最近は後述するJAXBなどを使うことが多いです。

DOM、JDOM、dom4jなどを使用する時は、個人的には

  • さまざまな構造のXMLを扱うため、汎用的に処理を書きたい
  • XPathなどを使いたい
  • JAXBなどのマッピング系を使うには、オーバースペックに感じる

といった時に使うようにしています。

ちなみに、Java標準APIだとStAXを使ってもXMLを出力できますが、これよりさらに面倒だと思います…。

XMLとオブジェクトをマッピングする

データベースとのO/Rマッピング的なものになります。これは、Java標準で入っているJAXBをよく使います。

ところにより、XStreamあたりも。

XStream
http://xstream.codehaus.org/

今回は、JAXBで。

JAXBについては、以前エントリを書いたことがあります。

JAXBをXML Schemaなしで使ってみる
http://d.hatena.ne.jp/Kazuhira/20120716/1342435297

標準APIなので、Maven依存関係は不要です。

では、コードを。

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.DataBindingException;
import javax.xml.bind.JAXB;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlValue;
import javax.xml.bind.annotation.XmlRootElement;

public class JaxbExample {
    public static void main(String[] args) {
        Product product = new Product();

        for (int i = 0; i < 5; i++) {
            Option option = new Option("Opt" + i);
            option
                .name
                .addLang(new Jpn("オプション品" + i))
                .addLang(new Eng("Option Item" + i));
            product.options.add(option);
        }

        JAXB.marshal(product, System.out);
    }
}


@XmlRootElement(name = "Product")
class Product {
    @XmlElement(name = "Option")
    List<Option> options = new ArrayList<>();
}

@XmlAccessorType(XmlAccessType.FIELD)
class Option {
    @XmlAttribute(name = "Id")
    String id;

    @XmlElement(name = "Name")
    Name name = new Name();

    Option(String id) {
        this.id = id;
    }
    
}

@XmlAccessorType(XmlAccessType.FIELD)
class Name {
    @XmlElements({@XmlElement(name = "JPN", type = Jpn.class),
                  @XmlElement(name = "ENG", type = Eng.class)})
    List<Lang> langs = new ArrayList<>();

    public Name addLang(Lang lang) {
        langs.add(lang);
        return this;
    }
}

@XmlAccessorType(XmlAccessType.FIELD)
abstract class Lang {
    @XmlValue
    String value;

    Lang(String value) {
        this.value = value;
    }
}

class Jpn extends Lang {
    Jpn(String value) {
        super(value);
    }
}

class Eng extends Lang {
    Eng(String value) {
        super(value);
    }
}

…実は、こっちはけっこう厄介でした。JPNやENGタグという存在がけっこう微妙で、きっとこれはもうちょっと言語ごとに増えたりするんだろうなぁとか予想してみると、だいぶ冗長感たっぷりな感じに(笑)。Listにしなかった場合は、JPNとかENGの単位でフィールド定義とかをする羽目になるんじゃないかと。

オブジェクトを組むところ自体はDOM系に比べるとすっきりしますが、あとはどれだけ簡単にマッピング先のクラスを書けるかというところでしょうか。

この手のライブラリは、結局クラスの定義でXMLの構造が決まってしまうので、XMLの定義が完全に決定していて、なおかつある程度マッピング項目がある場合に使っています。

読み込み時の性能が気になる時とかは、また別ですが。

今回の例だと、完全にDOM系の方が簡単でした…。

テンプレートエンジンを使う

別にXMLに関するAPIを使わなくても、XMLは出力できますよということで、時々使うのがテンプレートエンジン。

大抵は、FreeMarkerかVelocityを使います。

FreeMarker
http://freemarker.org/

Apache Velocity
http://velocity.apache.org/

今回は、FreeMarkerを使用します。

Maven依存関係。

    <dependency>
      <groupId>org.freemarker</groupId>
      <artifactId>freemarker</artifactId>
      <version>2.3.20</version>
    </dependency>

Javaコード。

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import freemarker.cache.StringTemplateLoader;
import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

public class FreeMarkerExample {
    public static void main(String[] args) {
        Configuration configuration = new Configuration();
        configuration.setNumberFormat("###");
        configuration.setTemplateLoader(new ClassTemplateLoader(FreeMarkerExample.class, ""));

        Map<String, Object> options = new LinkedHashMap<>();
        for (int i = 0; i < 5; i++) {
            List<Map<String, String>> langs = new ArrayList<>();

            Map<String, String> jpn = new LinkedHashMap<>();
            jpn.put("JPN", "オプション品" + i);
            langs.add(jpn);

            Map<String, String> eng = new LinkedHashMap<>();
            eng.put("ENG", "Option Item" + i);
            langs.add(eng);

            options.put("Opt" + i, langs);
        }

        Map<String, Object> context = new LinkedHashMap<>();
        context.put("options", options);

        try {
            Template template = configuration.getTemplate("template.ftl");
            StringWriter writer = new StringWriter();
            template.process(context, writer);

            System.out.println(writer);
        } catch (IOException | TemplateException e) {
            e.printStackTrace();
        }
    }
}

今回のデータ構造は、すべてMapとListで表現しました。

テンプレートエンジンなので、テンプレートも必要です。今回はクラスパス上にテンプレートを置きました。

<?xml version="1.0" encoding="UTF-8"?>
<Product>
    <#list options?keys as optionId>
    <Option id="${optionId?xml}">
        <Name>
            <#list options[optionId] as lang>
                <#list lang?keys as name>
            <${name}>${lang[name]?xml}</${name}>
                </#list>
            </#list>
        </Name>
    </Option>
    </#list>
</Product>

記法さえそれなりに覚えれば、まあ楽です。利用するシーンはけっこうDOM系とかぶるのですが、

  • XML自体にマッピングするデータ構造は、割と汎用的に作りたい
  • でも、複雑なデータ構造じゃない
  • 結果のXML定義はテンプレート定義で、柔軟に変更したい
  • (あんまりやりませんが)テンプレート上で、少し処理を挟みたい

みたいな時に使っています。

特に、XML定義を自分じゃなくて別の人に作ってもらうような時に使いがちですかね…。「SELECT文の結果をテンプレートに入れとくので、あとはテンプレートで好きに整形してね!」みたいな感じでしょうか。

今回紹介している例で、唯一XMLの読み込み機能がない方法です(笑)。

注意点としては、妥当なXMLであることをテンプレートエンジンは保障してくれないので、ちゃんと確認する必要があることと、XMLエスケープを忘れないこと、でしょうか。

だいたい、よく使うのはこんな感じですね。