[Practicalxml-commits] SF.net SVN: practicalxml:[55] trunk/src
Brought to you by:
kdgregory
From: Auto-Generated S. C. M. <pra...@li...> - 2008-12-17 03:35:38
|
Revision: 55 http://practicalxml.svn.sourceforge.net/practicalxml/?rev=55&view=rev Author: kdgregory Date: 2008-12-17 03:35:33 +0000 (Wed, 17 Dec 2008) Log Message: ----------- add AbstractFunction Added Paths: ----------- trunk/src/main/java/net/sf/practicalxml/xpath/AbstractFunction.java trunk/src/test/java/net/sf/practicalxml/xpath/TestAbstractFunction.java Added: trunk/src/main/java/net/sf/practicalxml/xpath/AbstractFunction.java =================================================================== --- trunk/src/main/java/net/sf/practicalxml/xpath/AbstractFunction.java (rev 0) +++ trunk/src/main/java/net/sf/practicalxml/xpath/AbstractFunction.java 2008-12-17 03:35:33 UTC (rev 55) @@ -0,0 +1,396 @@ +package net.sf.practicalxml.xpath; + +import java.util.Collections; +import java.util.List; + +import javax.xml.namespace.QName; +import javax.xml.xpath.XPathFunction; +import javax.xml.xpath.XPathFunctionException; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + + +/** + * A base class for writing XPath functions that replaces much of the + * boilerplate code using a "template method" approach. Execution goes + * through the following three stages: + * <ol> + * <li> Initialization: the subclass method {@link #init} is invoked. + * <li> Input: for each argument, the type-appropriate {@link #processArg} + * method is called. + * <li> Completion: the subclass method {@link #getResult} is invoked, and + * its return value is returned from {@link #evaluate}. + * </ol> + * <p> + * Subclasses define their name, namespace, and the number of arguments + * they accept (which may be undefined). + * <p> + * To support thread safety, subclasses are expected to make use of a helper + * object that holds intermediate results. The helper object is created by + * <code>init()</code>, is an argument to and return from <code>processArg() + * </code>, and is an argument to <code>getResult()</code>. Subclasses are + * parameterized with the helper object type, and the default behavior of + * <code>getResult()</code> is to return the helper (this makes life easier + * for a function that takes zero or one arguments). + * <p> + * <em>Note:</em> in JDK 1.5, DOM objects implement both <code>Node</code> + * and <code>NodeList</code>. While this means that <code>processArg(Node) + * </code> would never be called as part of the normal dispatch loop (unless + * the implementation changes), it <em>is</em> called as part of the default + * behavior of <code>processArg(NodeList)</code>, and is given the first + * element in the list. + */ +public class AbstractFunction<T> +implements XPathFunction +{ + /** + * Qualified name of this function. + */ + final private QName _qname; + + + /** + * Minimum number of arguments, 0 if no minimum. + */ + final private int _minArgCount; + + + /** + * Maximum number of arguments, Integer.MAX_VALUE if no maximum. + */ + final private int _maxArgCount; + + + /** + * Constructor for a function that can take any number of arguments. + */ + protected AbstractFunction(String nsUri, String localName) + { + _qname = new QName(nsUri, localName); + _minArgCount = 0; + _maxArgCount = Integer.MAX_VALUE; + } + + + /** + * Constructor for a function that has a fixed number of arguments. + */ + protected AbstractFunction(String nsUri, String localName, int numArgs) + { + _qname = new QName(nsUri, localName); + _minArgCount = numArgs; + _maxArgCount = numArgs; + } + + + /** + * Constructor for a function that has a variable number of arguments. + */ + protected AbstractFunction(String nsUri, String localName, int minArgs, int maxArgs) + { + _qname = new QName(nsUri, localName); + _minArgCount = minArgs; + _maxArgCount = maxArgs; + } + + +//---------------------------------------------------------------------------- +// Public methods +//---------------------------------------------------------------------------- + + /** + * Returns the qualified name of this function, consisting of name and + * namespace (but not prefix). + */ + public QName getQName() + { + return _qname; + } + + + /** + * Returns the namespace URI for this function. + */ + public String getNamespaceUri() + { + return _qname.getNamespaceURI(); + } + + + /** + * Returns the name of this function. + */ + public String getName() + { + return _qname.getLocalPart(); + } + + + /** + * Returns the minimum number of arguments handled by this function. + */ + public int getMinArgCount() + { + return _minArgCount; + } + + + /** + * Returns the maximum number of arguments handled by this function. + */ + public int getMaxArgCount() + { + return _maxArgCount; + } + + + /** + * Determines whether this function is a match for a call to + * <code>XPathFunctionResolver.resolveFunction()</code>. + */ + public boolean isMatch(QName qname, int arity) + { + return _qname.equals(qname) + && (arity >= _minArgCount) + && (arity <= _maxArgCount); + } + + +//---------------------------------------------------------------------------- +// Implementation of XPathFunction +//---------------------------------------------------------------------------- + + /** + * Invokes methods defined by the subclasses to evaluate the function. + * Will invoke the appropriate <code>processArg()</code> method in turn + * for each argument, then invokes <code>getResult()</code> to retrieve + * the function's result. + * + * @throws XPathFunctionException if the supplied argument list does + * not match the min/max for this function, or if any called + * method throws an exception. + */ + public Object evaluate(List args) + throws XPathFunctionException + { + if (args == null) + args = Collections.EMPTY_LIST; + + if ((args.size() < _minArgCount) || (args.size() > _maxArgCount)) + throw new XPathFunctionException("illegal argument count: " + args.size()); + + try + { + T helper = init(); + int idx = 0; + for (Object arg : args) + { + if (arg instanceof String) + helper = processArg(idx, (String)arg, helper); + else if (arg instanceof NodeList) + helper = processArg(idx, (NodeList)arg, helper); + else if (arg instanceof Node) + helper = processArg(idx, (Node)arg, helper); + else if (arg == null) + helper = processNullArg(idx, helper); + else + helper = processUnexpectedArg(idx, arg, helper); + idx++; + } + return getResult(helper); + } + catch (Exception e) + { + throw new XPathFunctionException(e); + } + } + + +//---------------------------------------------------------------------------- +// Subclasses override these methods +//---------------------------------------------------------------------------- + + /** + * Creates a helper object to preserve intermediate results. This object + * is passed to <code>processArg()</code> and <code>getResult()</code>. + * <p> + * The default implementation returns <code>null</code>. + * <p> + * Subclasses are permitted to throw any exception; it will be wrapped in + * an <code>XPathFunctionException</code>. + */ + protected T init() + throws Exception + { + return null; + } + + + /** + * Processes a String argument. + * + * @param index Index of the argument, numbered from 0. + * @param value Value of the argument. + * @param helper Helper object to preserve intermediate results. + * + * @return The helper object (subclasses may return a replacement). + * + * @throws Subclasses are permitted to throw any exception. It will be + * wrapped in <code>XPathFunctionException</code> and cause + * function processing to abort. + */ + protected T processArg(int index, String value, T helper) + throws Exception + { + return helper; + } + + + /** + * Processes a Number argument. Per the XPath spec, this should be a + * <code>Double</code> + * + * @param index Index of the argument, numbered from 0. + * @param value Value of the argument. + * @param helper Helper object to preserve intermediate results. + * + * @return The helper object (subclasses may return a replacement). + * + * @throws Subclasses are permitted to throw any exception. It will be + * wrapped in <code>XPathFunctionException</code> and cause + * function processing to abort. + */ + protected T processArg(int index, Number value, T helper) + throws Exception + { + return helper; + } + + + /** + * Processes a Boolean argument. + * + * @param index Index of the argument, numbered from 0. + * @param value Value of the argument. + * @param helper Helper object to preserve intermediate results. + * + * @return The helper object (subclasses may return a replacement). + * + * @throws Subclasses are permitted to throw any exception. It will be + * wrapped in <code>XPathFunctionException</code> and cause + * function processing to abort. + */ + protected T processArg(int index, Boolean value, T helper) + throws Exception + { + return helper; + } + + + /** + * Processes a Node argument. This function will be invoked by the + * default implementation of <code>processArg(NodeList)</code>. + * + * @param index Index of the argument, numbered from 0. + * @param value Value of the argument. May be <code>null</code>. + * @param helper Helper object to preserve intermediate results. + * + * @return The helper object (subclasses may return a replacement). + * + * @throws Subclasses are permitted to throw any exception. It will be + * wrapped in <code>XPathFunctionException</code> and cause + * function processing to abort. + */ + protected T processArg(int index, Node value, T helper) + throws Exception + { + return helper; + } + + + /** + * Processes a NodeList argument. + * <p> + * The default implementation calls <code>processArg(Node)</code> with + * the first element in the list (<code>null</code> if the list is empty). + * + * @param index Index of the argument, numbered from 0. + * @param value Value of the argument. + * @param helper Helper object to preserve intermediate results. + * + * @return The helper object (subclasses may return a replacement). + * + * @throws Subclasses are permitted to throw any exception. It will be + * wrapped in <code>XPathFunctionException</code> and cause + * function processing to abort. + */ + protected T processArg(int index, NodeList value, T helper) + throws Exception + { + return processArg(index, value.item(0), helper); + } + + + /** + * Processes a <code>null</code> argument — it's unclear whether + * this can ever happen. The default implementation throws <code> + * IllegalArgumentException</code>. + * + * @param index Index of the argument, numbered from 0. + * @param helper Helper object to preserve intermediate results. + * + * @return The helper object (subclasses may return a replacement). + * + * @throws Subclasses are permitted to throw any exception. It will be + * wrapped in <code>XPathFunctionException</code> and cause + * function processing to abort. + */ + protected T processNullArg(int index, T helper) + throws Exception + { + throw new IllegalArgumentException("null argument: " + index); + } + + + /** + * Processes an argument that is not one of the defined types for XPath + * evaluation — it is unclear whether this can actually happen. + * The default implementation throws <code>IllegalArgumentException</code>. + * + * @param index Index of the argument, numbered from 0. + * @param value The argument value + * @param helper Helper object to preserve intermediate results. + * + * @return The helper object (subclasses may return a replacement). + * + * @throws Subclasses are permitted to throw any exception. It will be + * wrapped in <code>XPathFunctionException</code> and cause + * function processing to abort. + */ + protected T processUnexpectedArg(int index, Object value, T helper) + throws Exception + { + throw new IllegalArgumentException( + "unexpected argument: " + index + + " (" + value.getClass().getName() + ")"); + } + + + /** + * Returns the result of this invocation. + * + * @param helper Helper object to preserve intermediate results. + * + * @return The helper object (subclasses may return whatever they want). + * + * @throws Subclasses are permitted to throw any exception. It will be + * wrapped in <code>XPathFunctionException</code> and cause + * function processing to abort. + */ + protected Object getResult(T helper) + throws Exception + { + return helper; + } +} Added: trunk/src/test/java/net/sf/practicalxml/xpath/TestAbstractFunction.java =================================================================== --- trunk/src/test/java/net/sf/practicalxml/xpath/TestAbstractFunction.java (rev 0) +++ trunk/src/test/java/net/sf/practicalxml/xpath/TestAbstractFunction.java 2008-12-17 03:35:33 UTC (rev 55) @@ -0,0 +1,320 @@ +package net.sf.practicalxml.xpath; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javax.xml.namespace.QName; +import javax.xml.xpath.XPathFunctionException; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import net.sf.practicalxml.AbstractTestCase; +import net.sf.practicalxml.DomUtil; + + +public class TestAbstractFunction +extends AbstractTestCase +{ +//---------------------------------------------------------------------------- +// Support code +//---------------------------------------------------------------------------- + + /** + * A mock function implementation that records its invocations, and + * passes a dummy helper object through the call chain (returning it + * as the invocation result). + */ + private static class MyMockFunction + extends AbstractFunction<Object> + { + public Object _helper; + public int _initCalls; + public int _processStringCalls; + public int _processNumberCalls; + public int _processBooleanCalls; + public int _procesNodeCalls; + public int _procesNodeListCalls; + public int _getResultCalls; + + public MyMockFunction(Object helper) + { + super("foo", "bar"); + _helper = helper; + } + + @Override + protected Object init() throws Exception + { + _initCalls++; + return _helper; + } + + @Override + protected Object processArg(int index, String value, Object helper) + throws Exception + { + _processStringCalls++; + return helper; + } + + @Override + protected Object processArg(int index, Number value, Object helper) + throws Exception + { + _processNumberCalls++; + return helper; + } + + @Override + protected Object processArg(int index, Boolean value, Object helper) + throws Exception + { + _processBooleanCalls++; + return helper; + } + + @Override + protected Object processArg(int index, Node value, Object helper) + throws Exception + { + _procesNodeCalls++; + return helper; + } + + @Override + protected Object processArg(int index, NodeList value, Object helper) + throws Exception + { + _procesNodeListCalls++; + return helper; + } + + @Override + protected Object getResult(Object helper) + { + _getResultCalls++; + assertSame(_helper, helper); + return _helper; + } + } + + +//---------------------------------------------------------------------------- +// Test cases +//---------------------------------------------------------------------------- + + public void testConstructionAndAccessors() throws Exception + { + QName testName = new QName("foo", "bar"); + + AbstractFunction<Object> fn1 = new AbstractFunction<Object>("foo", "bar"); + assertEquals(testName, fn1.getQName()); + assertEquals("foo", fn1.getNamespaceUri()); + assertEquals("bar", fn1.getName()); + assertEquals(0, fn1.getMinArgCount()); + assertEquals(Integer.MAX_VALUE, fn1.getMaxArgCount()); + + assertTrue(fn1.isMatch(testName, 0)); + assertTrue(fn1.isMatch(testName, 1)); + assertTrue(fn1.isMatch(testName, Integer.MAX_VALUE)); + + AbstractFunction<Object> fn2 = new AbstractFunction<Object>("foo", "bar", 5); + assertEquals(testName, fn2.getQName()); + assertEquals("foo", fn2.getNamespaceUri()); + assertEquals("bar", fn2.getName()); + assertEquals(5, fn2.getMinArgCount()); + assertEquals(5, fn2.getMaxArgCount()); + + assertFalse(fn2.isMatch(testName, 0)); + assertFalse(fn2.isMatch(testName, 1)); + assertTrue(fn2.isMatch(testName, 5)); + assertFalse(fn2.isMatch(testName, 6)); + assertFalse(fn2.isMatch(testName, Integer.MAX_VALUE)); + + AbstractFunction<Object> fn3 = new AbstractFunction<Object>("foo", "bar", 1, 5); + assertEquals(testName, fn3.getQName()); + assertEquals("foo", fn3.getNamespaceUri()); + assertEquals("bar", fn3.getName()); + assertEquals(1, fn3.getMinArgCount()); + assertEquals(5, fn3.getMaxArgCount()); + + assertFalse(fn3.isMatch(testName, 0)); + assertTrue(fn3.isMatch(testName, 1)); + assertTrue(fn3.isMatch(testName, 5)); + assertFalse(fn3.isMatch(testName, 6)); + assertFalse(fn3.isMatch(testName, Integer.MAX_VALUE)); + } + + + public void testEvaluateInvalidArgCount() throws Exception + { + try + { + new AbstractFunction<Object>("foo", "bar", 5).evaluate( + Arrays.asList(new String[] {"foo", "bar"})); + fail("invalid argument count"); + } + catch (XPathFunctionException e) + { + // success + } + } + + + public void testEvaluateNullArglist() throws Exception + { + MyMockFunction fn = new MyMockFunction("zippy"); + assertEquals("zippy", fn.evaluate(null)); + assertEquals(1, fn._initCalls); + assertEquals(0, fn._processStringCalls); + assertEquals(0, fn._processNumberCalls); + assertEquals(0, fn._processBooleanCalls); + assertEquals(0, fn._procesNodeCalls); + assertEquals(0, fn._procesNodeListCalls); + assertEquals(1, fn._getResultCalls); + } + + + public void testEvaluateNullArgument() throws Exception + { + List args = Arrays.asList(new Object[] {null}); + MyMockFunction fn = new MyMockFunction("zippy"); + try + { + fn.evaluate(Arrays.asList(args)); + fail("evaluated null argument"); + } + catch (XPathFunctionException e) + { + // success + } + } + + + public void testEvaluateUnsupportedArgumentType() throws Exception + { + List args = Arrays.asList(new Object[] {new Exception()}); + MyMockFunction fn = new MyMockFunction("zippy"); + try + { + fn.evaluate(args); + fail("evaluated unsupported argument type"); + } + catch (XPathFunctionException e) + { + // success + } + } + + + public void testEvaluateString() throws Exception + { + List args = Arrays.asList(new Object[] {"foo", "bar"}); + MyMockFunction fn = new MyMockFunction("zippy"); + + assertEquals("zippy", fn.evaluate(args)); + assertEquals(1, fn._initCalls); + assertEquals(2, fn._processStringCalls); + assertEquals(0, fn._processNumberCalls); + assertEquals(0, fn._processBooleanCalls); + assertEquals(0, fn._procesNodeCalls); + assertEquals(0, fn._procesNodeListCalls); + assertEquals(1, fn._getResultCalls); + } + + + public void testEvaluateNode() throws Exception + { + // since any JDK Node implementation is also a NodeList, we have to + // get tricky + Node node = (Node)Proxy.newProxyInstance( + Node.class.getClassLoader(), + new Class[] {Node.class}, + new InvocationHandler() + { + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable + { + return null; + } + }); + + List args = Arrays.asList(new Object[] {node}); + MyMockFunction fn = new MyMockFunction("zippy"); + + assertEquals("zippy", fn.evaluate(args)); + assertEquals(1, fn._initCalls); + assertEquals(0, fn._processStringCalls); + assertEquals(0, fn._processNumberCalls); + assertEquals(0, fn._processBooleanCalls); + assertEquals(1, fn._procesNodeCalls); + assertEquals(0, fn._procesNodeListCalls); + assertEquals(1, fn._getResultCalls); + } + + + public void testEvaluateNodeList() throws Exception + { + List args = Arrays.asList(new Object[] {DomUtil.newDocument("foo").getChildNodes()}); + MyMockFunction fn = new MyMockFunction("zippy"); + + assertEquals("zippy", fn.evaluate(args)); + assertEquals(1, fn._initCalls); + assertEquals(0, fn._processStringCalls); + assertEquals(0, fn._processNumberCalls); + assertEquals(0, fn._processBooleanCalls); + assertEquals(0, fn._procesNodeCalls); + assertEquals(1, fn._procesNodeListCalls); + assertEquals(1, fn._getResultCalls); + } + + + public void testDefaultNodeListBehavior() + throws Exception + { + final Element root = DomUtil.newDocument("foo"); + final Element child1 = DomUtil.appendChild(root, "bar"); + final Element child2 = DomUtil.appendChild(root, "baz"); + + new AbstractFunction<Object>("argle", "bargle") + { + @Override + protected Object processArg(int index, NodeList value, Object helper) + throws Exception + { + assertEquals(2, value.getLength()); + return super.processArg(index, value, helper); + } + + @Override + protected Object processArg(int index, Node value, Object helper) + throws Exception + { + assertSame(child1, value); + return null; + } + }.evaluate(Arrays.asList(new Object[] {root.getChildNodes()})); + } + + + public void testDefaultGetResultBehavior() + throws Exception + { + final Object helper = new Object(); + AbstractFunction<Object> fn = new AbstractFunction<Object>("argle", "bargle") + { + @Override + protected Object init() throws Exception + { + return helper; + } + }; + + assertSame(helper, fn.evaluate(Collections.<String>emptyList())); + } +} This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |