Skip to content

Commit

Permalink
[plugin-xml] Add step to check if XML document is well formed (#5643)
Browse files Browse the repository at this point in the history
  • Loading branch information
uarlouski authored Jan 6, 2025
1 parent 7f092b8 commit a9d8828
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 314 deletions.
25 changes: 25 additions & 0 deletions docs/modules/plugins/pages/plugin-xml.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,28 @@ Then XML `
</xs:schema>
`
----

=== Validate well formed XML document

Validates if the provided XML document is well formed.

[source,gherkin]
----
Then XML `$xml` is well formed
----

* `$xml` - The {xml} document.

.Check a message is well formed XML document
[source,gherkin]
----
Then XML `
<?xml version="1.0" encoding="UTF-8"?>
<message>
<to>Bob</to>
<from>Alice</from>
<heading>Reminder</heading>
<body>Don't forget to fill TJ gaps for this week</body>
</message>
` is well formed
----
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2021 the original author or authors.
* Copyright 2019-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,6 +21,8 @@
import java.io.IOException;
import java.util.Set;

import javax.xml.xpath.XPathExpressionException;

import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;
import org.vividus.context.VariableContext;
Expand Down Expand Up @@ -51,9 +53,11 @@ public XmlSteps(ISoftAssert softAssert, VariableContext variableContext)
* @param xml XML
* @param scopes The set of variable scopes (comma separated list of scopes e.g.: STORY, NEXT_BATCHES)
* @param variableName Name of variable
* @throws XPathExpressionException If an XPath expression error has occurred
*/
@When("I save data found by xpath `$xpath` in XML `$xml` to $scopes variable `$variableName`")
public void saveDataByXpath(String xpath, String xml, Set<VariableScope> scopes, String variableName)
throws XPathExpressionException
{
XmlUtils.getXmlByXpath(xml, xpath).ifPresent(
data -> variableContext.putVariable(scopes, variableName, data));
Expand All @@ -78,9 +82,11 @@ public void saveTransformedXml(String xml, String xslt, Set<VariableScope> scope
* Checks if xml contains element by XPath
* @param xml XML
* @param xpath XPath
* @throws IOException If an I/O error has occurred
* @throws SAXException If an XML processing error has occurred
*/
@Then("XML `$xml` contains element by xpath `$xpath`")
public void doesElementExistByXpath(String xml, String xpath)
public void doesElementExistByXpath(String xml, String xpath) throws SAXException, IOException
{
Document doc = XmlUtils.convertToDocument(xml);
softAssert.assertThat("XML has element with XPath: " + xpath, doc, hasXPath(xpath));
Expand Down Expand Up @@ -129,4 +135,23 @@ public void validateXmlAgainstXsd(String xml, String xsd)
softAssert.recordFailedAssertion(e);
}
}

/**
* Validates if the XML document is well formed
*
* @param xml The XML document
*/
@Then("XML `$xml` is well formed")
public void validateXmlIsWellFormed(String xml)
{
try
{
XmlUtils.convertToDocument(xml);
softAssert.recordPassedAssertion("The XML document is well formed");
}
catch (SAXException | IOException e)
{
softAssert.recordFailedAssertion(e);
}
}
}
95 changes: 33 additions & 62 deletions vividus-plugin-xml/src/main/java/org/vividus/util/xml/XmlUtils.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2023 the original author or authors.
* Copyright 2019-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -41,10 +41,6 @@
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.vividus.util.pool.UnsafeGenericObjectPool;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
Expand All @@ -59,84 +55,57 @@ public final class XmlUtils
private static final String EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities";
private static final String EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities";

private static final UnsafeGenericObjectPool<XPathFactory> XPATH_FACTORY = new UnsafeGenericObjectPool<>(
XPathFactory::newInstance);
private static final UnsafeGenericObjectPool<TransformerFactory> TRANSFORMER_FACTORY =
new UnsafeGenericObjectPool<>(TransformerFactory::newInstance);
private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
private static final UnsafeGenericObjectPool<DocumentBuilder> DOCUMENT_BUILDER;
private static final XPathFactory XPATH_FACTORY = XPathFactory.newInstance();
private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
private static final DocumentBuilder DOCUMENT_BUILDER;

static
{
DOCUMENT_BUILDER_FACTORY.setNamespaceAware(true);
try
{
DOCUMENT_BUILDER_FACTORY.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
DOCUMENT_BUILDER_FACTORY.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
DOCUMENT_BUILDER_FACTORY.setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
documentBuilderFactory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
DOCUMENT_BUILDER = documentBuilderFactory.newDocumentBuilder();
}
catch (ParserConfigurationException e)
{
throw new IllegalStateException(e);
}
DOCUMENT_BUILDER = new UnsafeGenericObjectPool<>(new BasePooledObjectFactory<>()
{
@Override
public DocumentBuilder create() throws ParserConfigurationException
{
return DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
}

@Override
public PooledObject<DocumentBuilder> wrap(DocumentBuilder obj)
{
return new DefaultPooledObject<>(obj);
}
});
}

private XmlUtils()
{
}

public static Document convertToDocument(String xml)
public static Document convertToDocument(String xml) throws SAXException, IOException
{
return DOCUMENT_BUILDER.apply(documentBuilder ->
synchronized (DOCUMENT_BUILDER)
{
try
{
return documentBuilder.parse(createInputSource(xml));
}
catch (SAXException | IOException e)
{
throw new IllegalStateException(e.getMessage(), e);
}
});
return DOCUMENT_BUILDER.parse(createInputSource(xml));
}
}

/**
* Search by XPath in XML
* @param xml XML
* @param xpath xpath
* @return Search result
* @throws XPathExpressionException If an XPath expression error has occurred
*/
public static Optional<String> getXmlByXpath(String xml, String xpath)
public static Optional<String> getXmlByXpath(String xml, String xpath) throws XPathExpressionException
{
return XPATH_FACTORY.apply(xPathFactory -> {
try
{
InputSource source = createInputSource(xml);
NodeList nodeList = (NodeList) xPathFactory.newXPath().evaluate(xpath, source, XPathConstants.NODESET);
Node singleNode = nodeList.item(0);
Properties outputProperties = new Properties();
outputProperties.setProperty(OutputKeys.OMIT_XML_DECLARATION, YES);
return transform(new DOMSource(singleNode), outputProperties);
}
catch (XPathExpressionException e)
{
throw new IllegalStateException(e.getMessage(), e);
}
});
synchronized (XPATH_FACTORY)
{
InputSource source = createInputSource(xml);
NodeList nodeList = (NodeList) XPATH_FACTORY.newXPath().evaluate(xpath, source, XPathConstants.NODESET);
Node singleNode = nodeList.item(0);
Properties outputProperties = new Properties();
outputProperties.setProperty(OutputKeys.OMIT_XML_DECLARATION, YES);
return transform(new DOMSource(singleNode), outputProperties);
}
}

public static void validateXmlAgainstXsd(String xml, String xsd) throws SAXException, IOException
Expand All @@ -152,19 +121,20 @@ public static void transform(String xml, String xslt, Consumer<String> transform
{
StreamSource xmlSource = createStreamSource(xml);
StreamSource xsltSource = createStreamSource(xslt);
TRANSFORMER_FACTORY.accept(transformerFactory ->

synchronized (TRANSFORMER_FACTORY)
{
try
{
Transformer transformer = transformerFactory.newTransformer(xsltSource);
Transformer transformer = TRANSFORMER_FACTORY.newTransformer(xsltSource);
String transformedXml = transform(xmlSource, transformer);
transformedXmlConsumer.accept(transformedXml);
}
catch (TransformerException e)
{
transformerExceptionConsumer.accept(e);
}
});
}
}

