GeekFactory

int128.hatenablog.com

他ライブラリに依存せずにXML設定ファイルを読み込む方法

ライブラリをパッケージして配布する時、なるべく依存するjarを減らしたいことがあります。Mavenを使えば何ともないのですが、敷居を下げる意味では「1つのjarだけ入れれば動きます」が望ましいと思います。*1

Commons DigesterやJAXBを使うと、XML設定ファイルの読み込みが飛躍的に簡単になります。一方で、ライブラリを配布する時は同梱するか注意書きを加える必要があります。そこで、他ライブラリに依存せずJREだけでXMLを読み込む方法を考えてみました。

Java 5から標準になったXPathを使うと、XMLから任意のノードを簡単に取り出すことが可能です。XPathクラスをそのまま使うと記述量が増えるので、XPathをラップするクラスを用意します。

package org.hidetake.util.xml;

import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathVariableResolver;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class XPathEvaluator
{
	private final Document document;
	private final XPath xpath;

	public XPathEvaluator(InputStream stream)
	throws SAXException, IOException, ParserConfigurationException
	{
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		//factory.setNamespaceAware(true);
		document = factory.newDocumentBuilder().parse(stream);
		xpath = XPathFactory.newInstance().newXPath();
	}

	public Iterable<Node> getNodeList(String expression)
	{
		try {
			final NodeList nodeList = (NodeList) xpath.evaluate(expression, document, XPathConstants.NODESET);
			return new Iterable<Node>()
			{
				public Iterator<Node> iterator()
				{
					return new Iterator<Node>()
					{
						private int index = 0;

						public boolean hasNext()
						{
							return index < nodeList.getLength();
						}

						public Node next()
						{
							return nodeList.item(index++);
						}

						public void remove()
						{
							throw new UnsupportedOperationException("NodeList is read-only");
						}
					};
				}
			};
		}
		catch (XPathExpressionException e) {
			throw new RuntimeException(e);
		}
	}

	public Iterable<String> getNodeValueList(String expression)
	{
		try {
			final NodeList nodeList = (NodeList) xpath.evaluate(expression, document, XPathConstants.NODESET);
			return new Iterable<String>()
			{
				public Iterator<String> iterator()
				{
					return new Iterator<String>()
					{
						private int index = 0;

						public boolean hasNext()
						{
							return index < nodeList.getLength();
						}

						public String next()
						{
							return nodeList.item(index++).getNodeValue();
						}

						public void remove()
						{
							throw new UnsupportedOperationException("NodeList is read-only");
						}
					};
				}
			};
		}
		catch (XPathExpressionException e) {
			throw new RuntimeException(e);
		}
	}

	public String getString(String expression)
	{
		try {
			return (String) xpath.evaluate(expression, document, XPathConstants.STRING);
		}
		catch (XPathExpressionException e) {
			throw new RuntimeException(e);
		}
	}

	public void setVariable(final Map<QName, Object> variableMap)
	{
		xpath.setXPathVariableResolver(new XPathVariableResolver()
		{
			public Object resolveVariable(QName variableName)
			{
				return variableMap.get(variableName);
			}
		});

	}
}

XPathEvaluatorを使うと、下記のような感じで設定を取得できます。

XPathEvaluator evaluator = new XPathEvaluator(stream);

// 属性値を取得する
String appId = evaluator.getString("/config/app/@app-id");
String appUrl = evaluator.getString("/config/app/@app-url");

// 変数を使う
Map<QName, Object> variableMap = new HashMap<QName, Object>();
variableMap.put(new QName("id"), appId);
evaluator.setVariable(variableMap);
		
String description = evaluator.getString(
	"/config/context[@app-id=$id]/description/text()");

わずか128行のコードを追加するだけでXML設定ファイルを読み込むことができます。ぜひお試しください。

*1:あと、私だけかもしれませんが、Google App Engineではjarが増えるだけでコンテキストの初期化が長くなり、チャリンチャリンかかってしまう気がしています。