seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

設定ファイル等の読み書きにXML形式を使う3つの方法(簡単なもの順)

java.util.Propertiesを使う

J2SE5以降のjava.util.Propertiesは、XMLファイル形式をサポートしている。


日本語などを使う場合も、昔ながらの native2ascii のお世話になる手間がなく、
デコードし直さなくても、ちゃんと人間が読める文字なので修正もらくちんだし、
改行を含む長い文字列だって、末尾の空白に神経質にならずとも簡単に扱える。


今となっては、*.propertiesファイルは過去のデータを引き継ぐ場合以外に有用性はないと思う。

XMLPropertiesTest.java
import java.io.IOException;
import java.util.Properties;

public class XMLPropertiesTest {
	public static void main(String[] args) throws IOException {
		Properties props = new Properties();
		props.loadFromXML(XMLPropertiesTest.class.getResourceAsStream("data.xml"));
		System.out.println(props);
	}
}

データは、以下のものとする。

data.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>XML形式のプロパティファイル</comment>

<!-- 単純なキーと値のケース -->
<entry key="key1">value1</entry>

<!-- native2asciiが不要になるしコメントも自由自在 -->
<entry key="key2">日本語も大丈夫</entry>

<!-- 長い文字列だって問題ない  -->
<entry key="key3"><![CDATA[-- 改行込み、不等号入りテスト
select VALUE1
  from TESTTBL
 where KEY1 > :KEY1]]></entry>
</properties>
実行結果
{key3=-- 改行込み、不等号入りテスト
select VALUE1
from TESTTBL
where KEY1 > :KEY1, key2=日本語も大丈夫, key1=value1}
注意点あり

Properties#loadFromXML(InputStream)の引数の中で、直接、クラスリソースをオープンして渡しているが、これはJavaSE6以降ならば安全である。

このメソッドの中で入力ストリームはcloseされることになっているからである。

 http://docs.oracle.com/javase/6/docs/api/java/util/Properties.html#loadFromXML(java.io.InputStream)

http://docs.oracle.com/javase/7/docs/api/java/util/Properties.html#loadFromXML(java.io.InputStream)

The specified stream is closed after this method returns.

しかし、J2SE5の場合は、

http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Properties.html#loadFromXML(java.io.InputStream)

The specified stream remains open after this method returns.

とあるので、呼び出し側がCloseしてやる必要がある。

J2SE5以降のサポートとするのであれば、呼出し側でクローズ処理をいれるようにしておけば、JavaSE6以降で使っても複数回クローズが走るが、それ自身は無害であるので問題なく使えるようになる。

XMLResourceBundleを使う

PropertiesがXML対応しているならば、リソースバンドルも対応しているのでは? と淡い期待を抱いても、残念ながらJavaSE7になってもリソースバンドルはXMLに対応していない。


が、XMLリソースバンドルを自前で作る仕組みがJavaSE6からサポートされており、APIJavadocにサンプル事例としてXMLリソースバンドルが書かれている。


なので、これを、そのまま流用すればXMLResourceBundleが使えるわけである。

XMLResourceBundle.java
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Properties;
import java.util.ResourceBundle;

/**
 * XMLファイルから構築されるリソースバンドル
 */
public class XMLResourceBundle extends ResourceBundle {

	/**
	 * XMLリソースバンドルのバックエンドとして、
	 * Propertiesを使う.
	 */
	private Properties props = new Properties();

	XMLResourceBundle(InputStream is) throws IOException {
		props.loadFromXML(is);
	}

	@Override
	protected Object handleGetObject(String key) {
		return props.getProperty(key);
	}

	@Override
	@SuppressWarnings("unchecked")
	public Enumeration<String> getKeys() {
		// Properties#keys()は、Objectを列挙するような型宣言だが
		// 実体はStringなので単にキャストすれば良い.
		// 互換性のない総称型同士をキャストすることはできないが、
		// 一旦、raw経由にすればキャスト可能になる。
		@SuppressWarnings("rawtypes")
		Enumeration enm = props.keys();
		return enm;
	}
}

