2012 11 30

フレームワークを作るの9

この辺りで一旦終了にするかと思ったのだが、最後にもう一つ、XMLの検証について。

と、ここまで2つのXMLファイルを使用しているのだが、構造が簡単なこともあって書きっ放しの well-formed 扱いになっている。 今のサンプルのように小さいうちはこれでいいが、実際に使うとなったら、意味構造の検証はやっぱり必要だよな。 ということで、それぞれのXMLにスキーマ定義を用意することにした。

まずは sqlDef.xml 用の定義ファイル sqlDef.xsd

<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:lns="http://hiro666.sakura.ne.jp/XMLSchema" targetNamespace="http://hiro666.sakura.ne.jp/XMLSchema" elementFormDefault="qualified"> <xsd:element name="sqls" type="lns:sqls_type" /> <xsd:complexType name="sqls_type"> <xsd:sequence minOccurs="1" maxOccurs="unbounded"> <xsd:element name="sql" type="lns:sql_type" /> </xsd:sequence> </xsd:complexType> <xsd:complexType name="sql_type"> <xsd:simpleContent> <xsd:extension base="xsd:string"> <xsd:attribute name="id" type="xsd:ID" use="required" /> </xsd:extension> </xsd:simpleContent> </xsd:complexType> </xsd:schema>

これを使うように sqlDef.xml のルート要素を変更する。

<?xml version="1.0" encoding="UTF-8"?> <sqls xmlns="http://hiro666.sakura.ne.jp/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://hiro666.sakura.ne.jp/XMLSchema sqlDef.xsd"> 〜中略〜 </sqls>

次に serviceDef.xml 用の定義ファイル serviceDef.xsd

<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:lns="http://hiro666.sakura.ne.jp/XMLSchema" targetNamespace="http://hiro666.sakura.ne.jp/XMLSchema" elementFormDefault="qualified"> <xsd:element name="defs" type="lns:defs_type" /> <xsd:complexType name="defs_type"> <xsd:sequence> <xsd:element name="services" type="lns:services_type" /> <xsd:element name="daos" type="lns:daos_type" /> </xsd:sequence> </xsd:complexType> <xsd:complexType name="services_type"> <xsd:sequence minOccurs="1" maxOccurs="unbounded"> <xsd:element name="service" type="lns:service_type" /> </xsd:sequence> </xsd:complexType> <xsd:complexType name="service_type"> <xsd:sequence minOccurs="1" maxOccurs="unbounded"> <xsd:element name="dao" type="lns:dao_use_type" /> </xsd:sequence> <xsd:attribute name="id" type="xsd:ID" use="required" /> <xsd:attribute name="class" type="xsd:string" use="required" /> </xsd:complexType> <xsd:complexType name="dao_use_type"> <xsd:attribute name="id" type="xsd:string" use="required" /> </xsd:complexType> <xsd:complexType name="daos_type"> <xsd:sequence minOccurs="1" maxOccurs="unbounded"> <xsd:element name="dao" type="lns:dao_def_type" /> </xsd:sequence> </xsd:complexType> <xsd:complexType name="dao_def_type"> <xsd:attribute name="id" type="xsd:ID" use="required" /> <xsd:attribute name="class" type="xsd:string" use="required" /> </xsd:complexType> </xsd:schema>

これを使うように serviceDef.xml のルート要素を変更する。

<?xml version="1.0" encoding="UTF-8"?> <defs xmlns="http://hiro666.sakura.ne.jp/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://hiro666.sakura.ne.jp/XMLSchema serviceDef.xsd"> 〜中略〜 </defs>

最初は、簡単な階層構造だし要素を独立定義せずに全部入れ子にしてもいいかと思ったのだが、実際にやってみたらやっぱり読み難かった。 DTDに比べて色々と制約を設定できるのはいいが、その分どうしても面倒になってしまうのがXMLSchemaの弱点だよな。

名前空間は一意性を保てれば何でもいいので、 ここでは http://hiro666.sakura.ne.jp/XMLSchema を指定している。

先にスキーマを作ってからXMLを作れば、eclipseのXMLエディタは入力候補の表示やエラーチェックをしてくれて便利なんだよな。 well-formedレベルの構文チェックだけでも便利ではあるのだけど、これだけだとやっぱり物足りないし。 今更だけど、先に作っておけばよかったな。

