Skip to content

Commit

Permalink
Merge pull request #2717 from wiremock/xpath-helper-single-item-bug
Browse files Browse the repository at this point in the history
Fixes #2696 - xPath helper returns error when result is primitive value
  • Loading branch information
dieppa committed May 20, 2024
2 parents 3f02154 + 7bdcda8 commit a378ee6
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 122 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2021 Thomas Akehurst
* Copyright (C) 2020-2024 Thomas Akehurst
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,21 +15,19 @@
*/
package com.github.tomakehurst.wiremock.common.xml;

import static javax.xml.xpath.XPathConstants.NODESET;

import com.github.tomakehurst.wiremock.common.ListOrSingle;
import java.util.HashMap;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.transform.dom.DOMSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathEvaluationResult;
import javax.xml.xpath.XPathExpressionException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xmlunit.util.Convert;

public class XmlDocument extends XmlNode {
public class XmlDocument extends XmlDomNode {

private final Document document;

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

NodeList nodeSet;
XPathEvaluationResult<?> xPathEvaluationResult;
if (namespaces != null) {
Map<String, String> fullNamespaces = addStandardNamespaces(namespaces);
NamespaceContext namespaceContext = Convert.toNamespaceContext(fullNamespaces);
xPath.setNamespaceContext(namespaceContext);
nodeSet =
(NodeList)
xPath.evaluate(
xPathExpression, Convert.toInputSource(new DOMSource(document)), NODESET);
xPathEvaluationResult =
xPath.evaluateExpression(
xPathExpression, Convert.toInputSource(new DOMSource(document)));
} else {
nodeSet = (NodeList) xPath.evaluate(xPathExpression, document, NODESET);
xPathEvaluationResult = xPath.evaluateExpression(xPathExpression, document);
}

return toListOrSingle(nodeSet);
return toListOrSingle(xPathEvaluationResult);
} catch (XPathExpressionException e) {
throw XPathException.fromXPathException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright (C) 2024 Thomas Akehurst
*
* Licensed 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
*
* http://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 com.github.tomakehurst.wiremock.common.xml;

import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;

import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.XMLReader;

public class XmlDomNode extends XmlNode {

private final Node domNode;
private final Map<String, String> attributes;

public XmlDomNode(Node domNode) {
this.domNode = domNode;
attributes =
domNode.hasAttributes()
? convertAttributeMap(domNode.getAttributes())
: Collections.emptyMap();
}

private static Map<String, String> convertAttributeMap(NamedNodeMap namedNodeMap) {
Map<String, String> map = new HashMap<>();
for (int i = 0; i < namedNodeMap.getLength(); i++) {
Node node = namedNodeMap.item(i);
map.put(node.getNodeName(), node.getNodeValue());
}

return Collections.unmodifiableMap(map);
}

public Map<String, String> getAttributes() {
return attributes;
}

public String getName() {
return domNode.getNodeName();
}

public String getText() {
return domNode.getTextContent();
}

@Override
public String toString() {
switch (domNode.getNodeType()) {
case Node.TEXT_NODE:
case Node.ATTRIBUTE_NODE:
return domNode.getTextContent();
case Node.DOCUMENT_NODE:
case Node.ELEMENT_NODE:
return render();
default:
return domNode.toString();
}
}

private String render() {
try {
Transformer transformer = TRANSFORMER_CACHE.get();
StreamResult result = new StreamResult(new StringWriter());
Source source = getSourceForTransform(domNode);
transformer.transform(source, result);
return result.getWriter().toString();
} catch (Exception e) {
return throwUnchecked(e, String.class);
}
}

private static final Class<XMLReader> DOM2SAX_XMLREADER_CLASS = getDom2SaxAvailability();

@SuppressWarnings("unchecked")
private static Class<XMLReader> getDom2SaxAvailability() {
try {
return (Class<XMLReader>)
Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.DOM2SAX");
} catch (ClassNotFoundException e) {
return null;
}
}

// This nasty little hack attempts to ensure no exception is thrown when attempting to print an
// XML node with
// unbound namespace prefixes (which can happen when you've selected an element via XPath whose
// namespaces are declared in a parent element).
// For some reason Transformer is happy to do this with a SAX source, but not a DOM source.
private static Source getSourceForTransform(Node node) {
if (DOM2SAX_XMLREADER_CLASS != null) {
try {
Constructor<XMLReader> constructor = DOM2SAX_XMLREADER_CLASS.getConstructor(Node.class);
XMLReader dom2SAX = constructor.newInstance(node);
SAXSource saxSource = new SAXSource();
saxSource.setXMLReader(dom2SAX);
return saxSource;
} catch (NoSuchMethodException
| InstantiationException
| IllegalAccessException
| InvocationTargetException e) {
return new DOMSource(node);
}
}

return new DOMSource(node);
}
}
129 changes: 20 additions & 109 deletions src/main/java/com/github/tomakehurst/wiremock/common/xml/XmlNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,16 @@
import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION;

import com.github.tomakehurst.wiremock.common.ListOrSingle;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathEvaluationResult;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.XMLReader;

public class XmlNode {
public abstract class XmlNode {

protected static final ThreadLocal<XPath> XPATH_CACHE =
ThreadLocal.withInitial(
Expand Down Expand Up @@ -74,106 +66,25 @@ public class XmlNode {
}
});

private static final Class<XMLReader> DOM2SAX_XMLREADER_CLASS = getDom2SaxAvailability();
public abstract Map<String, String> getAttributes();

private static Class<XMLReader> getDom2SaxAvailability() {
try {
return (Class<XMLReader>)
Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.DOM2SAX");
} catch (ClassNotFoundException e) {
return null;
}
}

private final Node domNode;
private final Map<String, String> attributes;

public XmlNode(Node domNode) {
this.domNode = domNode;
attributes =
domNode.hasAttributes()
? convertAttributeMap(domNode.getAttributes())
: Collections.emptyMap();
}

private static Map<String, String> convertAttributeMap(NamedNodeMap namedNodeMap) {
Map<String, String> map = new HashMap<>();
for (int i = 0; i < namedNodeMap.getLength(); i++) {
Node node = namedNodeMap.item(i);
map.put(node.getNodeName(), node.getNodeValue());
}

return Collections.unmodifiableMap(map);
}

public Map<String, String> getAttributes() {
return attributes;
}

protected static ListOrSingle<XmlNode> toListOrSingle(NodeList nodeList) {
ListOrSingle<XmlNode> nodes = new ListOrSingle<>();
for (int i = 0; i < nodeList.getLength(); i++) {
nodes.add(new XmlNode(nodeList.item(i)));
}
@SuppressWarnings("unchecked")
protected static ListOrSingle<XmlNode> toListOrSingle(XPathEvaluationResult<?> evaluationResult) {
ListOrSingle<XmlNode> xmlNodes = new ListOrSingle<>();

return nodes;
}

public String getName() {
return domNode.getNodeName();
}

public String getText() {
return domNode.getTextContent();
}

@Override
public String toString() {
switch (domNode.getNodeType()) {
case Node.TEXT_NODE:
case Node.ATTRIBUTE_NODE:
return domNode.getTextContent();
case Node.DOCUMENT_NODE:
case Node.ELEMENT_NODE:
return render();
switch (evaluationResult.type()) {
case NODESET:
Iterable<Node> nodes = (Iterable<Node>) evaluationResult.value();
nodes.forEach(node -> xmlNodes.add(new XmlDomNode(node)));
break;
case NODE:
xmlNodes.add(new XmlDomNode((Node) evaluationResult.value()));
break;
default:
return domNode.toString();
}
}

private String render() {
try {
Transformer transformer = TRANSFORMER_CACHE.get();
StreamResult result = new StreamResult(new StringWriter());
Source source = getSourceForTransform(domNode);
transformer.transform(source, result);
return result.getWriter().toString();
} catch (Exception e) {
return throwUnchecked(e, String.class);
}
}

// This nasty little hack attempts to ensure no exception is thrown when attempting to print an
// XML node with
// unbound namespace prefixes (which can happen when you've selected an element via XPath whose
// namespaces are declared in a parent element).
// For some reason Transformer is happy to do this with a SAX source, but not a DOM source.
private static Source getSourceForTransform(Node node) {
if (DOM2SAX_XMLREADER_CLASS != null) {
try {
Constructor<XMLReader> constructor = DOM2SAX_XMLREADER_CLASS.getConstructor(Node.class);
XMLReader dom2SAX = constructor.newInstance(node);
SAXSource saxSource = new SAXSource();
saxSource.setXMLReader(dom2SAX);
return saxSource;
} catch (NoSuchMethodException
| InstantiationException
| IllegalAccessException
| InvocationTargetException e) {
return new DOMSource(node);
}
xmlNodes.add(new XmlPrimitiveNode<>(evaluationResult.value()));
break;
}

return new DOMSource(node);
return xmlNodes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 Thomas Akehurst
*
* Licensed 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
*
* http://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 com.github.tomakehurst.wiremock.common.xml;

import java.text.NumberFormat;
import java.util.Collections;
import java.util.Map;

public class XmlPrimitiveNode<T> extends XmlNode {

private final T value;

public XmlPrimitiveNode(T value) {
this.value = value;
}

@Override
public String toString() {
return value instanceof Number ? NumberFormat.getInstance().format(value) : value.toString();
}

@Override
public Map<String, String> getAttributes() {
return Collections.emptyMap();
}
}
Loading

0 comments on commit a378ee6

Please sign in to comment.