これ単体だと、単純にPropertiesをラップしているだけで特にメリットがない。

ResourceBundleであることのメリットとは、現在のロケールにしたがってリソースファイルを選択してくれることなわけで、そちらの対応も必要となる。

XMLResourceBundleControl.java
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * XMLファイルによるリソースバンドルの扱いを可能にするコントローラ
 */
public class XMLResourceBundleControl extends ResourceBundle.Control {

	/**
	 * 対応するフォーマット形式
	 */
	private static final String ext = "xml";

	/**
	 * 対応するフォーマットを返す.
	 */
	public List<String> getFormats(String baseName) {
		return Arrays.asList(ext);
	}

	/**
	 * 指定されたクラスローダから、名前、ロケール情報でリソースを選択し、
	 * リソースバンドルを返します.
	 * http://docs.oracle.com/javase/6/docs/api/java/util/ResourceBundle.Control.html
	 */
	public ResourceBundle newBundle(String baseName, Locale locale,
			String format, ClassLoader loader, boolean reload)
			throws IllegalAccessException, InstantiationException, IOException {
		if (baseName == null || locale == null || format == null
				|| loader == null) {
			throw new NullPointerException();
		}

		ResourceBundle bundle = null;
		if (format.equals(ext)) {
			// xml形式の場合

			// ロケールと結合したリソース名を求める
			String bundleName = toBundleName(baseName, locale);

			// 対応するフォーマットと結合したリソース名を求める
			String resourceName = toResourceName(bundleName, format);

			// リソース名をクラスローダから取得する. (なければnull)
			InputStream stream = null;
			URL url = loader.getResource(resourceName);
			System.out.println("load resource bundle: " + resourceName + "=" + url); // ☆実験用☆

			if (url != null) {
				URLConnection connection = url.openConnection();
				if (connection != null) {
					if (reload) {
						// リロードの場合はキャッシュを無効にしてロードを試みる
						connection.setUseCaches(false);
					}
					stream = connection.getInputStream();
				}
			}

			// 取得されたリソースからXMLリソースバンドルとして読み込む
			if (stream != null) {
				BufferedInputStream bis = new BufferedInputStream(stream);
				try {
					bundle = new XMLResourceBundle(bis);
				} finally {
					bis.close();
				}
			}
		}
		return bundle;
	}
}

JavaSE6以降でサポートされたResourceBundleの拡張のメカニズムは、基本的に、そのControlクラスの拡張によって行われる。


そして、このControlクラスを使うようにResourceBundle#getBundleに指示することで、XMLファイルの読み込みがサポートされるようになるわけである。


このResouceBundle.Controlクラスはリソースバンドルの種類を拡張するだけでなく、従来のリソースバンドルの欠点であった「一度読み込んだら二度と読み込まない」という性質を緩和できるようにキャッシュメカニズムにもオプションを指定できるようになっている。

XMLResourceBundleTest.java
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * XML形式のプロパティファイルをリソースバンドルとして使う。
 * また、キャッシュ期間を指定し、リソースバンドルの再読み込みを確認する。
 * @author seraphy
 */
public class XMLResourceBundleTest {

	/**
	 * XML形式でのリソースバンドルの読み込みと、
	 * キャッシュ期間を限定したコントローラの構築
	 */
	private static final ResourceBundle.Control ctrl = new XMLResourceBundleControl() {
		@Override
		public long getTimeToLive(String baseName, Locale locale) {
			return 1000; // 1秒間でキャッシュ切れ
		}
	};

	private static ResourceBundle getBundle() {
		// リソースバンドルを取得する.
		String resName = "jp.seraphyware.easytousepersistxmldemo.resource1";
		ResourceBundle res = ResourceBundle.getBundle(resName, ctrl);
		return res;
	}
	
	private static void dump(String prefix, ResourceBundle res) {
		// 内容をダンプする.
		System.out.println("res=@" + res.hashCode());
		for (String key : res.keySet()) {
			String value = res.getString(key);
			System.out.println(prefix + key + "=" + value);
		}
	}
	
