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.model;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertNotEquals;
22  import static org.junit.jupiter.api.Assertions.assertThrows;
23  
24  import org.apache.commons.jxpath.AbstractFactory;
25  import org.apache.commons.jxpath.AbstractJXPathTest;
26  import org.apache.commons.jxpath.JXPathContext;
27  import org.apache.commons.jxpath.JXPathException;
28  import org.apache.commons.jxpath.Pointer;
29  import org.apache.commons.jxpath.Variables;
30  import org.apache.commons.jxpath.xml.DocumentContainer;
31  import org.junit.jupiter.api.BeforeEach;
32  import org.junit.jupiter.api.Test;
33  
34  /**
35   * Abstract superclass for pure XPath 1.0. Subclasses apply the same XPaths to contexts using different models: DOM, JDOM etc.
36   */
37  public abstract class AbstractXMLModelTest extends AbstractJXPathTest {
38  
39      protected JXPathContext context;
40  
41      protected void assertXMLSignature(final JXPathContext context, final String path, final String signature, final boolean elements, final boolean attributes,
42              final boolean text, final boolean pi) {
43          final Object node = context.getPointer(path).getNode();
44          final String sig = getXMLSignature(node, elements, attributes, text, pi);
45          assertEquals(signature, sig, "XML Signature mismatch: ");
46      }
47  
48      protected JXPathContext createContext() {
49          final JXPathContext context = JXPathContext.newContext(createDocumentContainer());
50          context.setFactory(getAbstractFactory());
51          context.registerNamespace("product", "productNS");
52          return context;
53      }
54  
55      protected DocumentContainer createDocumentContainer() {
56          return new DocumentContainer(AbstractJXPathTest.class.getResource("Vendor.xml"), getModel());
57      }
58  
59      protected abstract AbstractFactory getAbstractFactory();
60  
61      protected abstract String getModel();
62  
63      /**
64       * An XML signature is used to determine if we have the right result after a modification of XML by JXPath. It is basically a piece of simplified XML.
65       */
66      protected abstract String getXMLSignature(Object node, boolean elements, boolean attributes, boolean text, boolean pi);
67  
68      @Override
69      @BeforeEach
70      public void setUp() {
71          if (context == null) {
72              final DocumentContainer docCtr = createDocumentContainer();
73              context = createContext();
74              final Variables vars = context.getVariables();
75              vars.declareVariable("document", docCtr.getValue());
76              vars.declareVariable("container", docCtr);
77              vars.declareVariable("element", context.getPointer("vendor/location/address/street").getNode());
78          }
79      }
80  
81      @Test
82      public void testAxisAncestor() {
83          // ancestor::
84          assertXPathValue(context, "vendor/product/price:sale/saleEnds/" + "ancestor::price:sale/saleEnds", "never");
85          // ancestor:: with a wildcard
86          assertXPathValue(context, "vendor/product/price:sale/saleEnds/ancestor::price:*" + "/saleEnds", "never");
87      }
88  
89      @Test
90      public void testAxisAncestorOrSelf() {
91          // ancestor-or-self::
92          assertXPathValue(context, "vendor/product/price:sale/" + "ancestor-or-self::price:sale/saleEnds", "never");
93      }
94  
95      @Test
96      public void testAxisAttribute() {
97          // attribute::
98          assertXPathValue(context, "vendor/location/@id", "100");
99          // attribute:: produces the correct pointer
100         assertXPathPointer(context, "vendor/location/@id", "/vendor[1]/location[1]/@id");
101         // iterate over attributes
102         assertXPathValueIterator(context, "vendor/location/@id", list("100", "101"));
103         // Using different prefixes for the same namespace
104         assertXPathValue(context, "vendor/product/price:amount/@price:discount", "10%");
105         // namespace uri for an attribute
106         assertXPathValue(context, "namespace-uri(vendor/product/price:amount/@price:discount)", "priceNS");
107         // local name of an attribute
108         assertXPathValue(context, "local-name(vendor/product/price:amount/@price:discount)", "discount");
109         // name for an attribute
110         assertXPathValue(context, "name(vendor/product/price:amount/@price:discount)", "price:discount");
111         // attribute:: with the default namespace
112         assertXPathValue(context, "vendor/product/price:amount/@discount", "20%");
113         // namespace uri of an attribute with the default namespace
114         assertXPathValue(context, "namespace-uri(vendor/product/price:amount/@discount)", "");
115         // local name of an attribute with the default namespace
116         assertXPathValue(context, "local-name(vendor/product/price:amount/@discount)", "discount");
117         // name of an attribute with the default namespace
118         assertXPathValue(context, "name(vendor/product/price:amount/@discount)", "discount");
119         // attribute:: with a namespace and wildcard
120         assertXPathValueIterator(context, "vendor/product/price:amount/@price:*", list("10%"));
121         // attribute:: with a wildcard
122         assertXPathValueIterator(context, "vendor/location[1]/@*", set("100", "", "local"));
123         // attribute:: with default namespace and wildcard
124         assertXPathValueIterator(context, "vendor/product/price:amount/@*",
125                 // use a set because DOM returns attrs sorted by name, JDOM by occurrence order:
126                 set("10%", "20%"));
127         // attribute::node()
128         assertXPathValueIterator(context, "vendor/product/price:amount/attribute::node()",
129                 // use a set because DOM returns attrs sorted by name, JDOM by occurrence order:
130                 set("10%", "20%"));
131         // attribute:: select non-ns'd attributes only
132         assertXPathValueIterator(context, "vendor/product/price:amount/@*[namespace-uri() = '']", list("20%"));
133         // Empty attribute
134         assertXPathValue(context, "vendor/location/@manager", "");
135         // Missing attribute
136         assertXPathValueLenient(context, "vendor/location/@missing", null);
137         // Missing attribute with namespace
138         assertXPathValueLenient(context, "vendor/location/@miss:missing", null);
139         // Using attribute in a predicate
140         assertXPathValue(context, "vendor/location[@id='101']//street", "Tangerine Drive");
141         assertXPathValueIterator(context, "/vendor/location[1]/@*[name()!= 'manager']", list("100", "local"));
142     }
143 
144     @Test
145     public void testAxisChild() {
146         assertXPathValue(context, "vendor/location/address/street", "Orchard Road");
147         // child:: - first child does not match, need to search
148         assertXPathValue(context, "vendor/location/address/city", "Fruit Market");
149         // local-name(qualified)
150         assertXPathValue(context, "local-name(vendor/product/price:amount)", "amount");
151         // local-name(non-qualified)
152         assertXPathValue(context, "local-name(vendor/location)", "location");
153         // name (qualified)
154         assertXPathValue(context, "name(vendor/product/price:amount)", "value:amount");
155         // name (non-qualified)
156         assertXPathValue(context, "name(vendor/location)", "location");
157         // namespace-uri (qualified)
158         assertXPathValue(context, "namespace-uri(vendor/product/price:amount)", "priceNS");
159         // default namespace does not affect search
160         assertXPathValue(context, "vendor/product/prix", "934.99");
161         assertXPathValue(context, "/vendor/contact[@name='jim']", "Jim");
162         assertThrows(JXPathException.class, () -> {
163             context.setLenient(false);
164             context.getValue("/vendor/contact[@name='jane']");
165         }, "No such value: /vendor/contact[@name='jim']");
166         assertThrows(JXPathException.class, () -> {
167             context.setLenient(false);
168             context.getValue("/vendor/contact[@name='jane']/*");
169         }, "No such value: /vendor/contact[@name='jane']/*");
170         // child:: with a wildcard
171         assertXPathValue(context, "count(vendor/product/price:*)", Double.valueOf(2));
172         // child:: with the default namespace
173         assertXPathValue(context, "count(vendor/product/*)", Double.valueOf(4));
174         // child:: with a qualified name
175         assertXPathValue(context, "vendor/product/price:amount", "45.95");
176         // null default namespace
177         context.registerNamespace("x", "temp");
178         assertXPathValue(context, "vendor/x:pos//number", "109");
179     }
180 
181     @Test
182     public void testAxisChildIndexPredicate() {
183         assertXPathValue(context, "vendor/location[2]/address/street", "Tangerine Drive");
184     }
185 
186     @Test
187     public void testAxisDescendant() {
188         // descendant::
189         assertXPathValue(context, "//street", "Orchard Road");
190         // descendent:: with a namespace and wildcard
191         assertXPathValue(context, "count(//price:*)", Double.valueOf(2));
192         assertXPathValueIterator(context, "vendor//saleEnds", list("never"));
193         assertXPathValueIterator(context, "vendor//promotion", list(""));
194         assertXPathValueIterator(context, "vendor//saleEnds[../@stores = 'all']", list("never"));
195         assertXPathValueIterator(context, "vendor//promotion[../@stores = 'all']", list(""));
196     }
197 //
198 //    @Test
199 //    public void testAxisDescendantDocumentOrder() {
200 //        Iterator iter = context.iteratePointers("//*");
201 //        while (iter.hasNext()) {
202 //            System.err.println(iter.next());
203 //        }
204 //    }
205 
206     @Test
207     public void testAxisFollowing() {
208         assertXPathValueIterator(context, "vendor/contact/following::location//street", list("Orchard Road", "Tangerine Drive"));
209         // following:: with a namespace
210         assertXPathValue(context, "//location/following::price:sale/saleEnds", "never");
211         assertXPathPointer(context, "//location[2]/following::node()[2]", "/vendor[1]/product[1]");
212     }
213 
214     @Test
215     public void testAxisFollowingSibling() {
216         // following-sibling::
217         assertXPathValue(context, "vendor/location[.//employeeCount = 10]/" + "following-sibling::location//street", "Tangerine Drive");
218         // following-sibling:: produces the correct pointer
219         assertXPathPointer(context, "vendor/location[.//employeeCount = 10]/" + "following-sibling::location//street",
220                 "/vendor[1]/location[2]/address[1]/street[1]");
221     }
222 
223     @Test
224     public void testAxisNamespace() {
225         // namespace::
226         assertXPathValueAndPointer(context, "vendor/product/prix/namespace::price", "priceNS", "/vendor[1]/product[1]/prix[1]/namespace::price");
227         // namespace::*
228         assertXPathValue(context, "count(vendor/product/namespace::*)", Double.valueOf(3));
229         // name of namespace
230         assertXPathValue(context, "name(vendor/product/prix/namespace::price)", "price");
231         // local name of namespace
232         assertXPathValue(context, "local-name(vendor/product/prix/namespace::price)", "price");
233     }
234 
235     @Test
236     public void testAxisParent() {
237         // parent::
238         assertXPathPointer(context, "//street/..", "/vendor[1]/location[1]/address[1]");
239         // parent:: (note reverse document order)
240         assertXPathPointerIterator(context, "//street/..", list("/vendor[1]/location[2]/address[1]", "/vendor[1]/location[1]/address[1]"));
241         // parent:: with a namespace and wildcard
242         assertXPathValue(context, "vendor/product/price:sale/saleEnds/parent::price:*" + "/saleEnds", "never");
243     }
244 
245     @Test
246     public void testAxisPreceding() {
247         // preceding::
248         assertXPathPointer(context, "//location[2]/preceding-sibling::location//street", "/vendor[1]/location[1]/address[1]/street[1]");
249         assertXPathPointer(context, "//location[2]/preceding::*[1]", "/vendor[1]/location[1]/employeeCount[1]");
250         assertXPathPointer(context, "//location[2]/preceding::node()[3]", "/vendor[1]/location[1]/employeeCount[1]/text()[1]");
251         assertXPathPointer(context, "//location[2]/preceding::node()[4]", "/vendor[1]/location[1]/employeeCount[1]");
252     }
253 
254     @Test
255     public void testAxisPrecedingSibling() {
256         // preceding-sibling:: produces the correct pointer
257         assertXPathPointer(context, "//location[2]/preceding-sibling::location//street", "/vendor[1]/location[1]/address[1]/street[1]");
258     }
259 
260     @Test
261     public void testAxisSelf() {
262         // self:: with a namespace
263         assertXPathValue(context, "//price:sale/self::price:sale/saleEnds", "never");
264         // self:: with an unmatching name
265         assertXPathValueLenient(context, "//price:sale/self::x/saleEnds", null);
266     }
267 
268     @Test
269     public void testBooleanFunction() {
270         assertXPathValue(context, "boolean(vendor//saleEnds[../@stores = 'all'])", Boolean.TRUE);
271         assertXPathValue(context, "boolean(vendor//promotion[../@stores = 'all'])", Boolean.TRUE);
272         assertXPathValue(context, "boolean(vendor//promotion[../@stores = 'some'])", Boolean.FALSE);
273     }
274 
275     @Test
276     public void testContainer() {
277         assertXPathValue(context, "$container/vendor//street", "Orchard Road");
278         assertXPathValue(context, "$container//street", "Orchard Road");
279         assertXPathPointer(context, "$container//street", "$container/vendor[1]/location[1]/address[1]/street[1]");
280         // Conversion to number
281         assertXPathValue(context, "number(vendor/location/employeeCount)", Double.valueOf(10));
282     }
283 
284     /**
285      * Test JXPathContext.createPath() with various arguments
286      */
287     @Test
288     public void testCreatePath() {
289         // Create a DOM element
290         assertXPathCreatePath(context, "/vendor[1]/location[3]", "", "/vendor[1]/location[3]");
291         // Create a DOM element with contents
292         assertXPathCreatePath(context, "/vendor[1]/location[3]/address/street", "", "/vendor[1]/location[3]/address[1]/street[1]");
293         // Create a DOM attribute
294         assertXPathCreatePath(context, "/vendor[1]/location[2]/@manager", "", "/vendor[1]/location[2]/@manager");
295         assertXPathCreatePath(context, "/vendor[1]/location[1]/@name", "local", "/vendor[1]/location[1]/@name");
296         assertXPathCreatePathAndSetValue(context, "/vendor[1]/location[4]/@manager", "", "/vendor[1]/location[4]/@manager");
297         context.registerNamespace("price", "priceNS");
298         // Create a DOM element
299         assertXPathCreatePath(context, "/vendor[1]/price:foo/price:bar", "", "/vendor[1]/price:foo[1]/price:bar[1]");
300     }
301 
302     /**
303      * Test JXPath.createPathAndSetValue() with various arguments
304      */
305     @Test
306     public void testCreatePathAndSetValue() {
307         // Create a XML element
308         assertXPathCreatePathAndSetValue(context, "vendor/location[3]", "", "/vendor[1]/location[3]");
309         // Create a DOM element with contents
310         assertXPathCreatePathAndSetValue(context, "vendor/location[3]/address/street", "Lemon Circle", "/vendor[1]/location[3]/address[1]/street[1]");
311         // Create an attribute
312         assertXPathCreatePathAndSetValue(context, "vendor/location[2]/@manager", "John Doe", "/vendor[1]/location[2]/@manager");
313         assertXPathCreatePathAndSetValue(context, "vendor/location[1]/@manager", "John Doe", "/vendor[1]/location[1]/@manager");
314         assertXPathCreatePathAndSetValue(context, "/vendor[1]/location[4]/@manager", "James Dow", "/vendor[1]/location[4]/@manager");
315         assertXPathCreatePathAndSetValue(context, "vendor/product/product:name/attribute::price:language", "English",
316                 "/vendor[1]/product[1]/product:name[1]/@price:language");
317         context.registerNamespace("price", "priceNS");
318         // Create a DOM element
319         assertXPathCreatePathAndSetValue(context, "/vendor[1]/price:foo/price:bar", "123.20", "/vendor[1]/price:foo[1]/price:bar[1]");
320     }
321 
322     @Test
323     public void testDocument() {
324         assertXPathValue(context, "$document/vendor/location[1]//street", "Orchard Road");
325         assertXPathPointer(context, "$document/vendor/location[1]//street", "$document/vendor[1]/location[1]/address[1]/street[1]");
326         assertXPathValue(context, "$document/vendor//street", "Orchard Road");
327     }
328 
329     @Test
330     public void testDocumentOrder() {
331         assertDocumentOrder(context, "vendor/location", "vendor/location/address/street", -1);
332         assertDocumentOrder(context, "vendor/location[@id = '100']", "vendor/location[@id = '101']", -1);
333         assertDocumentOrder(context, "vendor//price:amount", "vendor/location", 1);
334     }
335 
336     @Test
337     public void testElementInVariable() {
338         assertXPathValue(context, "$element", "Orchard Road");
339     }
340 
341     @Test
342     public void testFunctionsLastAndPosition() {
343         assertXPathPointer(context, "vendor//location[last()]", "/vendor[1]/location[2]");
344     }
345 
346     @Test
347     public void testID() {
348         context.setIdentityManager((context, id) -> {
349             NodePointer ptr = (NodePointer) context.getPointer("/");
350             ptr = ptr.getValuePointer(); // Unwrap the container
351             return ptr.getPointerByID(context, id);
352         });
353         assertXPathValueAndPointer(context, "id(101)//street", "Tangerine Drive", "id('101')/address[1]/street[1]");
354         assertXPathPointerLenient(context, "id(105)/address/street", "id(105)/address/street");
355     }
356 
357     @Test
358     public void testLang() {
359         // xml:lang built-in attribute
360         assertXPathValue(context, "//product/prix/@xml:lang", "fr");
361         // lang() used the built-in xml:lang attribute
362         assertXPathValue(context, "//product/prix[lang('fr')]", "934.99");
363         // Default language
364         assertXPathValue(context, "//product/price:sale[lang('en')]/saleEnds", "never");
365     }
366 
367     @Test
368     public void testNamespaceMapping() {
369         context.registerNamespace("rate", "priceNS");
370         context.registerNamespace("goods", "productNS");
371         assertEquals("priceNS", context.getNamespaceURI("price"), "Context node namespace resolution");
372         assertEquals("priceNS", context.getNamespaceURI("rate"), "Registered namespace resolution");
373         // child:: with a namespace and wildcard
374         assertXPathValue(context, "count(vendor/product/rate:*)", Double.valueOf(2));
375         assertXPathValue(context, "vendor[1]/product[1]/rate:amount[1]/@rate:discount", "10%");
376         assertXPathValue(context, "vendor[1]/product[1]/rate:amount[1]/@price:discount", "10%");
377         assertXPathValue(context, "vendor[1]/product[1]/price:amount[1]/@rate:discount", "10%");
378         assertXPathValue(context, "vendor[1]/product[1]/price:amount[1]/@price:discount", "10%");
379         // Preference for externally registered namespace prefix
380         assertXPathValueAndPointer(context, "//product:name", "Box of oranges", "/vendor[1]/product[1]/goods:name[1]");
381         // Same, but with a child context
382         final JXPathContext childCtx = JXPathContext.newContext(context, context.getContextBean());
383         assertXPathValueAndPointer(childCtx, "//product:name", "Box of oranges", "/vendor[1]/product[1]/goods:name[1]");
384         // Same, but with a relative context
385         final JXPathContext relativeCtx = context.getRelativeContext(context.getPointer("/vendor"));
386         assertXPathValueAndPointer(relativeCtx, "product/product:name", "Box of oranges", "/vendor[1]/product[1]/goods:name[1]");
387     }
388 
389     @Test
390     public void testNodes() {
391         final Pointer pointer = context.getPointer("/vendor[1]/contact[1]");
392         assertNotEquals(pointer.getNode(), pointer.getValue());
393     }
394 
395     @Test
396     public void testNodeTypeComment() {
397         // comment()
398         assertXPathValue(context, "//product/comment()", "We are not buying this product, ever");
399     }
400 
401     @Test
402     public void testNodeTypeProcessingInstruction() {
403         // processing-instruction() without an argument
404         assertXPathValue(context, "//product/processing-instruction()", "do not show anybody");
405         // processing-instruction() with an argument
406         assertXPathValue(context, "//product/processing-instruction('report')", "average only");
407         // processing-instruction() pointer without an argument
408         assertXPathPointer(context, "//product/processing-instruction('report')", "/vendor[1]/product[1]/processing-instruction('report')[1]");
409         // processing-instruction name
410         assertXPathValue(context, "name(//product/processing-instruction()[1])", "security");
411     }
412 
413     @Test
414     public void testNodeTypeText() {
415         // text()
416         // Note that this is questionable as the XPath spec tells us "." is short for self::node() and text() is by definition _not_ a node:
417         assertXPathValue(context, "//product/text()[. != '']", "We love this product.");
418         // text() pointer
419         assertXPathPointer(context, "//product/text()", "/vendor[1]/product[1]/text()[1]");
420     }
421 
422     /**
423      * Test JXPathContext.removePath() with various arguments
424      */
425     @Test
426     public void testRemovePath() {
427         // Remove XML nodes
428         context.removePath("vendor/location[@id = '101']//street/text()");
429         assertEquals("", context.getValue("vendor/location[@id = '101']//street"), "Remove DOM text");
430         context.removePath("vendor/location[@id = '101']//street");
431         assertEquals(Double.valueOf(0), context.getValue("count(vendor/location[@id = '101']//street)"), "Remove DOM element");
432         context.removePath("vendor/location[@id = '100']/@name");
433         assertEquals(Double.valueOf(0), context.getValue("count(vendor/location[@id = '100']/@name)"), "Remove DOM attribute");
434     }
435 
436     @Test
437     public void testSetValue() {
438         assertXPathSetValue(context, "vendor/location[@id = '100']", "New Text");
439         assertXMLSignature(context, "vendor/location[@id = '100']", "<E>New Text</E>", false, false, true, false);
440         assertXPathSetValue(context, "vendor/location[@id = '101']", "Replacement Text");
441         assertXMLSignature(context, "vendor/location[@id = '101']", "<E>Replacement Text</E>", false, false, true, false);
442     }
443 
444     @Test
445     public void testTypeConversions() {
446         // Implicit conversion to number
447         assertXPathValue(context, "vendor/location/employeeCount + 1", Double.valueOf(11));
448         // Implicit conversion to boolean
449         assertXPathValue(context, "vendor/location/employeeCount and true()", Boolean.TRUE);
450     }
451 
452     @Test
453     public void testUnion() {
454         assertXPathValue(context, "/vendor[1]/contact[1] | /vendor[1]/contact[4]", "John");
455         assertXPathValue(context, "/vendor[1]/contact[4] | /vendor[1]/contact[1]", "John");
456     }
457 }