XPath 関数拡張

XPath2.0 は正規表現が使えるようになっていて反則的に便利なのだけど、標準で使える環境は(たぶん)あまりなく Java でもまだ使えない。

Java から XPath2.0 使いたい
10:47 PM December 14, 2007 from TwitterIrcGateway

http://twitter.com/hetappi/statuses/500570172

Twitter で呟けば何か反応があるかと期待したのだけど、私のコミュニケーション能力が低いせいか全くありませんでした…。
で、調べた。

XPath2.0 実装ライブラリ

このへんのライブラリで使えるようになる。

XPath 関数拡張

このへんのインタフェースを実装すれば XPath 関数を拡張できるので、自分で作っちゃってもよいかも。

  • javax.xml.namespace.NamespaceContext
  • javax.xml.xpath.XPathFunctionResolver
  • javax.xml.xpath.XPathFunction

さっくり matches

7.6.2 fn:matches
fn:matches($input as xs:string?, $pattern as xs:string) as xs:boolean
fn:matches($input as xs:string?, $pattern as xs:string, $flags as xs:string) as xs:boolean

http://www.w3.org/TR/xquery-operators/#func-matches

を実装してみた。

ソース
package tmp;

import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathFunctionException;
import javax.xml.xpath.XPathFunctionResolver;

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

class XPathFunctionImplLazyMatches implements XPathFunction {
  public Object evaluate(List args) throws XPathFunctionException {
    Pattern pat;
    if (args.size() == 2) {
      pat = Pattern.compile((String) args.get(1));
    } else if (args.size() == 3) {
      int flag = 0;
      String arg2 = (String) args.get(2);
      for (int i = arg2.length() - 1; i >= 0; --i) {
        switch (arg2.charAt(i)) {
        case 's':
          flag |= Pattern.DOTALL;
          break;
        case 'm':
          flag |= Pattern.MULTILINE;
          break;
        case 'i':
          flag |= Pattern.CASE_INSENSITIVE;
          break;
        case 'x':
          flag |= Pattern.COMMENTS;
          break;
        default:
          return new XPathFunctionException("");
        }
      }
      pat = Pattern.compile((String) args.get(1), flag);
    } else {
      return new XPathFunctionException("");
    }
    Object obj = args.get(0);
    String value = (obj instanceof NodeList)
      ? ((NodeList) obj).item(0).getTextContent() : obj.toString();
    return pat.matcher(value).matches();
  }
}

class XPathFunctionResolverImpl implements XPathFunctionResolver {
  private static final QName qname =
    new QName(NamespaceContextImpl.XPATH_EXT_NS_URI, "matches");

  public XPathFunction resolveFunction(QName functionName, int arity) {
    return qname.equals(functionName) && (arity == 2 || arity == 3)
      ? new XPathFunctionImplLazyMatches() : null;
  }
}

class NamespaceContextImpl implements NamespaceContext {
  public static final String XPATH_EXT_NS_URI =
    "http://d.hatena.ne.jp/hetappi/xpath-ext";

  public String getNamespaceURI(String prefix) {
    if (prefix == null)
      throw new NullPointerException();
    return
      "ext".equals(prefix) ? XPATH_EXT_NS_URI :
      "xml".equals(prefix) ? XMLConstants.XML_NS_URI :
      XMLConstants.NULL_NS_URI;
  }

  public String getPrefix(String namespaceURI) {
    throw new UnsupportedOperationException();
  }

  public Iterator getPrefixes(String namespaceURI) {
    throw new UnsupportedOperationException();
  }
}

public class TestXPathExt {
  public static void main(String[] args) throws Exception {
    XPathFactory xpf = XPathFactory.newInstance();
    xpf.setXPathFunctionResolver(new XPathFunctionResolverImpl());

    XPath xpath = xpf.newXPath();
    xpath.setNamespaceContext(new NamespaceContextImpl());

    Document doc =
      DocumentBuilderFactory.newInstance().newDocumentBuilder().parse("./in.xml");
    NodeList nodes =
      (NodeList) xpath.evaluate(
        "//item[ext:matches(.,'\\d{1,3}')]", doc, XPathConstants.NODESET);
    for (int i = nodes.getLength() - 1; i >= 0; --i) {
      nodes.item(i).setTextContent("99999");
    }
    Node node
      = (Node) xpath.evaluate(
        "//item[ext:matches(.,'abCdEF', 'i')]", doc, XPathConstants.NODE);
    if (node != null)
      node.setTextContent("XXXXX");

    Transformer trans = TransformerFactory.newInstance().newTransformer();
    trans.setOutputProperty(OutputKeys.INDENT, "yes");
    trans.transform(new DOMSource(doc), new StreamResult(System.out));
//    trans.transform(new DOMSource(doc), new StreamResult("./out.xml"));
  }
}
実行結果
D:\Temp>type in.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<items>
  <item>1</item>
  <item>123</item>
  <item>123456</item>
  <item>aBcdeF</item>
</items>

D:\Temp>java -classpath bin tmp.TestXPathExt
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<items>
  <item>99999</item>
  <item>99999</item>
  <item>123456</item>
  <item>XXXXX</item>
</items>

D:\Temp>