	public static void main(String[] args) throws Exception {

		// 初回読み込み
		System.out.println("☆1");
		ResourceBundle res1 = getBundle();
		dump("1>", res1);

		// 二回目連続読み込み
		// キャッシュ済みなので読み込み動作なし
		System.out.println("☆2");
		ResourceBundle res2 = getBundle();
		dump("2>", res2);

		// 10秒待ち
		System.out.println("wait...");
		Thread.sleep(10 * 1000);

		// キャッシュ切れにより、リロードが試行される.
		// ※ ただし、前回読み込みに失敗しているものは再試行されるが、
		// 読み込まれているファイルで、ファイルに変更がない場合はリロードはされない様子。
		// ※ ファイルに変更があれば再読み込みされるので、キャッシュ期間を短くしても
		// 無駄にリロードが繰り返される心配はしなくて良さそう。
		System.out.println("☆3");
		ResourceBundle res3 = getBundle();
		dump("2(cur)>", res2);
		dump("3(new)>", res3);

		// 強制的にキャッシュアウトさせる。
		// 指定方法は全てのリソースバンドル、もしくはクラスローダー単位.
		ResourceBundle.clearCache();

		// 初回読み込みと同じことになる。
		System.out.println("☆4");
		ResourceBundle res4 = getBundle();
		dump("4>", res4);
	}
}

読み込むリソースは以下のとおり。
デフォルトのリソースと、ja指定リソースの2つを用意し、オーバーラップさせている。

resource1.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>resource1.xml</comment>

<entry key="key1">value1</entry>
<entry key="key2">value2</entry>

</properties>
resource1_ja.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>resource1_ja.xml</comment>

<entry key="key2">value2-ja</entry>
<entry key="key3">value3-ja</entry>

</properties>
実行結果
☆1
load resource bundle: jp/seraphyware/easytousepersistxmldemo/resource1.xml=file:/Users/seraphy/Documents/workspace/EasyToUsePersistXMLDemo/bin/jp/seraphyware/easytousepersistxmldemo/resource1.xml
load resource bundle: jp/seraphyware/easytousepersistxmldemo/resource1_ja.xml=file:/Users/seraphy/Documents/workspace/EasyToUsePersistXMLDemo/bin/jp/seraphyware/easytousepersistxmldemo/resource1_ja.xml
load resource bundle: jp/seraphyware/easytousepersistxmldemo/resource1_ja_JP.xml=null
res=@1439781957
1>key3=value3-ja
1>key2=value2-ja
1>key1=value1
☆2
res=@1439781957
2>key3=value3-ja
2>key2=value2-ja
2>key1=value1
wait...
☆3
load resource bundle: jp/seraphyware/easytousepersistxmldemo/resource1_ja.xml=file:/Users/seraphy/Documents/workspace/EasyToUsePersistXMLDemo/bin/jp/seraphyware/easytousepersistxmldemo/resource1_ja.xml
load resource bundle: jp/seraphyware/easytousepersistxmldemo/resource1_ja_JP.xml=null
res=@1439781957
2(old)>key3=value3-ja
2(old)>key2=value2-ja
2(old)>key1=value1
res=@1612304545
3(new)>key3=value3-yyyja
3(new)>key2=value2-xxxja
3(new)>key1=value1
☆4
load resource bundle: jp/seraphyware/easytousepersistxmldemo/resource1.xml=file:/Users/seraphy/Documents/workspace/EasyToUsePersistXMLDemo/bin/jp/seraphyware/easytousepersistxmldemo/resource1.xml
load resource bundle: jp/seraphyware/easytousepersistxmldemo/resource1_ja.xml=file:/Users/seraphy/Documents/workspace/EasyToUsePersistXMLDemo/bin/jp/seraphyware/easytousepersistxmldemo/resource1_ja.xml
load resource bundle: jp/seraphyware/easytousepersistxmldemo/resource1_ja_JP.xml=null
res=@1439781957
2(old)>key3=value3-ja
2(old)>key2=value2-ja
2(old)>key1=value1
res=@569140886
4(new)>key3=value3-yyyja
4(new)>key2=value2-xxxja
4(new)>key1=value1

wait…のタイミングで、一度読み込んだresource1_ja.xmlファイルを書き換えてみた場合のコンソール出力である。

XMLResourceBundleの動きと注意点
  • リソースバンドルのリソース検索方法にしたがって、デフォルト、ja、ja_JPの順にリソースをロードしようとしている。
    • 順番にロードして上書きしてゆくので、jaファイルにないキーでもデフォルトにあれば、それが使われている。(単純なPropertiesではないメリット)
  • キャッシュが効いている場合は、getBundleを再度呼び出してもxmlファイルをリロードすることはない。
  • キャッシュが切れるとリロードを行う。
    • ただし、すでに読み込んでおり、且つ、ファイルが変更されていない場合はリロードを行わない、という最適化がされているようである。(ウェイト時間を長くしてファイルを変更したところ、リロードが行われることを確認した。)
    • 読み込まれていないファイル、この場合だとresource1_ja_JP.xml はロードを試行しているようである。
    • リソースバンドル全体をリセットすると、ファイルの変更の有無にかかわらず、かならずリロードされる。
    • リロードされるのは新しく取得したリソースバンドルのオブジェクトとであり、キャッシュクリアやリロードによって、すでに取得済みのリソースバンドルオブジェクトの中身が変更されることはない。(読み込んだリソースが動的に書き変わったりしない、ということ。)

JAXBを使う

JavaSE6からJAXB(The Java Architecture for XML Binding)は標準機能になっている。


JAXBは、オブジエクトツリーの構造をxmlマッピングするもので、

  1. XML Schema(XMLの構造)からクラスを生成する
  2. クラスからXML Schema(XMLの構造)を生成する

の、いずれの方法でも準備できる。


他システムとの連携などでXMLの構造があらかじめ決められている場合は、XML Schemaからクラス構造を作ればよいし、自分自身の設定ファイル等をXMLとして保存するのが目的であれば先にクラスを作って、それに対するXML Schemaを生成すれば良い。


XML Schemaとクラス構造とのマッピングは、xsdファイルを生成したり、クラスファイルを生成するなどの動作となるため、JDK付属のxjc, schemagenなどのツールによって行う。


今回は、クラスからXML Schemaを起こすタイプとする。

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

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "address-book")
public class AddressBook {

	private String title;

	private ArrayList<Institution> institutions = new ArrayList<Institution>();

	@XmlAttribute(name = "title") // 属性にする場合は@XmlAttributeを指定する.
	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	/**
	 * 施設のリストを取得する.
	 *
	 * コレクションを子にもつ場合、配列に対するgetter/setterをつける方法と、
	 * リストへのgetterを用意してアクセスさせる方法がある。
	 * この場合、setterはいらない。(使われないし、使う必要もない。)
	 * リストの実装クラスを制御するなら、この方法を用いる.
	 *
	 * メソッド名としては「〜s」のように複数形になるが、
	 * XML化した場合には「institutions」の複数形の名前の要素が並ぶのはおかしいので、
	 * 「institution」というように「s」をとった名前を明示すること。
	 * @return
	 */
	@XmlElement(name = "institution")
	public List<Institution> getInstitutions() {
		return institutions;
	}

	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append("title=").append(title);
		buf.append(", institutions=").append(institutions);
		return buf.toString();
	}
}
Institution.java
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSchemaType;
import javax.xml.datatype.XMLGregorianCalendar;

public class Institution {

	private String name;

	private String address;

	private int carParkingCapacity;

	private XMLGregorianCalendar registeredDate;

	private XMLGregorianCalendar openingTime;

	private XMLGregorianCalendar closingTime;

