diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/DocumentBuilderUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/DocumentBuilderUtil.java new file mode 100755 index 000000000..5106e3dad --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/DocumentBuilderUtil.java @@ -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} + *

+ * 默认使用"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"
+ *

+ * + * @param namespaceAware 是否打开命名空间支持 + * @return {@link DocumentBuilderFactory} + */ + public static DocumentBuilderFactory createDocumentBuilderFactory(final boolean namespaceAware) { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + // 默认打开NamespaceAware,getElementsByTagNameNS可以使用命名空间 + factory.setNamespaceAware(namespaceAware); + return XXEUtil.disableXXE(factory); + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/SAXParserFactoryUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/SAXParserFactoryUtil.java new file mode 100755 index 000000000..de23ff5fc --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/SAXParserFactoryUtil.java @@ -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}
+ * + * + * @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); + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/UniversalNamespaceCache.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/UniversalNamespaceCache.java new file mode 100755 index 000000000..da0b5ef76 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/UniversalNamespaceCache.java @@ -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; + +/** + * 全局命名空间上下文
+ * 见: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 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 getPrefixes(final String namespaceURI) { + // Not implemented + return null; + } + +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/XPathUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XPathUtil.java new file mode 100755 index 000000000..3aac78c56 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XPathUtil.java @@ -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
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @return {@link XPath} + * @since 3.2.0 + */ + public static XPath createXPath() { + return XPathFactory.newInstance().newXPath(); + } + + /** + * 通过XPath方式读取XML节点等信息
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @param expression XPath表达式 + * @param source 资源,可以是Docunent、Node节点等 + * @return 匹配返回类型的值 + * @since 4.0.9 + */ + public static Element getElementByXPath(final String expression, final Object source) { + return (Element) getNodeByXPath(expression, source); + } + + /** + * 通过XPath方式读取XML的NodeList
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @param expression XPath表达式 + * @param source 资源,可以是Docunent、Node节点等 + * @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节点等信息
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @param expression XPath表达式 + * @param source 资源,可以是Docunent、Node节点等 + * @return 匹配返回类型的值 + * @since 4.0.9 + */ + public static Node getNodeByXPath(final String expression, final Object source) { + return (Node) getByXPath(expression, source, XPathConstants.NODE); + } + + /** + * 通过XPath方式读取XML节点等信息
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @param expression XPath表达式 + * @param source 资源,可以是Docunent、Node节点等 + * @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节点等信息
+ * Xpath相关文章:
+ * https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html
+ * https://www.ibm.com/developerworks/cn/xml/x-nmspccontext/ + * + * @param expression XPath表达式 + * @param source 资源,可以是Docunent、Node节点等 + * @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); + } + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/XXEUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XXEUtil.java new file mode 100755 index 000000000..693b10b14 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XXEUtil.java @@ -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漏洞修复相关工具类
+ * 参考:https://blog.spoock.com/2018/10/23/java-xxe/ + * + * @author looly + * @since 6.0.0 + */ +public class XXEUtil { + + /** + * 关闭XXE,避免漏洞攻击
+ * see: https://www.owasp.org/index.php/XML_External_Entity_(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; + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlConstants.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlConstants.java new file mode 100755 index 000000000..c281aeb8c --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlConstants.java @@ -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 " " -> " "} + */ + public static final String NBSP = " "; + + /** + * 字符串常量:XML And 符转义 {@code "&" -> "&"} + */ + public static final String AMP = "&"; + + /** + * 字符串常量:XML 双引号转义 {@code """ -> "\""} + */ + public static final String QUOTE = """; + + /** + * 字符串常量:XML 单引号转义 {@code "&apos" -> "'"} + */ + public static final String APOS = "'"; + + /** + * 字符串常量:XML 小于号转义 {@code "<" -> "<"} + */ + public static final String LT = "<"; + + /** + * 字符串常量:XML 大于号转义 {@code ">" -> ">"} + */ + public static final String 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; +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlFeatures.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlFeatures.java new file mode 100755 index 000000000..c1fbdcf38 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlFeatures.java @@ -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安全相关参数
+ * 见:https://blog.spoock.com/2018/10/23/java-xxe/ + * + * @author looly + * @since 6.0.0 + */ +public class XmlFeatures { + + /** + * 禁用xml中的inline DOCTYPE 声明,即禁用DTD
+ * 不允许将外部实体包含在传入的 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"; +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlMapper.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlMapper.java new file mode 100755 index 000000000..5d0f0f6f6 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlMapper.java @@ -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
+ * 如果XML根节点只有一个,且节点名和Bean的名称一致,则直接转换子节点 + * + * @param bean类型 + * @param bean bean类 + * @return bean + * @since 5.2.4 + */ + public T toBean(final Class bean) { + final Map 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 toMap() { + return toMap(new LinkedHashMap<>()); + } + + /** + * XML节点转Map + * + * @param result 结果Map + * @return map + */ + public Map toMap(final Map result) { + return toMap(this.node, result); + } + + /** + * XML节点转Map + * + * @param result 结果Map + * @return map + */ + @SuppressWarnings("unchecked") + private static Map toMap(final Node node, Map 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 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) value).add(newValue); + } else { + result.put(childEle.getNodeName(), ListUtil.of(value, newValue)); + } + } else { + result.put(childEle.getNodeName(), newValue); + } + } + } + return result; + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlSaxReader.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlSaxReader.java new file mode 100755 index 000000000..7488ae755 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlSaxReader.java @@ -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); + } + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlUtil.java index 341d4b971..4d78c9b3a 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlUtil.java @@ -13,38 +13,26 @@ 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.exception.HutoolException; import org.dromara.hutool.core.io.IORuntimeException; import org.dromara.hutool.core.io.IoUtil; import org.dromara.hutool.core.io.file.FileUtil; import org.dromara.hutool.core.lang.Assert; -import org.dromara.hutool.core.map.BiMap; -import org.dromara.hutool.core.map.MapUtil; +import org.dromara.hutool.core.regex.ReUtil; import org.dromara.hutool.core.text.StrUtil; -import org.dromara.hutool.core.text.escape.EscapeUtil; import org.dromara.hutool.core.util.CharsetUtil; -import org.w3c.dom.*; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; 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.XMLConstants; -import javax.xml.namespace.NamespaceContext; -import javax.xml.namespace.QName; -import javax.xml.parsers.*; -import javax.xml.transform.*; -import javax.xml.transform.dom.DOMSource; -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.XPathFactory; +import javax.xml.parsers.DocumentBuilder; import java.beans.XMLEncoder; import java.io.*; +import java.nio.charset.Charset; import java.util.*; /** @@ -52,115 +40,11 @@ import java.util.*; * 此工具使用w3c dom工具,不需要依赖第三方包。
* 工具类封装了XML文档的创建、读取、写出和部分XML操作 * - * @author xiaoleilu + * @author looly */ -public class XmlUtil { +public class XmlUtil extends XmlConstants { - /** - * 字符串常量:XML 不间断空格转义 {@code " " -> " "} - */ - public static final String NBSP = " "; - - /** - * 字符串常量:XML And 符转义 {@code "&" -> "&"} - */ - public static final String AMP = "&"; - - /** - * 字符串常量:XML 双引号转义 {@code """ -> "\""} - */ - public static final String QUOTE = """; - - /** - * 字符串常量:XML 单引号转义 {@code "&apos" -> "'"} - */ - public static final String APOS = "'"; - - /** - * 字符串常量:XML 小于号转义 {@code "<" -> "<"} - */ - public static final String LT = "<"; - - /** - * 字符串常量:XML 大于号转义 {@code ">" -> ">"} - */ - public static final String GT = ">"; - - /** - * 在XML中无效的字符 正则 - */ - public static final String INVALID_REGEX = "[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]"; - /** - * 在XML中注释的内容 正则 - */ - public static final String COMMENT_REGEX = "(?s)"; - /** - * XML格式化输出默认缩进量 - */ - public static final int INDENT_DEFAULT = 2; - - /** - * 默认的DocumentBuilderFactory实现 - */ - private static String defaultDocumentBuilderFactory = "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"; - - /** - * 是否打开命名空间支持 - */ - private static boolean namespaceAware = true; - /** - * Sax读取器工厂缓存 - */ - private static SAXParserFactory factory; - - /** - * 禁用默认的DocumentBuilderFactory,禁用后如果有第三方的实现(如oracle的xdb包中的xmlparse),将会自动加载实现。 - */ - synchronized public static void disableDefaultDocumentBuilderFactory() { - defaultDocumentBuilderFactory = null; - } - - /** - * 设置是否打开命名空间支持,默认打开 - * - * @param isNamespaceAware 是否命名空间支持 - * @since 5.3.1 - */ - synchronized public static void setNamespaceAware(final boolean isNamespaceAware) { - namespaceAware = isNamespaceAware; - } - - // -------------------------------------------------------------------------------------- Read - - /** - * 读取解析XML文件 - * - * @param file XML文件 - * @return XML文档对象 - */ - public static Document readXML(File file) { - Assert.notNull(file, "Xml file is null !"); - if (!file.exists()) { - throw new HutoolException("File [{}] not a exist!", file.getAbsolutePath()); - } - if (!file.isFile()) { - throw new HutoolException("[{}] not a file!", file.getAbsolutePath()); - } - - try { - file = file.getCanonicalFile(); - } catch (final IOException e) { - // ignore - } - - BufferedInputStream in = null; - try { - in = FileUtil.getInputStream(file); - return readXML(in); - } finally { - IoUtil.closeQuietly(in); - } - } + // region ----- readXml /** * 读取解析XML文件
@@ -171,12 +55,34 @@ public class XmlUtil { * @return XML文档对象 * @since 3.0.9 */ - public static Document readXML(String pathOrContent) { + public static Document readXml(String pathOrContent) { pathOrContent = StrUtil.trim(pathOrContent); if (StrUtil.startWith(pathOrContent, '<')) { return parseXml(pathOrContent); } - return readXML(FileUtil.file(pathOrContent)); + return readXml(FileUtil.file(pathOrContent)); + } + + /** + * 读取解析XML文件 + * + * @param file XML文件 + * @return XML文档对象 + */ + public static Document readXml(final File file) { + Assert.notNull(file, "Xml file is null !"); + if (!file.exists()) { + throw new HutoolException("File [{}] not a exist!", file.getAbsolutePath()); + } + if (!file.isFile()) { + throw new HutoolException("[{}] not a file!", file.getAbsolutePath()); + } + + try (final BufferedInputStream in = FileUtil.getInputStream(file)) { + return readXml(in); + } catch (final IOException e) { + throw new IORuntimeException(e); + } } /** @@ -188,8 +94,8 @@ public class XmlUtil { * @throws HutoolException IO异常或转换异常 * @since 3.0.9 */ - public static Document readXML(final InputStream inputStream) throws HutoolException { - return readXML(new InputSource(inputStream)); + public static Document readXml(final InputStream inputStream) throws HutoolException { + return readXml(new InputSource(inputStream), true); } /** @@ -200,20 +106,20 @@ public class XmlUtil { * @throws HutoolException IO异常或转换异常 * @since 3.0.9 */ - public static Document readXML(final Reader reader) throws HutoolException { - return readXML(new InputSource(reader)); + public static Document readXml(final Reader reader) throws HutoolException { + return readXml(new InputSource(reader), true); } /** * 读取解析XML文件
* 编码在XML中定义 * - * @param source {@link InputSource} + * @param namespaceAware 是否打开命名空间支持 + * @param source {@link InputSource} * @return XML文档对象 - * @since 3.0.9 */ - public static Document readXML(final InputSource source) { - final DocumentBuilder builder = createDocumentBuilder(); + public static Document readXml(final InputSource source, final boolean namespaceAware) { + final DocumentBuilder builder = DocumentBuilderUtil.createDocumentBuilder(namespaceAware); try { return builder.parse(source); } catch (final Exception e) { @@ -221,6 +127,22 @@ public class XmlUtil { } } + /** + * 将String类型的XML转换为XML文档 + * + * @param xmlStr XML字符串 + * @return XML文档 + */ + public static Document parseXml(final String xmlStr) { + if (StrUtil.isBlank(xmlStr)) { + throw new IllegalArgumentException("XML content string is empty !"); + } + return readXml(StrUtil.getReader(cleanInvalid(xmlStr))); + } + // endregion + + // region ----- readBySax + /** * 使用Sax方式读取指定的XML
* 如果用户传入的contentHandler为{@link DefaultHandler},则其接口都会被处理 @@ -280,67 +202,11 @@ public class XmlUtil { * @since 5.4.4 */ public static void readBySax(final InputSource source, final ContentHandler contentHandler) { - // 1.获取解析工厂 - if (null == factory) { - factory = SAXParserFactory.newInstance(); - factory.setValidating(false); - factory.setNamespaceAware(namespaceAware); - - // https://blog.spoock.com/2018/10/23/java-xxe/ - try{ - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - factory.setFeature("http://xml.org/sax/features/external-general-entities", false); - factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - } catch (final Exception ignore){ - // ignore - } - } - // 2.从解析工厂获取解析器 - final SAXParser parse; - final XMLReader reader; - try { - parse = factory.newSAXParser(); - if (contentHandler instanceof DefaultHandler) { - parse.parse(source, (DefaultHandler) contentHandler); - return; - } - - // 3.得到解读器 - reader = parse.getXMLReader(); - // 防止XEE攻击,见:https://www.jianshu.com/p/1a857905b22c - // https://blog.spoock.com/2018/10/23/java-xxe/ - reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - // 忽略外部DTD - reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",false); - // 不包括外部一般实体。 - reader.setFeature("http://xml.org/sax/features/external-general-entities",false); - // 不包含外部参数实体或外部DTD子集。 - reader.setFeature("http://xml.org/sax/features/external-parameter-entities",false); - - reader.setContentHandler(contentHandler); - reader.parse(source); - } catch (final ParserConfigurationException | SAXException e) { - throw new HutoolException(e); - } catch (final IOException e) { - throw new IORuntimeException(e); - } + XmlSaxReader.of(source).read(contentHandler); } + // endregion - /** - * 将String类型的XML转换为XML文档 - * - * @param xmlStr XML字符串 - * @return XML文档 - */ - public static Document parseXml(String xmlStr) { - if (StrUtil.isBlank(xmlStr)) { - throw new IllegalArgumentException("XML content string is empty !"); - } - xmlStr = cleanInvalid(xmlStr); - return readXML(StrUtil.getReader(xmlStr)); - } - // -------------------------------------------------------------------------------------- Write + // region ----- write /** * 将XML文档转换为String
@@ -355,18 +221,6 @@ public class XmlUtil { return toStr(doc, false); } - /** - * 将XML文档转换为String
- * 字符编码使用XML文档中的编码,获取不到则使用UTF-8
- * 默认非格式化输出,若想格式化请使用{@link #format(Document)} - * - * @param doc XML文档 - * @return XML字符串 - */ - public static String toStr(final Document doc) { - return toStr((Node) doc); - } - /** * 将XML文档转换为String
* 字符编码使用XML文档中的编码,获取不到则使用UTF-8 @@ -377,20 +231,7 @@ public class XmlUtil { * @since 5.4.5 */ public static String toStr(final Node doc, final boolean isPretty) { - return toStr(doc, CharsetUtil.NAME_UTF_8, isPretty); - } - - /** - * 将XML文档转换为String
- * 字符编码使用XML文档中的编码,获取不到则使用UTF-8 - * - * @param doc XML文档 - * @param isPretty 是否格式化输出 - * @return XML字符串 - * @since 3.0.9 - */ - public static String toStr(final Document doc, final boolean isPretty) { - return toStr((Node) doc, isPretty); + return toStr(doc, CharsetUtil.UTF_8, isPretty); } /** @@ -403,24 +244,10 @@ public class XmlUtil { * @return XML字符串 * @since 5.4.5 */ - public static String toStr(final Node doc, final String charset, final boolean isPretty) { + public static String toStr(final Node doc, final Charset charset, final boolean isPretty) { return toStr(doc, charset, isPretty, false); } - /** - * 将XML文档转换为String
- * 字符编码使用XML文档中的编码,获取不到则使用UTF-8 - * - * @param doc XML文档 - * @param charset 编码 - * @param isPretty 是否格式化输出 - * @return XML字符串 - * @since 3.0.9 - */ - public static String toStr(final Document doc, final String charset, final boolean isPretty) { - return toStr((Node) doc, charset, isPretty); - } - /** * 将XML文档转换为String
* 字符编码使用XML文档中的编码,获取不到则使用UTF-8 @@ -432,13 +259,9 @@ public class XmlUtil { * @return XML字符串 * @since 5.1.2 */ - public static String toStr(final Node doc, final String charset, final boolean isPretty, final boolean omitXmlDeclaration) { + public static String toStr(final Node doc, final Charset charset, final boolean isPretty, final boolean omitXmlDeclaration) { final StringWriter writer = StrUtil.getWriter(); - try { - write(doc, writer, charset, isPretty ? INDENT_DEFAULT : 0, omitXmlDeclaration); - } catch (final Exception e) { - throw new HutoolException(e, "Trans xml document to string error!"); - } + write(doc, writer, charset, isPretty ? INDENT_DEFAULT : 0, omitXmlDeclaration); return writer.toString(); } @@ -468,35 +291,16 @@ public class XmlUtil { * 将XML文档写入到文件
* 使用Document中的编码 * - * @param doc XML文档 - * @param absolutePath 文件绝对路径,不存在会自动创建 - */ - public static void toFile(final Document doc, final String absolutePath) { - toFile(doc, absolutePath, null); - } - - /** - * 将XML文档写入到文件
- * * @param doc XML文档 - * @param path 文件路径绝对路径或相对ClassPath路径,不存在会自动创建 - * @param charsetName 自定义XML文件的编码,如果为{@code null} 读取XML文档中的编码,否则默认UTF-8 + * @param file 文件 + * @param charset 编码 */ - public static void toFile(final Document doc, final String path, String charsetName) { - if (StrUtil.isBlank(charsetName)) { - charsetName = doc.getXmlEncoding(); - } - if (StrUtil.isBlank(charsetName)) { - charsetName = CharsetUtil.NAME_UTF_8; - } - - BufferedWriter writer = null; - try { - writer = FileUtil.getWriter(path, CharsetUtil.charset(charsetName), false); - write(doc, writer, charsetName, INDENT_DEFAULT); - } finally { - IoUtil.closeQuietly(writer); - } + public static void write(final Document doc, final File file, final Charset charset) { + XmlWriter.of(doc) + .setCharset(charset) + .setIndent(INDENT_DEFAULT) + .setOmitXmlDeclaration(false) + .write(file); } /** @@ -508,8 +312,8 @@ public class XmlUtil { * @param indent 格式化输出中缩进量,小于1表示不格式化输出 * @since 3.0.9 */ - public static void write(final Node node, final Writer writer, final String charset, final int indent) { - transform(new DOMSource(node), new StreamResult(writer), charset, indent); + public static void write(final Node node, final Writer writer, final Charset charset, final int indent) { + write(node, writer, charset, indent, false); } /** @@ -522,8 +326,12 @@ public class XmlUtil { * @param omitXmlDeclaration 是否输出 xml Declaration * @since 5.1.2 */ - public static void write(final Node node, final Writer writer, final String charset, final int indent, final boolean omitXmlDeclaration) { - transform(new DOMSource(node), new StreamResult(writer), charset, indent, omitXmlDeclaration); + public static void write(final Node node, final Writer writer, final Charset charset, final int indent, final boolean omitXmlDeclaration) { + XmlWriter.of(node) + .setCharset(charset) + .setIndent(indent) + .setOmitXmlDeclaration(omitXmlDeclaration) + .write(writer); } /** @@ -535,8 +343,8 @@ public class XmlUtil { * @param indent 格式化输出中缩进量,小于1表示不格式化输出 * @since 4.0.8 */ - public static void write(final Node node, final OutputStream out, final String charset, final int indent) { - transform(new DOMSource(node), new StreamResult(out), charset, indent); + public static void write(final Node node, final OutputStream out, final Charset charset, final int indent) { + write(node, out, charset, indent, false); } /** @@ -549,60 +357,17 @@ public class XmlUtil { * @param omitXmlDeclaration 是否输出 xml Declaration * @since 5.1.2 */ - public static void write(final Node node, final OutputStream out, final String charset, final int indent, final boolean omitXmlDeclaration) { - transform(new DOMSource(node), new StreamResult(out), charset, indent, omitXmlDeclaration); + public static void write(final Node node, final OutputStream out, + final Charset charset, final int indent, final boolean omitXmlDeclaration) { + XmlWriter.of(node) + .setCharset(charset) + .setIndent(indent) + .setOmitXmlDeclaration(omitXmlDeclaration) + .write(out); } + // endregion - /** - * 将XML文档写出
- * 格式化输出逻辑参考:https://stackoverflow.com/questions/139076/how-to-pretty-print-xml-from-java - * - * @param source 源 - * @param result 目标 - * @param charset 编码 - * @param indent 格式化输出中缩进量,小于1表示不格式化输出 - * @since 4.0.9 - */ - public static void transform(final Source source, final Result result, final String charset, final int indent) { - transform(source, result, charset, indent, false); - } - - /** - * 将XML文档写出
- * 格式化输出逻辑参考:https://stackoverflow.com/questions/139076/how-to-pretty-print-xml-from-java - * - * @param source 源 - * @param result 目标 - * @param charset 编码 - * @param indent 格式化输出中缩进量,小于1表示不格式化输出 - * @param omitXmlDeclaration 是否输出 xml Declaration - * @since 5.1.2 - */ - public static void transform(final Source source, final Result result, final String charset, final int indent, final boolean omitXmlDeclaration) { - final TransformerFactory factory = TransformerFactory.newInstance(); - try { - // 防止XXE攻击,见:https://www.jianshu.com/p/1a857905b22c - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - 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 (StrUtil.isNotBlank(charset)) { - xformer.setOutputProperty(OutputKeys.ENCODING, charset); - } - 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!"); - } - } - - // -------------------------------------------------------------------------------------- Create + // region ----- create /** * 创建XML文档
@@ -612,45 +377,7 @@ public class XmlUtil { * @since 4.0.8 */ public static Document createXml() { - return createDocumentBuilder().newDocument(); - } - - /** - * 创建 DocumentBuilder - * - * @return DocumentBuilder - * @since 4.1.2 - */ - public static DocumentBuilder createDocumentBuilder() { - final DocumentBuilder builder; - try { - builder = createDocumentBuilderFactory().newDocumentBuilder(); - } catch (final Exception e) { - throw new HutoolException(e, "Create xml document error!"); - } - return builder; - } - - /** - * 创建{@link DocumentBuilderFactory} - *

- * 默认使用"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"
- * 如果使用第三方实现,请调用{@link #disableDefaultDocumentBuilderFactory()} - *

- * - * @return {@link DocumentBuilderFactory} - */ - public static DocumentBuilderFactory createDocumentBuilderFactory() { - final DocumentBuilderFactory factory; - if (StrUtil.isNotEmpty(defaultDocumentBuilderFactory)) { - factory = DocumentBuilderFactory.newInstance(defaultDocumentBuilderFactory, null); - } else { - factory = DocumentBuilderFactory.newInstance(); - } - - // 默认打开NamespaceAware,getElementsByTagNameNS可以使用命名空间 - factory.setNamespaceAware(namespaceAware); - return disableXXE(factory); + return DocumentBuilderUtil.createDocumentBuilder(true).newDocument(); } /** @@ -678,6 +405,7 @@ public class XmlUtil { doc.appendChild(null == namespace ? doc.createElement(rootElementName) : doc.createElementNS(namespace, rootElementName)); return doc; } + // endregion // -------------------------------------------------------------------------------------- Function @@ -714,7 +442,8 @@ public class XmlUtil { if (xmlContent == null) { return null; } - return xmlContent.replaceAll(INVALID_REGEX, ""); + + return ReUtil.replaceAll(xmlContent, INVALID_PATTERN, StrUtil.EMPTY); } /** @@ -728,7 +457,7 @@ public class XmlUtil { if (xmlContent == null) { return null; } - return xmlContent.replaceAll(COMMENT_REGEX, StrUtil.EMPTY); + return ReUtil.replaceAll(xmlContent, COMMENT_PATTERN, StrUtil.EMPTY); } /** @@ -844,131 +573,19 @@ public class XmlUtil { } } + // region ----- xmlToMap or xmlToBean /** - * 创建XPath
- * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * XML转Java Bean
+ * 如果XML根节点只有一个,且节点名和Bean的名称一致,则直接转换子节点 * - * @return {@link XPath} - * @since 3.2.0 + * @param bean类型 + * @param node XML节点 + * @param beanClass bean类 + * @return bean + * @since 5.2.4 */ - public static XPath createXPath() { - return XPathFactory.newInstance().newXPath(); - } - - /** - * 通过XPath方式读取XML节点等信息
- * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html - * - * @param expression XPath表达式 - * @param source 资源,可以是Docunent、Node节点等 - * @return 匹配返回类型的值 - * @since 4.0.9 - */ - public static Element getElementByXPath(final String expression, final Object source) { - return (Element) getNodeByXPath(expression, source); - } - - /** - * 通过XPath方式读取XML的NodeList
- * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html - * - * @param expression XPath表达式 - * @param source 资源,可以是Docunent、Node节点等 - * @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节点等信息
- * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html - * - * @param expression XPath表达式 - * @param source 资源,可以是Docunent、Node节点等 - * @return 匹配返回类型的值 - * @since 4.0.9 - */ - public static Node getNodeByXPath(final String expression, final Object source) { - return (Node) getByXPath(expression, source, XPathConstants.NODE); - } - - /** - * 通过XPath方式读取XML节点等信息
- * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html - * - * @param expression XPath表达式 - * @param source 资源,可以是Docunent、Node节点等 - * @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节点等信息
- * Xpath相关文章:
- * https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html
- * https://www.ibm.com/developerworks/cn/xml/x-nmspccontext/ - * - * @param expression XPath表达式 - * @param source 资源,可以是Docunent、Node节点等 - * @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); - } - } - - /** - * 转义XML特殊字符: - * - *
-	 * & (ampersand) 替换为 &amp;
-	 * < (小于) 替换为 &lt;
-	 * > (大于) 替换为 &gt;
-	 * " (双引号) 替换为 &quot;
-	 * 
- * - * @param string 被替换的字符串 - * @return 替换后的字符串 - * @since 4.0.8 - */ - public static String escape(final String string) { - return EscapeUtil.escapeHtml4(string); - } - - /** - * 反转义XML特殊字符: - * - * @param string 被替换的字符串 - * @return 替换后的字符串 - * @see EscapeUtil#unescape(String) - * @since 5.0.6 - */ - public static String unescape(final String string) { - return EscapeUtil.unescapeHtml4(string); + public static T xmlToBean(final Node node, final Class beanClass) { + return XmlMapper.of(node).toBean(beanClass); } /** @@ -979,40 +596,7 @@ public class XmlUtil { * @since 4.0.8 */ public static Map xmlToMap(final String xmlStr) { - return xmlToMap(xmlStr, new HashMap<>()); - } - - /** - * XML转Java Bean
- * 如果XML根节点只有一个,且节点名和Bean的名称一致,则直接转换子节点 - * - * @param bean类型 - * @param node XML节点 - * @param bean bean类 - * @return bean - * @since 5.2.4 - */ - public static T xmlToBean(final Node node, final Class bean) { - final Map map = xmlToMap(node); - 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 - * - * @param node XML节点 - * @return XML数据转换后的Map - * @since 4.0.8 - */ - public static Map xmlToMap(final Node node) { - return xmlToMap(node, new HashMap<>()); + return xmlToMap(xmlStr, new LinkedHashMap<>()); } /** @@ -1032,6 +616,17 @@ public class XmlUtil { return xmlToMap(root, result); } + /** + * XML格式字符串转换为Map + * + * @param node XML节点 + * @return XML数据转换后的Map + * @since 4.0.8 + */ + public static Map xmlToMap(final Node node) { + return xmlToMap(node, new LinkedHashMap<>()); + } + /** * XML节点转换为Map * @@ -1040,52 +635,13 @@ public class XmlUtil { * @return XML数据转换后的Map * @since 4.0.8 */ - @SuppressWarnings("unchecked") - public static Map xmlToMap(final Node node, Map 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 (!isElement(childNode)) { - continue; - } - - childEle = (Element) childNode; - final Object value = result.get(childEle.getNodeName()); - final Object newValue; - if (childEle.hasChildNodes()) { - // 子节点继续递归遍历 - final Map map = xmlToMap(childEle); - 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) value).add(newValue); - } else { - result.put(childEle.getNodeName(), ListUtil.of(value, newValue)); - } - } else { - result.put(childEle.getNodeName(), newValue); - } - } - } + public static Map xmlToMap(final Node node, final Map result) { + XmlMapper.of(node).toMap(result); return result; } + // endregion + // region ----- toXml /** * 将Map转换为XML格式的字符串 * @@ -1106,7 +662,7 @@ public class XmlUtil { * @since 5.1.2 */ public static String mapToXmlStr(final Map data, final boolean omitXmlDeclaration) { - return toStr(mapToXml(data, "xml"), CharsetUtil.NAME_UTF_8, false, omitXmlDeclaration); + return toStr(mapToXml(data, "xml"), CharsetUtil.UTF_8, false, omitXmlDeclaration); } /** @@ -1145,7 +701,7 @@ public class XmlUtil { * @since 5.1.2 */ public static String mapToXmlStr(final Map data, final String rootName, final String namespace, final boolean omitXmlDeclaration) { - return toStr(mapToXml(data, rootName, namespace), CharsetUtil.NAME_UTF_8, false, omitXmlDeclaration); + return toStr(mapToXml(data, rootName, namespace), CharsetUtil.UTF_8, false, omitXmlDeclaration); } /** @@ -1160,7 +716,7 @@ public class XmlUtil { * @since 5.1.2 */ public static String mapToXmlStr(final Map data, final String rootName, final String namespace, final boolean isPretty, final boolean omitXmlDeclaration) { - return toStr(mapToXml(data, rootName, namespace), CharsetUtil.NAME_UTF_8, isPretty, omitXmlDeclaration); + return toStr(mapToXml(data, rootName, namespace), CharsetUtil.UTF_8, isPretty, omitXmlDeclaration); } /** @@ -1175,7 +731,7 @@ public class XmlUtil { * @return XML格式的字符串 * @since 5.1.2 */ - public static String mapToXmlStr(final Map data, final String rootName, final String namespace, final String charset, final boolean isPretty, final boolean omitXmlDeclaration) { + public static String mapToXmlStr(final Map data, final String rootName, final String namespace, final Charset charset, final boolean isPretty, final boolean omitXmlDeclaration) { return toStr(mapToXml(data, rootName, namespace), charset, isPretty, omitXmlDeclaration); } @@ -1245,8 +801,9 @@ public class XmlUtil { return null; } return mapToXml(BeanUtil.beanToMap(bean, false, ignoreNull), - bean.getClass().getSimpleName(), namespace); + bean.getClass().getSimpleName(), namespace); } + // endregion /** * 给定节点是否为{@link Element} 类型节点 @@ -1259,6 +816,7 @@ public class XmlUtil { return (null != node) && Node.ELEMENT_NODE == node.getNodeType(); } + // region ----- append /** * 在已有节点上创建子节点 * @@ -1309,6 +867,8 @@ public class XmlUtil { public static void append(final Node node, final Object data) { append(getOwnerDocument(node), node, data); } + // endregion + // ---------------------------------------------------------------------------------------- Private method start /** @@ -1391,149 +951,6 @@ public class XmlUtil { private static Node appendText(final Document doc, final Node node, final CharSequence text) { return node.appendChild(doc.createTextNode(StrUtil.str(text))); } - - /** - * 关闭XXE,避免漏洞攻击
- * see: https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#JAXP_DocumentBuilderFactory.2C_SAXParserFactory_and_DOM4J - * - * @param dbf DocumentBuilderFactory - * @return DocumentBuilderFactory - */ - private static DocumentBuilderFactory disableXXE(final DocumentBuilderFactory dbf) { - String feature; - 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 - feature = "http://apache.org/xml/features/disallow-doctype-decl"; - dbf.setFeature(feature, 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 - feature = "http://xml.org/sax/features/external-general-entities"; - dbf.setFeature(feature, 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 - feature = "http://xml.org/sax/features/external-parameter-entities"; - dbf.setFeature(feature, false); - // Disable external DTDs as well - feature = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; - dbf.setFeature(feature, false); - // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" - dbf.setXIncludeAware(false); - dbf.setExpandEntityReferences(false); - } catch (final ParserConfigurationException e) { - // ignore - } - return dbf; - } - - /** - * 全局命名空间上下文
- * 见:https://www.ibm.com/developerworks/cn/xml/x-nmspccontext/ - */ - public static class UniversalNamespaceCache implements NamespaceContext { - private static final String DEFAULT_NS = "DEFAULT"; - private final BiMap 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 getPrefixes(final String namespaceURI) { - // Not implemented - return null; - } - - } // ---------------------------------------------------------------------------------------- Private method end } diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlWriter.java b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlWriter.java new file mode 100755 index 000000000..0427b6dfe --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/xml/XmlWriter.java @@ -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文档写出
+ * 格式化输出逻辑参考: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!"); + } + } +} diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/text/escape/EscapeUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/text/escape/EscapeUtilTest.java index d09256435..83bd910b2 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/text/escape/EscapeUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/text/escape/EscapeUtilTest.java @@ -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); + } } diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/util/XmlUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/xml/XmlUtilTest.java similarity index 74% rename from hutool-core/src/test/java/org/dromara/hutool/core/util/XmlUtilTest.java rename to hutool-core/src/test/java/org/dromara/hutool/core/xml/XmlUtilTest.java index ac6aebe12..c4b7b1b7a 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/util/XmlUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/xml/XmlUtilTest.java @@ -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 = ""// - + ""// - + "Success"// - + "ok"// - + "1490"// - + "885"// - + "1"// - + ""; + + ""// + + "Success"// + + "ok"// + + "1490"// + + "885"// + + "1"// + + ""; 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 = ""// - + ""// - + "Success(成功)"// - + "ok"// - + "1490"// - + "885"// - + "1"// - + ""; + + ""// + + "Success(成功)"// + + "ok"// + + "1490"// + + "885"// + + "1"// + + ""; 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 = ""// - + ""// - + "Success(成功)"// - + "ok"// - + "1490"// - + "885"// - + "1"// - + ""; + + ""// + + "Success(成功)"// + + "ok"// + + "1490"// + + "885"// + + "1"// + + ""; 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 = ""// - + ""// - + "Success"// - + "ok"// - + "1490"// - + "885"// - + "1"// - + "subText"// - + ""; + + ""// + + "Success"// + + "ok"// + + "1490"// + + "885"// + + "1"// + + "subText"// + + ""; final Map map = XmlUtil.xmlToMap(xml); Assertions.assertEquals(6, map.size()); @@ -130,51 +131,51 @@ public class XmlUtilTest { @Test public void mapToXmlTest() { final Map map = MapBuilder.of(new LinkedHashMap())// - .put("name", "张三")// - .put("age", 12)// - .put("game", MapUtil.builder(new LinkedHashMap()).put("昵称", "Looly").put("level", 14).build())// - .build(); + .put("name", "张三")// + .put("age", 12)// + .put("game", MapUtil.builder(new LinkedHashMap()).put("昵称", "Looly").put("level", 14).build())// + .build(); final Document doc = XmlUtil.mapToXml(map, "user"); // Console.log(XmlUtil.toStr(doc, false)); Assertions.assertEquals(""// - + ""// - + "张三"// - + "12"// - + ""// - + "<昵称>Looly"// - + "14"// - + ""// - + "", // - XmlUtil.toStr(doc, false)); + + ""// + + "张三"// + + "12"// + + ""// + + "<昵称>Looly"// + + "14"// + + ""// + + "", // + XmlUtil.toStr(doc, false)); } @Test public void mapToXmlTest2() { // 测试List final Map map = MapBuilder.of(new LinkedHashMap()) - .put("Town", ListUtil.of("town1", "town2")) - .build(); + .put("Town", ListUtil.of("town1", "town2")) + .build(); final Document doc = XmlUtil.mapToXml(map, "City"); Assertions.assertEquals("" + - "" + - "town1" + - "town2" + - "", - XmlUtil.toStr(doc)); + "" + + "town1" + + "town2" + + "", + 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 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 map = MapBuilder.of(new LinkedHashMap()) - .put("name", "ddatsh") - .build(); + .put("name", "ddatsh") + .build(); final String xml = XmlUtil.mapToXmlStr(map, true); Assertions.assertEquals("ddatsh", xml); } @@ -195,18 +196,18 @@ public class XmlUtilTest { @Test public void getByPathTest() { final String xmlStr = "\n" + - "\n" + - " \n" + - " \n" + - " 2020/04/15 21:01:21\n" + - " \n" + - " \n" + - "\n"; + "\n" + + " \n" + + " \n" + + " 2020/04/15 21:01:21\n" + + " \n" + + " \n" + + "\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 = "\n" + - " 222222\n" + - ""; + " 222222\n" + + ""; 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 = "张三20zhangsan@example.com"; - 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()); diff --git a/hutool-json/src/test/java/org/dromara/hutool/json/IssueI676ITTest.java b/hutool-json/src/test/java/org/dromara/hutool/json/IssueI676ITTest.java index 4cbf87eaf..67592c29d 100644 --- a/hutool-json/src/test/java/org/dromara/hutool/json/IssueI676ITTest.java +++ b/hutool-json/src/test/java/org/dromara/hutool/json/IssueI676ITTest.java @@ -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"); } }