View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.jxpath.ri.compiler;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
22  
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.List;
26  import java.util.Locale;
27  
28  import org.apache.commons.jxpath.AbstractJXPathTest;
29  import org.apache.commons.jxpath.ClassFunctions;
30  import org.apache.commons.jxpath.ExpressionContext;
31  import org.apache.commons.jxpath.Function;
32  import org.apache.commons.jxpath.FunctionLibrary;
33  import org.apache.commons.jxpath.Functions;
34  import org.apache.commons.jxpath.JXPathContext;
35  import org.apache.commons.jxpath.NodeSet;
36  import org.apache.commons.jxpath.PackageFunctions;
37  import org.apache.commons.jxpath.Pointer;
38  import org.apache.commons.jxpath.TestBean;
39  import org.apache.commons.jxpath.Variables;
40  import org.apache.commons.jxpath.ri.model.NodePointer;
41  import org.apache.commons.jxpath.util.JXPath11CompatibleTypeConverter;
42  import org.apache.commons.jxpath.util.TypeConverter;
43  import org.apache.commons.jxpath.util.TypeUtils;
44  import org.junit.jupiter.api.AfterEach;
45  import org.junit.jupiter.api.BeforeEach;
46  import org.junit.jupiter.api.Test;
47  
48  /**
49   * Test extension functions.
50   */
51  public class ExtensionFunctionTest extends AbstractJXPathTest {
52  
53      private static final class Context implements ExpressionContext {
54  
55          private final Object object;
56  
57          public Context(final Object object) {
58              this.object = object;
59          }
60  
61          @Override
62          public List<Pointer> getContextNodeList() {
63              return null;
64          }
65  
66          @Override
67          public Pointer getContextNodePointer() {
68              return NodePointer.newNodePointer(null, object, Locale.getDefault());
69          }
70  
71          @Override
72          public JXPathContext getJXPathContext() {
73              return null;
74          }
75  
76          @Override
77          public int getPosition() {
78              return 0;
79          }
80      }
81  
82      private Functions functions;
83      private JXPathContext context;
84      private TestBean testBean;
85      private TypeConverter typeConverter;
86  
87      @Override
88      @BeforeEach
89      public void setUp() {
90          if (context == null) {
91              testBean = new TestBean();
92              context = JXPathContext.newContext(testBean);
93              final Variables vars = context.getVariables();
94              vars.declareVariable("test", new TestFunctions(4, "test"));
95              final FunctionLibrary lib = new FunctionLibrary();
96              lib.addFunctions(new ClassFunctions(TestFunctions.class, "test"));
97              lib.addFunctions(new ClassFunctions(TestFunctions2.class, "test"));
98              lib.addFunctions(new PackageFunctions("", "call"));
99              lib.addFunctions(new PackageFunctions("org.apache.commons.jxpath.ri.compiler.", "jxpathtest"));
100             lib.addFunctions(new PackageFunctions("", null));
101             context.setFunctions(lib);
102             context.getVariables().declareVariable("List.class", List.class);
103             context.getVariables().declareVariable("NodeSet.class", NodeSet.class);
104         }
105         functions = new ClassFunctions(TestFunctions.class, "test");
106         typeConverter = TypeUtils.getTypeConverter();
107     }
108 
109     @AfterEach
110     public void tearDown() {
111         TypeUtils.setTypeConverter(typeConverter);
112     }
113 
114     @Test
115     public void testAllocation() {
116         // Allocate new object using the default constructor
117         assertXPathValue(context, "string(test:new())", "foo=0; bar=null");
118         // Allocate new object using PackageFunctions and class name
119         assertXPathValue(context, "string(jxpathtest:TestFunctions.new())", "foo=0; bar=null");
120         // Allocate new object using a fully qualified class name
121         assertXPathValue(context, "string(" + TestFunctions.class.getName() + ".new())", "foo=0; bar=null");
122         // Allocate new object using a custom constructor
123         assertXPathValue(context, "string(test:new(3, 'baz'))", "foo=3; bar=baz");
124         // Allocate new object using a custom constructor - type conversion
125         assertXPathValue(context, "string(test:new('3', 4))", "foo=3; bar=4.0");
126         context.getVariables().declareVariable("A", "baz");
127         assertXPathValue(context, "string(test:new(2, $A, false))", "foo=2; bar=baz");
128     }
129 
130     @Test
131     public void testBCNodeSetHack() {
132         TypeUtils.setTypeConverter(new JXPath11CompatibleTypeConverter());
133         assertXPathValue(context, "test:isInstance(//strings, $List.class)", Boolean.FALSE);
134         assertXPathValue(context, "test:isInstance(//strings, $NodeSet.class)", Boolean.TRUE);
135     }
136 
137     @Test
138     public void testCollectionMethodCall() {
139         final List list = new ArrayList();
140         list.add("foo");
141         context.getVariables().declareVariable("myList", list);
142         assertXPathValue(context, "size($myList)", Integer.valueOf(1));
143         assertXPathValue(context, "size(beans)", Integer.valueOf(2));
144         context.getValue("add($myList, 'hello')");
145         assertEquals(2, list.size(), "After adding an element");
146         final JXPathContext context = JXPathContext.newContext(new ArrayList());
147         assertEquals("0", String.valueOf(context.getValue("size(/)")), "Extension function on root collection");
148     }
149 
150     @Test
151     public void testCollectionReturn() {
152         assertXPathValueIterator(context, "test:collection()/name", list("foo", "bar"));
153         assertXPathPointerIterator(context, "test:collection()/name", list("/.[1]/name", "/.[2]/name"));
154         assertXPathValue(context, "test:collection()/name", "foo");
155         assertXPathValue(context, "test:collection()/@name", "foo");
156         final List list = new ArrayList();
157         list.add("foo");
158         list.add("bar");
159         context.getVariables().declareVariable("list", list);
160         final Object values = context.getValue("test:items($list)");
161         assertInstanceOf(Collection.class, values, "Return type: ");
162         assertEquals(list, new ArrayList((Collection) values), "Return values: ");
163     }
164 
165     @Test
166     public void testConstructorLookup() {
167         final Object[] args = { Integer.valueOf(1), "x" };
168         final Function func = functions.getFunction("test", "new", args);
169         assertEquals("foo=1; bar=x", func.invoke(new Context(null), args).toString(), "test:new(1, x)");
170     }
171 
172     @Test
173     public void testConstructorLookupWithExpressionContext() {
174         final Object[] args = { "baz" };
175         final Function func = functions.getFunction("test", "new", args);
176         assertEquals("foo=1; bar=baz", func.invoke(new Context(Integer.valueOf(1)), args).toString(), "test:new('baz')");
177     }
178 
179     @Test
180     public void testEstablishNodeSetBaseline() {
181         assertXPathValue(context, "test:isInstance(//strings, $List.class)", Boolean.TRUE);
182         assertXPathValue(context, "test:isInstance(//strings, $NodeSet.class)", Boolean.FALSE);
183     }
184 
185     @Test
186     public void testExpressionContext() {
187         // Execute an extension function for each node while searching
188         // The function uses ExpressionContext to get to the current
189         // node.
190         assertXPathValue(context, "//.[test:isMap()]/Key1", "Value 1");
191         // The function gets all
192         // nodes in the context that match the pattern.
193         assertXPathValue(context, "count(//.[test:count(strings) = 3])", Double.valueOf(7));
194         // The function receives a collection of strings
195         // and checks their type for testing purposes
196         assertXPathValue(context, "test:count(//strings)", Integer.valueOf(21));
197         // The function receives a collection of pointers
198         // and checks their type for testing purposes
199         assertXPathValue(context, "test:countPointers(//strings)", Integer.valueOf(21));
200         // The function uses ExpressionContext to get to the current
201         // pointer and returns its path.
202         assertXPathValue(context, "/beans[contains(test:path(), '[2]')]/name", "Name 2");
203     }
204 
205     @Test
206     public void testMethodCall() {
207         assertXPathValue(context, "length('foo')", Integer.valueOf(3));
208         // We are just calling a method - prefix is ignored
209         assertXPathValue(context, "call:substring('foo', 1, 2)", "o");
210         // Invoke a function implemented as a regular method
211         assertXPathValue(context, "string(test:getFoo($test))", "4");
212         // Note that the prefix is ignored anyway, we are just calling a method
213         assertXPathValue(context, "string(call:getFoo($test))", "4");
214         // We don't really need to supply a prefix in this case
215         assertXPathValue(context, "string(getFoo($test))", "4");
216         // Method with two arguments
217         assertXPathValue(context, "string(test:setFooAndBar($test, 7, 'biz'))", "foo=7; bar=biz");
218     }
219 
220     @Test
221     public void testMethodLookup() {
222         final Object[] args = { new TestFunctions() };
223         final Function func = functions.getFunction("test", "getFoo", args);
224         assertEquals("0", func.invoke(new Context(null), args).toString(), "test:getFoo($test, 1, x)");
225     }
226 
227     @Test
228     public void testMethodLookupWithExpressionContext() {
229         final Object[] args = { new TestFunctions() };
230         final Function func = functions.getFunction("test", "instancePath", args);
231         assertEquals("1", func.invoke(new Context(Integer.valueOf(1)), args), "test:instancePath()");
232     }
233 
234     @Test
235     public void testMethodLookupWithExpressionContextAndArgument() {
236         final Object[] args = { new TestFunctions(), "*" };
237         final Function func = functions.getFunction("test", "pathWithSuffix", args);
238         assertEquals("1*", func.invoke(new Context(Integer.valueOf(1)), args), "test:pathWithSuffix('*')");
239     }
240 
241     @Test
242     public void testNodeSetReturn() {
243         assertXPathValueIterator(context, "test:nodeSet()/name", list("Name 1", "Name 2"));
244         assertXPathValueIterator(context, "test:nodeSet()", list(testBean.getBeans()[0], testBean.getBeans()[1]));
245         assertXPathPointerIterator(context, "test:nodeSet()/name", list("/beans[1]/name", "/beans[2]/name"));
246         assertXPathValueAndPointer(context, "test:nodeSet()/name", "Name 1", "/beans[1]/name");
247         assertXPathValueAndPointer(context, "test:nodeSet()/@name", "Name 1", "/beans[1]/@name");
248         assertEquals(2, ((Number) context.getValue("count(test:nodeSet())")).intValue());
249         assertXPathValue(context, "test:nodeSet()", testBean.getBeans()[0]);
250     }
251 
252     @Test
253     public void testStaticMethodCall() {
254         assertXPathValue(context, "string(test:build(8, 'goober'))", "foo=8; bar=goober");
255         // Call a static method using PackageFunctions and class name
256         assertXPathValue(context, "string(jxpathtest:TestFunctions.build(8, 'goober'))", "foo=8; bar=goober");
257         // Call a static method with a fully qualified class name
258         assertXPathValue(context, "string(" + TestFunctions.class.getName() + ".build(8, 'goober'))", "foo=8; bar=goober");
259         // Two ClassFunctions are sharing the same prefix.
260         // This is TestFunctions2
261         assertXPathValue(context, "string(test:increment(8))", "9");
262         // See that a NodeSet gets properly converted to a string
263         assertXPathValue(context, "test:string(/beans/name)", "Name 1");
264     }
265 
266     @Test
267     public void testStaticMethodLookup() {
268         final Object[] args = { Integer.valueOf(1), "x" };
269         final Function func = functions.getFunction("test", "build", args);
270         assertEquals("foo=1; bar=x", func.invoke(new Context(null), args).toString(), "test:build(1, x)");
271     }
272 
273     @Test
274     public void testStaticMethodLookupWithConversion() {
275         final Object[] args = { "7", Integer.valueOf(1) };
276         final Function func = functions.getFunction("test", "build", args);
277         assertEquals("foo=7; bar=1", func.invoke(new Context(null), args).toString(), "test:build('7', 1)");
278     }
279 
280     @Test
281     public void testStaticMethodLookupWithExpressionContext() {
282         final Object[] args = {};
283         final Function func = functions.getFunction("test", "path", args);
284         assertEquals("1", func.invoke(new Context(Integer.valueOf(1)), args), "test:path()");
285     }
286 }