Skip to content

Commit a378ee6

Browse files
authored
Merge pull request #2717 from wiremock/xpath-helper-single-item-bug
Fixes #2696 - xPath helper returns error when result is primitive value
2 parents 3f02154 + 7bdcda8 commit a378ee6

File tree

5 files changed

+229
-122
lines changed

5 files changed

+229
-122
lines changed

src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlDocument.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2020-2021 Thomas Akehurst
2+
* Copyright (C) 2020-2024 Thomas Akehurst
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,21 +15,19 @@
1515
*/
1616
package com.github.tomakehurst.wiremock.common.xml;
1717

18-
import static javax.xml.xpath.XPathConstants.NODESET;
19-
2018
import com.github.tomakehurst.wiremock.common.ListOrSingle;
2119
import java.util.HashMap;
2220
import java.util.Map;
2321
import javax.xml.XMLConstants;
2422
import javax.xml.namespace.NamespaceContext;
2523
import javax.xml.transform.dom.DOMSource;
2624
import javax.xml.xpath.XPath;
25+
import javax.xml.xpath.XPathEvaluationResult;
2726
import javax.xml.xpath.XPathExpressionException;
2827
import org.w3c.dom.Document;
29-
import org.w3c.dom.NodeList;
3028
import org.xmlunit.util.Convert;
3129