	@XmlAttribute(name = "name") // 文字列
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@XmlAttribute(name = "address") // 文字列
	public String getAddress() {
		return address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

	@XmlAttribute(name = "parking-capacity") // 数値
	public int getCarParkingCapacity() {
		return carParkingCapacity;
	}

	public void setCarParkingCapacity(int carParkingCapacity) {
		this.carParkingCapacity = carParkingCapacity;
	}

	@XmlAttribute(name = "registered-date") // 日付型
	@XmlSchemaType(name = "date") // 日付・時刻型にする場合は、日付・時刻のタイプを指定する.
	public XMLGregorianCalendar getRegisteredDate() {
		return registeredDate;
	}

	public void setRegisteredDate(XMLGregorianCalendar registeredDate) {
		this.registeredDate = registeredDate;
	}

	@XmlElement(name = "opening-time") // 時刻型
	@XmlSchemaType(name = "time") // 日付・時刻型にする場合は、日付・時刻のタイプを指定する.
	public XMLGregorianCalendar getOpeningTime() {
		return openingTime;
	}

	public void setOpeningTime(XMLGregorianCalendar openingTime) {
		this.openingTime = openingTime;
	}

	@XmlElement(name = "closing-time")
	@XmlSchemaType(name = "time")
	public XMLGregorianCalendar getClosingTime() {
		return closingTime;
	}

	public void setClosingTime(XMLGregorianCalendar closingTime) {
		this.closingTime = closingTime;
	}

	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append("name=").append(name);
		buf.append(", address=").append(address);
		buf.append(", carParkingCapacity=").append(carParkingCapacity);
		buf.append(", registeredDate=").append(registeredDate);
		buf.append(", openingTime=").append(openingTime);
		buf.append(", closingTime=").append(closingTime);
		return buf.toString();
	}
}

日付や時刻を扱う場合は、データの型としては「XMLGregorianCalendar」を用いて、
XMLとのマッピングでは「@XmlSchemaType」を使って、日時、日付のみ、時刻のみ、あるいは年、月、日単体などの形式を指定する。


メソッドやクラス名と、XML上のタグや属性名は、それぞれ別のものを指定できる。
たとえば「getInstitutions()メソッド」はリストを返すため複数形の名前とするが、
そのままではXML上では「Institutions」という要素が繰り返し出現することになるため、単数系に名前を明示しておくべきである。
(また、Javaでは標準であるCamlスタイルの命名も、xmlにはそぐわないので、基本的にはxmlには別に名前をつけるほうが良いと思われる。)