さて、スキーマ定義ができて、XMLファイルが編集のタイミングでチェックできるようになったのはいいのだが、予想外だったのはプログラムが動かなくなったこと。

具体的には、XPathで指定した要素が取得できなくなった。 XMLファイルのルート要素に指定しているスキーマ指定の部分を削除するとちゃんと動くのだが、これを指定すると駄目。

いろいろ調べてみるに、名前空間を指定する場合はXPathの扱いが違うのだそうだ。 それがデフォルトの名前空間でプレフィックスが指定されていない場合でも、とにかく名前空間の指定 xmlns がある場合は、XPathでは何らかのプレフィックスが必要になるとのこと。

なんでそんな仕様になっているのかは判らないが、そうなっているものはしょうがない。 プレフィックスに p の一文字を設定することにして、名前空間に対応させるためにプログラムを修正する。

まずは名前空間とプレフィックスを対応させるために、 LocalNamespaceContext を新規作成。 パッケージ framework.util に置く。

package framework.util; import java.util.Iterator; import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import framework.exception.SystemException; public class LocalNamespaceContext implements NamespaceContext { @Override public String getNamespaceURI(String prefix) { if (prefix == null) { throw new SystemException("Null prefix"); } else if ("p".equals(prefix)) { return "http://hiro666.sakura.ne.jp/XMLSchema"; } else { return XMLConstants.XML_NS_URI; } } @Override public String getPrefix(String arg0) { throw new SystemException(new UnsupportedOperationException()); } @Override public Iterator<?> getPrefixes(String arg0) { throw new SystemException(new UnsupportedOperationException()); } }

例外を返しているだけのメソッドは、 NamespaceContext で実装を指定されているが、ここでは使わないもの。 むしろ使われるとおかしいので、例外 UnsupportedOperationException を返すようにしている。 フレームワーク内部から投げる例外は SystemException に集約することにしているので、投げる例外の生成が二重になっているのがくどい感じ。

次にXPathを使用する側の変更。 変更対象のクラスは2つ。

データアクセス層の定義取得用クラス SqlDefLoader

package framework.dao; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import framework.exception.SystemException; import framework.util.LocalNamespaceContext; import framework.util.XmlUtil; public class SqlDefLoader { private static final String SCHEMA_FILE_NAME = "sqlDef.xsd"; private static final String SQL_XPATH_FMT = "//p:sql[@id='%s']/text()"; private static final Pattern PRM_PTN = Pattern.compile(":([A-Za-z0-9_]+)"); private static XPath xpath = null; private Document doc = null; public SqlDefLoader(final String sqlDefFileName) { xpath = XPathFactory.newInstance().newXPath(); xpath.setNamespaceContext(new LocalNamespaceContext()); DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); domFactory.setNamespaceAware(true); try { doc = domFactory.newDocumentBuilder().parse(sqlDefFileName); XmlUtil.validate(doc, SCHEMA_FILE_NAME); } catch (SAXException | ParserConfigurationException | IOException e) { throw new SystemException(e); } } public SqlDef getSql(final String id) { return getPrms(new SqlDef(getText(id), new ArrayList<String>())); } private String getText(final String id) { NodeList nodes = getNodeList(SQL_XPATH_FMT, id); if (nodes == null || nodes.getLength() != 1) { throw new SystemException("invalid id:" + id); } return nodes.item(0).getNodeValue(); } private static SqlDef getPrms(final SqlDef def) { Matcher m = PRM_PTN.matcher(def.getText()); if (!m.find()) { return def; } String text = def.getText().replaceFirst(m.group(0), "?"); List<String> prms = new ArrayList<String>(def.getPrms()); prms.add(m.group(1)); return getPrms(new SqlDef(text, prms)); } private NodeList getNodeList(final String xpathFormat, final String id) { try { XPathExpression expr = xpath.compile(String.format(xpathFormat, id)); return (NodeList) expr.evaluate(doc, XPathConstants.NODESET); } catch (XPathExpressionException e) { throw new SystemException(e); } } }

サービス層の定義取得用クラス ServiceDefLoader