32-
public class XmlDocument extends XmlNode {
30+
public class XmlDocument extends XmlDomNode {
3331

3432
private final Document document;
3533

@@ -47,20 +45,19 @@ public ListOrSingle<XmlNode> findNodes(String xPathExpression, Map<String, Strin
4745
final XPath xPath = XPATH_CACHE.get();
4846
xPath.reset();
4947

50-
NodeList nodeSet;
48+
XPathEvaluationResult<?> xPathEvaluationResult;
5149
if (namespaces != null) {
5250
Map<String, String> fullNamespaces = addStandardNamespaces(namespaces);
5351
NamespaceContext namespaceContext = Convert.toNamespaceContext(fullNamespaces);
5452
xPath.setNamespaceContext(namespaceContext);
55-
nodeSet =
56-
(NodeList)
57-
xPath.evaluate(
58-
xPathExpression, Convert.toInputSource(new DOMSource(document)), NODESET);
53+
xPathEvaluationResult =
54+
xPath.evaluateExpression(
55+
xPathExpression, Convert.toInputSource(new DOMSource(document)));
5956
} else {
60-
nodeSet = (NodeList) xPath.evaluate(xPathExpression, document, NODESET);
57+
xPathEvaluationResult = xPath.evaluateExpression(xPathExpression, document);
6158
}
6259

63-
return toListOrSingle(nodeSet);
60+
return toListOrSingle(xPathEvaluationResult);
6461
} catch (XPathExpressionException e) {
6562
throw XPathException.fromXPathException(e);
6663
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright (C) 2024 Thomas Akehurst
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.tomakehurst.wiremock.common.xml;
17+
18+
import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
19+
20+
import java.io.StringWriter;
21+
import java.lang.reflect.Constructor;
22+
import java.lang.reflect.InvocationTargetException;
23+
import java.util.Collections;
24+
import java.util.HashMap;
25+
import java.util.Map;
26+
import javax.xml.transform.Source;
27+
import javax.xml.transform.Transformer;
28+
import javax.xml.transform.dom.DOMSource;
29+
import javax.xml.transform.sax.SAXSource;
30+
import javax.xml.transform.stream.StreamResult;
31+
import org.w3c.dom.NamedNodeMap;
32+
import org.w3c.dom.Node;
33+
import org.xml.sax.XMLReader;
34+
35+
public class XmlDomNode extends XmlNode {
36+
37+
private final Node domNode;
38+
private final Map<String, String> attributes;
39+
40+
public XmlDomNode(Node domNode) {
41+
this.domNode = domNode;
42+
attributes =
43+
domNode.hasAttributes()
44+
? convertAttributeMap(domNode.getAttributes())
45+
: Collections.emptyMap();
46+
}
47+
48+
private static Map<String, String> convertAttributeMap(NamedNodeMap namedNodeMap) {
49+
Map<String, String> map = new HashMap<>();
50+
for (int i = 0; i < namedNodeMap.getLength(); i++) {
51+
Node node = namedNodeMap.item(i);
52+
map.put(node.getNodeName(), node.getNodeValue());
53+
}
54+
55+
return Collections.unmodifiableMap(map);
56+
}
57+
58+
public Map<String, String> getAttributes() {
59+
return attributes;
60+
}
61+
62+
public String getName() {
63+
return domNode.getNodeName();
64+
}
65+
66+
public String getText() {
67+
return domNode.getTextContent();
68+
}
69+
70+
@Override
71+
public String toString() {
72+
switch (domNode.getNodeType()) {
73+
case Node.TEXT_NODE:
74+
case Node.ATTRIBUTE_NODE:
75+
return domNode.getTextContent();
76+
case Node.DOCUMENT_NODE:
77+
case Node.ELEMENT_NODE:
78+
return render();
79+
default:
80+
return domNode.toString();
81+
}
82+
}
83+
84+
private String render() {
85+
try {
86+
Transformer transformer = TRANSFORMER_CACHE.get();
87+
StreamResult result = new StreamResult(new StringWriter());
88+
Source source = getSourceForTransform(domNode);
89+
transformer.transform(source, result);
90+
return result.getWriter().toString();
91+
} catch (Exception e) {
92+
return throwUnchecked(e, String.class);
93+
}
94+
}
95+
96+
private static final Class<XMLReader> DOM2SAX_XMLREADER_CLASS = getDom2SaxAvailability();
97+
98+
@SuppressWarnings("unchecked")
99+
private static Class<XMLReader> getDom2SaxAvailability() {
100+
try {
101+
return (Class<XMLReader>)
102+
Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.DOM2SAX");
103+
} catch (ClassNotFoundException e) {
104+
return null;
105+
}
106+
}
107+
108+
// This nasty little hack attempts to ensure no exception is thrown when attempting to print an
109+
// XML node with
110+
// unbound namespace prefixes (which can happen when you've selected an element via XPath whose
111+
// namespaces are declared in a parent element).
112+
// For some reason Transformer is happy to do this with a SAX source, but not a DOM source.
113+
private static Source getSourceForTransform(Node node) {
114+
if (DOM2SAX_XMLREADER_CLASS != null) {
115+
try {
116+
Constructor<XMLReader> constructor = DOM2SAX_XMLREADER_CLASS.getConstructor(Node.class);
117+
XMLReader dom2SAX = constructor.newInstance(node);
118+
SAXSource saxSource = new SAXSource();
119+
saxSource.setXMLReader(dom2SAX);
120+
return saxSource;
121+
} catch (NoSuchMethodException
122+
| InstantiationException
123+
| IllegalAccessException
124+
| InvocationTargetException e) {
125+
return new DOMSource(node);
126+
}
127+
}
128+
129+
return new DOMSource(node);
130+
}
131+
}

src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlNode.java

Lines changed: 20 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,16 @@
2020
import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION;
2121

2222
import com.github.tomakehurst.wiremock.common.ListOrSingle;
23-
import java.io.StringWriter;
24-
import java.lang.reflect.Constructor;
25-
import java.lang.reflect.InvocationTargetException;
26-
import java.util.Collections;
27-
import java.util.HashMap;
2823
import java.util.Map;
29-
import javax.xml.transform.*;
30-
import javax.xml.transform.dom.DOMSource;
31-
import javax.xml.transform.sax.SAXSource;
32-
import javax.xml.transform.stream.StreamResult;
24+
import javax.xml.transform.Transformer;
25+
import javax.xml.transform.TransformerConfigurationException;
26+
import javax.xml.transform.TransformerFactory;
3327
import javax.xml.xpath.XPath;
28+
import javax.xml.xpath.XPathEvaluationResult;
3429
import javax.xml.xpath.XPathFactory;
35-
import org.w3c.dom.NamedNodeMap;
3630
import org.w3c.dom.Node;
37-
import org.w3c.dom.NodeList;
38-
import org.xml.sax.XMLReader;
3931

40-
public class XmlNode {
32+
public abstract class XmlNode {
4133

4234
protected static final ThreadLocal<XPath> XPATH_CACHE =
4335
ThreadLocal.withInitial(
@@ -74,106 +66,25 @@ public class XmlNode {
7466
}
7567
});
7668

77-
private static final Class<XMLReader> DOM2SAX_XMLREADER_CLASS = getDom2SaxAvailability();
69+
public abstract Map<String, String> getAttributes();
7870

79-
private static Class<XMLReader> getDom2SaxAvailability() {
80-
try {
81-
return (Class<XMLReader>)
82-
Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.DOM2SAX");
83-
} catch (ClassNotFoundException e) {
84-
return null;
85-
}
86-
}
87-
88-
private final Node domNode;
89-
private final Map<String, String> attributes;
90-
91-
public XmlNode(Node domNode) {
92-
this.domNode = domNode;
93-
attributes =
94-
domNode.hasAttributes()
95-
? convertAttributeMap(domNode.getAttributes())
96-
: Collections.emptyMap();
97-
}
98-
99-
private static Map<String, String> convertAttributeMap(NamedNodeMap namedNodeMap) {
100-
Map<String, String> map = new HashMap<>();
101-
for (int i = 0; i < namedNodeMap.getLength(); i++) {
102-
Node node = namedNodeMap.item(i);
103-
map.put(node.getNodeName(), node.getNodeValue());
104-
}
105-
106-
return Collections.unmodifiableMap(map);
107-
}
108-
109-
public Map<String, String> getAttributes() {
110-
return attributes;
111-
}
112-
113-
protected static ListOrSingle<XmlNode> toListOrSingle(NodeList nodeList) {
114-
ListOrSingle<XmlNode> nodes = new ListOrSingle<>();
115-
for (int i = 0; i < nodeList.getLength(); i++) {
116-
nodes.add(new XmlNode(nodeList.item(i)));
117-
}
71+
@SuppressWarnings("unchecked")
72+
protected static ListOrSingle<XmlNode> toListOrSingle(XPathEvaluationResult<?> evaluationResult) {
73+
ListOrSingle<XmlNode> xmlNodes = new ListOrSingle<>();
11874

119-
return nodes;
120-
}
121-
122-
public String getName() {
123-
return domNode.getNodeName();
124-
}
125-
126-
public String getText() {
127-
return domNode.getTextContent();
128-
}
129-
130-
@Override
131-
public String toString() {
132-
switch (domNode.getNodeType()) {
133-
case Node.TEXT_NODE:
134-
case Node.ATTRIBUTE_NODE:
135-
return domNode.getTextContent();
136-
case Node.DOCUMENT_NODE:
137-
case Node.ELEMENT_NODE:
138-
return render();
75+
switch (evaluationResult.type()) {
76+
case NODESET:
77+
Iterable<Node> nodes = (Iterable<Node>) evaluationResult.value();
78+
nodes.forEach(node -> xmlNodes.add(new XmlDomNode(node)));
79+
break;
80+
case NODE:
81+
xmlNodes.add(new XmlDomNode((Node) evaluationResult.value()));
82+
break;
13983
default:
140-
return domNode.toString();
141-
}
142-
}
143-
144-
private String render() {
145-
try {
146-
Transformer transformer = TRANSFORMER_CACHE.get();
147-
StreamResult result = new StreamResult(new StringWriter());
148-
Source source = getSourceForTransform(domNode);
149-
transformer.transform(source, result);
150-
return result.getWriter().toString();
151-
} catch (Exception e) {
152-
return throwUnchecked(e, String.class);
153-
}
154-
}
155-
156-
// This nasty little hack attempts to ensure no exception is thrown when attempting to print an
157-
// XML node with
158-
// unbound namespace prefixes (which can happen when you've selected an element via XPath whose
159-
// namespaces are declared in a parent element).
160-
// For some reason Transformer is happy to do this with a SAX source, but not a DOM source.
161-
private static Source getSourceForTransform(Node node) {
162-
if (DOM2SAX_XMLREADER_CLASS != null) {
163-
try {
164-
Constructor<XMLReader> constructor = DOM2SAX_XMLREADER_CLASS.getConstructor(Node.class);
165-
XMLReader dom2SAX = constructor.newInstance(node);
166-
SAXSource saxSource = new SAXSource();
167-
saxSource.setXMLReader(dom2SAX);
168-
return saxSource;
169-
} catch (NoSuchMethodException
170-
| InstantiationException
171-
| IllegalAccessException
172-
| InvocationTargetException e) {
173-
return new DOMSource(node);
174-
}
84+
xmlNodes.add(new XmlPrimitiveNode<>(evaluationResult.value()));
85+
break;
17586
}
17687

177-
return new DOMSource(node);
88+
return xmlNodes;
17889
}
17990
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (C) 2024 Thomas Akehurst
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.github.tomakehurst.wiremock.common.xml;
17+
18+
import java.text.NumberFormat;
19+
import java.util.Collections;
20+
import java.util.Map;
21+
22+
public class XmlPrimitiveNode<T> extends XmlNode {
23+
24+
private final T value;
25+
26+
public XmlPrimitiveNode(T value) {
27+
this.value = value;
28+
}
29+
30+
@Override
31+
public String toString() {
32+
return value instanceof Number ? NumberFormat.getInstance().format(value) : value.toString();
33+
}
34+
35+
@Override
36+
public Map<String, String> getAttributes() {
37+
return Collections.emptyMap();
38+
}
39+
}

0 commit comments

Comments
 (0)