ExtensionFunctionTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.jxpath.ri.compiler;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;

import org.apache.commons.jxpath.AbstractJXPathTest;
import org.apache.commons.jxpath.ClassFunctions;
import org.apache.commons.jxpath.ExpressionContext;
import org.apache.commons.jxpath.Function;
import org.apache.commons.jxpath.FunctionLibrary;
import org.apache.commons.jxpath.Functions;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.NodeSet;
import org.apache.commons.jxpath.PackageFunctions;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.jxpath.TestBean;
import org.apache.commons.jxpath.Variables;
import org.apache.commons.jxpath.ri.model.NodePointer;
import org.apache.commons.jxpath.util.JXPath11CompatibleTypeConverter;
import org.apache.commons.jxpath.util.TypeConverter;
import org.apache.commons.jxpath.util.TypeUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Test extension functions.
 */
class ExtensionFunctionTest extends AbstractJXPathTest {

    private static final class Context implements ExpressionContext {

        private final Object object;

        public Context(final Object object) {
            this.object = object;
        }

        @Override
        public List<Pointer> getContextNodeList() {
            return null;
        }

        @Override
        public Pointer getContextNodePointer() {
            return NodePointer.newNodePointer(null, object, Locale.getDefault());
        }

        @Override
        public JXPathContext getJXPathContext() {
            return null;
        }

        @Override
        public int getPosition() {
            return 0;
        }
    }

    private Functions functions;
    private JXPathContext context;
    private TestBean testBean;
    private TypeConverter typeConverter;

    @Override
    @BeforeEach
    public void setUp() {
        if (context == null) {
            testBean = new TestBean();
            context = JXPathContext.newContext(testBean);
            final Variables vars = context.getVariables();
            vars.declareVariable("test", new TestFunctions(4, "test"));
            final FunctionLibrary lib = new FunctionLibrary();
            lib.addFunctions(new ClassFunctions(TestFunctions.class, "test"));
            lib.addFunctions(new ClassFunctions(TestFunctions2.class, "test"));
            lib.addFunctions(new PackageFunctions("", "call"));
            lib.addFunctions(new PackageFunctions("org.apache.commons.jxpath.ri.compiler.", "jxpathtest"));
            lib.addFunctions(new PackageFunctions("", null));
            context.setFunctions(lib);
            context.getVariables().declareVariable("List.class", List.class);
            context.getVariables().declareVariable("NodeSet.class", NodeSet.class);
        }
        functions = new ClassFunctions(TestFunctions.class, "test");
        typeConverter = TypeUtils.getTypeConverter();
    }

    @AfterEach
    public void tearDown() {
        TypeUtils.setTypeConverter(typeConverter);
    }

    @Test
    void testAllocation() {
        // Allocate new object using the default constructor
        assertXPathValue(context, "string(test:new())", "foo=0; bar=null");
        // Allocate new object using PackageFunctions and class name
        assertXPathValue(context, "string(jxpathtest:TestFunctions.new())", "foo=0; bar=null");
        // Allocate new object using a fully qualified class name
        assertXPathValue(context, "string(" + TestFunctions.class.getName() + ".new())", "foo=0; bar=null");
        // Allocate new object using a custom constructor
        assertXPathValue(context, "string(test:new(3, 'baz'))", "foo=3; bar=baz");
        // Allocate new object using a custom constructor - type conversion
        assertXPathValue(context, "string(test:new('3', 4))", "foo=3; bar=4.0");
        context.getVariables().declareVariable("A", "baz");
        assertXPathValue(context, "string(test:new(2, $A, false))", "foo=2; bar=baz");
    }

    @Test
    void testBCNodeSetHack() {
        TypeUtils.setTypeConverter(new JXPath11CompatibleTypeConverter());
        assertXPathValue(context, "test:isInstance(//strings, $List.class)", Boolean.FALSE);
        assertXPathValue(context, "test:isInstance(//strings, $NodeSet.class)", Boolean.TRUE);
    }

    @Test
    void testCollectionMethodCall() {
        final List list = new ArrayList();
        list.add("foo");
        context.getVariables().declareVariable("myList", list);
        assertXPathValue(context, "size($myList)", Integer.valueOf(1));
        assertXPathValue(context, "size(beans)", Integer.valueOf(2));
        context.getValue("add($myList, 'hello')");
        assertEquals(2, list.size(), "After adding an element");
        final JXPathContext context = JXPathContext.newContext(new ArrayList());
        assertEquals("0", String.valueOf(context.getValue("size(/)")), "Extension function on root collection");
    }

    @Test
    void testCollectionReturn() {
        assertXPathValueIterator(context, "test:collection()/name", list("foo", "bar"));
        assertXPathPointerIterator(context, "test:collection()/name", list("/.[1]/name", "/.[2]/name"));
        assertXPathValue(context, "test:collection()/name", "foo");
        assertXPathValue(context, "test:collection()/@name", "foo");
        final List list = new ArrayList();
        list.add("foo");
        list.add("bar");
        context.getVariables().declareVariable("list", list);
        final Object values = context.getValue("test:items($list)");
        assertInstanceOf(Collection.class, values, "Return type: ");
        assertEquals(list, new ArrayList((Collection) values), "Return values: ");
    }

    @Test
    void testConstructorLookup() {
        final Object[] args = { Integer.valueOf(1), "x" };
        final Function func = functions.getFunction("test", "new", args);
        assertEquals("foo=1; bar=x", func.invoke(new Context(null), args).toString(), "test:new(1, x)");
    }

    @Test
    void testConstructorLookupWithExpressionContext() {
        final Object[] args = { "baz" };
        final Function func = functions.getFunction("test", "new", args);
        assertEquals("foo=1; bar=baz", func.invoke(new Context(Integer.valueOf(1)), args).toString(), "test:new('baz')");
    }

    @Test
    void testEstablishNodeSetBaseline() {
        assertXPathValue(context, "test:isInstance(//strings, $List.class)", Boolean.TRUE);
        assertXPathValue(context, "test:isInstance(//strings, $NodeSet.class)", Boolean.FALSE);
    }

    @Test
    void testExpressionContext() {
        // Execute an extension function for each node while searching
        // The function uses ExpressionContext to get to the current
        // node.
        assertXPathValue(context, "//.[test:isMap()]/Key1", "Value 1");
        // The function gets all
        // nodes in the context that match the pattern.
        assertXPathValue(context, "count(//.[test:count(strings) = 3])", Double.valueOf(7));
        // The function receives a collection of strings
        // and checks their type for testing purposes
        assertXPathValue(context, "test:count(//strings)", Integer.valueOf(21));
        // The function receives a collection of pointers
        // and checks their type for testing purposes
        assertXPathValue(context, "test:countPointers(//strings)", Integer.valueOf(21));
        // The function uses ExpressionContext to get to the current
        // pointer and returns its path.
        assertXPathValue(context, "/beans[contains(test:path(), '[2]')]/name", "Name 2");
    }

    @Test
    void testMethodCall() {
        assertXPathValue(context, "length('foo')", Integer.valueOf(3));
        // We are just calling a method - prefix is ignored
        assertXPathValue(context, "call:substring('foo', 1, 2)", "o");
        // Invoke a function implemented as a regular method
        assertXPathValue(context, "string(test:getFoo($test))", "4");
        // Note that the prefix is ignored anyway, we are just calling a method
        assertXPathValue(context, "string(call:getFoo($test))", "4");
        // We don't really need to supply a prefix in this case
        assertXPathValue(context, "string(getFoo($test))", "4");
        // Method with two arguments
        assertXPathValue(context, "string(test:setFooAndBar($test, 7, 'biz'))", "foo=7; bar=biz");
    }

    @Test
    void testMethodLookup() {
        final Object[] args = { new TestFunctions() };
        final Function func = functions.getFunction("test", "getFoo", args);
        assertEquals("0", func.invoke(new Context(null), args).toString(), "test:getFoo($test, 1, x)");
    }

    @Test
    void testMethodLookupWithExpressionContext() {
        final Object[] args = { new TestFunctions() };
        final Function func = functions.getFunction("test", "instancePath", args);
        assertEquals("1", func.invoke(new Context(Integer.valueOf(1)), args), "test:instancePath()");
    }

    @Test
    void testMethodLookupWithExpressionContextAndArgument() {
        final Object[] args = { new TestFunctions(), "*" };
        final Function func = functions.getFunction("test", "pathWithSuffix", args);
        assertEquals("1*", func.invoke(new Context(Integer.valueOf(1)), args), "test:pathWithSuffix('*')");
    }

    @Test
    void testNodeSetReturn() {
        assertXPathValueIterator(context, "test:nodeSet()/name", list("Name 1", "Name 2"));
        assertXPathValueIterator(context, "test:nodeSet()", list(testBean.getBeans()[0], testBean.getBeans()[1]));
        assertXPathPointerIterator(context, "test:nodeSet()/name", list("/beans[1]/name", "/beans[2]/name"));
        assertXPathValueAndPointer(context, "test:nodeSet()/name", "Name 1", "/beans[1]/name");
        assertXPathValueAndPointer(context, "test:nodeSet()/@name", "Name 1", "/beans[1]/@name");
        assertEquals(2, ((Number) context.getValue("count(test:nodeSet())")).intValue());
        assertXPathValue(context, "test:nodeSet()", testBean.getBeans()[0]);
    }

    @Test
    void testStaticMethodCall() {
        assertXPathValue(context, "string(test:build(8, 'goober'))", "foo=8; bar=goober");
        // Call a static method using PackageFunctions and class name
        assertXPathValue(context, "string(jxpathtest:TestFunctions.build(8, 'goober'))", "foo=8; bar=goober");
        // Call a static method with a fully qualified class name
        assertXPathValue(context, "string(" + TestFunctions.class.getName() + ".build(8, 'goober'))", "foo=8; bar=goober");
        // Two ClassFunctions are sharing the same prefix.
        // This is TestFunctions2
        assertXPathValue(context, "string(test:increment(8))", "9");
        // See that a NodeSet gets properly converted to a string
        assertXPathValue(context, "test:string(/beans/name)", "Name 1");
    }

    @Test
    void testStaticMethodLookup() {
        final Object[] args = { Integer.valueOf(1), "x" };
        final Function func = functions.getFunction("test", "build", args);
        assertEquals("foo=1; bar=x", func.invoke(new Context(null), args).toString(), "test:build(1, x)");
    }

    @Test
    void testStaticMethodLookupWithConversion() {
        final Object[] args = { "7", Integer.valueOf(1) };
        final Function func = functions.getFunction("test", "build", args);
        assertEquals("foo=7; bar=1", func.invoke(new Context(null), args).toString(), "test:build('7', 1)");
    }

    @Test
    void testStaticMethodLookupWithExpressionContext() {
        final Object[] args = {};
        final Function func = functions.getFunction("test", "path", args);
        assertEquals("1", func.invoke(new Context(Integer.valueOf(1)), args), "test:path()");
    }
}