package framework.service; import java.io.IOException; import java.util.HashMap; import java.util.Map; 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 org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import framework.exception.SystemException; import framework.util.LocalNamespaceContext; import framework.util.XmlUtil; public class ServiceDefLoader { private static final String SCHEMA_FILE_NAME = "serviceDef.xsd"; private static final String SERVICE_DEF_XPATH_FMT = "//p:services/p:service[@id='%s']"; private static final String DAO_DEF_XPATH_FMT = "//p:daos/p:dao[@id='%s']"; private static XPath xpath = null; private static Document doc = null; public ServiceDefLoader(final String serviceDefFileName) { xpath = XPathFactory.newInstance().newXPath(); xpath.setNamespaceContext(new LocalNamespaceContext()); DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); domFactory.setNamespaceAware(true); domFactory.setValidating(false); try { doc = domFactory.newDocumentBuilder().parse(serviceDefFileName); XmlUtil.validate(doc, SCHEMA_FILE_NAME); } catch (SAXException | ParserConfigurationException | IOException e) { throw new SystemException(e); } } public ServiceDef getServiceDef(final String id) { try { Node serviceNode = getNode(SERVICE_DEF_XPATH_FMT, id); String serviceClass = getAttributeValue(serviceNode, "class"); Map<String, String> daoDef = getDaoDef(serviceNode.getChildNodes()); return new ServiceDef(serviceClass, daoDef); } catch (XPathExpressionException e) { throw new SystemException(e); } } private static Map<String, String> getDaoDef(final NodeList nodes) throws XPathExpressionException { Map<String, String> daoDef = new HashMap<>(); for (int i = 0; i < nodes.getLength(); i++) { if (nodes.item(i).getNodeName().equalsIgnoreCase("dao")) { String id = getAttributeValue(nodes.item(i), "id"); String className = getAttributeValue(getNode(DAO_DEF_XPATH_FMT, id), "class"); daoDef.put(id, className); } } return daoDef; } private static Node getNode(final String fmt, final String id) throws XPathExpressionException { return ((NodeList) xpath.compile(String.format(fmt, id)).evaluate(doc, XPathConstants.NODESET)).item(0); } private static String getAttributeValue(final Node node, final String attributeName) { return node.getAttributes().getNamedItem(attributeName).getNodeValue(); } }

変更のポイントはどちらも同じで、次の2点。

一つは、XPathの評価の前に xpath.setNamespaceContext(new LocalNamespaceContext()) として名前空間のプレフィックスを処理させるように指定したこと。

もう一つは、XPathの定義文字列中で //p:services/p:service のように、要素名にプレフィックス 「p:」 を付けたこと。

ほんとに何でこんな仕様にしちゃったのかね。 一々プレフィックスを付けるのが面倒だし、付けた結果は読み難いのに。 明示的にプレフィックスを指定した場合だけにできなかったのか?

と愚痴ってもしょうがないので、変更するついでにプログラム側でもXMLファイルの妥当性をチェックするようにしてみた。 上記コード中の XmlUtil.validate(doc, SCHEMA_FILE_NAME); が実際に検証しているところ。 ここで使用している XmlUtil は、パッケージ framework.util に置いている。

package framework.util; import java.io.File; import java.io.IOException; import javax.xml.XMLConstants; import javax.xml.transform.dom.DOMSource; import javax.xml.validation.SchemaFactory; import org.w3c.dom.Document; import org.xml.sax.SAXException; import framework.exception.SystemException; public class XmlUtil { public static void validate(final Document document, final String schemaFileName) { try { SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(new File(schemaFileName)).newValidator().validate(new DOMSource(document)); } catch (SAXException | IOException e) { throw new SystemException(e); } } }

面倒なので、検証以前の設定が拙い場合も、検証での問題も、全て SystemException を投げている。

現在の構成は以下の通り。

apl Main.java service BooksService.java impl BooksServiceImpl.java dao BooksDao.java impl BooksDaoImpl.java dto BookDto.java util DbUtil.java framework annotation Commit.java Rollback.java service ServiceClassLoader.java ServiceDef.java ServiceDefLoader.java ServiceFactory.java TransactionInvocationHandler.java dao CmnDao.java DtoConverter.java SqlDefLoader.java SqlDef.java exception BusinessException.java SystemException.java util Each.java LineConverter.java LineFileWriter.java LocalNamespaceContext.java StringUtil.java TsvConverter.java XmlUtil.java serviceDef.xml serviceDef.xsd sqlDef.xml sqlDef.xsd

いやもう、本当にコードが増えてきたな。 まあ、増えたとは言っても、最低限の実装なので仕事で作るのとは比べ物にならないんだけど、こうして並べて最初の状態と比べてみると、何か沁み沁みと、ねえ。