また、この場合、メソッドの戻り型はInstitution型のリストであることが分かるため、JAXBがアンマーシャリングするときにXMLからInstitutionオブジェクトを生成してリストに格納してくれる。(おそらく、JAXBは、Method#getGenericReturnType()で返される型から判断していると思われる。これには総称型の型パラメータの情報も含まれるため。)

戻り型に総称型の型情報が含まれていない場合はコレクションの要素として、どのクラスを使用するべきか判断できないため、
@XmlElementアノテーションに「type」パラメータを指定してクラスを明示する必要がある。

XMLスキーマの生成

スキーマはjaxbを使う上で必須ではないが、検証するには必要なため生成しておく。

$ schemagen -cp bin jp.seraphyware.easytousepersistxmldemo.data.AddressBook
??:Writing /Users/seraphy/Documents/workspace/EasyToUsePersistXMLDemo/schema1.xsd

生成されるXMLスキーマは、ルート要素となるクラスを指定すれば、子クラス分のスキーマも含まれて作られる。

生成されたスキーマは適当な名前に変更して、プログラムから参照できる場所に置く。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="address-book" type="addressBook"/>

  <xs:complexType name="addressBook">
    <xs:sequence>
      <xs:element name="institution" type="institution" minOccurs="0" maxOccurs="unbounded"/>
    </xs:sequence>
    <xs:attribute name="title" type="xs:string"/>
  </xs:complexType>

  <xs:complexType name="institution">
    <xs:sequence>
      <xs:element name="closing-time" type="xs:time" minOccurs="0"/>
      <xs:element name="opening-time" type="xs:time" minOccurs="0"/>
    </xs:sequence>
    <xs:attribute name="address" type="xs:string"/>
    <xs:attribute name="parking-capacity" type="xs:int" use="required"/>
    <xs:attribute name="name" type="xs:string"/>
    <xs:attribute name="registered-date" type="xs:date"/>
  </xs:complexType>
</xs:schema>
JAXBを使ってみる

準備ができたので、実際に使ってみる。

import java.io.StringReader;
import java.io.StringWriter;
import java.util.Date;
import java.util.GregorianCalendar;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import jp.seraphyware.easytousepersistxmldemo.data.AddressBook;
import jp.seraphyware.easytousepersistxmldemo.data.Institution;

public class SimpleJAXBTest {

	public static void main(String[] args) throws Exception {

		// テストデータの生成
		AddressBook addressBook = generateTestData();

		// JAXB準備
		JAXBContext ctx = JAXBContext.newInstance(AddressBook.class);

		SchemaFactory schemaFactory = SchemaFactory.newInstance(
				XMLConstants.W3C_XML_SCHEMA_NS_URI);
		Schema schema = schemaFactory.newSchema(
				SimpleJAXBTest.class.getResource("AddressBook.xsd"));

		// XMLへの変換 (書き込み)
		Marshaller marshaller = ctx.createMarshaller();

		// スキーマを設定する.
		marshaller.setSchema(schema);

		// XMLを整形して出力する. (指定しない場合は改行なしのフラットなものになる)
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
		marshaller.setProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, "AddressBook.xsd");

		StringWriter wr = new StringWriter();
		try {
			marshaller.marshal(addressBook, wr);
		} finally {
			wr.close();
		}
		System.out.println("marshaller=" + wr.toString());

		// XMLからの変換 (読み込み)
		Unmarshaller unmarshaller = ctx.createUnmarshaller();

		// スキーマを設定する.(検証する)
		unmarshaller.setSchema(schema);

		AddressBook addressBook2;
		StringReader rd = new StringReader(wr.toString());
		try {
			addressBook2 = (AddressBook)unmarshaller.unmarshal(rd);
		} finally {
			rd.close();
		}
		System.out.println("unmarshaller=" + addressBook2);
	}

	/**
	 * テストデータの生成
	 * @return 生成されたテストデータ
	 * @throws DatatypeConfigurationException 失敗
	 */
	private static AddressBook generateTestData() throws DatatypeConfigurationException {
		DatatypeFactory xmlDateFactory = DatatypeFactory.newInstance();
		GregorianCalendar cal = new GregorianCalendar();

		AddressBook addressBook = new AddressBook();
		addressBook.setTitle("たいとる");

		for (int idx = 0; idx < 10; idx++) {
			Institution inst = new Institution();

			// 文字列の場合
			inst.setName(String.format("施設%04d", idx));

			// 数値の場合
			inst.setCarParkingCapacity(idx * 3);

			// 日付・時刻の場合、XML Schemaに対応する日付型にする必要あり
			// @XmlSchemaType属性で、日付や時刻、あるいは日時などの形式を指定可能.
			cal.setTime(new Date());
			XMLGregorianCalendar dt = xmlDateFactory.newXMLGregorianCalendar(cal);
			inst.setRegisteredDate(dt);

			// ミリ秒はいらないので、未使用として指示する.
			dt.setMillisecond(DatatypeConstants.FIELD_UNDEFINED);

			if (idx % 4 > 1) {
				inst.setOpeningTime(dt);
			}
			if (idx % 4 < 3) {
				inst.setClosingTime(dt);
			}

			addressBook.getInstitutions().add(inst);
		}
		return addressBook;
	}
}

プログラム中でスキーマを読み込み指定しているので、XML読み込み時にはスキーマに適合するか検証が行われる。

実行結果
marshaller=<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<address-book title="たいとる" xsi:noNamespaceSchemaLocation="AddressBook.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <institution registered-date="2012-06-15+09:00" name="施設0000" parking-capacity="0">
        <closing-time>02:25:30+09:00</closing-time>
    </institution>
    <institution registered-date="2012-06-15+09:00" name="施設0001" parking-capacity="3">
        <closing-time>02:25:30+09:00</closing-time>
    </institution>
    <institution registered-date="2012-06-15+09:00" name="施設0002" parking-capacity="6">
        <closing-time>02:25:30+09:00</closing-time>
        <opening-time>02:25:30+09:00</opening-time>
    </institution>
    <institution registered-date="2012-06-15+09:00" name="施設0003" parking-capacity="9">
        <opening-time>02:25:30+09:00</opening-time>
    </institution>
    <institution registered-date="2012-06-15+09:00" name="施設0004" parking-capacity="12">
        <closing-time>02:25:30+09:00</closing-time>
    </institution>
    <institution registered-date="2012-06-15+09:00" name="施設0005" parking-capacity="15">
        <closing-time>02:25:30+09:00</closing-time>
    </institution>
    <institution registered-date="2012-06-15+09:00" name="施設0006" parking-capacity="18">
        <closing-time>02:25:30+09:00</closing-time>
        <opening-time>02:25:30+09:00</opening-time>
    </institution>
    <institution registered-date="2012-06-15+09:00" name="施設0007" parking-capacity="21">
        <opening-time>02:25:30+09:00</opening-time>
    </institution>
    <institution registered-date="2012-06-15+09:00" name="施設0008" parking-capacity="24">
        <closing-time>02:25:30+09:00</closing-time>
    </institution>
    <institution registered-date="2012-06-15+09:00" name="施設0009" parking-capacity="27">
        <closing-time>02:25:30+09:00</closing-time>
    </institution>
</address-book>

unmarshaller=title=たいとる, institutions=[name=施設0000, address=null, carParkingCapacity=0, registeredDate=2012-06-15+09:00, openingTime=null, closingTime=02:25:30+09:00, name=施設0001, address=null, carParkingCapacity=3, registeredDate=2012-06-15+09:00, openingTime=null, closingTime=02:25:30+09:00, name=施設0002, address=null, carParkingCapacity=6, registeredDate=2012-06-15+09:00, openingTime=02:25:30+09:00, closingTime=02:25:30+09:00, name=施設0003, address=null, carParkingCapacity=9, registeredDate=2012-06-15+09:00, openingTime=02:25:30+09:00, closingTime=null, name=施設0004, address=null, carParkingCapacity=12, registeredDate=2012-06-15+09:00, openingTime=null, closingTime=02:25:30+09:00, name=施設0005, address=null, carParkingCapacity=15, registeredDate=2012-06-15+09:00, openingTime=null, closingTime=02:25:30+09:00, name=施設0006, address=null, carParkingCapacity=18, registeredDate=2012-06-15+09:00, openingTime=02:25:30+09:00, closingTime=02:25:30+09:00, name=施設0007, address=null, carParkingCapacity=21, registeredDate=2012-06-15+09:00, openingTime=02:25:30+09:00, closingTime=null, name=施設0008, address=null, carParkingCapacity=24, registeredDate=2012-06-15+09:00, openingTime=null, closingTime=02:25:30+09:00, name=施設0009, address=null, carParkingCapacity=27, registeredDate=2012-06-15+09:00, openingTime=null, closingTime=02:25:30+09:00]
試しにXMLを壊してみた場合

XML Schemaを指定している場合は、以下のようなエラーが発生する。

Exception in thread "main" javax.xml.bind.UnmarshalException
 - with linked exception:
[org.xml.sax.SAXParseException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'institutionx'. One of '{institution}' is expected.]
	at javax.xml.bind.helpers.AbstractUnmarshallerImpl.createUnmarshalException(AbstractUnmarshallerImpl.java:315)

スキーマを指定していない場合、XMLとして破綻していなければ読み取れないところは無視して実行される様子である。

もしスペルミスしていた場合にも実行時にはノーエラーで素通りすることになると思われるため、設定ファイル等の用途であればスキーマチェックさせたほうが良いと思われる。

JAXBの出力するXMLの形式に、もう少しこだわってみる。

