diff --git a/extras/build.xml b/extras/build.xml index 6d950371100..8f739ca7edd 100644 --- a/extras/build.xml +++ b/extras/build.xml @@ -105,20 +105,13 @@ - - - - - - Report generated at ${report.datestamp} - + - - - - - - - - - - - Cannot find all xalan and/or serialiser jars - The XSLT formatting may not work correctly. - Check you have xalan and serializer jars in ${lib.dir} - - - diff --git a/gradle.properties b/gradle.properties index 874c65a5204..c66221b8add 100644 --- a/gradle.properties +++ b/gradle.properties @@ -134,7 +134,6 @@ spock-core.version=2.1-groovy-3.0 springframework.version=4.3.17.RELEASE svgSalamander.version=1.1.2.4 tika.version=1.28.3 -xalan.version=2.7.2 xercesImpl.version=2.12.2 xml-apis.version=1.4.01 xmlgraphics-commons.version=2.7 diff --git a/lib/aareadme.txt b/lib/aareadme.txt index ee01ff8812d..4db7c7f48e1 100644 --- a/lib/aareadme.txt +++ b/lib/aareadme.txt @@ -237,11 +237,6 @@ rsyntaxtextarea-3.0.4 http://fifesoft.com/rsyntaxtextarea/ - syntax coloration -serialiser-2.7.1 ----------------- -http://www.apache.org/dyn/closer.cgi/xml/xalan-j -- xalan - slf4j-api-1.7.28 ---------------- http://www.slf4j.org/ @@ -258,7 +253,7 @@ commons-dbcp2-2.5.0 (org.apache.commons.dbcp2) -------------------------- - DataSourceElement (JDBC) -Saxon-HE-9.9.1-5 (net.sf.saxon) +Saxon-HE-11.3 (net.sf.saxon) -------------------------- - XPath2Extractor (XML) @@ -267,11 +262,6 @@ velocity-1.7 http://velocity.apache.org/download.cgi - Anakia (create documentation) Not used by JMeter runtime -xalan_2.7.1 ------------ -http://www.apache.org/dyn/closer.cgi/xml/xalan-j -+org.apache.xalan|xml|xpath - xercesImpl-2.12.0 ---------------- http://xerces.apache.org/xerces2-j/download.cgi diff --git a/src/bom/build.gradle.kts b/src/bom/build.gradle.kts index 79e4e4c548f..23fe0860155 100644 --- a/src/bom/build.gradle.kts +++ b/src/bom/build.gradle.kts @@ -162,8 +162,6 @@ dependencies { apiv("org.slf4j:slf4j-api", "slf4j") apiv("org.spockframework:spock-core") apiv("oro:oro") - apiv("xalan:serializer", "xalan") - apiv("xalan:xalan", "xalan") apiv("xerces:xercesImpl") apiv("xml-apis:xml-apis") apiv("xmlpull:xmlpull") diff --git a/src/components/src/main/java/org/apache/jmeter/extractor/XPath2Extractor.java b/src/components/src/main/java/org/apache/jmeter/extractor/XPath2Extractor.java index 77683d147b5..cf68c314912 100644 --- a/src/components/src/main/java/org/apache/jmeter/extractor/XPath2Extractor.java +++ b/src/components/src/main/java/org/apache/jmeter/extractor/XPath2Extractor.java @@ -22,6 +22,7 @@ import java.util.List; import javax.xml.stream.FactoryConfigurationError; +import javax.xml.transform.TransformerException; import org.apache.jmeter.assertions.AssertionResult; import org.apache.jmeter.processor.PostProcessor; @@ -223,7 +224,7 @@ public void setFragment(boolean selected) { * @throws FactoryConfigurationError */ private void getValuesForXPath(String query, List matchStrings, int matchNumber, String responseData) - throws SaxonApiException, FactoryConfigurationError { + throws SaxonApiException, TransformerException, FactoryConfigurationError { XPathUtil.putValuesForXPathInListUsingSaxon(responseData, query, matchStrings, getFragment(), matchNumber, getNamespaces()); } diff --git a/src/components/src/test/java/org/apache/jmeter/assertions/XPathAssertionTest.java b/src/components/src/test/java/org/apache/jmeter/assertions/XPathAssertionTest.java index b98f32a4b3a..048f79a04cb 100644 --- a/src/components/src/test/java/org/apache/jmeter/assertions/XPathAssertionTest.java +++ b/src/components/src/test/java/org/apache/jmeter/assertions/XPathAssertionTest.java @@ -240,7 +240,8 @@ public void testAssertionNumber() throws Exception { testLog.debug("isError() {} isFailure() {}", res.isError(), res.isFailure()); testLog.debug("failure message: {}", res.getFailureMessage()); assertFalse("Should not be an error", res.isError()); - assertTrue("Should be a failure",res.isFailure()); + //this used to result in isFailure()=true but is now fixed (after switching from Xalan to Saxon for XPath support) + assertFalse("Should not be a failure", res.isFailure()); } @Test diff --git a/src/components/src/test/java/org/apache/jmeter/extractor/TestXPathExtractor.java b/src/components/src/test/java/org/apache/jmeter/extractor/TestXPathExtractor.java index 6138fb1dbf7..8ba118c9df5 100644 --- a/src/components/src/test/java/org/apache/jmeter/extractor/TestXPathExtractor.java +++ b/src/components/src/test/java/org/apache/jmeter/extractor/TestXPathExtractor.java @@ -32,6 +32,7 @@ import org.apache.jmeter.threads.JMeterContext; import org.apache.jmeter.threads.JMeterContextService; import org.apache.jmeter.threads.JMeterVariables; +import org.apache.jmeter.util.XPathUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -195,7 +196,7 @@ public void testVariableExtraction() throws Exception { // No text, but using fragment mode extractor.setXPathQuery("//a"); extractor.process(); - assertEquals("", vars.get(VAL_NAME)); + assertEquals(XPathUtil.formatXml(""), XPathUtil.formatXml(vars.get(VAL_NAME))); } @Test @@ -283,7 +284,7 @@ public void testInvalidXpath() throws Exception { if (Locale.getDefault().getLanguage().startsWith(Locale.ENGLISH.getLanguage())) { assertThat( firstResult.getFailureMessage(), - containsString("A location path was expected, but the following token was encountered") + containsString("Unexpected token \"<\" at start of expression") ); } assertEquals("Default", vars.get(VAL_NAME)); diff --git a/src/core/build.gradle.kts b/src/core/build.gradle.kts index db221daf451..b957dcaa8d9 100644 --- a/src/core/build.gradle.kts +++ b/src/core/build.gradle.kts @@ -50,11 +50,6 @@ dependencies { api("oro:oro") { because("Perl5Matcher org.apache.jmeter.util.JMeterUtils.getMatcher()") } - api("xalan:xalan") { - because("PropertiesBasedPrefixResolver extends PrefixResolverDefault") - } - // Note: Saxon should go AFTER xalan so xalan XSLT is used - // org.apache.jmeter.util.XPathUtilTest.testFormatXmlSimple assumes xalan transformer api("net.sf.saxon:Saxon-HE") { because("XPathUtil: throws SaxonApiException") } diff --git a/src/core/src/main/java/org/apache/jmeter/util/PrefixResolverDefault.java b/src/core/src/main/java/org/apache/jmeter/util/PrefixResolverDefault.java new file mode 100644 index 00000000000..f760a7b2417 --- /dev/null +++ b/src/core/src/main/java/org/apache/jmeter/util/PrefixResolverDefault.java @@ -0,0 +1,146 @@ +/* + * 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 + * + * 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 org.apache.jmeter.util; + +import javax.xml.XMLConstants; + +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +/** + * This class implements a generic PrefixResolver that + * can be used to perform prefix-to-namespace lookup + * for the XPath object. + */ +public class PrefixResolverDefault +{ + + /** + * The context to resolve the prefix from, if the context + * is not given. + */ + Node m_context; + + /** + * Construct a PrefixResolverDefault object. + * @param xpathExpressionContext The context from + * which XPath expression prefixes will be resolved. + * Warning: This will not work correctly if xpathExpressionContext + * is an attribute node. + */ + public PrefixResolverDefault(Node xpathExpressionContext) + { + m_context = xpathExpressionContext; + } + + /** + * Given a namespace, get the corrisponding prefix. This assumes that + * the PrevixResolver hold's it's own namespace context, or is a namespace + * context itself. + * @param prefix Prefix to resolve. + * @return Namespace that prefix resolves to, or null if prefix + * is not bound. + */ + public String getNamespaceForPrefix(String prefix) + { + return getNamespaceForPrefix(prefix, m_context); + } + + /** + * Given a namespace, get the corrisponding prefix. + * Warning: This will not work correctly if namespaceContext + * is an attribute node. + * @param prefix Prefix to resolve. + * @param namespaceContext Node from which to start searching for a + * xmlns attribute that binds a prefix to a namespace. + * @return Namespace that prefix resolves to, or null if prefix + * is not bound. + */ + public String getNamespaceForPrefix(String prefix, + org.w3c.dom.Node namespaceContext) + { + + Node parent = namespaceContext; + String namespace = null; + + if (prefix.equals("xml")) + { + namespace = XMLConstants.XML_NS_URI; + } + else + { + int type; + + while ((null != parent) && (null == namespace) + && (((type = parent.getNodeType()) == Node.ELEMENT_NODE) + || (type == Node.ENTITY_REFERENCE_NODE))) + { + if (type == Node.ELEMENT_NODE) + { + if (parent.getNodeName().indexOf(prefix+":") == 0) + { + return parent.getNamespaceURI(); + } + NamedNodeMap nnm = parent.getAttributes(); + + for (int i = 0; i < nnm.getLength(); i++) + { + Node attr = nnm.item(i); + String aname = attr.getNodeName(); + boolean isPrefix = aname.startsWith("xmlns:"); + + if (isPrefix || aname.equals("xmlns")) + { + int index = aname.indexOf(':'); + String p = isPrefix ? aname.substring(index + 1) : ""; + + if (p.equals(prefix)) + { + namespace = attr.getNodeValue(); + + break; + } + } + } + } + + parent = parent.getParentNode(); + } + } + + return namespace; + } + + /** + * Return the base identifier. + * + * @return null by default + */ + public String getBaseIdentifier() + { + return null; + } + + /** + * @return false by default + */ + public boolean handlesNullPrefixes() { + return false; + } + +} diff --git a/src/core/src/main/java/org/apache/jmeter/util/PropertiesBasedPrefixResolver.java b/src/core/src/main/java/org/apache/jmeter/util/PropertiesBasedPrefixResolver.java index 4cd789485f1..d6df1ee3615 100644 --- a/src/core/src/main/java/org/apache/jmeter/util/PropertiesBasedPrefixResolver.java +++ b/src/core/src/main/java/org/apache/jmeter/util/PropertiesBasedPrefixResolver.java @@ -23,21 +23,22 @@ import java.io.IOException; import java.io.InputStream; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Properties; +import javax.xml.namespace.NamespaceContext; + import org.apache.commons.lang3.StringUtils; import org.apache.jorphan.util.JOrphanUtils; -import org.apache.xml.utils.PrefixResolver; -import org.apache.xml.utils.PrefixResolverDefault; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; /** - * {@link PrefixResolver} implementation that loads prefix configuration from jmeter property xpath.namespace.config + * PrefixResolver implementation that loads prefix configuration from jmeter property xpath.namespace.config */ -public class PropertiesBasedPrefixResolver extends PrefixResolverDefault { +public class PropertiesBasedPrefixResolver extends PrefixResolverDefault implements NamespaceContext { private static final Logger log = LoggerFactory.getLogger(PropertiesBasedPrefixResolver.class); private static final String XPATH_NAMESPACE_CONFIG = "xpath.namespace.config"; private static final Map NAMESPACE_MAP = new HashMap<>(); @@ -85,11 +86,44 @@ public PropertiesBasedPrefixResolver(Node xpathExpressionContext) { */ @Override public String getNamespaceForPrefix(String prefix, Node namespaceContext) { - String namespace = NAMESPACE_MAP.get(prefix); + String namespace = getNamespaceURI(prefix); if(namespace==null) { return super.getNamespaceForPrefix(prefix, namespaceContext); } else { return namespace; } } + + @Override + public String getNamespaceURI(String prefix) { + return NAMESPACE_MAP.get(prefix); + } + + @Override + public String getPrefix(String namespaceURI) { + if (namespaceURI != null) { + for (Map.Entry entry : NAMESPACE_MAP.entrySet()) { + if (namespaceURI.equals(entry.getValue())) { + return entry.getValue(); + } + } + } + return null; + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + return NAMESPACE_MAP.keySet().iterator(); + } + + String getNamespacesAsLineDelimitedProperties() { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : NAMESPACE_MAP.entrySet()) { + builder.append(entry.getKey()) + .append('=') + .append(entry.getValue()) + .append('\n'); + } + return builder.toString(); + } } diff --git a/src/core/src/main/java/org/apache/jmeter/util/PropertiesBasedPrefixResolverForXpath2.java b/src/core/src/main/java/org/apache/jmeter/util/PropertiesBasedPrefixResolverForXpath2.java index 44eb5296fc4..dbb1fd2cca2 100644 --- a/src/core/src/main/java/org/apache/jmeter/util/PropertiesBasedPrefixResolverForXpath2.java +++ b/src/core/src/main/java/org/apache/jmeter/util/PropertiesBasedPrefixResolverForXpath2.java @@ -18,17 +18,18 @@ package org.apache.jmeter.util; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; -import org.apache.xml.utils.PrefixResolver; -import org.apache.xml.utils.PrefixResolverDefault; +import javax.xml.namespace.NamespaceContext; + import org.w3c.dom.Node; /** - * {@link PrefixResolver} implementation that loads prefix configuration from + * PrefixResolver implementation that loads prefix configuration from * jmeter property xpath.namespace.config */ -public class PropertiesBasedPrefixResolverForXpath2 extends PrefixResolverDefault { +public class PropertiesBasedPrefixResolverForXpath2 extends PrefixResolverDefault implements NamespaceContext { private Map namespaceMap = new HashMap<>(); /** @@ -55,11 +56,33 @@ public PropertiesBasedPrefixResolverForXpath2(Node xpathExpressionContext, Strin */ @Override public String getNamespaceForPrefix(String prefix, Node namespaceContext) { - String namespace = namespaceMap.get(prefix); + String namespace = getNamespaceURI(prefix); if (namespace == null) { return super.getNamespaceForPrefix(prefix, namespaceContext); } else { return namespace; } } + + @Override + public String getNamespaceURI(String prefix) { + return namespaceMap.get(prefix); + } + + @Override + public String getPrefix(String namespaceURI) { + if (namespaceURI != null) { + for (Map.Entry entry : namespaceMap.entrySet()) { + if (namespaceURI.equals(entry.getValue())) { + return entry.getValue(); + } + } + } + return null; + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + return namespaceMap.keySet().iterator(); + } } diff --git a/src/core/src/main/java/org/apache/jmeter/util/XPathUtil.java b/src/core/src/main/java/org/apache/jmeter/util/XPathUtil.java index d183e7da5ea..ca5a11938fd 100644 --- a/src/core/src/main/java/org/apache/jmeter/util/XPathUtil.java +++ b/src/core/src/main/java/org/apache/jmeter/util/XPathUtil.java @@ -29,6 +29,7 @@ import java.util.List; import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -41,21 +42,20 @@ import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactoryConfigurationException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.jmeter.assertions.AssertionResult; -import org.apache.xml.utils.PrefixResolver; -import org.apache.xpath.XPathAPI; -import org.apache.xpath.objects.XObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; -import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.tidy.Tidy; @@ -65,6 +65,8 @@ import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; +import net.sf.saxon.jaxp.SaxonTransformerFactory; +import net.sf.saxon.s9api.ItemType; import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XPathExecutable; @@ -72,6 +74,7 @@ import net.sf.saxon.s9api.XdmItem; import net.sf.saxon.s9api.XdmNode; import net.sf.saxon.s9api.XdmValue; +import net.sf.saxon.xpath.XPathFactoryImpl; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; @@ -303,26 +306,6 @@ public void fatalError(SAXParseException ex) throws SAXException { } } - /** - * Return value for node including node element - * @param node Node - * @return String - */ - private static String getNodeContent(Node node) { - StringWriter sw = new StringWriter(); - try { - TransformerFactory factory = TransformerFactory.newInstance(); - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - Transformer t = factory.newTransformer(); - t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); - t.transform(new DOMSource(node), new StreamResult(sw)); - } catch (TransformerException e) { - sw.write(e.getMessageAndLocation()); - } - return sw.toString(); - } - - /** * @param node {@link Node} * @return String content of node @@ -345,8 +328,11 @@ public static String getValueForNode(Node node) { * @throws TransformerException when the internally used xpath engine fails */ public static NodeList selectNodeList(Document document, String xPathExpression) throws TransformerException { - XObject xObject = XPathAPI.eval(document, xPathExpression, getPrefixResolver(document)); - return xObject.nodelist(); + try { + return (NodeList) newXPath(document).evaluate(xPathExpression, document, XPathConstants.NODESET); + } catch (XPathExpressionException|XPathFactoryConfigurationException e) { + throw new TransformerException(e); + } } /** @@ -374,41 +360,14 @@ public static void putValuesForXPathInList(Document document, */ public static void putValuesForXPathInList(Document document, String xPathQuery, List matchStrings, boolean fragment, int matchNumber) throws TransformerException { - String val = null; - XObject xObject = XPathAPI.eval(document, xPathQuery, getPrefixResolver(document)); - final int objectType = xObject.getType(); - if (objectType == XObject.CLASS_NODESET) { - NodeList matches = xObject.nodelist(); - int length = matches.getLength(); - int indexToMatch = matchNumber; - if(matchNumber == 0 && length>0) { - indexToMatch = JMeterUtils.getRandomInt(length)+1; - } - for (int i = 0 ; i < length; i++) { - Node match = matches.item(i); - if(indexToMatch >= 0 && indexToMatch != (i+1)) { - continue; - } - if ( match instanceof Element ){ - if (fragment){ - val = getNodeContent(match); - } else { - val = getValueForNode(match); - } - } else { - val = match.getNodeValue(); - } - matchStrings.add(val); - } - } else if (objectType == XObject.CLASS_NULL - || objectType == XObject.CLASS_UNKNOWN - || objectType == XObject.CLASS_UNRESOLVEDVARIABLE) { - if (log.isWarnEnabled()) { - log.warn("Unexpected object type: {} returned for: {}", xObject.getTypeString(), xPathQuery); - } - } else { - val = xObject.toString(); - matchStrings.add(val); + PropertiesBasedPrefixResolver namespaceContext = getPrefixResolver(document); + String namespaces = namespaceContext.getNamespacesAsLineDelimitedProperties(); + net.sf.saxon.s9api.DocumentBuilder builder = PROCESSOR.newDocumentBuilder(); + try { + XdmNode xdmNode = builder.build(new DOMSource(document)); + putValuesForXPathInListUsingSaxon(xdmNode, xPathQuery, matchStrings, fragment, matchNumber, namespaces, true); + } catch (SaxonApiException|FactoryConfigurationError e) { + throw new TransformerException(unwrapException(e)); } } @@ -416,7 +375,23 @@ public static void putValuesForXPathInListUsingSaxon( String xmlFile, String xPathQuery, List matchStrings, boolean fragment, int matchNumber, String namespaces) - throws SaxonApiException, FactoryConfigurationError { + throws SaxonApiException, TransformerException, FactoryConfigurationError { + try (StringReader reader = new StringReader(xmlFile)) { + // We could instantiate it once but might trigger issues in the future + // Sharing of a DocumentBuilder across multiple threads is not recommended. + // However, in the current implementation sharing a DocumentBuilder (once initialized) + // will only cause problems if a SchemaValidator is used. + net.sf.saxon.s9api.DocumentBuilder builder = PROCESSOR.newDocumentBuilder(); + XdmNode xdmNode = builder.build(new SAXSource(new InputSource(reader))); + putValuesForXPathInListUsingSaxon(xdmNode, xPathQuery, matchStrings, fragment, matchNumber, namespaces, false); + } + } + + private static void putValuesForXPathInListUsingSaxon( + XdmNode xdmNode, String xPathQuery, + List matchStrings, boolean fragment, + int matchNumber, String namespaces, boolean useNullForEmpty) + throws SaxonApiException, TransformerException, FactoryConfigurationError { // generating the cache key final ImmutablePair key = ImmutablePair.of(xPathQuery, namespaces); @@ -424,59 +399,63 @@ public static void putValuesForXPathInListUsingSaxon( //check the cache XPathExecutable xPathExecutable; if(StringUtils.isNotEmpty(xPathQuery)) { - xPathExecutable = XPATH_CACHE.get(key); + try { + xPathExecutable = XPATH_CACHE.get(key); + } catch (RuntimeException e) { + Throwable unwrapped = unwrapException(e); + if (unwrapped instanceof TransformerException) { + throw (TransformerException) unwrapped; + } + throw e; + } } else { log.warn("Error : {}", JMeterUtils.getResString("xpath2_extractor_empty_query")); return; } - try (StringReader reader = new StringReader(xmlFile)) { - // We could instantiate it once but might trigger issues in the future - // Sharing of a DocumentBuilder across multiple threads is not recommended. - // However, in the current implementation sharing a DocumentBuilder (once initialized) - // will only cause problems if a SchemaValidator is used. - net.sf.saxon.s9api.DocumentBuilder builder = PROCESSOR.newDocumentBuilder(); - XdmNode xdmNode = builder.build(new SAXSource(new InputSource(reader))); - - if(xPathExecutable!=null) { - XPathSelector selector = null; - try { - selector = xPathExecutable.load(); - selector.setContextItem(xdmNode); - XdmValue nodes = selector.evaluate(); - int length = nodes.size(); - int indexToMatch = matchNumber; - // In case we need to extract everything - if(matchNumber < 0) { - for(XdmItem item : nodes) { - if(fragment) { - matchStrings.add(item.toString()); - } - else { - matchStrings.add(item.getStringValue()); - } + if(xPathExecutable!=null) { + XPathSelector selector = null; + try { + selector = xPathExecutable.load(); + selector.setContextItem(xdmNode); + XdmValue nodes = selector.evaluate(); + int length = nodes.size(); + int indexToMatch = matchNumber; + // In case we need to extract everything + if(matchNumber < 0) { + for(XdmItem item : nodes) { + if(fragment) { + matchStrings.add(item.toString()); } - } else { - if(indexToMatch <= length) { - if(matchNumber == 0 && length>0) { - indexToMatch = JMeterUtils.getRandomInt(length)+1; - } - XdmItem item = nodes.itemAt(indexToMatch-1); - matchStrings.add(fragment ? item.toString() : item.getStringValue()); - } else { - if(log.isWarnEnabled()) { - log.warn("Error : {}{}", JMeterUtils.getResString("xpath2_extractor_match_number_failure"),indexToMatch); + else { + String value = item.getStringValue(); + if (useNullForEmpty && value != null && value.isEmpty()) { + matchStrings.add(null); + } else { + matchStrings.add(value); } } } - } finally { - if(selector != null) { - try { - selector.getUnderlyingXPathContext().setContextItem(null); - } catch (Exception e) { // NOSONAR Ignored on purpose - // NOOP + } else { + if(indexToMatch <= length) { + if(matchNumber == 0 && length>0) { + indexToMatch = JMeterUtils.getRandomInt(length)+1; } + XdmItem item = nodes.itemAt(indexToMatch-1); + matchStrings.add(fragment ? item.toString() : item.getStringValue()); + } else { + if(log.isWarnEnabled()) { + log.warn("Error : {}{}", JMeterUtils.getResString("xpath2_extractor_match_number_failure"),indexToMatch); + } + } + } + } finally { + if(selector != null) { + try { + selector.getUnderlyingXPathContext().setContextItem(null); + } catch (Exception e) { // NOSONAR Ignored on purpose + // NOOP } } } @@ -560,9 +539,9 @@ private static void addToList(XMLStreamReader reader, List res) { /** * * @param document XML Document - * @return {@link PrefixResolver} + * @return {@link PropertiesBasedPrefixResolver} */ - private static PrefixResolver getPrefixResolver(Document document) { + private static PropertiesBasedPrefixResolver getPrefixResolver(Document document) { return new PropertiesBasedPrefixResolver(document.getDocumentElement()); } @@ -573,24 +552,30 @@ private static PrefixResolver getPrefixResolver(Document document) { * @throws TransformerException if expression fails to evaluate */ public static void validateXPath(Document document, String xpathString) throws TransformerException { - if (XPathAPI.eval(document, xpathString, getPrefixResolver(document)) == null) { - // We really should never get here - // because eval will throw an exception - // if xpath is invalid, but whatever, better - // safe - throw new IllegalArgumentException("xpath eval of '" + xpathString + "' was null"); + try { + XPath xPath = newXPath(document); + if (xPath.evaluate(xpathString, document) == null) { + // We really should never get here + // because eval will throw an exception + // if xpath is invalid, but whatever, better + // safe + throw new IllegalArgumentException("xpath eval of '" + xpathString + "' was null"); + } + } catch (XPathExpressionException|XPathFactoryConfigurationException e) { + throw new TransformerException(e); } } /** * * @param document XML Document - * @param namespaces String series of prefix/namespace values separateur by line break - * @return {@link PrefixResolver} + * @param namespaces String series of prefix/namespace values separator by line break + * @return {@link PropertiesBasedPrefixResolverForXpath2} */ - private static PrefixResolver getPrefixResolverForXPath2(Document document,String namespaces) { - return new PropertiesBasedPrefixResolverForXpath2(document.getDocumentElement(),namespaces); + private static PropertiesBasedPrefixResolverForXpath2 getPrefixResolverForXPath2(Document document, String namespaces) { + return new PropertiesBasedPrefixResolverForXpath2(document.getDocumentElement(), namespaces); } + /** * Validate xpathString is a valid XPath expression * @param document XML Document @@ -598,15 +583,21 @@ private static PrefixResolver getPrefixResolverForXPath2(Document document,Strin * @param namespaces Space separated set of prefix=namespace * @throws TransformerException if expression fails to evaluate */ - public static void validateXPath2(Document document, String xpathString,String namespaces) throws TransformerException { - if (XPathAPI.eval(document, xpathString, getPrefixResolverForXPath2(document,namespaces)) == null) { - // We really should never get here - // because eval will throw an exception - // if xpath is invalid, but whatever, better - // safe - throw new IllegalArgumentException("xpath eval of '" + xpathString + "' was null"); + public static void validateXPath2(Document document, String xpathString, String namespaces) throws TransformerException { + try { + XPath xPath = newXPath(getPrefixResolverForXPath2(document, namespaces)); + if (xPath.evaluate(xpathString, document) == null) { + // We really should never get here + // because eval will throw an exception + // if xpath is invalid, but whatever, better + // safe + throw new IllegalArgumentException("xpath eval of '" + xpathString + "' was null"); + } + } catch (XPathExpressionException|XPathFactoryConfigurationException e) { + throw new TransformerException(e); } } + /** * Fills result * @param result {@link AssertionResult} @@ -618,116 +609,83 @@ public static void computeAssertionResult(AssertionResult result, Document doc, String xPathExpression, boolean isNegated) { + net.sf.saxon.s9api.DocumentBuilder builder = PROCESSOR.newDocumentBuilder(); try { - XObject xObject = XPathAPI.eval(doc, xPathExpression, getPrefixResolver(doc)); - switch (xObject.getType()) { - case XObject.CLASS_NODESET: - NodeList nodeList = xObject.nodelist(); - final int len = (nodeList != null) ? nodeList.getLength() : 0; - log.debug("nodeList length {}", len); - // length == 0 means nodelist is null - if (len == 0) { - log.debug("nodeList is null or empty. No match by xpath expression: {}", xPathExpression); - result.setFailure(!isNegated); - result.setFailureMessage("No Nodes Matched " + xPathExpression); - return; + XdmNode xdmNode = builder.build(new DOMSource(doc)); + String namespaces = getPrefixResolver(doc).getNamespacesAsLineDelimitedProperties(); + computeAssertionResultUsingSaxon(result, xdmNode, xPathExpression, namespaces, isNegated); + } catch (Exception e) { + result.setError(true); + result.setFailureMessage("TransformerException: " + unwrapException(e).getMessage() + " for: " + xPathExpression); + } + } + + /*** + * + * @param result The result of xpath2 assertion + * @param xmlFile XML data + * @param xPathQuery XPath Query + * @param namespaces Space separated set of prefix=namespace + * @param isNegated invert result + * @throws SaxonApiException when the parser has problems with the given xml or xpath query + * @throws FactoryConfigurationError when the parser can not be instantiated + */ + public static void computeAssertionResultUsingSaxon(AssertionResult result, String xmlFile, String xPathQuery, + String namespaces, Boolean isNegated) throws SaxonApiException, FactoryConfigurationError { + try (StringReader reader = new StringReader(xmlFile)) { + // We could instantiate it once but might trigger issues in the future + // Sharing of a DocumentBuilder across multiple threads is not recommended. + // However, in the current implementation sharing a DocumentBuilder (once + // initialized) + // will only cause problems if a SchemaValidator is used. + net.sf.saxon.s9api.DocumentBuilder builder = PROCESSOR.newDocumentBuilder(); + XdmNode xdmNode = builder.build(new SAXSource(new InputSource(reader))); + computeAssertionResultUsingSaxon(result, xdmNode, xPathQuery, namespaces, isNegated); + } + } + + private static void computeAssertionResultUsingSaxon(AssertionResult result, XdmNode xdmNode, String xPathQuery, + String namespaces, Boolean isNegated) throws SaxonApiException, FactoryConfigurationError { + // generating the cache key + final ImmutablePair key = ImmutablePair.of(xPathQuery, namespaces); + // check the cache + XPathExecutable xPathExecutable; + if (StringUtils.isNotEmpty(xPathQuery)) { + xPathExecutable = XPATH_CACHE.get(key); + } else { + log.warn("Error : {}", JMeterUtils.getResString("xpath2_extractor_empty_query")); + return; + } + if (xPathExecutable != null) { + XPathSelector selector = null; + try { + selector = xPathExecutable.load(); + selector.setContextItem(xdmNode); + XdmValue nodes = selector.evaluate(); + boolean resultOfEval = true; + int length = nodes.size(); + // In case we need to extract everything + if (length == 0) { + resultOfEval = false; + } else if (nodes.itemAt(0).matches(ItemType.BOOLEAN)) { + resultOfEval = Boolean.parseBoolean(nodes.itemAt(0).getStringValue()); } - if (log.isDebugEnabled() && !isNegated) { - for (int i = 0; i < len; i++) { - log.debug("nodeList[{}]: {}", i, nodeList.item(i)); + result.setFailure(isNegated ? resultOfEval : !resultOfEval); + result.setFailureMessage( + isNegated ? "Nodes Matched for " + xPathQuery : "No Nodes Matched for " + xPathQuery); + } finally { + if (selector != null) { + try { + selector.getUnderlyingXPathContext().setContextItem(null); + } catch (Exception e) { // NOSONAR Ignored on purpose + result.setError(true); + result.setFailureMessage("Exception: " + e.getMessage() + " for:" + xPathQuery); } } - result.setFailure(isNegated); - if (isNegated) { - result.setFailureMessage("Specified XPath was found... Turn off negate if this is not desired"); - } - return; - case XObject.CLASS_BOOLEAN: - boolean resultOfEval = xObject.bool(); - result.setFailure(isNegated ? resultOfEval : !resultOfEval); - result.setFailureMessage(isNegated ? - "Nodes Matched for " + xPathExpression - : "No Nodes Matched for " + xPathExpression); - return; - default: - result.setFailure(true); - result.setFailureMessage("Cannot understand: " + xPathExpression); - return; } - } catch (TransformerException e) { - result.setError(true); - result.setFailureMessage("TransformerException: " + e.getMessage() + " for: " + xPathExpression); } } - - /*** - * - * @param result The result of xpath2 assertion - * @param xmlFile XML data - * @param xPathQuery XPath Query - * @param namespaces Space separated set of prefix=namespace - * @param isNegated invert result - * @throws SaxonApiException when the parser has problems with the given xml or xpath query - * @throws FactoryConfigurationError when the parser can not be instantiated - */ - public static void computeAssertionResultUsingSaxon(AssertionResult result, String xmlFile, String xPathQuery, - String namespaces, Boolean isNegated) throws SaxonApiException, FactoryConfigurationError { - // generating the cache key - final ImmutablePair key = ImmutablePair.of(xPathQuery, namespaces); - // check the cache - XPathExecutable xPathExecutable; - if (StringUtils.isNotEmpty(xPathQuery)) { - xPathExecutable = XPATH_CACHE.get(key); - } else { - log.warn("Error : {}", JMeterUtils.getResString("xpath2_extractor_empty_query")); - return; - } - try (StringReader reader = new StringReader(xmlFile)) { - // We could instantiate it once but might trigger issues in the future - // Sharing of a DocumentBuilder across multiple threads is not recommended. - // However, in the current implementation sharing a DocumentBuilder (once - // initialized) - // will only cause problems if a SchemaValidator is used. - net.sf.saxon.s9api.DocumentBuilder builder = PROCESSOR.newDocumentBuilder(); - XdmNode xdmNode = builder.build(new SAXSource(new InputSource(reader))); - if (xPathExecutable != null) { - XPathSelector selector = null; - try { - Document doc; - doc = XPathUtil.makeDocumentBuilder(false, false, false, false).newDocument(); - XObject xObject = XPathAPI.eval(doc, xPathQuery, getPrefixResolverForXPath2(doc, namespaces)); - selector = xPathExecutable.load(); - selector.setContextItem(xdmNode); - XdmValue nodes = selector.evaluate(); - boolean resultOfEval = true; - int length = nodes.size(); - // In case we need to extract everything - if (length == 0) { - resultOfEval = false; - } else if (xObject.getType() == XObject.CLASS_BOOLEAN) { - resultOfEval = Boolean.parseBoolean(nodes.itemAt(0).getStringValue()); - } - result.setFailure(isNegated ? resultOfEval : !resultOfEval); - result.setFailureMessage( - isNegated ? "Nodes Matched for " + xPathQuery : "No Nodes Matched for " + xPathQuery); - } catch (ParserConfigurationException | TransformerException e) { // NOSONAR Exception handled by return - result.setError(true); - result.setFailureMessage("Exception: " + e.getMessage() + " for:" + xPathQuery); - } finally { - if (selector != null) { - try { - selector.getUnderlyingXPathContext().setContextItem(null); - } catch (Exception e) { // NOSONAR Ignored on purpose - result.setError(true); - result.setFailureMessage("Exception: " + e.getMessage() + " for:" + xPathQuery); - } - } - } - } - } - } - /** * Formats XML * @param xml string to format @@ -735,11 +693,12 @@ public static void computeAssertionResultUsingSaxon(AssertionResult result, Stri */ public static String formatXml(String xml){ try { - TransformerFactory factory = TransformerFactory.newInstance(); + SaxonTransformerFactory factory = new SaxonTransformerFactory(); factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - Transformer serializer= factory.newTransformer(); + Transformer serializer = factory.newTransformer(); + serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); serializer.setOutputProperty(OutputKeys.INDENT, "yes"); - serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + //serializer.setOutputProperty("{http://saxon.sf.net/}indent-spaces", "2"); Source xmlSource = new SAXSource(new InputSource(new StringReader(xml))); StringWriter stringWriter = new StringWriter(); StreamResult res = new StreamResult(stringWriter); @@ -750,4 +709,23 @@ public static String formatXml(String xml){ } } + private static XPath newXPath(Document document) throws XPathFactoryConfigurationException { + return newXPath(getPrefixResolver(document)); + } + + private static XPath newXPath(NamespaceContext namespaceContext) throws XPathFactoryConfigurationException { + XPathFactoryImpl xPathFactory = new XPathFactoryImpl(); + xPathFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + XPath xPath = xPathFactory.newXPath(); + xPath.setNamespaceContext(namespaceContext); + return xPath; + } + + private static Throwable unwrapException(Throwable t) { + Throwable cause = t.getCause(); + if (cause != null && cause != t) { + return unwrapException(cause); + } + return t; + } } diff --git a/src/core/src/test/java/org/apache/jmeter/util/XPathUtilTest.java b/src/core/src/test/java/org/apache/jmeter/util/XPathUtilTest.java index d916487f10d..70c1f344371 100644 --- a/src/core/src/test/java/org/apache/jmeter/util/XPathUtilTest.java +++ b/src/core/src/test/java/org/apache/jmeter/util/XPathUtilTest.java @@ -78,7 +78,7 @@ public void testBug63033() throws SaxonApiException { } @Test - public void testputValuesForXPathInListUsingSaxon() throws SaxonApiException, FactoryConfigurationError{ + public void testputValuesForXPathInListUsingSaxon() throws Exception { String xPathQuery="//Employees/Employee/role"; ArrayList matchStrings = new ArrayList(); boolean fragment = false; @@ -147,8 +147,8 @@ public void testnamespacesParse(String namespaces, String key, String value, int @Test public void testFormatXmlSimple() { assertThat(XPathUtil.formatXml("Test"), - CoreMatchers.is("" - + "Test" + lineSeparator)); + CoreMatchers.is("" + "\n" + + "Test" + "\n")); } @Test @@ -156,11 +156,12 @@ public void testFormatXmlComplex() { assertThat( XPathUtil.formatXml( "..."), - CoreMatchers.is(String.join(lineSeparator, "", - " ", - " ", - " ", - " ...", + CoreMatchers.is(String.join("\n", "", + "", + " ", + " ", + " ", + " ...", ""))); } diff --git a/src/functions/src/main/java/org/apache/jmeter/functions/XPathFileContainer.java b/src/functions/src/main/java/org/apache/jmeter/functions/XPathFileContainer.java index 7c6e998428c..dfa20ebe23b 100644 --- a/src/functions/src/main/java/org/apache/jmeter/functions/XPathFileContainer.java +++ b/src/functions/src/main/java/org/apache/jmeter/functions/XPathFileContainer.java @@ -62,7 +62,7 @@ public XPathFileContainer(String file, String xpath) throws FileNotFoundExceptio nodeList=load(xpath); } - private NodeList load(String xpath) throws IOException, FileNotFoundException, ParserConfigurationException, SAXException, + private NodeList load(String xpath) throws IOException, ParserConfigurationException, SAXException, TransformerException { NodeList nl = null; try ( FileInputStream fis = new FileInputStream(fileName);