From 166d46d137c60854ca9bbf0db9131b04a015ceca Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 31 Aug 2021 10:15:54 +0800 Subject: [PATCH] add XML content custom support --- CHANGELOG.md | 5 +- .../java/cn/hutool/json/InternalJSONUtil.java | 45 +-- .../src/main/java/cn/hutool/json/XML.java | 323 ++---------------- .../cn/hutool/json/xml/JSONXMLParser.java | 182 ++++++++++ .../cn/hutool/json/xml/JSONXMLSerializer.java | 159 +++++++++ .../java/cn/hutool/json/xml/package-info.java | 6 + .../cn/hutool/json/{ => xml}/XMLTest.java | 15 +- 7 files changed, 417 insertions(+), 318 deletions(-) create mode 100644 hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLSerializer.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/xml/package-info.java rename hutool-json/src/test/java/cn/hutool/json/{ => xml}/XMLTest.java (58%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 101ebcf48..66d048ca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,14 @@ ------------------------------------------------------------------------------------------------------------- -# 5.7.11 (2021-08-30) +# 5.7.11 (2021-08-31) ### 🐣新特性 * 【crypto 】 修改SymmetricCrypto初始化逻辑 * 【core 】 FileTypeUtil增加对wps编辑的docx的识别(issue#I47JGH@Gitee) * 【core 】 Money修改构造,0表示读取所有分(issue#1796@Github) - +* 【json 】 增加JSONXMLParser和JSONXMLSerializer +* 【json 】 XML支持自定义内容标签(issue#I47TV8@Gitee) ### 🐞Bug修复 * 【cron 】 **重要**修复Scheduler启动默认线程池为null的bug(issue#I47PZW@Gitee) diff --git a/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java index 13ac191ea..960b6d9ef 100644 --- a/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java +++ b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java @@ -1,6 +1,7 @@ package cn.hutool.json; import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.ObjectUtil; @@ -17,7 +18,7 @@ import java.util.SortedMap; * * @author Looly */ -final class InternalJSONUtil { +public final class InternalJSONUtil { private InternalJSONUtil() { } @@ -28,27 +29,30 @@ final class InternalJSONUtil { * @param obj 被检查的对象 * @throws JSONException If o is a non-finite number. */ - protected static void testValidity(Object obj) throws JSONException { + static void testValidity(Object obj) throws JSONException { if (false == ObjectUtil.isValidIfNumber(obj)) { throw new JSONException("JSON does not allow non-finite numbers."); } } /** - * 值转为String,用于JSON中。 If the object has an value.toJSONString() method, then that method will be used to produce the JSON text.
- * The method is required to produce a strictly conforming text.
- * If the object does not contain a toJSONString method (which is the most common case), then a text will be produced by other means.
- * If the value is an array or Collection, then a JSONArray will be made from it and its toJSONString method will be called.
- * If the value is a MAP, then a JSONObject will be made from it and its toJSONString method will be called.
- * Otherwise, the value's toString method will be called, and the result will be quoted.
+ * 值转为String,用于JSON中。规则为: + * * * @param value 需要转为字符串的对象 * @return 字符串 * @throws JSONException If the value is or contains an invalid number. */ - protected static String valueToString(Object value) throws JSONException { + static String valueToString(Object value) throws JSONException { if (value == null || value instanceof JSONNull) { - return "null"; + return JSONNull.NULL.toString(); } if (value instanceof JSONString) { try { @@ -66,7 +70,7 @@ final class InternalJSONUtil { } else if (value instanceof Collection) { Collection coll = (Collection) value; return new JSONArray(coll).toString(); - } else if (value.getClass().isArray()) { + } else if (ArrayUtil.isArray(value)) { return new JSONArray(value).toString(); } else { return JSONUtil.quote(value.toString()); @@ -79,16 +83,13 @@ final class InternalJSONUtil { * @param string A String. * @return A simple JSON value. */ - protected static Object stringToValue(String string) { + public static Object stringToValue(String string) { // null处理 - if (null == string || "null".equalsIgnoreCase(string)) { + if (StrUtil.isEmpty(string) || StrUtil.NULL.equalsIgnoreCase(string)) { return JSONNull.NULL; } // boolean处理 - if (0 == string.length()) { - return StrUtil.EMPTY; - } if ("true".equalsIgnoreCase(string)) { return Boolean.TRUE; } @@ -130,7 +131,7 @@ final class InternalJSONUtil { * @param value 值 * @return JSONObject */ - protected static JSONObject propertyPut(JSONObject jsonObject, Object key, Object value) { + static JSONObject propertyPut(JSONObject jsonObject, Object key, Object value) { final String[] path = StrUtil.splitToArray(Convert.toStr(key), CharUtil.DOT); int last = path.length - 1; JSONObject target = jsonObject; @@ -160,7 +161,7 @@ final class InternalJSONUtil { * @return 是否忽略null值 * @since 4.3.1 */ - protected static boolean defaultIgnoreNullValue(Object obj) { + static boolean defaultIgnoreNullValue(Object obj) { return (false == (obj instanceof CharSequence))// && (false == (obj instanceof JSONTokener))// && (false == (obj instanceof Map)); @@ -178,12 +179,12 @@ final class InternalJSONUtil { * @return 是否有序 * @since 5.7.0 */ - protected static boolean isOrder(Object value){ - if(value instanceof LinkedHashMap || value instanceof SortedMap){ + static boolean isOrder(Object value) { + if (value instanceof LinkedHashMap || value instanceof SortedMap) { return true; - } else if(value instanceof JSONGetter){ + } else if (value instanceof JSONGetter) { final JSONConfig config = ((JSONGetter) value).getConfig(); - if(null != config){ + if (null != config) { return config.isOrder(); } } diff --git a/hutool-json/src/main/java/cn/hutool/json/XML.java b/hutool-json/src/main/java/cn/hutool/json/XML.java index 7d7b9e967..570f20d92 100644 --- a/hutool-json/src/main/java/cn/hutool/json/XML.java +++ b/hutool-json/src/main/java/cn/hutool/json/XML.java @@ -1,16 +1,15 @@ package cn.hutool.json; -import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.CharUtil; -import cn.hutool.core.util.EscapeUtil; -import cn.hutool.core.util.StrUtil; - -import java.util.Iterator; +import cn.hutool.json.xml.JSONXMLParser; +import cn.hutool.json.xml.JSONXMLSerializer; /** * 提供静态方法在XML和JSONObject之间转换 * - * @author JSON.org + * @author JSON.org, looly + * @see JSONXMLParser + * @see JSONXMLSerializer */ public class XML { @@ -92,189 +91,23 @@ public class XML { * 转换过程中一些信息可能会丢失,JSON中无法区分节点和属性,相同的节点将被处理为JSONArray。 * * @param jo JSONObject - * @param string XML字符串 - * @param keepStrings If true, then values will not be coerced into boolean or numeric values and will instead be left as strings - * @return A JSONObject containing the structured data from the XML string. - * @throws JSONException Thrown if there is an errors while parsing the string + * @param xmlStr XML字符串 + * @param keepStrings 如果为{@code true},则值保持String类型,不转换为数字或boolean + * @return A JSONObject 解析后的JSON对象,与传入的jo为同一对象 + * @throws JSONException 解析异常 * @since 5.3.1 */ - public static JSONObject toJSONObject(JSONObject jo, String string, boolean keepStrings) throws JSONException { - XMLTokener x = new XMLTokener(string, jo.getConfig()); - while (x.more() && x.skipPast("<")) { - parse(x, jo, null, keepStrings); - } + public static JSONObject toJSONObject(JSONObject jo, String xmlStr, boolean keepStrings) throws JSONException { + JSONXMLParser.parseJSONObject(jo, xmlStr, keepStrings); return jo; } - /** - * Scan the content following the named tag, attaching it to the context. - * - * @param x The XMLTokener containing the source string. - * @param context The JSONObject that will include the new material. - * @param name The tag name. - * @return true if the close tag is processed. - * @throws JSONException JSON异常 - */ - private static boolean parse(XMLTokener x, JSONObject context, String name, boolean keepStrings) throws JSONException { - char c; - int i; - JSONObject jsonobject; - String string; - String tagName; - Object token; - - // Test for and skip past these forms: - // - // - // - // - // Report errors for these forms: - // <> - // <= - // << - - token = x.nextToken(); - - // "); - return false; - } - x.back(); - } else if (c == '[') { - token = x.nextToken(); - if ("CDATA".equals(token)) { - if (x.next() == '[') { - string = x.nextCDATA(); - if (string.length() > 0) { - context.accumulate("content", string); - } - return false; - } - } - throw x.syntaxError("Expected 'CDATA['"); - } - i = 1; - do { - token = x.nextMeta(); - if (token == null) { - throw x.syntaxError("Missing '>' after ' 0); - return false; - } else if (token == QUEST) { - - // "); - return false; - } else if (token == SLASH) { - - // Close tag - if (x.nextToken() != GT) { - throw x.syntaxError("Misshaped tag"); - } - if (jsonobject.size() > 0) { - context.accumulate(tagName, jsonobject); - } else { - context.accumulate(tagName, ""); - } - return false; - - } else if (token == GT) { - // Content, between <...> and - for (; ; ) { - token = x.nextContent(); - if (token == null) { - if (tagName != null) { - throw x.syntaxError("Unclosed tag " + tagName); - } - return false; - } else if (token instanceof String) { - string = (String) token; - if (string.length() > 0) { - jsonobject.accumulate("content", keepStrings ? token : InternalJSONUtil.stringToValue(string)); - } - - } else if (token == LT) { - // Nested element - if (parse(x, jsonobject, tagName, keepStrings)) { - if (jsonobject.size() == 0) { - context.accumulate(tagName, ""); - } else if (jsonobject.size() == 1 && jsonobject.get("content") != null) { - context.accumulate(tagName, jsonobject.get("content")); - } else { - context.accumulate(tagName, jsonobject); - } - return false; - } - } - } - } else { - throw x.syntaxError("Misshaped tag"); - } - } - } - } - /** * 转换JSONObject为XML - * Convert a JSONObject into a well-formed, element-normal XML string. * - * @param object A JSONObject. - * @return A string. - * @throws JSONException Thrown if there is an error parsing the string + * @param object JSON对象或数组 + * @return XML字符串 + * @throws JSONException JSON解析异常 */ public static String toXml(Object object) throws JSONException { return toXml(object, null); @@ -282,122 +115,26 @@ public class XML { /** * 转换JSONObject为XML - * Convert a JSONObject into a well-formed, element-normal XML string. * - * @param object A JSONObject. - * @param tagName The optional name of the enclosing tag. + * @param object JSON对象或数组 + * @param tagName 可选标签名称,名称为空时忽略标签 * @return A string. - * @throws JSONException Thrown if there is an error parsing the string + * @throws JSONException JSON解析异常 */ public static String toXml(Object object, String tagName) throws JSONException { - if (null == object) { - return null; - } - - StringBuilder sb = new StringBuilder(); - JSONArray ja; - JSONObject jo; - String key; - Iterator keys; - Object value; - - if (object instanceof JSONObject) { - - // Emit - if (tagName != null) { - sb.append('<'); - sb.append(tagName); - sb.append('>'); - } - - // Loop thru the keys. - jo = (JSONObject) object; - keys = jo.keySet().iterator(); - while (keys.hasNext()) { - key = keys.next(); - value = jo.get(key); - if (value == null) { - value = StrUtil.EMPTY; - } else if (ArrayUtil.isArray(value)) { - value = new JSONArray(value); - } - - // Emit content in body - if ("content".equals(key)) { - if (value instanceof JSONArray) { - ja = (JSONArray) value; - int i = 0; - for (Object val : ja) { - if (i > 0) { - sb.append('\n'); - } - sb.append(EscapeUtil.escapeXml(val.toString())); - i++; - } - } else { - sb.append(EscapeUtil.escapeXml(value.toString())); - } - - // Emit an array of similar keys - - } else if (value instanceof JSONArray) { - ja = (JSONArray) value; - for (Object val : ja) { - if (val instanceof JSONArray) { - sb.append('<'); - sb.append(key); - sb.append('>'); - sb.append(toXml(val)); - sb.append("'); - } else { - sb.append(toXml(val, key)); - } - } - } else if ("".equals(value)) { - sb.append('<'); - sb.append(key); - sb.append("/>"); - - // Emit a new tag - - } else { - sb.append(toXml(value, key)); - } - } - if (tagName != null) { - - // Emit the close tag - sb.append("'); - } - return sb.toString(); - - } - - if (ArrayUtil.isArray(object)) { - object = new JSONArray(object); - } - - if (object instanceof JSONArray) { - ja = (JSONArray) object; - for (Object val : ja) { - // XML does not have good support for arrays. If an array - // appears in a place where XML is lacking, synthesize an - // element. - sb.append(toXml(val, tagName == null ? "array" : tagName)); - } - return sb.toString(); - } - - String string = EscapeUtil.escapeXml(object.toString()); - return (tagName == null) ? - "\"" + string + "\"" : (string.length() == 0) ? "<" + tagName + "/>" - : "<" + tagName + ">" + string + ""; - + return toXml(object, tagName, "content"); } - + /** + * 转换JSONObject为XML + * + * @param object JSON对象或数组 + * @param tagName 可选标签名称,名称为空时忽略标签 + * @param contentKeys 标识为内容的key,遇到此key直接解析内容而不增加对应名称标签 + * @return A string. + * @throws JSONException JSON解析异常 + */ + public static String toXml(Object object, String tagName, String... contentKeys) throws JSONException { + return JSONXMLSerializer.toXml(object, tagName, contentKeys); + } } diff --git a/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java b/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java new file mode 100644 index 000000000..6b16ca506 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java @@ -0,0 +1,182 @@ +package cn.hutool.json.xml; + +import cn.hutool.json.InternalJSONUtil; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONObject; +import cn.hutool.json.XML; +import cn.hutool.json.XMLTokener; + +/** + * XML解析器,将XML解析为JSON对象 + * + * @author JSON.org, looly + * @since 5.7.11 + */ +public class JSONXMLParser { + + /** + * 转换XML为JSONObject + * 转换过程中一些信息可能会丢失,JSON中无法区分节点和属性,相同的节点将被处理为JSONArray。 + * + * @param jo JSONObject + * @param xmlStr XML字符串 + * @param keepStrings 如果为{@code true},则值保持String类型,不转换为数字或boolean + * @throws JSONException 解析异常 + */ + public static void parseJSONObject(JSONObject jo, String xmlStr, boolean keepStrings) throws JSONException { + XMLTokener x = new XMLTokener(xmlStr, jo.getConfig()); + while (x.more() && x.skipPast("<")) { + parse(x, jo, null, keepStrings); + } + } + + /** + * Scan the content following the named tag, attaching it to the context. + * + * @param x The XMLTokener containing the source string. + * @param context The JSONObject that will include the new material. + * @param name The tag name. + * @return true if the close tag is processed. + * @throws JSONException JSON异常 + */ + private static boolean parse(XMLTokener x, JSONObject context, String name, boolean keepStrings) throws JSONException { + char c; + int i; + JSONObject jsonobject; + String string; + String tagName; + Object token; + + token = x.nextToken(); + + if (token == XML.BANG) { + c = x.next(); + if (c == '-') { + if (x.next() == '-') { + x.skipPast("-->"); + return false; + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if ("CDATA".equals(token)) { + if (x.next() == '[') { + string = x.nextCDATA(); + if (string.length() > 0) { + context.accumulate("content", string); + } + return false; + } + } + throw x.syntaxError("Expected 'CDATA['"); + } + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + return false; + } else if (token == XML.QUEST) { + + // "); + return false; + } else if (token == XML.SLASH) { + + // Close tag + if (x.nextToken() != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + if (jsonobject.size() > 0) { + context.accumulate(tagName, jsonobject); + } else { + context.accumulate(tagName, ""); + } + return false; + + } else if (token == XML.GT) { + // Content, between <...> and + for (; ; ) { + token = x.nextContent(); + if (token == null) { + if (tagName != null) { + throw x.syntaxError("Unclosed tag " + tagName); + } + return false; + } else if (token instanceof String) { + string = (String) token; + if (string.length() > 0) { + jsonobject.accumulate("content", keepStrings ? token : InternalJSONUtil.stringToValue(string)); + } + + } else if (token == XML.LT) { + // Nested element + if (parse(x, jsonobject, tagName, keepStrings)) { + if (jsonobject.size() == 0) { + context.accumulate(tagName, ""); + } else if (jsonobject.size() == 1 && jsonobject.get("content") != null) { + context.accumulate(tagName, jsonobject.get("content")); + } else { + context.accumulate(tagName, jsonobject); + } + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLSerializer.java b/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLSerializer.java new file mode 100644 index 000000000..fa6dd1e34 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLSerializer.java @@ -0,0 +1,159 @@ +package cn.hutool.json.xml; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.EscapeUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONObject; + +/** + * JSON转XML字符串工具 + * + * @author looly + * @since 5.7.11 + */ +public class JSONXMLSerializer { + /** + * 转换JSONObject为XML + * Convert a JSONObject into a well-formed, element-normal XML string. + * + * @param object A JSONObject. + * @return A string. + * @throws JSONException Thrown if there is an error parsing the string + */ + public static String toXml(Object object) throws JSONException { + return toXml(object, null); + } + + /** + * 转换JSONObject为XML + * + * @param object JSON对象或数组 + * @param tagName 可选标签名称,名称为空时忽略标签 + * @return A string. + * @throws JSONException JSON解析异常 + */ + public static String toXml(Object object, String tagName) throws JSONException { + return toXml(object, tagName, "content"); + } + + /** + * 转换JSONObject为XML + * + * @param object JSON对象或数组 + * @param tagName 可选标签名称,名称为空时忽略标签 + * @param contentKeys 标识为内容的key,遇到此key直接解析内容而不增加对应名称标签 + * @return A string. + * @throws JSONException JSON解析异常 + */ + public static String toXml(Object object, String tagName, String... contentKeys) throws JSONException { + if (null == object) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + if (object instanceof JSONObject) { + + // Emit + appendTag(sb, tagName, false); + + // Loop thru the keys. + ((JSONObject) object).forEach((key, value) -> { + if (ArrayUtil.isArray(value)) { + value = new JSONArray(value); + } + + // Emit content in body + if (ArrayUtil.contains(contentKeys, key)) { + if (value instanceof JSONArray) { + int i = 0; + for (Object val : (JSONArray) value) { + if (i > 0) { + sb.append(CharUtil.LF); + } + sb.append(EscapeUtil.escapeXml(val.toString())); + i++; + } + } else { + sb.append(EscapeUtil.escapeXml(value.toString())); + } + + // Emit an array of similar keys + + } else if (StrUtil.isEmptyIfStr(value)) { + sb.append(wrapWithTag(null, key)); + } else if (value instanceof JSONArray) { + for (Object val : (JSONArray) value) { + if (val instanceof JSONArray) { + sb.append(wrapWithTag(toXml(val), key)); + } else { + sb.append(toXml(val, key)); + } + } + } else { + sb.append(toXml(value, key)); + } + }); + + // Emit the close tag + appendTag(sb, tagName, true); + return sb.toString(); + } + + if (ArrayUtil.isArray(object)) { + object = new JSONArray(object); + } + + if (object instanceof JSONArray) { + for (Object val : (JSONArray) object) { + // XML does not have good support for arrays. If an array + // appears in a place where XML is lacking, synthesize an + // element. + sb.append(toXml(val, tagName == null ? "array" : tagName)); + } + return sb.toString(); + } + + return wrapWithTag(EscapeUtil.escapeXml(object.toString()), tagName); + } + + /** + * 追加标签 + * + * @param sb XML内容 + * @param tagName 标签名 + * @param isEndTag 是否结束标签 + * @since 5.7.11 + */ + private static void appendTag(StringBuilder sb, String tagName, boolean isEndTag) { + if (StrUtil.isNotBlank(tagName)) { + sb.append('<'); + if (isEndTag) { + sb.append('/'); + } + sb.append(tagName).append('>'); + } + } + + /** + * 将内容使用标签包装为XML + * + * @param tagName 标签名 + * @param content 内容 + * @return 包装后的XML + * @since 5.7.11 + */ + private static String wrapWithTag(String content, String tagName) { + if (StrUtil.isBlank(tagName)) { + return StrUtil.wrap(content, "\""); + } + + if (StrUtil.isEmpty(content)) { + return "<" + tagName + "/>"; + } else { + return "<" + tagName + ">" + content + ""; + } + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/xml/package-info.java b/hutool-json/src/main/java/cn/hutool/json/xml/package-info.java new file mode 100644 index 000000000..3c55d30e6 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/xml/package-info.java @@ -0,0 +1,6 @@ +/** + * JSON与XML相互转换封装,基于json.org官方库改造 + * + * @author looly + */ +package cn.hutool.json.xml; diff --git a/hutool-json/src/test/java/cn/hutool/json/XMLTest.java b/hutool-json/src/test/java/cn/hutool/json/xml/XMLTest.java similarity index 58% rename from hutool-json/src/test/java/cn/hutool/json/XMLTest.java rename to hutool-json/src/test/java/cn/hutool/json/xml/XMLTest.java index 927808de7..99d095fce 100644 --- a/hutool-json/src/test/java/cn/hutool/json/XMLTest.java +++ b/hutool-json/src/test/java/cn/hutool/json/xml/XMLTest.java @@ -1,5 +1,8 @@ -package cn.hutool.json; +package cn.hutool.json.xml; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.hutool.json.XML; import org.junit.Assert; import org.junit.Test; @@ -25,4 +28,14 @@ public class XMLTest { Assert.assertEquals(xml, xml2); } + @Test + public void xmlContentTest(){ + JSONObject jsonObject = JSONUtil.createObj().set("content","123456"); + + String xml = XML.toXml(jsonObject); + Assert.assertEquals("123456", xml); + + xml = XML.toXml(jsonObject, null, new String[0]); + Assert.assertEquals("123456", xml); + } }