データの保存・復元であれば現状のままでも十分使えるが、XMLの見た目的な構造に、もうすこしこだわることもできる。
たとえば、以下のような形のXMLを出したいとする。(以下が生成結果となる。)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<books xsi:noNamespaceSchemaLocation="Books.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <book>
        <title>title0</title>
        <authors>
            <author author-id="A00">なまえ0-0</author>
            <author author-id="A01">なまえ0-1</author>
            <author author-id="A02">なまえ0-2</author>
        </authors>
    </book>
    <book>
        <title>title1</title>
        <authors>
            <author author-id="A30">なまえ1-0</author>
            <author author-id="A31">なまえ1-1</author>
        </authors>
    </book>
</books>

スキーマは、以下のような定義となる。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="books" type="books"/>

  <xs:complexType name="books">
    <xs:sequence>
      <xs:element name="comment" type="xs:string" minOccurs="0"/>
      <xs:element name="book" type="book" minOccurs="0" maxOccurs="unbounded"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="book">
    <xs:sequence>
      <xs:element name="title" type="xs:string"/>
      <xs:element name="authors" minOccurs="0">
        <xs:complexType>
          <xs:sequence>
            <xs:element name="author" type="author" minOccurs="0" maxOccurs="unbounded"/>
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="author">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="author-id" type="xs:string"/>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>
</xs:schema>
考慮すべき点は3つ
  • 要素の出現順序がある。
  • 「author」要素は、要素に属性があり、且つ、子要素を持たずに自身のコンテンツを値としている。(xsdでいうところの、simpleContent型)
  • 「author」は、親要素として「authors」の中に列挙されている。
Authorの定義方法

属性をもち、且つ、子要素を持たずに自身のコンテンツを値とする場合には以下のように記述する。

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlValue;

public class Author {

	private String authorId;
	private String name;
	
	@XmlAttribute(name = "author-id") // 要素の属性を示す
	public String getAuthorId() {
		return authorId;
	}
	
	public void setAuthorId(String authorId) {
		this.authorId = authorId;
	}

	@XmlValue // 要素自身のコンテンツを示す
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
}
Author要素をauthorsで囲み、title, authorsの順に出力する定義方法

要素のコレクションを出力する際に、コレクションを束ねる親要素を持たせる場合は、以下のように記述する。

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

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;

@XmlType(propOrder = {"title", "authors"}) // プロパティの定義順
public class Book {
	private String title;
	private ArrayList<Author> authors = new ArrayList<Author>();
	
	@XmlElement(name = "title", required = true) // 必須要素は required = true
	public String getTitle() {
		return title;
	}
	
	public void setTitle(String title) {
		this.title = title;
	}
	
	@XmlElementWrapper(name = "authors") // 外側の要素をつける
	@XmlElement(name = "author", type = Author.class) // 明示的に要素の型を指示しても良い
	public List<Author> getAuthors() {
		return authors;
	}
}
  • @XmlElementWrapper アノテーションにより、@XmlElementの出力の親要素を指定できるようになる。
  • @XmlElement では、コレクションの要素の型をtypeを使って明示しても良い。
  • @XmlElement, @XmlAttribute にはデータの省略可否を示す required 属性を指定できる。(無いと実行時エラーにすることができる。)
  • @XmlTypeのpropOrderで出力するプロパティの順序を指定できる。(スキーマも順序が要求される。)

結論

  • 単純な「キー = 値」形式のデータでよければ、Propertiesを使う。
    • Propertiesは断然簡単。非常にお手軽。
  • だが、少し手間をかけてもXMLResourceBundleを使うメリットがある。
    • 国際化が必要な場合
    • アプリケーションを停止させずに設定ファイルの再読み込みが必要な場合
    • XMLResourceBundleは定型的なものなので一度書いてしまえば使い回しは効く。
  • 構造化されたオブジェクトをテキスト形式で簡単に保存・復元するならばJAXBを使うのが良い。
    • データクラスに少しのアノテーションをつけてあげれば良いだけ。難しくはない。
    • 日付・時刻の扱いが少し面倒ではある。

以上、メモ終了。