This commit is contained in:
Looly 2023-06-16 23:27:49 +08:00
parent f901c9419d
commit 0760e44c12
14 changed files with 1308 additions and 806 deletions

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
import org.dromara.hutool.core.exception.HutoolException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
/**
* {@link DocumentBuilder} 工具类
*
* @author looly
* @since 6.0.0
*/
public class DocumentBuilderUtil {
/**
* 创建 DocumentBuilder
*
* @param namespaceAware 是否打开命名空间支持
* @return DocumentBuilder
*/
public static DocumentBuilder createDocumentBuilder(final boolean namespaceAware) {
final DocumentBuilder builder;
try {
builder = createDocumentBuilderFactory(namespaceAware).newDocumentBuilder();
} catch (final Exception e) {
throw new HutoolException(e, "Create xml document error!");
}
return builder;
}
/**
* 创建{@link DocumentBuilderFactory}
* <p>
* 默认使用"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"<br>
* </p>
*
* @param namespaceAware 是否打开命名空间支持
* @return {@link DocumentBuilderFactory}
*/
public static DocumentBuilderFactory createDocumentBuilderFactory(final boolean namespaceAware) {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 默认打开NamespaceAwaregetElementsByTagNameNS可以使用命名空间
factory.setNamespaceAware(namespaceAware);
return XXEUtil.disableXXE(factory);
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
import javax.xml.parsers.SAXParserFactory;
/**
* {@link SAXParserFactory} 工具
*
* @author looly
* @since 6.0.0
*/
public class SAXParserFactoryUtil {
/**
* Sax读取器工厂缓存
*/
private static volatile SAXParserFactory factory;
/**
* 获取全局{@link SAXParserFactory}<br>
* <ul>
* <li>默认不验证</li>
* <li>默认打开命名空间支持</li>
* </ul>
*
* @return {@link SAXParserFactory}
*/
public static SAXParserFactory getFactory() {
if (null == factory) {
synchronized (SAXParserFactoryUtil.class) {
if (null == factory) {
factory = createFactory(false, true);
}
}
}
return factory;
}
/**
* 创建{@link SAXParserFactory}
*
* @param validating 是否验证
* @param namespaceAware 是否打开命名空间支持
* @return {@link SAXParserFactory}
*/
public static SAXParserFactory createFactory(final boolean validating, final boolean namespaceAware) {
final SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(validating);
factory.setNamespaceAware(namespaceAware);
return XXEUtil.disableXXE(factory);
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
import org.dromara.hutool.core.map.BiMap;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import java.util.HashMap;
import java.util.Iterator;
/**
* 全局命名空间上下文<br>
* https://www.ibm.com/developerworks/cn/xml/x-nmspccontext/
*
* @author looly
*/
public class UniversalNamespaceCache implements NamespaceContext {
private static final String DEFAULT_NS = "DEFAULT";
private final BiMap<String, String> prefixUri = new BiMap<>(new HashMap<>());
/**
* This constructor parses the document and stores all namespaces it can
* find. If toplevelOnly is true, only namespaces in the root are used.
*
* @param node source Node
* @param toplevelOnly restriction of the search to enhance performance
*/
public UniversalNamespaceCache(final Node node, final boolean toplevelOnly) {
examineNode(node.getFirstChild(), toplevelOnly);
}
/**
* A single node is read, the namespace attributes are extracted and stored.
*
* @param node to examine
* @param attributesOnly, if true no recursion happens
*/
private void examineNode(final Node node, final boolean attributesOnly) {
final NamedNodeMap attributes = node.getAttributes();
if (null != attributes) {
final int length = attributes.getLength();
for (int i = 0; i < length; i++) {
final Node attribute = attributes.item(i);
storeAttribute(attribute);
}
}
if (!attributesOnly) {
final NodeList childNodes = node.getChildNodes();
//noinspection ConstantConditions
if (null != childNodes) {
Node item;
final int childLength = childNodes.getLength();
for (int i = 0; i < childLength; i++) {
item = childNodes.item(i);
if (item.getNodeType() == Node.ELEMENT_NODE)
examineNode(item, false);
}
}
}
}
/**
* This method looks at an attribute and stores it, if it is a namespace
* attribute.
*
* @param attribute to examine
*/
private void storeAttribute(final Node attribute) {
if (null == attribute) {
return;
}
// examine the attributes in namespace xmlns
if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attribute.getNamespaceURI())) {
// Default namespace xmlns="uri goes here"
if (XMLConstants.XMLNS_ATTRIBUTE.equals(attribute.getNodeName())) {
prefixUri.put(DEFAULT_NS, attribute.getNodeValue());
} else {
// The defined prefixes are stored here
prefixUri.put(attribute.getLocalName(), attribute.getNodeValue());
}
}
}
/**
* This method is called by XPath. It returns the default namespace, if the
* prefix is null or "".
*
* @param prefix to search for
* @return uri
*/
@Override
public String getNamespaceURI(final String prefix) {
if (prefix == null || XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
return prefixUri.get(DEFAULT_NS);
} else {
return prefixUri.get(prefix);
}
}
/**
* This method is not needed in this context, but can be implemented in a
* similar way.
*/
@Override
public String getPrefix(final String namespaceURI) {
return prefixUri.getInverse().get(namespaceURI);
}
@Override
public Iterator<String> getPrefixes(final String namespaceURI) {
// Not implemented
return null;
}
}

View File

@ -0,0 +1,132 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
import org.dromara.hutool.core.exception.HutoolException;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
/**
* {@link XPath}相关工具类
*
* @author looly
* @since 6.0.0
*/
public class XPathUtil {
/**
* 创建XPath<br>
* Xpath相关文章<a href="https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html">https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html</a>
*
* @return {@link XPath}
* @since 3.2.0
*/
public static XPath createXPath() {
return XPathFactory.newInstance().newXPath();
}
/**
* 通过XPath方式读取XML节点等信息<br>
* Xpath相关文章<a href="https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html">https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html</a>
*
* @param expression XPath表达式
* @param source 资源可以是DocunentNode节点等
* @return 匹配返回类型的值
* @since 4.0.9
*/
public static Element getElementByXPath(final String expression, final Object source) {
return (Element) getNodeByXPath(expression, source);
}
/**
* 通过XPath方式读取XML的NodeList<br>
* Xpath相关文章<a href="https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html">https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html</a>
*
* @param expression XPath表达式
* @param source 资源可以是DocunentNode节点等
* @return NodeList
* @since 4.0.9
*/
public static NodeList getNodeListByXPath(final String expression, final Object source) {
return (NodeList) getByXPath(expression, source, XPathConstants.NODESET);
}
/**
* 通过XPath方式读取XML节点等信息<br>
* Xpath相关文章<a href="https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html">https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html</a>
*
* @param expression XPath表达式
* @param source 资源可以是DocunentNode节点等
* @return 匹配返回类型的值
* @since 4.0.9
*/
public static Node getNodeByXPath(final String expression, final Object source) {
return (Node) getByXPath(expression, source, XPathConstants.NODE);
}
/**
* 通过XPath方式读取XML节点等信息<br>
* Xpath相关文章<a href="https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html">https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html</a>
*
* @param expression XPath表达式
* @param source 资源可以是DocunentNode节点等
* @param returnType 返回类型{@link javax.xml.xpath.XPathConstants}
* @return 匹配返回类型的值
* @since 3.2.0
*/
public static Object getByXPath(final String expression, final Object source, final QName returnType) {
NamespaceContext nsContext = null;
if (source instanceof Node) {
nsContext = new UniversalNamespaceCache((Node) source, false);
}
return getByXPath(expression, source, returnType, nsContext);
}
/**
* 通过XPath方式读取XML节点等信息<br>
* Xpath相关文章<br>
* <a href="https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html">https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html</a><br>
* <a href="https://www.ibm.com/developerworks/cn/xml/x-nmspccontext/">https://www.ibm.com/developerworks/cn/xml/x-nmspccontext/</a>
*
* @param expression XPath表达式
* @param source 资源可以是DocunentNode节点等
* @param returnType 返回类型{@link javax.xml.xpath.XPathConstants}
* @param nsContext {@link NamespaceContext}
* @return 匹配返回类型的值
* @since 5.3.1
*/
public static Object getByXPath(final String expression, final Object source, final QName returnType, final NamespaceContext nsContext) {
final XPath xPath = createXPath();
if (null != nsContext) {
xPath.setNamespaceContext(nsContext);
}
try {
if (source instanceof InputSource) {
return xPath.evaluate(expression, (InputSource) source, returnType);
} else {
return xPath.evaluate(expression, source, returnType);
}
} catch (final XPathExpressionException e) {
throw new HutoolException(e);
}
}
}

View File

@ -0,0 +1,172 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
import org.dromara.hutool.core.exception.HutoolException;
import org.dromara.hutool.core.text.StrUtil;
import org.xml.sax.XMLReader;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
/**
* XXE漏洞修复相关工具类<br>
* 参考https://blog.spoock.com/2018/10/23/java-xxe/
*
* @author looly
* @since 6.0.0
*/
public class XXEUtil {
/**
* 关闭XXE避免漏洞攻击<br>
* see: <a href="https://www.owasp.org/index.php/XML_External_Entity_">https://www.owasp.org/index.php/XML_External_Entity_</a>(XXE)_Prevention_Cheat_Sheet#JAXP_DocumentBuilderFactory.2C_SAXParserFactory_and_DOM4J
*
* @param factory DocumentBuilderFactory
* @return DocumentBuilderFactory
*/
public static DocumentBuilderFactory disableXXE(final DocumentBuilderFactory factory) {
try {
// This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented
// Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
factory.setFeature(XmlFeatures.DISALLOW_DOCTYPE_DECL, true);
// If you can't completely disable DTDs, then at least do the following:
// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
// JDK7+ - http://xml.org/sax/features/external-general-entities
factory.setFeature(XmlFeatures.EXTERNAL_GENERAL_ENTITIES, false);
// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
// JDK7+ - http://xml.org/sax/features/external-parameter-entities
factory.setFeature(XmlFeatures.EXTERNAL_PARAMETER_ENTITIES, false);
// Disable external DTDs as well
factory.setFeature(XmlFeatures.LOAD_EXTERNAL_DTD, false);
} catch (final ParserConfigurationException e) {
// ignore
}
// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
return factory;
}
/**
* 关闭XEE避免漏洞攻击
*
* @param factory {@link SAXParserFactory}
* @return {@link SAXParserFactory}
*/
public static SAXParserFactory disableXXE(final SAXParserFactory factory) {
try {
factory.setFeature(XmlFeatures.DISALLOW_DOCTYPE_DECL, true);
factory.setFeature(XmlFeatures.EXTERNAL_GENERAL_ENTITIES, false);
factory.setFeature(XmlFeatures.EXTERNAL_PARAMETER_ENTITIES, false);
factory.setFeature(XmlFeatures.LOAD_EXTERNAL_DTD, false);
} catch (final Exception ignore) {
// ignore
}
factory.setXIncludeAware(false);
return factory;
}
/**
* 关闭XEE避免漏洞攻击
*
* @param reader {@link XMLReader}
* @return {@link XMLReader}
*/
public static XMLReader disableXXE(final XMLReader reader) {
try {
reader.setFeature(XmlFeatures.DISALLOW_DOCTYPE_DECL, true);
reader.setFeature(XmlFeatures.EXTERNAL_GENERAL_ENTITIES, false);
reader.setFeature(XmlFeatures.EXTERNAL_PARAMETER_ENTITIES, false);
reader.setFeature(XmlFeatures.LOAD_EXTERNAL_DTD, false);
} catch (final Exception ignore) {
// ignore
}
return reader;
}
/**
* 关闭XEE避免漏洞攻击
*
* @param factory {@link TransformerFactory }
* @return {@link TransformerFactory }
*/
public static TransformerFactory disableXXE(final TransformerFactory factory) {
try {
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (final TransformerConfigurationException e) {
throw new HutoolException(e);
}
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, StrUtil.EMPTY);
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, StrUtil.EMPTY);
return factory;
}
/**
* 关闭XEE避免漏洞攻击
*
* @param validator {@link Validator }
* @return {@link Validator }
*/
public static Validator disableXXE(final Validator validator) {
try {
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, StrUtil.EMPTY);
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, StrUtil.EMPTY);
} catch (final Exception ignore) {
// ignore
}
return validator;
}
/**
* 关闭XEE避免漏洞攻击
*
* @param factory {@link SAXTransformerFactory}
* @return {@link SAXTransformerFactory}
*/
public static SAXTransformerFactory disableXXE(final SAXTransformerFactory factory) {
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, StrUtil.EMPTY);
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, StrUtil.EMPTY);
return factory;
}
/**
* 关闭XEE避免漏洞攻击
*
* @param factory {@link SchemaFactory}
* @return {@link SchemaFactory}
*/
public static SchemaFactory disableXXE(final SchemaFactory factory) {
try {
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, StrUtil.EMPTY);
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, StrUtil.EMPTY);
} catch (final Exception ignore) {
// ignore
}
return factory;
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
import java.util.regex.Pattern;
/**
* XML相关常量
*
* @author looly
* @since 6.0.0
*/
public class XmlConstants {
/**
* 字符串常量XML 不间断空格转义 {@code "&nbsp;" -> " "}
*/
public static final String NBSP = "&nbsp;";
/**
* 字符串常量XML And 符转义 {@code "&amp;" -> "&"}
*/
public static final String AMP = "&amp;";
/**
* 字符串常量XML 双引号转义 {@code "&quot;" -> "\""}
*/
public static final String QUOTE = "&quot;";
/**
* 字符串常量XML 单引号转义 {@code "&apos" -> "'"}
*/
public static final String APOS = "&apos;";
/**
* 字符串常量XML 小于号转义 {@code "&lt;" -> "<"}
*/
public static final String LT = "&lt;";
/**
* 字符串常量XML 大于号转义 {@code "&gt;" -> ">"}
*/
public static final String GT = "&gt;";
/**
* 在XML中无效的字符 正则
*/
public static final Pattern INVALID_PATTERN = Pattern.compile("[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]");
/**
* 在XML中注释的内容 正则
*/
public static final Pattern COMMENT_PATTERN = Pattern.compile("(?s)<!--.+?-->");
/**
* XML格式化输出默认缩进量
*/
public static final int INDENT_DEFAULT = 2;
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
/**
* XXE安全相关参数<br>
* https://blog.spoock.com/2018/10/23/java-xxe/
*
* @author looly
* @since 6.0.0
*/
public class XmlFeatures {
/**
* 禁用xml中的inline DOCTYPE 声明即禁用DTD<br>
* 不允许将外部实体包含在传入的 XML 文档中从而防止XML实体注入XML External Entities 攻击利用能够在处理时动态构建文档的 XML 功能注入外部实体
*/
public static final String DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";
/**
* 忽略外部DTD
*/
public static final String LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
/**
* 不包括外部一般实体
*/
public static final String EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities";
/**
* 不包含外部参数实体或外部DTD子集
*/
public static final String EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities";
}

View File

@ -0,0 +1,147 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
import org.dromara.hutool.core.bean.BeanUtil;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.map.MapUtil;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* XML转换器用于转换Map或Bean等
*
* @author looly
* @since 6.0.0
*/
public class XmlMapper {
/**
* 创建XmlMapper
*
* @param node {@link Node}XML节点
* @return XmlMapper
*/
public static XmlMapper of(final Node node) {
return new XmlMapper(node);
}
private final Node node;
/**
* 构造
*
* @param node {@link Node}XML节点
*/
public XmlMapper(final Node node) {
this.node = node;
}
/**
* XML转Java Bean<br>
* 如果XML根节点只有一个且节点名和Bean的名称一致则直接转换子节点
*
* @param <T> bean类型
* @param bean bean类
* @return bean
* @since 5.2.4
*/
public <T> T toBean(final Class<T> bean) {
final Map<String, Object> map = toMap();
if (null != map && map.size() == 1) {
final String nodeName = CollUtil.getFirst(map.keySet());
if (bean.getSimpleName().equalsIgnoreCase(nodeName)) {
// 只有key和bean的名称匹配时才做单一对象转换
return BeanUtil.toBean(CollUtil.get(map.values(), 0), bean);
}
}
return BeanUtil.toBean(map, bean);
}
/**
* XML节点转Map
*
* @return map
*/
public Map<String, Object> toMap() {
return toMap(new LinkedHashMap<>());
}
/**
* XML节点转Map
*
* @param result 结果Map
* @return map
*/
public Map<String, Object> toMap(final Map<String, Object> result) {
return toMap(this.node, result);
}
/**
* XML节点转Map
*
* @param result 结果Map
* @return map
*/
@SuppressWarnings("unchecked")
private static Map<String, Object> toMap(final Node node, Map<String, Object> result) {
if (null == result) {
result = new HashMap<>();
}
final NodeList nodeList = node.getChildNodes();
final int length = nodeList.getLength();
Node childNode;
Element childEle;
for (int i = 0; i < length; ++i) {
childNode = nodeList.item(i);
if (!XmlUtil.isElement(childNode)) {
continue;
}
childEle = (Element) childNode;
final Object value = result.get(childEle.getNodeName());
final Object newValue;
if (childEle.hasChildNodes()) {
// 子节点继续递归遍历
final Map<String, Object> map = toMap(childEle, new LinkedHashMap<>());
if (MapUtil.isNotEmpty(map)) {
newValue = map;
} else {
newValue = childEle.getTextContent();
}
} else {
newValue = childEle.getTextContent();
}
if (null != newValue) {
if (null != value) {
if (value instanceof List) {
((List<Object>) value).add(newValue);
} else {
result.put(childEle.getNodeName(), ListUtil.of(value, newValue));
}
} else {
result.put(childEle.getNodeName(), newValue);
}
}
}
return result;
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
import org.dromara.hutool.core.exception.HutoolException;
import org.dromara.hutool.core.io.IORuntimeException;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
/**
* XML SAX方式读取器
*
* @author looly
* @since 6.0.0
*/
public class XmlSaxReader {
private final SAXParserFactory factory;
private final InputSource source;
/**
* 创建XmlSaxReader使用全局{@link SAXParserFactory}
*
* @param source XML源可以是文件路径等
* @return XmlSaxReader
*/
public static XmlSaxReader of(final InputSource source) {
return of(SAXParserFactoryUtil.getFactory(), source);
}
/**
* 创建XmlSaxReader
*
* @param factory {@link SAXParserFactory}
* @param source XML源可以是文件路径等
* @return XmlSaxReader
*/
public static XmlSaxReader of(final SAXParserFactory factory, final InputSource source) {
return new XmlSaxReader(factory, source);
}
/**
* 构造
*
* @param factory {@link SAXParserFactory}
* @param source XML源可以是文件路径等
*/
public XmlSaxReader(final SAXParserFactory factory, final InputSource source) {
this.factory = factory;
this.source = source;
}
/**
* 读取内容
*
* @param contentHandler XML流处理器用于按照Element处理xml
*/
public void read(final ContentHandler contentHandler) {
final SAXParser parse;
final XMLReader reader;
try {
parse = factory.newSAXParser();
if (contentHandler instanceof DefaultHandler) {
parse.parse(source, (DefaultHandler) contentHandler);
return;
}
// 得到解读器
reader = XXEUtil.disableXXE(parse.getXMLReader());
reader.setContentHandler(contentHandler);
reader.parse(source);
} catch (final ParserConfigurationException | SAXException e) {
throw new HutoolException(e);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.xml;
import org.dromara.hutool.core.exception.HutoolException;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.core.util.ObjUtil;
import org.w3c.dom.Node;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.Charset;
/**
* XML生成器
*
* @author looly
* @since 6.0.0
*/
public class XmlWriter {
/**
* 构建XmlWriter
*
* @param node {@link Node} XML文档节点或文档本身
* @return XmlWriter
*/
public static XmlWriter of(final Node node) {
return of(new DOMSource(node));
}
/**
* 构建XmlWriter
*
* @param source XML数据源
* @return XmlWriter
*/
public static XmlWriter of(final Source source) {
return new XmlWriter(source);
}
private final Source source;
private Charset charset = CharsetUtil.UTF_8;
private int indent;
private boolean omitXmlDeclaration;
/**
* 构造
*
* @param source XML数据源
*/
public XmlWriter(final Source source) {
this.source = source;
}
/**
* 设置编码
*
* @param charset 编码null跳过
* @return this
*/
public XmlWriter setCharset(final Charset charset) {
if (null != charset) {
this.charset = charset;
}
return this;
}
/**
* 设置缩进
*
* @param indent 缩进
* @return this
*/
public XmlWriter setIndent(final int indent) {
this.indent = indent;
return this;
}
/**
* 设置是否输出 xml Declaration
*
* @param omitXmlDeclaration 是否输出 xml Declaration
* @return this
*/
public XmlWriter setOmitXmlDeclaration(final boolean omitXmlDeclaration) {
this.omitXmlDeclaration = omitXmlDeclaration;
return this;
}
/**
* 获得XML字符串
*
* @return XML字符串
*/
public String getStr(){
final StringWriter writer = StrUtil.getWriter();
write(writer);
return writer.toString();
}
/**
* 将XML文档写出
*
* @param file 目标
*/
public void write(final File file) {
write(new StreamResult(file));
}
/**
* 将XML文档写出
*
* @param writer 目标
*/
public void write(final Writer writer) {
write(new StreamResult(writer));
}
/**
* 将XML文档写出
*
* @param out 目标
*/
public void write(final OutputStream out) {
write(new StreamResult(out));
}
/**
* 将XML文档写出<br>
* 格式化输出逻辑参考https://stackoverflow.com/questions/139076/how-to-pretty-print-xml-from-java
*
* @param result 目标
*/
public void write(final Result result) {
final TransformerFactory factory = XXEUtil.disableXXE(TransformerFactory.newInstance());
try {
final Transformer xformer = factory.newTransformer();
if (indent > 0) {
xformer.setOutputProperty(OutputKeys.INDENT, "yes");
//fix issue#1232@Github
xformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");
xformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(indent));
}
if (ObjUtil.isNotNull(this.charset)) {
xformer.setOutputProperty(OutputKeys.ENCODING, charset.name());
}
if (omitXmlDeclaration) {
xformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
}
xformer.transform(source, result);
} catch (final Exception e) {
throw new HutoolException(e, "Trans xml document to string error!");
}
}
}

View File

@ -1,5 +1,6 @@
package org.dromara.hutool.core.text.escape;
import org.dromara.hutool.core.lang.Console;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -66,4 +67,11 @@ public class EscapeUtilTest {
final String s = EscapeUtil.unescapeHtml4(str);
Assertions.assertEquals("'some text with single quotes'", s);
}
@Test
public void escapeXmlTest(){
final String a = "<>";
final String escape = EscapeUtil.escape(a);
Console.log(escape);
}
}

View File

@ -10,17 +10,18 @@
* See the Mulan PSL v2 for more details.
*/
package org.dromara.hutool.core.util;
package org.dromara.hutool.core.xml;
import lombok.Data;
import org.dromara.hutool.core.bean.BeanUtil;
import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.collection.set.SetUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.io.resource.ResourceUtil;
import org.dromara.hutool.core.lang.Console;
import org.dromara.hutool.core.map.MapBuilder;
import org.dromara.hutool.core.map.MapUtil;
import org.dromara.hutool.core.xml.XmlUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -46,13 +47,13 @@ public class XmlUtilTest {
@Test
public void parseTest() {
final String result = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"//
+ "<returnsms>"//
+ "<returnstatus>Success</returnstatus>"//
+ "<message>ok</message>"//
+ "<remainpoint>1490</remainpoint>"//
+ "<taskID>885</taskID>"//
+ "<successCounts>1</successCounts>"//
+ "</returnsms>";
+ "<returnsms>"//
+ "<returnstatus>Success</returnstatus>"//
+ "<message>ok</message>"//
+ "<remainpoint>1490</remainpoint>"//
+ "<taskID>885</taskID>"//
+ "<successCounts>1</successCounts>"//
+ "</returnsms>";
final Document docResult = XmlUtil.parseXml(result);
final String elementText = XmlUtil.elementText(docResult.getDocumentElement(), "returnstatus");
Assertions.assertEquals("Success", elementText);
@ -62,29 +63,29 @@ public class XmlUtilTest {
@Disabled
public void writeTest() {
final String result = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"//
+ "<returnsms>"//
+ "<returnstatus>Success成功</returnstatus>"//
+ "<message>ok</message>"//
+ "<remainpoint>1490</remainpoint>"//
+ "<taskID>885</taskID>"//
+ "<successCounts>1</successCounts>"//
+ "</returnsms>";
+ "<returnsms>"//
+ "<returnstatus>Success成功</returnstatus>"//
+ "<message>ok</message>"//
+ "<remainpoint>1490</remainpoint>"//
+ "<taskID>885</taskID>"//
+ "<successCounts>1</successCounts>"//
+ "</returnsms>";
final Document docResult = XmlUtil.parseXml(result);
XmlUtil.toFile(docResult, "e:/aaa.xml", "utf-8");
XmlUtil.write(docResult, FileUtil.file("d:/test/aaa.xml"), CharsetUtil.UTF_8);
}
@Test
public void xpathTest() {
final String result = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"//
+ "<returnsms>"//
+ "<returnstatus>Success成功</returnstatus>"//
+ "<message>ok</message>"//
+ "<remainpoint>1490</remainpoint>"//
+ "<taskID>885</taskID>"//
+ "<successCounts>1</successCounts>"//
+ "</returnsms>";
+ "<returnsms>"//
+ "<returnstatus>Success成功</returnstatus>"//
+ "<message>ok</message>"//
+ "<remainpoint>1490</remainpoint>"//
+ "<taskID>885</taskID>"//
+ "<successCounts>1</successCounts>"//
+ "</returnsms>";
final Document docResult = XmlUtil.parseXml(result);
final Object value = XmlUtil.getByXPath("//returnsms/message", docResult, XPathConstants.STRING);
final Object value = XPathUtil.getByXPath("//returnsms/message", docResult, XPathConstants.STRING);
Assertions.assertEquals("ok", value);
}
@ -92,21 +93,21 @@ public class XmlUtilTest {
public void xpathTest2() {
final String result = ResourceUtil.readUtf8Str("test.xml");
final Document docResult = XmlUtil.parseXml(result);
final Object value = XmlUtil.getByXPath("//returnsms/message", docResult, XPathConstants.STRING);
final Object value = XPathUtil.getByXPath("//returnsms/message", docResult, XPathConstants.STRING);
Assertions.assertEquals("ok", value);
}
@Test
public void xmlToMapTest() {
final String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"//
+ "<returnsms>"//
+ "<returnstatus>Success</returnstatus>"//
+ "<message>ok</message>"//
+ "<remainpoint>1490</remainpoint>"//
+ "<taskID>885</taskID>"//
+ "<successCounts>1</successCounts>"//
+ "<newNode><sub>subText</sub></newNode>"//
+ "</returnsms>";
+ "<returnsms>"//
+ "<returnstatus>Success</returnstatus>"//
+ "<message>ok</message>"//
+ "<remainpoint>1490</remainpoint>"//
+ "<taskID>885</taskID>"//
+ "<successCounts>1</successCounts>"//
+ "<newNode><sub>subText</sub></newNode>"//
+ "</returnsms>";
final Map<String, Object> map = XmlUtil.xmlToMap(xml);
Assertions.assertEquals(6, map.size());
@ -130,51 +131,51 @@ public class XmlUtilTest {
@Test
public void mapToXmlTest() {
final Map<String, Object> map = MapBuilder.of(new LinkedHashMap<String, Object>())//
.put("name", "张三")//
.put("age", 12)//
.put("game", MapUtil.builder(new LinkedHashMap<String, Object>()).put("昵称", "Looly").put("level", 14).build())//
.build();
.put("name", "张三")//
.put("age", 12)//
.put("game", MapUtil.builder(new LinkedHashMap<String, Object>()).put("昵称", "Looly").put("level", 14).build())//
.build();
final Document doc = XmlUtil.mapToXml(map, "user");
// Console.log(XmlUtil.toStr(doc, false));
Assertions.assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"//
+ "<user>"//
+ "<name>张三</name>"//
+ "<age>12</age>"//
+ "<game>"//
+ "<昵称>Looly</昵称>"//
+ "<level>14</level>"//
+ "</game>"//
+ "</user>", //
XmlUtil.toStr(doc, false));
+ "<user>"//
+ "<name>张三</name>"//
+ "<age>12</age>"//
+ "<game>"//
+ "<昵称>Looly</昵称>"//
+ "<level>14</level>"//
+ "</game>"//
+ "</user>", //
XmlUtil.toStr(doc, false));
}
@Test
public void mapToXmlTest2() {
// 测试List
final Map<String, Object> map = MapBuilder.of(new LinkedHashMap<String, Object>())
.put("Town", ListUtil.of("town1", "town2"))
.build();
.put("Town", ListUtil.of("town1", "town2"))
.build();
final Document doc = XmlUtil.mapToXml(map, "City");
Assertions.assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
"<City>" +
"<Town>town1</Town>" +
"<Town>town2</Town>" +
"</City>",
XmlUtil.toStr(doc));
"<City>" +
"<Town>town1</Town>" +
"<Town>town2</Town>" +
"</City>",
XmlUtil.toStr(doc));
}
@Test
public void readTest() {
final Document doc = XmlUtil.readXML("test.xml");
final Document doc = XmlUtil.readXml("test.xml");
Assertions.assertNotNull(doc);
}
@Test
public void readBySaxTest(){
public void readBySaxTest() {
final Set<String> eles = SetUtil.of(
"returnsms", "returnstatus", "message", "remainpoint", "taskID", "successCounts");
XmlUtil.readBySax(ResourceUtil.getStream("test.xml"), new DefaultHandler(){
"returnsms", "returnstatus", "message", "remainpoint", "taskID", "successCounts");
XmlUtil.readBySax(ResourceUtil.getStream("test.xml"), new DefaultHandler() {
@Override
public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
Assertions.assertTrue(eles.contains(localName));
@ -186,8 +187,8 @@ public class XmlUtilTest {
public void mapToXmlTestWithOmitXmlDeclaration() {
final Map<String, Object> map = MapBuilder.of(new LinkedHashMap<String, Object>())
.put("name", "ddatsh")
.build();
.put("name", "ddatsh")
.build();
final String xml = XmlUtil.mapToXmlStr(map, true);
Assertions.assertEquals("<xml><name>ddatsh</name></xml>", xml);
}
@ -195,18 +196,18 @@ public class XmlUtilTest {
@Test
public void getByPathTest() {
final String xmlStr = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
" <soap:Body>\n" +
" <ns2:testResponse xmlns:ns2=\"http://ws.xxx.com/\">\n" +
" <return>2020/04/15 21:01:21</return>\n" +
" </ns2:testResponse>\n" +
" </soap:Body>\n" +
"</soap:Envelope>\n";
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
" <soap:Body>\n" +
" <ns2:testResponse xmlns:ns2=\"http://ws.xxx.com/\">\n" +
" <return>2020/04/15 21:01:21</return>\n" +
" </ns2:testResponse>\n" +
" </soap:Body>\n" +
"</soap:Envelope>\n";
final Document document = XmlUtil.readXML(xmlStr);
final Object value = XmlUtil.getByXPath(
"//soap:Envelope/soap:Body/ns2:testResponse/return",
document, XPathConstants.STRING);//
final Document document = XmlUtil.readXml(xmlStr);
final Object value = XPathUtil.getByXPath(
"//soap:Envelope/soap:Body/ns2:testResponse/return",
document, XPathConstants.STRING);//
Assertions.assertEquals("2020/04/15 21:01:21", value);
}
@ -267,7 +268,7 @@ public class XmlUtilTest {
}
@Test
public void xmlToBeanTest2(){
public void xmlToBeanTest2() {
@Data
class SmsRes {
private String code;
@ -298,7 +299,7 @@ public class XmlUtilTest {
@Test
@Disabled
public void formatTest(){
public void formatTest() {
// https://github.com/looly/hutool/pull/1234
final Document xml = XmlUtil.createXml("NODES");
xml.setXmlStandalone(true);
@ -325,22 +326,15 @@ public class XmlUtilTest {
parentNode.item(0).appendChild(parent1Node);
final String format = XmlUtil.toStr(xml,"GBK",true);
final String format = XmlUtil.toStr(xml, CharsetUtil.GBK, true);
Console.log(format);
}
@Test
public void escapeTest(){
final String a = "<>";
final String escape = XmlUtil.escape(a);
Console.log(escape);
}
@Test
public void getParamTest(){
public void getParamTest() {
final String xml = "<Config name=\"aaaa\">\n" +
" <url>222222</url>\n" +
"</Config>";
" <url>222222</url>\n" +
"</Config>";
final Document doc = XmlUtil.parseXml(xml);
final String name = doc.getDocumentElement().getAttribute("name");
@ -349,24 +343,23 @@ public class XmlUtilTest {
@Test
@Disabled
public void issueI5DO8ETest(){
public void issueI5DO8ETest() {
// 增加子节点后格式会错乱JDK的bug
XmlUtil.setNamespaceAware(false);
final String xmlStr = ResourceUtil.readUtf8Str("issueI5DO8E.xml");
final Document doc = XmlUtil.readXML(xmlStr);
final Document doc = XmlUtil.readXml(xmlStr);
final Element item = doc.createElement("item");
item.setAttribute("id", "cover-image");
final Element manifestEl = XmlUtil.getElementByXPath("//package/manifest", doc);
final Element manifestEl = XPathUtil.getElementByXPath("//package/manifest", doc);
manifestEl.appendChild(item);
Console.log(XmlUtil.format(doc));
}
@Test
public void xmlStrToBeanTest(){
public void xmlStrToBeanTest() {
final String xml = "<userInfo><name>张三</name><age>20</age><email>zhangsan@example.com</email></userInfo>";
final Document document = XmlUtil.readXML(xml);
final Document document = XmlUtil.readXml(xml);
final UserInfo userInfo = XmlUtil.xmlToBean(document, UserInfo.class);
Assertions.assertEquals("张三", userInfo.getName());
Assertions.assertEquals("20", userInfo.getAge());

View File

@ -1,6 +1,7 @@
package org.dromara.hutool.json;
import org.dromara.hutool.core.io.resource.ResourceUtil;
import org.dromara.hutool.core.xml.XPathUtil;
import org.dromara.hutool.core.xml.XmlUtil;
import org.dromara.hutool.json.xml.JSONXMLSerializer;
import org.junit.jupiter.api.Assertions;
@ -13,7 +14,7 @@ public class IssueI676ITTest {
public void parseXMLTest() {
final JSONObject jsonObject = JSONUtil.parseObj(ResourceUtil.readUtf8Str("issueI676IT.json"));
final String xmlStr = JSONXMLSerializer.toXml(jsonObject, null, (String) null);
final String content = String.valueOf(XmlUtil.getByXPath("/page/orderItems[1]/content", XmlUtil.readXML(xmlStr), XPathConstants.STRING));
final String content = String.valueOf(XPathUtil.getByXPath("/page/orderItems[1]/content", XmlUtil.readXml(xmlStr), XPathConstants.STRING));
Assertions.assertEquals(content, "bar1");
}
}