/**
Expand Down Expand Up @@ -192,10 +162,11 @@ public static Optional<String> format(String xml)

private static Optional<String> transform(Source xmlSource, Properties outputProperties)
{
return TRANSFORMER_FACTORY.apply(transformerFactory -> {
synchronized (TRANSFORMER_FACTORY)
{
try
{
Transformer transformer = transformerFactory.newTransformer();
Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
transformer.setOutputProperties(outputProperties);
String result = transform(xmlSource, transformer);
return Optional.of(result);
Expand All @@ -204,7 +175,7 @@ private static Optional<String> transform(Source xmlSource, Properties outputPro
{
return Optional.empty();
}
});
}
}

private static String transform(Source xmlSource, Transformer transformer) throws TransformerException
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2021 the original author or authors.
* Copyright 2019-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,9 +20,11 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;

import java.io.IOException;
import java.util.Set;

import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;

import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
Expand All @@ -37,6 +39,7 @@
import org.vividus.variable.VariableScope;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

@ExtendWith(MockitoExtension.class)
class XmlStepsTests
Expand All @@ -54,7 +57,7 @@ class XmlStepsTests
private XmlSteps xmlValidationSteps;

@Test
void shouldSaveDataByXpathIntoScopeVariable()
void shouldSaveDataByXpathIntoScopeVariable() throws XPathExpressionException
{
Set<VariableScope> scopes = Set.of(VariableScope.STEP);
String name = "name";
Expand All @@ -64,7 +67,7 @@ void shouldSaveDataByXpathIntoScopeVariable()

@Test
@SuppressWarnings("unchecked")
void shouldValidateXmlElementExistenceByXpath()
void shouldValidateXmlElementExistenceByXpath() throws SAXException, IOException
{
Document doc = XmlUtils.convertToDocument(XML);
softAssert.assertThat(eq("XML has element with XPath: " + XPATH), eq(doc), any(Matcher.class));
Expand Down Expand Up @@ -114,6 +117,20 @@ void shouldRecordFailedAssertionOnTransformationException()
verify(softAssert).recordFailedAssertion(any(TransformerException.class));
}

@Test
void shouldRecordFailedAssertionIfXmlIsNotWellFormed()
{
xmlValidationSteps.validateXmlIsWellFormed("<test>xslt test<test>");
verify(softAssert).recordFailedAssertion(any(SAXParseException.class));
}

@Test
void shouldPassIfXMLDocumentIsWellFormed()
{
xmlValidationSteps.validateXmlIsWellFormed(XML);
verify(softAssert).recordPassedAssertion("The XML document is well formed");
}

private String loadXsd()
{
return loadResource("test.xsd");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 the original author or authors.
* Copyright 2019-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,6 +27,7 @@
import java.util.stream.Stream;

import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
Expand All @@ -36,6 +37,7 @@
import org.vividus.util.ResourceUtils;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

class XmlUtilsTests
{
Expand All @@ -47,27 +49,27 @@ class XmlUtilsTests
+ "</xs:schema>";

@Test
void shouldReturnXmlByXpath()
void shouldReturnXmlByXpath() throws XPathExpressionException
{
assertEquals(Optional.of("value1"), XmlUtils.getXmlByXpath(XML, "//data/text()"));
}

@Test
void shouldThrowExceptionInCaseOfInvalidXpath()
{
assertThrows(IllegalStateException.class, () -> XmlUtils.getXmlByXpath(XML, "<invalidXpath>"));
assertThrows(XPathExpressionException.class, () -> XmlUtils.getXmlByXpath(XML, "<invalidXpath>"));
}

@Test
void shouldConvertXmlStringToDocument()
void shouldConvertXmlStringToDocument() throws SAXException, IOException
{
assertThat(XmlUtils.convertToDocument(XML), instanceOf(Document.class));
}

@Test
void shouldThrowExceptionInCaseOfInvalidXmlOnConversion()
{
assertThrows(IllegalStateException.class, () -> XmlUtils.convertToDocument("<invalidXml>"));
assertThrows(SAXParseException.class, () -> XmlUtils.convertToDocument("<invalidXml>"));
}

@Test
Expand Down
1 change: 0 additions & 1 deletion vividus-util/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ test {
}

dependencies {
api(group: 'org.apache.commons', name: 'commons-pool2', version: '2.12.0')
api(group: 'org.freemarker', name: 'freemarker', version: '2.3.34')
api platform(group: 'com.fasterxml.jackson', name: 'jackson-bom', version: '2.18.2')
api(group: 'com.fasterxml.jackson.core', name: 'jackson-core')
Expand Down
Loading

0 comments on commit a9d8828

Please sign in to comment.