简体   繁体   中英

How to keep xml attribute in fasterXml Jackson XmlMapper?

I am writing test cases which test generated xml structures. I am supplying the xml structures via an xml file. I am using currently FasterXMLs Jackson XmlMapper for reading and testing for expected xml.

Java:            adoptopenjdk 11
Maven:           3.6.3
JUnit (Jupiter): 5.7.1 (JUnit Jupiter)
Mapper:          com.fasterxml.jackson.dataformat.xml.XmlMapper
Dependency:      <dependency>
                     <groupId>com.fasterxml.jackson.dataformat</groupId>
                     <artifactId>jackson-dataformat-xml</artifactId>
                     <version>2.11.4</version>
                 </dependency>

I have an xml file which contains expected xml (eg: /test/testcases.xml:

<testcases>
    <testcase1>
        <response>
            <sizegroup-list>
                <sizeGroup id="1">
                <sizes>
                    <size>
                        <technicalSize>38</technicalSize>
                        <textSize>38</textSize>
                    <size>
                    <size>
                        <technicalSize>705</technicalSize>
                        <textSize>110cm</textSize>
                    <size>
                </sizes>
            </sizeGroup-list>
        </response>
    </testcase1>
</testcases>

My code looks like this (simplified):

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;

import java.io.FileInputStream;
import java.io.InputStream;

import static org.junit.jupiter.api.Assertions.assertEquals;

class Testcases {
    private static final String OBJECT_NODE_START_TAG = "<ObjectNode>";
    private static final String OBJECT_NODE_CLOSE_TAG = "</ObjectNode>";
    private static final String TESTCASES_XML = "/test/testcases.xml";
    private static final XmlMapper XML_MAPPER = new XmlMapper();

    @Test
    void testcase1() throws Exception {
        final String nodePtr = "/testcase1/response";
        try (InputStream inputStream = new FileInputStream(TESTCASES_XML)) {
            JsonNode rootNode = XML_MAPPER.readTree(inputStream);
            JsonNode subNode = rootNode.at(nodePtr);

            if (subNode.isMissingNode()) {
                throw new IllegalArgumentException(
                        "Node '" + nodePtr + "' not found in file " + TESTCASES_XML);
            }

            String expectedXml = XML_MAPPER.writeValueAsString(subNode);
            expectedXml = unwrapObjectNode(expectedXml);

            // Testcalls, e.g. someService.generateXmlData()
            String generatedXml = "...";

            assertEquals(expectedXml, generatedXml);
        };
    }

    // FIXME: Ugly: Tell XmlMapper to unwrap ObjectNode automatically
    private String unwrapObjectNode(String xmlString) {
        if(StringUtils.isBlank(xmlString)) {
            return xmlString;
        }

        if(xmlString.startsWith(OBJECT_NODE_START_TAG)) {
            xmlString = xmlString.substring(OBJECT_NODE_START_TAG.length());
            if(xmlString.endsWith(OBJECT_NODE_CLOSE_TAG)) {
                xmlString = xmlString.substring(0, xmlString.length() - OBJECT_NODE_CLOSE_TAG.length());
            }
        }

        return xmlString;
    }

}

But the returned expected xml looks like this:

            <sizegroup-list>
                <sizeGroup>
                <id>1</id>
                <sizes>
                    <size>
                        <technicalSize>38</technicalSize>
                        <textSize>38</textSize>
                    <size>
                    <size>
                        <technicalSize>705</technicalSize>
                        <textSize>110cm</textSize>
                    <size>
                </sizes>
            </sizeGroup-list>

The former attribute id of the element sizeGroup gets mapped as a sub element and fails my test. How can I tell XmlMapper to keep the attributes of xml elements?

Best regards, David

i was not able to tell XmlMapper to keep the attributes of xml tags from the loaded xml file. But i have found another way by parsing xml test data with xPath expressions.

A simple String.equals(...) proofed to be unreliable if expected and actual xml contain different whitespaces or xml tag order. Luckily there is a library for comparing xml. XmlUnit!

Additional dependency (seems to be present as transitive dependency as of Spring Boot 2.6.x):

<dependency>
    <groupId>org.xmlunit</groupId>
    <artifactId>xmlunit-core</artifactId>
    <!-- version transitive in spring-boot-starter-parent 2.6.7 -->
    <version>2.8.4</version>
    <scope>test</test>
</dependency>

ResourceUtil.java:

import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URL;

public class ResourceUtil {
    private static final DocumentBuilderFactory XML_DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
    private static final XPathFactory X_PATH_FACTORY = XPathFactory.newInstance();

    private ResourceUtil() {}

    /** Reads an xml file named after the testcase class (e.g. MyTestcase.class
      * -> MyTestcase.xml) and parses the data at the supplied xPath expression. */
    public static String xmlData(Class<?> testClass, String xPathExpression) {
        return getXmlDocumentAsString(testClass, testClass.getSimpleName() + ".xml", xPathExpression);
    }

    /** Reads the specified xml file and parses the data at the supplied xPath
      * expression. The xml file is expected in the same package/directory as
      * the testcase class. */
    private static String getXmlDocumentAsString(Class<?> ctxtClass, String fileName, String xPathExpression) {
        Document xmlDocument = getXmlDocument(ctxtClass, fileName);
        XPath xPath = X_PATH_FACTORY.newXPath();

        try {
            Node subNode = (Node)xPath.compile(xPathExpression).evaluate(xmlDocument, XPathConstants.NODE);
            return nodeToString(subNode.getChildNodes());
        } catch (TransformerException | XPathExpressionException var6) {
            throw new IllegalArgumentException("Unable to read value of '" + xPathExpression + "' from file " + fileName, var6);
        }
    }

    /** Reads the specified xml file and returns a Document instance of the
      * xml data. The xml file is expected in the same package/directory as
      * the testcase class. */
    private static Document getXmlDocument(Class<?> ctxtClass, String xmlFileName) {
        InputStream inputStream = getResourceFile(ctxtClass, xmlFileName);

        try {
            DocumentBuilder builder = XML_DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
            return builder.parse(inputStream);
        } catch (SAXException | IOException | ParserConfigurationException var4) {
            throw new IllegalStateException("Unable to read xml content from file '" + xmlFileName + "'.", var4);
        }
    }

    /** Returns an InputStream of the specified xml file. The xml file is
      * expected in the same package/directory as the testcase class. */
    private static InputStream getResourceFile(Class<?> ctxtClass, String fileName) {
      String pkgPath = StringUtils.replaceChars(ctxtClass.getPackage().getName(), ".", "/");
      String filePath = "/" + pkgPath + "/" + fileName;
      URL url = ctxtClass.getResource(filePath);
      if (url == null) {
          throw new IllegalArgumentException("Resource file not found: " + filePath);
      }
      return ResourceTestUtil.class.getResourceAsStream(filePath);
    }

    /** Deserializes a NodeList to a String with (formatted) xml. */
    private static String nodeToString(NodeList nodeList) throws TransformerException {
        StringWriter buf = new StringWriter();
        Transformer xform = TransformerFactory.newInstance().newTransformer(getXsltAsResource());
        xform.setOutputProperty("omit-xml-declaration", "yes");
        xform.setOutputProperty("indent", "no");

        for(int i = 0; i < nodeList.getLength(); ++i) {
            xform.transform(new DOMSource(nodeList.item(i)), new StreamResult(buf));
        }

        return buf.toString().trim();
    }

    /** Returns a Source of an XSLT file for formatting xml data */
    private static Source getXsltAsResource() {
        return new StreamSource(ResourceTestUtil.class.getResourceAsStream("xmlstylesheet.xslt"));
    }

xmlstylesheet.xslt (works for me, you may alter to your preferences):

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:output method="xml" encoding="UTF-8"/>

    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

MyTestcase.java:

import org.xmlunit.builder.DiffBuilder;
import org.xmlunit.diff.DefaultNodeMatcher;
import org.xmlunit.diff.Diff;
import org.xmlunit.diff.ElementSelectors;

import static ResourceUtil.xmldata;

public class MyTestcase {
    @Test
    void testcase1() {
        // Execute logic to generate xml
        String xml = ...
       
        assertXmlEquals(xmlData(getClass(), "/test/testcase1/result"), xml);
    }

    /** Compare xml using XmlUnit assertion. Expected and actual xml need
      * to be equal in content (ignoring whitespace and xml tag order) */
    void assertXmlEquals(String expectedXml, String testXml) {
        Diff diff = DiffBuilder.compare(expectedXml)
                .withTest(testXml)
                .ignoreWhitespace()
                .checkForSimilar()
                .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText, ElementSelectors.byName))
                .build();
        assertFalse(diff.fullDescription(), diff.hasDifferences());
    }

}

MyTestcase.xml:

<test>
    <testcase1>
        <result>
            <myData>
                ...
            </myData>
        </result>
    </testcase1>
</test>

Best regards, David

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM