From 09b08e9cb080ee080e970362b22de977c470e412 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 4 Dec 2021 12:38:32 +0800
Subject: [PATCH 001/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/able/jdbc/model/DbFieldValue.java    | 2 +-
 .../src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java | 5 +++++
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/jdbc/model/DbFieldValue.java b/able/src/main/java/com/gitee/qdbp/able/jdbc/model/DbFieldValue.java
index 90a13d6..773ef54 100644
--- a/able/src/main/java/com/gitee/qdbp/able/jdbc/model/DbFieldValue.java
+++ b/able/src/main/java/com/gitee/qdbp/able/jdbc/model/DbFieldValue.java
@@ -29,7 +29,7 @@ public class DbFieldValue implements Serializable {
         return fieldValue;
     }
 
-    public void setFieldValue(String fieldValue) {
+    public void setFieldValue(Object fieldValue) {
         this.fieldValue = fieldValue;
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java
index 2a51505..7d553f4 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java
@@ -66,6 +66,11 @@ public class RsaCipher implements CipherService {
         return publicKey == null ? null : byteCodec.encode(publicKey);
     }
 
+    /** 私钥 **/
+    public String getPrivateKey() {
+        return privateKey == null ? null : byteCodec.encode(privateKey);
+    }
+
     /**
      * 加密, 使用公钥加密
      * 
-- 
Gitee


From 285d2b0cd5ec9888a65857191ee9bd55a2b53ae0 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 12 Dec 2021 11:37:28 +0800
Subject: [PATCH 002/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../eachfile/process/BaseFileProcessor.java   |  4 ++--
 .../qdbp/eachfile/process/FileProcessor.java  |  4 ++--
 .../qdbp/eachfile/runner/FileEachRunner.java  | 24 ++++++++++++++-----
 3 files changed, 22 insertions(+), 10 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/eachfile/process/BaseFileProcessor.java b/able/src/main/java/com/gitee/qdbp/eachfile/process/BaseFileProcessor.java
index cbb077b..4fd39ad 100644
--- a/able/src/main/java/com/gitee/qdbp/eachfile/process/BaseFileProcessor.java
+++ b/able/src/main/java/com/gitee/qdbp/eachfile/process/BaseFileProcessor.java
@@ -46,11 +46,11 @@ public abstract class BaseFileProcessor implements FileProcessor, Debugger.Aware
     }
 
     @Override
-    public void init() {
+    public void init(String root) {
     }
 
     @Override
-    public void destroy() {
+    public void destroy(String root) {
     }
 
     protected void setIncludeFolders(String... includeFolders) {
diff --git a/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java b/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java
index 4e57500..3c7a97f 100644
--- a/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java
+++ b/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java
@@ -11,10 +11,10 @@ import java.io.File;
 public interface FileProcessor {
 
     /** 初始化 **/
-    void init();
+    void init(String root);
 
     /** 销毁 **/
-    void destroy();
+    void destroy(String root);
 
     /**
      * 处理文件
diff --git a/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java b/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java
index 55a91f9..3b2f21e 100644
--- a/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java
+++ b/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java
@@ -46,20 +46,32 @@ public class FileEachRunner extends Thread {
 
     @Override
     public void run() {
+        try {
+            processor.init(root);
+        } catch (Exception e) {
+            debugger.debug("Failed to call init()", e);
+            return;
+        }
+
         File folder = new File(root);
         if (!checkRoot(folder)) {
             return;
         }
 
-        processor.init();
         try {
             each(folder);
+        } catch (Exception e) {
+            debugger.debug("Failed to each folder", e);
         } finally {
-            processor.destroy();
-        }
-
-        if (lock != null) {
-            lock.signal();
+            try {
+                processor.destroy(root);
+            } catch (Exception e) {
+                debugger.debug("Failed to call destroy()", e);
+            } finally {
+                if (lock != null) {
+                    lock.signal();
+                }
+            }
         }
     }
 
-- 
Gitee


From 0bcadc1083319693d08d09e62c9c5d5c67c1654b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 3 Jan 2022 19:35:47 +0800
Subject: [PATCH 003/160] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/StringTools.java   | 128 ++++++++----------
 1 file changed, 55 insertions(+), 73 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index e6f57ca..b487ff6 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -70,7 +70,6 @@ public class StringTools {
     /**
      * 判断字符串是不是手机号码
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是手机号码, 如果字符串等于null或空字符串, 返回false
      */
@@ -84,7 +83,6 @@ public class StringTools {
     /**
      * 判断字符串是不是手机号码
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是手机号码, 如果字符串等于null或空字符串, 返回false
      */
@@ -98,7 +96,6 @@ public class StringTools {
     /**
      * 判断字符串是不是邮箱地址
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是邮箱地址, 如果字符串等于null或空字符串, 返回false
      */
@@ -112,7 +109,6 @@ public class StringTools {
     /**
      * 判断字符串是不是邮箱地址
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是邮箱地址, 如果字符串等于null或空字符串, 返回false
      */
@@ -126,7 +122,6 @@ public class StringTools {
     /**
      * 判断字符串是不是网址
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是网址, 如果字符串等于null或空字符串, 返回false
      */
@@ -141,7 +136,6 @@ public class StringTools {
     /**
      * 判断字符串是不是网址
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是网址, 如果字符串等于null或空字符串, 返回false
      */
@@ -155,7 +149,6 @@ public class StringTools {
     /**
      * 判断字符串是不是纯数字
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是数字, 如果字符串等于null或空字符串, 返回false
      */
@@ -175,7 +168,6 @@ public class StringTools {
     /**
      * 判断字符串是不是纯数字
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是数字, 如果字符串等于null或空字符串, 返回false
      */
@@ -189,7 +181,6 @@ public class StringTools {
     /**
      * 判断字符串是不是数字 (可带+-号/小数点/逗号)
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是数字, 如果字符串等于null或空字符串, 返回false
      */
@@ -251,7 +242,6 @@ public class StringTools {
     /**
      * 判断字符串是不是数字 (可带+-号/小数点/逗号)
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是数字, 如果字符串等于null或空字符串, 返回false
      */
@@ -265,7 +255,6 @@ public class StringTools {
     /**
      * 判断字符串是不是英文字符
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是英文字符, 如果字符串等于null或空字符串, 返回false
      */
@@ -285,7 +274,6 @@ public class StringTools {
     /**
      * 判断字符串是不是英文字符
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是英文字符, 如果字符串等于null或空字符串, 返回false
      */
@@ -298,7 +286,7 @@ public class StringTools {
 
     /**
      * 是不是单词字符
-     * 
+     *
      * @param c 字符
      * @return 是不是单词字符
      */
@@ -308,7 +296,7 @@ public class StringTools {
 
     /**
      * 是不是单词
-     * 
+     *
      * @param string 字符串
      * @return 是不是单词
      */
@@ -331,7 +319,7 @@ public class StringTools {
 
     /**
      * 是不是单词
-     * 
+     *
      * @param string 字符串
      * @return 是不是单词
      */
@@ -353,7 +341,6 @@ public class StringTools {
      * String message = StringTools.format(ptn, params);
      * </pre>
      *
-     * @author zhaohuihua
      * @param string 原字符串
      * @param params 参数键值对
      * @return 参数替换后的字符串
@@ -390,7 +377,6 @@ public class StringTools {
      * String message = StringTools.format(ptn, "nickName", user.getNickName(), "phone", user.getPhone());
      * </pre>
      *
-     * @author zhaohuihua
      * @param string 原字符串
      * @param params 参数键值对, 参数个数必须是2的倍数
      * @return 参数替换后的字符串
@@ -405,7 +391,7 @@ public class StringTools {
         }
 
         Map<String, Object> map = new HashMap<String, Object>();
-        for (int i = 0; i < params.length;) {
+        for (int i = 0; i < params.length; ) {
             map.put(params[i++].toString(), params[i++]);
         }
         return format(string, map);
@@ -456,7 +442,7 @@ public class StringTools {
             return new String[0];
         }
         if (chars == null || chars.length == 0) {
-            return new String[] { string };
+            return new String[] {string};
         }
         List<String> list = splits(string, trim, chars);
         return list == null ? null : list.toArray(new String[0]);
@@ -497,7 +483,7 @@ public class StringTools {
             return new String[0];
         }
         if (delimiter == null || delimiter.length() == 0) {
-            return new String[] { string };
+            return new String[] {string};
         }
         List<String> list = splits(string, trim, delimiter);
         return list == null ? null : list.toArray(new String[0]);
@@ -655,7 +641,7 @@ public class StringTools {
 
     /**
      * 拆分字段名<br>
-     * 
+     *
      * @param fields 字段名字符串
      * @return 字段名数组
      * @deprecated move to {@link StringParser#splitFields(String)}
@@ -667,7 +653,7 @@ public class StringTools {
 
     /**
      * 拆分字段名
-     * 
+     *
      * @param fields 字段名字符串
      * @return 字段名数组
      * @deprecated move to {@link StringParser#splitsFields(String)}
@@ -682,7 +668,6 @@ public class StringTools {
      * 如果未超过指定长度则返回原字符, length小于20时省略最后部分而不是中间部分<br>
      * 如: 诺贝尔奖是以瑞典著名的 ... 基金创立的
      *
-     * @author zhaohuihua
      * @param string 长文本
      * @param length 指定长度
      * @return 省略中间部分的字符
@@ -703,7 +688,7 @@ public class StringTools {
 
     /**
      * 连接字符串
-     * 
+     *
      * @param parts 字符串片断
      * @return 完整字符串
      * @since 5.0.0
@@ -724,7 +709,7 @@ public class StringTools {
 
     /**
      * 连接字符串
-     * 
+     *
      * @param c 分隔符
      * @param parts 字符串片断
      * @return 完整字符串
@@ -735,7 +720,7 @@ public class StringTools {
 
     /**
      * 连接字符串
-     * 
+     *
      * @param c 分隔符
      * @param parts 字符串片断
      * @param start 起始位置
@@ -748,7 +733,7 @@ public class StringTools {
 
     /**
      * 连接字符串
-     * 
+     *
      * @param c 分隔符
      * @param prefix 前缀
      * @param parts 字符串片断
@@ -797,7 +782,7 @@ public class StringTools {
 
     /**
      * 指定字符串是不是以空白字符开头
-     * 
+     *
      * @param string 指定字符串
      * @return 是不是以空白字符开头
      * @since 5.0.0
@@ -811,7 +796,7 @@ public class StringTools {
 
     /**
      * 指定字符串是不是以空白字符结尾
-     * 
+     *
      * @param string 指定字符串
      * @return 是不是以空白字符结尾
      * @since 5.0.0
@@ -826,7 +811,7 @@ public class StringTools {
     /**
      * 是不是英文空白字符<br>
      * 不能用Character.isWhitespace(' '); 因为中文空格也会返回true
-     * 
+     *
      * @param c 指定字符
      * @return 是不是空白字符
      * @since 5.0.0
@@ -839,7 +824,7 @@ public class StringTools {
 
     /**
      * 指定字符串是否全是英文空白字符
-     * 
+     *
      * @param string 指定字符串
      * @return 是不是空白字符串
      * @since 5.2.0
@@ -896,7 +881,7 @@ public class StringTools {
 
     /**
      * 删除左右两侧的指定字符
-     * 
+     *
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
@@ -909,7 +894,7 @@ public class StringTools {
 
     /**
      * 删除左右两侧的指定字符
-     * 
+     *
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
@@ -920,7 +905,7 @@ public class StringTools {
 
     /**
      * 删除左侧的指定字符
-     * 
+     *
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
@@ -931,7 +916,7 @@ public class StringTools {
 
     /**
      * 删除右侧的指定字符
-     * 
+     *
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
@@ -960,7 +945,7 @@ public class StringTools {
 
     /**
      * 删除中间的字符
-     * 
+     *
      * @param string 原文本
      * @param start 左侧开始删除的数量(左侧保留的数量)
      * @param end 删除到右侧的位置(右侧保留的数量)
@@ -974,7 +959,7 @@ public class StringTools {
 
     /**
      * 删除中间的字符
-     * 
+     *
      * @param string 原文本
      * @param start 左侧开始删除的数量(左侧保留的数量)
      * @param end 删除到右侧的位置(右侧保留的数量)
@@ -997,7 +982,7 @@ public class StringTools {
 
     /**
      * 删除左右两侧指定数量的字符
-     * 
+     *
      * @param string 原文本
      * @param left 左侧删除的数量
      * @param right 右侧删除的数量
@@ -1022,7 +1007,7 @@ public class StringTools {
     /**
      * 删除前缀, 如果没有指定的前缀就返回原字符串<br>
      * removePrefix("userNameEquals", "userName") = "Equals"
-     * 
+     *
      * @param string 待处理的字符串
      * @param prefix 指定前缀
      * @return 处理后的字符串
@@ -1040,7 +1025,7 @@ public class StringTools {
     /**
      * 删除后缀, 如果没有指定的后缀就返回原字符串<br>
      * removeSuffix("userNameEquals", "Equals") = "userName"
-     * 
+     *
      * @param string 待处理的字符串
      * @param suffix 指定后缀
      * @return 处理后的字符串
@@ -1059,7 +1044,7 @@ public class StringTools {
     /**
      * 删除前缀和后缀, 如果没有指定的前缀和后缀就返回原字符串<br>
      * removePrefixSuffix("userNameEquals", "user", "Equals") = "Name"
-     * 
+     *
      * @param string 待处理的字符串
      * @param prefix 指定前缀
      * @param suffix 指定后缀
@@ -1085,7 +1070,7 @@ public class StringTools {
      * 删除前缀(删除到最后一个指定字符为止)<br>
      * removePrefixAt("userName$Equals", '$') = "Equals"<br>
      * removePrefixAt("user$Name$Equals", '$') = "Equals"<br>
-     * 
+     *
      * @param string 源字符串
      * @param c 指定字符
      * @return 处理后的字符串, 如果没有指定字符就返回源字符串
@@ -1102,7 +1087,7 @@ public class StringTools {
      * 删除前缀(删除到最后一个指定字符串为止)<br>
      * removePrefixAt("userName$$Equals", "$$") = "Equals"<br>
      * removePrefixAt("user$$Name$$Equals","$$") = "Equals"<br>
-     * 
+     *
      * @param string 源字符串
      * @param c 指定字符串
      * @return 处理后的字符串, 如果没有指定字符串就返回源字符串
@@ -1120,7 +1105,7 @@ public class StringTools {
      * 删除后缀(从第一个指定字符开始删除)<br>
      * removeSuffixAt("userName$Equals", '$') = "userName"<br>
      * removeSuffixAt("user$Name$Equals", '$') = "user"<br>
-     * 
+     *
      * @param string 源字符串
      * @param c 指定字符
      * @return 处理后的字符串, 如果没有指定字符就返回源字符串
@@ -1137,7 +1122,7 @@ public class StringTools {
      * 删除后缀(从第一个指定字符串开始删除)<br>
      * removeSuffixAt("userName$$Equals", "$$") = "userName"<br>
      * removeSuffixAt("user$$Name$$Equals", "$$") = "user"<br>
-     * 
+     *
      * @param string 源字符串
      * @param c 指定字符串
      * @return 处理后的字符串, 如果没有指定字符串就返回源字符串
@@ -1154,7 +1139,7 @@ public class StringTools {
     /**
      * 删除指定的子字符串<br>
      * 如: removeSubStrings("11223344", "22", "33") 输出 1144<br>
-     * 
+     *
      * @param string 源字符串
      * @param sub 待删除的字符串
      * @return 删除后的字符串
@@ -1169,7 +1154,7 @@ public class StringTools {
     /**
      * 删除指定的子字符串<br>
      * 如: removeSubString("11223344", "22") 输出 113344<br>
-     * 
+     *
      * @param string 源字符串
      * @param sub 待删除的字符串
      * @return 删除后的字符串
@@ -1193,7 +1178,7 @@ public class StringTools {
     /**
      * 删除指定的字符<br>
      * 如: removeChars("11223344", '2','3') 输出 1144<br>
-     * 
+     *
      * @param string 源字符串
      * @param chars 待删除的字符
      * @return 删除后的字符串
@@ -1220,7 +1205,7 @@ public class StringTools {
      * 删除成对的符号及包含在中间的内容<br>
      * 如: removeInPairedSymbol("111<!--xxx-->222<!--xxx-->333", "<!--", "-->") 输出 111222333<br>
      * 如: removeInPairedSymbol("111/&#42;xxx&#42;/222/&#42;xxx&#42;/333", "/&#42;", "&#42;/") 输出 111222333<br>
-     * 
+     *
      * @param string 源字符串
      * @param leftSymbol 左侧的符号
      * @param rightSymbol 右侧的符号
@@ -1250,7 +1235,7 @@ public class StringTools {
      * 获取成对符号之间的内容<br>
      * 如: getSubstringInPairedSymbol("111<!--222-->333", "<!--", "-->") 输出 222<br>
      * 如: getSubstringInPairedSymbol("111/&#42;222&#42;/333", "/&#42;", "&#42;/") 输出 222<br>
-     * 
+     *
      * @param string 源字符串
      * @param leftSymbol 左侧的符号
      * @param rightSymbol 右侧的符号
@@ -1274,7 +1259,7 @@ public class StringTools {
      * 字符串替换(非正则)<br>
      * 例如: \t替换为空格, \r\n替换为\n<br>
      * StringTools.replace("abc\tdef\r\nxyz", "\t", " ", "\r\n", "\n");
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @return 替换后的字符串
@@ -1299,7 +1284,7 @@ public class StringTools {
      * 字符串替换(非正则)<br>
      * 例如: \t替换为空格, \r\n替换为\n<br>
      * StringTools.replace("abc\tdef\r\nxyz", "\t", " ", "\r\n", "\n");
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1320,7 +1305,7 @@ public class StringTools {
      * 字符串替换(非正则)<br>
      * 例如: \t替换为空格, \r\n替换为\n<br>
      * StringTools.replace("abc\tdef\r\nxyz", "\t", " ", "\r\n", "\n");
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1338,7 +1323,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1358,7 +1343,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1382,7 +1367,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1406,7 +1391,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param pattern 替换规则
      * @param replacement 替换目标
@@ -1442,7 +1427,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param pattern 替换规则
      * @param replacement 替换目标
@@ -1485,7 +1470,7 @@ public class StringTools {
         }
 
         List<KeyString> list = new ArrayList<>();
-        for (int i = 0; i < patterns.length;) {
+        for (int i = 0; i < patterns.length; ) {
             list.add(new KeyString(patterns[i++], patterns[i++]));
         }
 
@@ -1519,7 +1504,7 @@ public class StringTools {
     /**
      * 左侧补字符<br>
      * pad("12345", '_', 10) 返回 _____12345<br>
-     * 
+     *
      * @param string 原字符串
      * @param c 补充的字符
      * @param length 目标长度
@@ -1532,7 +1517,7 @@ public class StringTools {
     /**
      * 左侧或右侧补字符<br>
      * pad("12345", '_', false, 10) 返回 12345_____<br>
-     * 
+     *
      * @param string 原字符串
      * @param c 补充的字符
      * @param left 左侧补(true)还是右侧补(false)
@@ -1551,7 +1536,7 @@ public class StringTools {
 
     /**
      * 统计子字符串出现次数
-     * 
+     *
      * @param string 源字符串
      * @param substring 子字符串
      * @return 次数
@@ -1566,7 +1551,7 @@ public class StringTools {
 
     /**
      * 统计字符出现次数
-     * 
+     *
      * @param string 源字符串
      * @param character 目标字符
      * @return 次数
@@ -1614,7 +1599,7 @@ public class StringTools {
 
     /**
      * 判断指定字符串是不是字段名称
-     * 
+     *
      * @param string 字符串
      * @return 是不是字段名称
      */
@@ -1649,13 +1634,13 @@ public class StringTools {
 
     /**
      * 是不是单词字符
-     * 
+     *
      * @param c 字符
      * @return 是不是单词字符
      */
     private static boolean isFieldChar(char c) {
         return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
-        // 支持这些特殊字符
+                // 支持这些特殊字符
                 || c == '$' || c == '_' || c == '-' || c == ':' || c == '.';
     }
 
@@ -1699,7 +1684,7 @@ public class StringTools {
 
     /**
      * 是否存在指定单词
-     * 
+     *
      * @param string 字符串
      * @param keyword 指定单词
      * @return 是否存在指定单词
@@ -1737,7 +1722,7 @@ public class StringTools {
      *
      * @param string 手机号码邮箱或名字
      * @return 隐藏后的字符串, 如: 139****1382, zh****ua@126.com, <br>
-     *         黄山-〇山, 昆仑山-〇〇山, 黄山毛峰-〇〇毛峰
+     * 黄山-〇山, 昆仑山-〇〇山, 黄山毛峰-〇〇毛峰
      */
     public static String hidden(String string) {
         if (string == null || string.length() == 0) {
@@ -1793,7 +1778,7 @@ public class StringTools {
     /**
      * 获取UTF8编码格式的长度<br>
      * 0-127长度为1, 如英文字符; 128~2047长度为2, 如ð; emoji为4, 如😎; 其他为3, 如中文字符
-     * 
+     *
      * @param string 字符串
      * @return 长度
      */
@@ -1817,7 +1802,7 @@ public class StringTools {
 
     /**
      * 如果全是大写字母, 则转换为小写字母
-     * 
+     *
      * @param name 待转换的名称
      * @return 转换后的名称
      * @deprecated 改为{@link NamingTools#toLowerCaseIfAllUpperCase(String)}
@@ -1831,7 +1816,6 @@ public class StringTools {
      * 转换为驼峰命名法格式<br>
      * 如: user_name = userName, iuser_service = iuserService, i_user_service = iUserService
      *
-     * @author zhaohuihua
      * @param name 待转换的名称
      * @return 驼峰命名法名称
      * @deprecated 改为{@link NamingTools#toCamelString(String)}
@@ -1846,7 +1830,6 @@ public class StringTools {
      * 如startsWithUpperCase=true时:<br>
      * user_name = UserName, iuser_service = IuserService, i_user_service = IUserService
      *
-     * @author zhaohuihua
      * @param name 待转换的名称
      * @param startsWithUpperCase 是否以大写字母开头
      * @return 驼峰命名法名称
@@ -1862,7 +1845,6 @@ public class StringTools {
      * 如: userName = user_name, SiteURL = site_url, IUserService = iuser_service<br>
      * user$Name = user$name, user_Name = user_name, user name = user_name, md5String = md5_string
      *
-     * @author zhaohuihua
      * @param name 待转换的名称
      * @return 下划线命名法名称
      * @deprecated 改为{@link NamingTools#toUnderlineNaming(String)}
-- 
Gitee


From 94f8f7e2719bdec607ef435802899eecb7464b59 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 3 Jan 2022 20:02:09 +0800
Subject: [PATCH 004/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/StringTools.java   | 44 +++++++++----------
 1 file changed, 21 insertions(+), 23 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index b487ff6..c01d976 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -304,13 +304,13 @@ public class StringTools {
         if (string == null || string.length() == 0) {
             return false;
         }
-        char[] chars = string.toCharArray();
-        char fc = chars[0];
+        char fc = string.charAt(0);
         if (fc >= '0' && fc <= '9') {
             return false; // 以数字开头的, 不是单词
         }
-        for (int i = 0; i < chars.length; i++) {
-            if (!isWordChar(chars[i])) {
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (!isWordChar(c)) {
                 return false;
             }
         }
@@ -390,7 +390,7 @@ public class StringTools {
             throw new IllegalArgumentException("参数必须是键值对, 参数个数必须是2的倍数");
         }
 
-        Map<String, Object> map = new HashMap<String, Object>();
+        Map<String, Object> map = new HashMap<>();
         for (int i = 0; i < params.length; ) {
             map.put(params[i++].toString(), params[i++]);
         }
@@ -541,13 +541,12 @@ public class StringTools {
         }
         List<String> list = new ArrayList<>();
         StringBuilder buffer = new StringBuilder();
-        char[] textChars = string.toCharArray();
         boolean lastIsSplitChar = false;
-        for (int i = 0; i < textChars.length; i++) {
-            char c = textChars[i];
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
             boolean isSplitChar = false;
-            for (int j = 0; j < chars.length; j++) {
-                if (c == chars[j]) {
+            for (char s : chars) {
+                if (c == s) {
                     isSplitChar = true;
                     break;
                 }
@@ -698,8 +697,7 @@ public class StringTools {
             return null;
         }
         StringBuilder buffer = new StringBuilder();
-        for (int i = 0, len = parts.length; i < len; i++) {
-            String part = parts[i];
+        for (String part : parts) {
             if (part != null && part.length() > 0) {
                 buffer.append(part);
             }
@@ -1186,16 +1184,16 @@ public class StringTools {
     public static String removeChars(String string, char... chars) {
         StringBuilder buffer = new StringBuilder();
         char[] sources = string.toCharArray();
-        for (int i = 0; i < sources.length; i++) {
+        for (char source : sources) {
             int index = -1;
             for (int j = 0; j < chars.length; j++) {
-                if (sources[i] == chars[j]) {
+                if (source == chars[j]) {
                     index = j;
                     break;
                 }
             }
             if (index < 0) {
-                buffer.append(sources[i]);
+                buffer.append(source);
             }
         }
         return buffer.toString();
@@ -1478,8 +1476,8 @@ public class StringTools {
     }
 
     private static boolean inArray(char c, char[] array) {
-        for (int i = 0, len = array.length; i < len; i++) {
-            if (array[i] == c) {
+        for (char item : array) {
+            if (c == item) {
                 return true;
             }
         }
@@ -1558,9 +1556,9 @@ public class StringTools {
      */
     public static int countCharacter(String string, char character) {
         int count = 0;
-        char[] chars = string.toCharArray();
-        for (int i = 0, z = chars.length; i < z; i++) {
-            if (chars[i] == character) {
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (c == character) {
                 count++;
             }
         }
@@ -1623,9 +1621,9 @@ public class StringTools {
     }
 
     private static boolean isFieldString(CharSequence string) {
-        char[] chars = string.toString().toCharArray();
-        for (int i = 0; i < chars.length; i++) {
-            if (!isFieldChar(chars[i])) {
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (!isFieldChar(c)) {
                 return false;
             }
         }
-- 
Gitee


From ee86aa01a4c92258a74a1aa6d06d3db936a2baa6 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 3 Jan 2022 20:02:58 +0800
Subject: [PATCH 005/160] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/VerifyTools.java    | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
index ad29a4b..3aff9f2 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
@@ -22,7 +22,7 @@ public class VerifyTools {
     /**
      * 检查指定的对象是否不为null, 如果为null将抛出ServiceException异常.<br>
      * 用法: VerifyTools.requireNonNull(entity.getEmail(), "email");
-     * 
+     *
      * @param object 待检查的对象
      * @param name 对象的描述或名称
      */
@@ -35,7 +35,7 @@ public class VerifyTools {
     /**
      * 检查指定的对象是否不为空, 如果为空(或为空字符串)将抛出ServiceException异常.<br>
      * 用法: VerifyTools.requireNotBlank(entity.getEmail(), "email");
-     * 
+     *
      * @param object 待检查的对象
      * @param name 对象的描述或名称
      */
@@ -51,7 +51,7 @@ public class VerifyTools {
     /**
      * 检查指定的字符串是否不为空, 如果为空(或为空字符串)将抛出ServiceException异常.<br>
      * 用法: VerifyTools.requireNotBlank(entity.getEmail(), "email");
-     * 
+     *
      * @param string 待检查的对象
      * @param name 对象的描述或名称
      */
@@ -80,7 +80,7 @@ public class VerifyTools {
     /**
      * 判断字符串是否为空<br>
      * 零长度的字符串将被判定为空
-     * 
+     *
      * @param string 目标对象
      * @return true or false
      */
@@ -90,7 +90,7 @@ public class VerifyTools {
 
     /**
      * 判断字符串是否为非空
-     * 
+     *
      * @param string 目标对象
      * @return true or false
      */
@@ -101,7 +101,7 @@ public class VerifyTools {
     /**
      * 判断对象是否为空<br>
      * 零长度的字符串, length=0的数组, 空的Collection, 空的Map, 空的Iterable都将被判定为空
-     * 
+     *
      * @param object 目标对象
      * @return true or false
      */
@@ -130,7 +130,7 @@ public class VerifyTools {
 
     /**
      * 判断对象是否为非空
-     * 
+     *
      * @param object 目标对象
      * @return true or false
      */
@@ -190,7 +190,7 @@ public class VerifyTools {
 
     /**
      * 判断对象是否存在于列表中
-     * 
+     *
      * @param object 目标对象
      * @param objects 列表
      * @return 是否存在
@@ -214,7 +214,7 @@ public class VerifyTools {
 
     /**
      * 判断对象是否不存在于列表中
-     * 
+     *
      * @param object 目标对象
      * @param objects 列表
      * @return 是否不存在
-- 
Gitee


From eb701ebb1e929931426c160555da416306d0b0b3 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 3 Jan 2022 20:03:33 +0800
Subject: [PATCH 006/160] =?UTF-8?q?=E5=88=A4=E6=96=AD=E6=98=AF=E4=B8=8D?=
 =?UTF-8?q?=E6=98=AFJson=E5=AD=97=E7=AC=A6=E4=B8=B2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/VerifyTools.java   | 31 +++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
index 3aff9f2..36688eb 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
@@ -264,4 +264,35 @@ public class VerifyTools {
     public static boolean isChanged(Object value, Object older) {
         return isNotBlank(value) && notEquals(value, older);
     }
+
+    /** 是不是JsonObject字符串 **/
+    public static boolean isJsonObjectString(String value) {
+        return checkJsonString(value, '{', '}');
+    }
+
+    /** 是不是JsonObject字符串 **/
+    public static boolean isJsonObjectString(Object value) {
+        return value instanceof String && checkJsonString((String) value, '{', '}');
+    }
+
+    /** 是不是JsonArray字符串 **/
+    public static boolean isJsonArrayString(String value) {
+        return checkJsonString(value, '[', ']');
+    }
+
+    /** 是不是JsonArray字符串 **/
+    public static boolean isJsonArrayString(Object value) {
+        return value instanceof String && checkJsonString((String) value, '[', ']');
+    }
+
+    private static boolean checkJsonString(String value, char first, char last) {
+        if (value == null) {
+            return false;
+        }
+        String string = value.trim();
+        if (string.length() > 0 && string.charAt(0) == first && string.charAt(string.length() - 1) == last) {
+            return true;
+        }
+        return false;
+    }
 }
-- 
Gitee


From bb10eefd755f78b45c979b42ec4844adbe83408b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 6 Jan 2022 23:19:34 +0800
Subject: [PATCH 007/160] =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=B1=BB=E4=BC=98?=
 =?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/able/beans/ModifyStatus.java   |  44 ++++++
 .../com/gitee/qdbp/tools/utils/DateTools.java | 102 ++++++++-----
 .../com/gitee/qdbp/tools/utils/MapTools.java  | 138 +++++++++++++++++-
 .../gitee/qdbp/tools/utils/StringTools.java   |  66 +++++++--
 .../gitee/qdbp/tools/utils/DateToolsTest.java |  20 +++
 .../qdbp/tools/utils/ParseParamsTest.java     |  42 ++++++
 .../qdbp/tools/utils/StringToolsTest.java     |  36 +++++
 7 files changed, 399 insertions(+), 49 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/beans/ModifyStatus.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/utils/ParseParamsTest.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/ModifyStatus.java b/able/src/main/java/com/gitee/qdbp/able/beans/ModifyStatus.java
new file mode 100644
index 0000000..8199f21
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/ModifyStatus.java
@@ -0,0 +1,44 @@
+package com.gitee.qdbp.able.beans;
+
+/**
+ * 可修改状态
+ *
+ * @author zhaohuihua
+ * @version 20211219
+ */
+public class ModifyStatus {
+
+    /** 是否可修改 **/
+    private boolean modifiable = true;
+
+    /** 构造函数 **/
+    public ModifyStatus() {
+    }
+
+    /** 构造函数 **/
+    public ModifyStatus(boolean modifiable) {
+        this.modifiable = modifiable;
+    }
+
+    /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
+    public void lockToUnmodifiable() {
+        this.modifiable = false;
+    }
+
+    /** 设置是否可修改 **/
+    public void setModifiable(boolean modifiable) {
+        this.modifiable = modifiable;
+    }
+
+    /** 判断是否可修改 **/
+    public boolean isModifiable() {
+        return modifiable;
+    }
+
+    /** 检查是否可修改 **/
+    public void checkModifiable() {
+        if (!modifiable) {
+            throw new UnsupportedOperationException("unmodifiable");
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
index 480641b..7861788 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
@@ -199,11 +199,14 @@ public class DateTools {
      * parse("2000-10-20");<br>
      * parse("2018/8/20 15:25:35");<br>
      * parse("8/20/2018 15:25:35");<br>
+     * parse("2000.10.20 15:25:35.450");<br>
+     * parse("2000.10.20");<br>
+     * parse("15:25:35.450"); // 时间<br>
+     * parse("15:25:35"); // 时间<br>
      * parse("20001020152535450");<br>
      * parse("20001020152535");<br>
-     * parse("20001020");<br>
-     * parse("15:25:35");<br>
-     * parse("152535");<br>
+     * parse("20001020"); // 日期<br>
+     * parse("152535"); // 时间<br>
      * parse("Fri Oct 20 15:25:35 CST 2000");<br>
      * 解析年月日格式说明<br>
      * 年月日(中式): 2018/8/8, 2018/8/20, 2018/12/12<br>
@@ -220,18 +223,22 @@ public class DateTools {
         if (string == null || string.trim().length() == 0) {
             return null;
         }
-        if (ZONED.supported(string)) {
-            // 解析带时区的时间, 如: Fri Oct 20 15:25:35 CST 2000
-            return ZONED.parse(string);
-        }
         String datetime = string.trim();
+        // 空格位置
         int spaceIndex = -1;
+        // 横杠位置
         int hbarIndex = -1;
+        // 斜杠位置
         int slashIndex = -1;
+        // 冒号位置
         int colonIndex = -1;
+        // 点的位置
         int dotIndex = -1;
+        // 其他字符位置 (0-9以及-/:.之外的其他字符)
         int otherCharIndex = -1;
+        // 是否存在中文日期单位
         boolean existChineseDateUnits = false;
+        // 是否存在中文时间单位
         boolean existChineseTimeUnits = false;
         char[] chars = datetime.toCharArray();
         for (char i = 0; i < chars.length; i++) {
@@ -268,6 +275,11 @@ public class DateTools {
             }
         }
 
+        if (otherCharIndex >= 0 && ZONED.supported(string)) {
+            // 解析带时区的时间, 如: Fri Oct 20 15:25:35 CST 2000
+            return ZONED.parse(string);
+        }
+
         if (existChineseDateUnits || existChineseTimeUnits) {
             // 2018年7月8日10时20分30秒400毫秒
             Calendar calendar = Calendar.getInstance();
@@ -326,47 +338,69 @@ public class DateTools {
         }
 
         if (otherCharIndex >= 0) {
+            // 非标准格式: 存在0-9以及-/:.之外的其他字符, 使用PARSERS解析
             return PARSERS.parse(datetime);
         } else if (spaceIndex > 0) {
+            // 有空格, 尝试按日期+时间解析
             String[] array = StringTools.split(datetime, ' ');
             if (array.length != 2) {
+                // 非标准格式: 不止一个空格
                 return PARSERS.parse(datetime);
             }
-            if (colonIndex >= 0 && colonIndex < spaceIndex || dotIndex >= 0 && dotIndex < spaceIndex) {
-                // 日期部分有冒号或点
+            // 日期部分有没有冒号
+            boolean colonInDate = colonIndex >= 0 && colonIndex < spaceIndex;
+            if (colonInDate) {
+                // 非标准格式: 日期部分有冒号
                 return PARSERS.parse(datetime);
-            } else if (hbarIndex >= 0 && hbarIndex > spaceIndex || slashIndex >= 0 && slashIndex > spaceIndex) {
-                // 时间部分有横框或斜杠
+            }
+            // 时间部分有没有冒号
+            boolean colonInTime = colonIndex >= 0 && colonIndex > spaceIndex;
+            // 时间部分有没有横杠
+            boolean hbarInTime = hbarIndex >= 0 && hbarIndex > spaceIndex;
+            // 时间部分有没有斜杠
+            boolean slashInTime = slashIndex >= 0 && slashIndex > spaceIndex;
+            if (hbarInTime || slashInTime || !colonInTime) {
+                // 非标准格式: 时间部分有横框或斜杠, 或时间部分没有冒号
                 return PARSERS.parse(datetime);
             }
+            // 日期部分有没有横杠
+            boolean hbarInDate = hbarIndex > 0 && hbarIndex < spaceIndex;
+            // 日期部分有没有斜杠
+            boolean slashInDate = slashIndex > 0 && slashIndex < spaceIndex;
+            // 日期部分有没有点
+            boolean dotInDate = dotIndex >= 0 && dotIndex < spaceIndex;
             Date date;
-            if ((slashIndex < 0 || slashIndex > spaceIndex) && (hbarIndex > 0 && hbarIndex < spaceIndex)) {
-                // 横杠分隔的日期: 没有斜杠,有横杠且横杠的位置在日期部分
+            if (hbarInDate && !slashInDate && !dotInDate) {
+                // 横杠分隔的日期: 日期部分有横杠, 没有斜杠, 没有点
                 date = parseYyyyMMdd(array[0], '-');
-            } else if ((hbarIndex < 0 || hbarIndex > spaceIndex) && (slashIndex > 0 && slashIndex < spaceIndex)) {
-                // 斜杠分隔的日期: 没有横杠,有斜杠且斜杠的位置在日期部分
+            } else if (slashInDate && !hbarInDate && !dotInDate) {
+                // 斜杠分隔的日期: 日期部分有斜杠, 没有横杠, 没有点
                 date = parseYyyyMMdd(array[0], '/');
+            } else if (dotInDate && !slashInDate && !hbarInDate) {
+                // 点分隔的日期: 日期部分有点, 没有斜杠, 没有横杠
+                date = parseYyyyMMdd(array[0], '.');
             } else {
                 return PARSERS.parse(datetime);
             }
-            if (colonIndex > spaceIndex) {
-                // 时间部分有冒号
-                return parseTime(date, array[1]);
-            } else {
-                return PARSERS.parse(datetime);
-            }
-        } else if (hbarIndex < 0 && slashIndex < 0 && colonIndex > 0) {
-            // 时间: 没有横杠和斜杠,有冒号
-            return parseTime(toStartTime(new Date()), datetime);
-        } else if (colonIndex < 0 && dotIndex < 0 && slashIndex < 0 && hbarIndex > 0) {
-            // 横杠分隔的日期: 没有冒号和点,没有斜杠,有横杠
-            return parseYyyyMMdd(datetime, '-');
-        } else if (colonIndex < 0 && dotIndex < 0 && hbarIndex < 0 && slashIndex > 0) {
-            // 横杠分隔的日期: 没有冒号和点,没有横杠,有斜杠
-            return parseYyyyMMdd(datetime, '/');
+            // 解析时间部分
+            return parseTime(date, array[1]);
         } else {
-            return PARSERS.parse(datetime);
+            // 没有空格, 判断是日期还是时间
+            if (colonIndex > 0 && hbarIndex < 0 && slashIndex < 0) {
+                // 时间: 有冒号,没有横杠和斜杠
+                return parseTime(toStartTime(new Date()), datetime);
+            } else if (hbarIndex > 0 && colonIndex < 0 && dotIndex < 0 && slashIndex < 0) {
+                // 横杠分隔的日期: 有横杠,没有冒号,没有点,没有斜杠
+                return parseYyyyMMdd(datetime, '-');
+            } else if (slashIndex > 0 && colonIndex < 0 && dotIndex < 0 && hbarIndex < 0) {
+                // 横杠分隔的日期: 有斜杠,没有冒号,没有点,没有横杠
+                return parseYyyyMMdd(datetime, '/');
+            } else if (dotIndex > 0 && colonIndex < 0 && hbarIndex < 0 && slashIndex < 0) {
+                // 点分隔的日期: 有点,没有冒号,没有横杠,没有斜杠
+                return parseYyyyMMdd(datetime, '.');
+            }
         }
+        return PARSERS.parse(datetime);
     }
 
     /**
@@ -1021,7 +1055,7 @@ public class DateTools {
         return calendar.getTime();
     }
 
-    /** 日期设置年 **/
+    /** 日期设置年份 **/
     public static Date setYear(Date date, int year) {
         Calendar calendar = Calendar.getInstance();
         calendar.setTime(date);
@@ -1029,7 +1063,7 @@ public class DateTools {
         return calendar.getTime();
     }
 
-    /** 日期设置月(从0开始) **/
+    /** 日期设置月份 (从0开始; 即传入0表示1月,1表示2月) **/
     public static Date setMonth(Date date, int month) {
         Calendar calendar = Calendar.getInstance();
         calendar.setTime(date);
@@ -1037,7 +1071,7 @@ public class DateTools {
         return calendar.getTime();
     }
 
-    // /** 日期设置月 **/
+    // /** 日期设置月份 **/
     // public static Date setMonth(Date date, Month month) {
     //     Calendar calendar = Calendar.getInstance();
     //     calendar.setTime(date);
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index 37a5b9a..d706004 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -55,9 +55,20 @@ public class MapTools {
         }
     }
 
+    /**
+     * 解析类似Json的参数: name:zhh;phone:139xxxx8888
+     *
+     * @param params 类似Json的参数
+     * @return Map
+     */
+    public static Map<String, String> parseJsonLikeParams(String params) {
+        List<String> array = StringTools.splits(params, true, true, ';', '\n');
+        return parseStringParams(array, ':', null);
+    }
+
     /**
      * 解析请求参数: name=zhh&phone=139xxxx8888
-     * 
+     *
      * @param params 请求参数
      * @return Map
      */
@@ -66,19 +77,34 @@ public class MapTools {
     }
 
     /**
-     * 解析请求参数
+     * 解析请求参数: name=zhh&phone=139xxxx8888&code=&amp;amp;
      * 
      * @param params 请求参数
      * @param charset 字符编码: 如果不为空, 将对value执行URLDecoder.decode(value, charset)
      * @return Map
      */
     public static Map<String, String> parseRequestParams(String params, String charset) {
+        List<String> array = splitParams(params);
+        return parseStringParams(array, '=', charset);
+    }
+
+    /**
+     * 解析字符串参数: name=zhh&phone=139xxxx8888
+     *
+     * @param params 请求参数
+     * @param separator 分隔符
+     * @param charset 字符编码: 如果不为空, 将对value执行URLDecoder.decode(value, charset)
+     * @return Map
+     */
+    private static Map<String, String> parseStringParams(List<String> params, char separator, String charset) {
         Map<String, String> map = new HashMap<>();
-        String[] array = StringTools.split(params, '&');
-        for (String item : array) {
+        if (params == null) {
+            return map;
+        }
+        for (String item : params) {
             // 不能用StringTools.split('=');
             // 因为有可能存在多个等号: contentType=application/json;charset=UTF-8
-            int index = item.indexOf('=');
+            int index = item.indexOf(separator);
             if (index < 0) {
                 map.put(item, null);
             } else if (index == 0) {
@@ -98,6 +124,108 @@ public class MapTools {
         return map;
     }
 
+    private static List<String> splitParams(String string) {
+        if (string == null) {
+            return null;
+        }
+        if (string.length() == 0) {
+            return new ArrayList<>();
+        }
+        List<String> list = new ArrayList<>();
+        StringBuilder buffer = new StringBuilder();
+        boolean lastIsSplitChar = false;
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (c == '\\' && i < z - 1) {
+                char next = string.charAt(++i);
+                if (next == '&' || next == '\\') {
+                    // 是转义字符 (只加下一字符, 吞掉斜杠)
+                    buffer.append(next);
+                    lastIsSplitChar = false;
+                    continue;
+                } else {
+                    // 斜杠后面不是转义字符, 补上斜杠, 继续处理下一字符
+                    buffer.append(c);
+                    c = next;
+                }
+            }
+            if (c == '&') {
+                // 判断是不是实体名称
+                String entityName = findEntityName(string, i + 1);
+                if (entityName != null) {
+                    buffer.append(c).append(entityName);
+                    lastIsSplitChar = false;
+                    i += entityName.length();
+                    continue;
+                }
+                // 遇到分隔符
+                list.add(StringTools.trim(buffer.toString()));
+                buffer.setLength(0);
+                lastIsSplitChar = true;
+            } else {
+                // 不是分隔符
+                buffer.append(c);
+                lastIsSplitChar = false;
+            }
+        }
+        if (buffer.length() > 0) {
+            list.add(StringTools.trim(buffer.toString()));
+            buffer.setLength(0);
+        }
+        if (lastIsSplitChar) {
+            list.add("");
+        }
+        return list;
+    }
+
+    private static String findEntityName(String string, int index) {
+        StringBuilder temp = new StringBuilder();
+        for (int i = index, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (temp.length() == 0 && c == '#') {
+                temp.append(c);
+            } else if (c == '$') {
+                // isWordChar('$')返回true,但这里不支持$,所以单独处理
+                break;
+            } else if (StringTools.isWordChar(c)) {
+                temp.append(c);
+            } else if (c == ';') {
+                temp.append(c);
+                return temp.toString();
+            } else {
+                break;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 解析请求参数
+     *
+     * @param params 请求参数, request.getParameterMap()
+     * @return Map
+     */
+    public static Map<String, Object> parseRequestMap(Map<String, String[]> params) {
+        Map<String, Object> map = new HashMap<>();
+        if (params == null) {
+            return map;
+        }
+
+        for (Map.Entry<String, String[]> entry : params.entrySet()) {
+            String key = entry.getKey();
+            String[] values = entry.getValue();
+            if (key == null || key.length() == 0 || values == null || values.length == 0) {
+                continue;
+            }
+            if (values.length == 1) {
+                map.put(key, values[0]);
+            } else {
+                map.put(key, values);
+            }
+        }
+        return map;
+    }
+
     /**
      * 判断Map中是否存在指定的KEY
      * 
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index c01d976..e377123 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -530,6 +530,23 @@ public class StringTools {
      * @return 拆分后的字符串数组
      */
     public static List<String> splits(String string, boolean trim, char... chars) {
+        return splits(string, trim, false, chars);
+    }
+
+    /**
+     * 按指定分隔符拆分字符串<br>
+     * splits("aa|bb|cc", '|') --&gt; [aa, bb, cc]<br>
+     * splits("aa|bb||cc", '|') --&gt; [aa, bb, , cc]<br>
+     * splits("aa\\|bb||cc|\\\\d", true, false, '|') --&gt; ["aa\\", "bb", "", "cc", "\\\\d"]<br>
+     * splits("aa\\|bb||cc|\\\\d", true, true, '|') --&gt; ["aa|bb", "", "cc", "\\d"]
+     *
+     * @param string 原字符串
+     * @param trim 每一个子字符串是否执行trim()
+     * @param escape 是否使用转义 (分隔符前面带反斜杠\当前普通字符)
+     * @param chars 分隔符
+     * @return 拆分后的字符串数组
+     */
+    public static List<String> splits(String string, boolean trim, boolean escape, char... chars) {
         if (string == null) {
             return null;
         }
@@ -537,24 +554,49 @@ public class StringTools {
             return new ArrayList<>();
         }
         if (chars == null || chars.length == 0) {
-            return new ArrayList<>(Collections.singletonList(string));
+            List<String> list = new ArrayList<>();
+            list.add(string);
+            return list;
+        }
+        Map<Character, ?> separatorMaps = new HashMap<>();
+        Map<Character, Character> escapeMaps = new HashMap<>();
+        escapeMaps.put('\\', '\\');
+        for (char c : chars) {
+            separatorMaps.put(c, null);
+            if (escape) {
+                if (c == '\r') {
+                    escapeMaps.put('r', '\r');
+                } else if (c == '\n') {
+                    escapeMaps.put('n', '\n');
+                } else if (c == '\t') {
+                    escapeMaps.put('t', '\t');
+                } else if (c == '\f') {
+                    escapeMaps.put('f', '\f');
+                } else {
+                    escapeMaps.put(c, c);
+                }
+            }
         }
         List<String> list = new ArrayList<>();
         StringBuilder buffer = new StringBuilder();
         boolean lastIsSplitChar = false;
         for (int i = 0, z = string.length(); i < z; i++) {
             char c = string.charAt(i);
-            boolean isSplitChar = false;
-            for (char s : chars) {
-                if (c == s) {
-                    isSplitChar = true;
-                    break;
+            if (escape && c == '\\' && i < z - 1) {
+                char next = string.charAt(++i);
+                if (escapeMaps.containsKey(next)) {
+                    // 是转义字符 (只加转换后的字符, 吞掉斜杠)
+                    buffer.append(escapeMaps.get(next));
+                    lastIsSplitChar = false;
+                    continue;
+                } else {
+                    // 斜杠后面不是转义字符, 补上斜杠, 继续处理下一字符
+                    buffer.append(c);
+                    c = next;
                 }
             }
-            if (!isSplitChar) {
-                buffer.append(c);
-                lastIsSplitChar = false;
-            } else {
+            if (separatorMaps.containsKey(c)) {
+                // 遇到分隔符
                 if (trim) {
                     list.add(trim(buffer.toString()));
                 } else {
@@ -562,6 +604,10 @@ public class StringTools {
                 }
                 buffer.setLength(0);
                 lastIsSplitChar = true;
+            } else {
+                // 不是分隔符
+                buffer.append(c);
+                lastIsSplitChar = false;
             }
         }
         if (buffer.length() > 0) {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/DateToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/DateToolsTest.java
index 35b20f0..c6d7a57 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/DateToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/DateToolsTest.java
@@ -28,6 +28,18 @@ public class DateToolsTest {
         testParse16("2018/12/24", "2018-12-24 00:00:00.000");
         testParse16("2016/2/29", "2016-02-29 00:00:00.000");
         testParse16("2018/2/29", "2018-03-01 00:00:00.000");
+        testParse16("2018-0-8", "2017-12-08 00:00:00.000");
+        testParse16("2018-7-8", "2018-07-08 00:00:00.000");
+        testParse16("2018-7-20", "2018-07-20 00:00:00.000");
+        testParse16("2018-12-24", "2018-12-24 00:00:00.000");
+        testParse16("2016-2-29", "2016-02-29 00:00:00.000");
+        testParse16("2018-2-29", "2018-03-01 00:00:00.000");
+        testParse16("2018.0.8", "2017-12-08 00:00:00.000");
+        testParse16("2018.7.8", "2018-07-08 00:00:00.000");
+        testParse16("2018.7.20", "2018-07-20 00:00:00.000");
+        testParse16("2018.12.24", "2018-12-24 00:00:00.000");
+        testParse16("2016.2.29", "2016-02-29 00:00:00.000");
+        testParse16("2018.2.29", "2018-03-01 00:00:00.000");
         testParse16("7/8/2018", "2018-07-08 00:00:00.000");
         testParse16("7/20/2018", "2018-07-20 00:00:00.000");
         testParse16("12/24/2018", "2018-12-24 00:00:00.000");
@@ -51,6 +63,14 @@ public class DateToolsTest {
         testParse30("2018/7/8 4:5:6.999", "2018-07-08 04:05:06.999");
         testParse30("2018/7/8 23:59:59.999999", "2018-07-08 23:59:59.999");
         testParse30("2018/7/8 23:59:59.9", "2018-07-08 23:59:59.900"); // 是900毫秒, 不009毫秒
+        testParse30("2018-7-8 20:30:40", "2018-07-08 20:30:40.000");
+        testParse30("2018-7-8 4:5:6.999", "2018-07-08 04:05:06.999");
+        testParse30("2018-7-8 23:59:59.999999", "2018-07-08 23:59:59.999");
+        testParse30("2018-7-8 23:59:59.9", "2018-07-08 23:59:59.900"); // 是900毫秒, 不009毫秒
+        testParse30("2018.7.8 20:30:40", "2018-07-08 20:30:40.000");
+        testParse30("2018.7.8 4:5:6.999", "2018-07-08 04:05:06.999");
+        testParse30("2018.7.8 23:59:59.999999", "2018-07-08 23:59:59.999");
+        testParse30("2018.7.8 23:59:59.9", "2018-07-08 23:59:59.900"); // 是900毫秒, 不009毫秒
         testParse35("20101020152535450+0000", "2010-10-20 23:25:35.450"); // yyyyMMddHHmmssSSSZ
         testParse35("20101020152535450GMT+00:00", "2010-10-20 23:25:35.450"); // yyyyMMddHHmmssSSSZ
         testParse35("2010-10-20 15:25:35.450 +0000", "2010-10-20 23:25:35.450"); // yyyy-MM-dd HH:mm:ss.SSS Z
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/ParseParamsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/ParseParamsTest.java
new file mode 100644
index 0000000..ef2f124
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/ParseParamsTest.java
@@ -0,0 +1,42 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.testng.annotations.Test;
+
+/**
+ * 解析参数测试类
+ *
+ * @author zhaohuihua
+ * @version 20220106
+ */
+@Test
+public class ParseParamsTest {
+
+    @Test
+    public void testParseJsonLikeParams() {
+        String s = "name:zhh; phone:139xxxx8888; code:&amp\\;aa&nbsp\\;bb&cc:dd;" +
+                " contentType:application/json\\;charset=UTF-8";
+        Map<String, String> expected = new HashMap<>();
+        expected.put("name", "zhh");
+        expected.put("phone", "139xxxx8888");
+        expected.put("code", "&amp;aa&nbsp;bb&cc:dd");
+        expected.put("contentType", "application/json;charset=UTF-8");
+        Map<String, String> actual = MapTools.parseJsonLikeParams(s);
+        System.out.println(actual);
+        AssertTools.assertDeepEquals(actual, expected);
+    }
+
+    @Test
+    public void testParseRequestParams() {
+        String s = "name=zhh&phone=139xxxx8888&code=&amp;aa&nbsp;bb\\&cc=dd&contentType=application/json;charset=UTF-8";
+        Map<String, String> expected = new HashMap<>();
+        expected.put("name", "zhh");
+        expected.put("phone", "139xxxx8888");
+        expected.put("code", "&amp;aa&nbsp;bb&cc=dd");
+        expected.put("contentType", "application/json;charset=UTF-8");
+        Map<String, String> actual = MapTools.parseRequestParams(s);
+        System.out.println(actual);
+        AssertTools.assertDeepEquals(actual, expected);
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/StringToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/StringToolsTest.java
index 8611117..6d0f541 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/StringToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/StringToolsTest.java
@@ -1,5 +1,7 @@
 package com.gitee.qdbp.tools.utils;
 
+import java.util.Arrays;
+import java.util.List;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -143,4 +145,38 @@ public class StringToolsTest {
         Assert.assertFalse(StringTools.isNumber("9999,.9"));
         Assert.assertFalse(StringTools.isNumber("9,.9999"));
     }
+
+    public void testSplit() {
+        {
+            char[] delimiter = new char[] {'|'};
+            testSplit("aa|bb|cc", false, delimiter, "aa", "bb", "cc");
+            testSplit("aa|bb||cc", false, delimiter, "aa", "bb", "", "cc");
+            testSplit("aa\\tbb||cc|\\\\dd", false, delimiter, "aa\\tbb", "", "cc", "\\\\dd");
+            testSplit("aa\\|bb||cc|\\\\dd", false, delimiter, "aa\\", "bb", "", "cc", "\\\\dd");
+        }
+        {
+            char[] delimiter = new char[] {'|'};
+            testSplit("aa|bb|cc", true, delimiter, "aa", "bb", "cc");
+            testSplit("aa|bb||cc", true, delimiter, "aa", "bb", "", "cc");
+            testSplit("aa\\tbb||cc|\\\\dd", true, delimiter, "aa\\tbb", "", "cc", "\\dd");
+            testSplit("aa\\|bb||cc|\\\\dd", true, delimiter, "aa|bb", "", "cc", "\\dd");
+        }
+        {
+            char[] delimiter = new char[] {'|', '\t'};
+            testSplit("aa\\tbb||cc|\\\\dd", false, delimiter, "aa\\tbb", "", "cc", "\\\\dd");
+        }
+        {
+            char[] delimiter = new char[] {'|', '\t'};
+            testSplit("aa\\tbb||cc|\\\\dd", true, delimiter, "aa\tbb", "", "cc", "\\dd");
+        }
+    }
+
+    private void testSplit(String source, boolean escape, char[] delimiter, String... targets) {
+        List<String> result = StringTools.splits(source, true, escape, delimiter);
+        System.out.println("escape=" + escape);
+        System.out.println(source);
+        System.out.println(ConvertTools.joinToString(result, ", "));
+        AssertTools.assertDeepEquals(result, Arrays.asList(targets));
+        System.out.println("================");
+    }
 }
-- 
Gitee


From 69a2f61f6b2b6235ca573870945b797d46e40b35 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 6 Jan 2022 23:21:38 +0800
Subject: [PATCH 008/160] =?UTF-8?q?=E6=96=87=E6=9C=AC=E6=AF=94=E5=AF=B9?=
 =?UTF-8?q?=E5=B7=A5=E5=85=B7=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/tools/compare/CompareTools.java      | 45 +++++++++++++++----
 1 file changed, 37 insertions(+), 8 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
index 3dad859..6a97147 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
@@ -144,7 +144,7 @@ public class CompareTools {
      * @param source 原文本
      * @param target 目标文本
      * @param findMode 是否为查找模式
-     * @param ignoreChars 忽略字符, 如标点符号以及"的了吗吧"
+     * @param ignoreChars 忽略字符, 如标点符号
      * @return 差异信息
      */
     public static DiffInfo textCompare(String source, String target, boolean findMode, String ignoreChars) {
@@ -232,8 +232,15 @@ public class CompareTools {
                 }
             }
             if (prevDelete != null) {
-                isMatched = false;
-                diffItems.add(new DiffItem(DiffType.delete, prevDelete.text));
+                // 原文比目标多的部分
+                boolean isLike = isIgnoreWords(prevDelete.text, ignoreChars);
+                if (isLike) {
+                    equalsTotal += prevDelete.text.length();
+                    diffItems.add(new DiffItem(DiffType.like, prevDelete.text));
+                } else {
+                    isMatched = false;
+                    diffItems.add(new DiffItem(DiffType.delete, prevDelete.text));
+                }
                 prevDelete = null;
             }
             if (item.operation == Operation.DELETE) {
@@ -241,6 +248,7 @@ public class CompareTools {
                 continue;
             }
             if (item.operation == Operation.INSERT) {
+                // 原文比目标少的部分
                 boolean isLike = isIgnoreWords(item.text, ignoreChars);
                 if (isLike) {
                     equalsTotal += item.text.length();
@@ -255,16 +263,31 @@ public class CompareTools {
             }
         }
         if (prevDelete != null) {
-            isMatched = false;
-            diffItems.add(new DiffItem(DiffType.delete, prevDelete.text));
+            boolean isLike = isIgnoreWords(prevDelete.text, ignoreChars);
+            if (isLike) {
+                equalsTotal += prevDelete.text.length();
+                diffItems.add(new DiffItem(DiffType.like, prevDelete.text));
+            } else {
+                isMatched = false;
+                diffItems.add(new DiffItem(DiffType.delete, prevDelete.text));
+            }
+        }
+
+        int sourceLength = source.length() - prefixLength - suffixLength;
+        int targetLength = target.length();
+
+        double score = 1.0D;
+        if (!isMatched) {
+            // 分数: 相等部分占目标长度的比率占75%, 相等部分占原文长度的比率占25%
+            score = 0.75 * equalsTotal / targetLength + 0.25 * equalsTotal / sourceLength;
         }
 
         DiffInfo result = new DiffInfo();
         result.setMatched(isMatched);
-        result.setScore(1.0 * equalsTotal / target.length());
+        result.setScore(score);
         result.setDetails(diffItems);
         result.setStartIndex(prefixLength);
-        result.setEndIndex(target.length() - suffixLength);
+        result.setEndIndex(source.length() - suffixLength);
         return result;
     }
 
@@ -322,7 +345,13 @@ public class CompareTools {
                     if (item.getText() != null) {
                         text = doEscapeXml(item.getText());
                     }
-                    String title = doEscapeXml(item.getReplacement());
+                    String title;
+                    if (item.getText() != null && item.getReplacement() == null) {
+                        // 有原文没有替换内容, 就是删除
+                        title = deleteTitle;
+                    } else {
+                        title = doEscapeXml(item.getReplacement());
+                    }
                     appendHtmlTag(buffer, span, type, text, likeTitlePrefix, title);
                     break;
                 }
-- 
Gitee


From 8b62d0ef728bbe803764fad82e9c2ce18f2a85cf Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 6 Jan 2022 23:22:03 +0800
Subject: [PATCH 009/160] =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=B7=A5=E5=85=B7?=
 =?UTF-8?q?=E7=B1=BB=E5=A2=9E=E5=BC=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/files/ImageTools.java    | 223 ++++++++++++++++--
 1 file changed, 207 insertions(+), 16 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/files/ImageTools.java b/tools/src/main/java/com/gitee/qdbp/tools/files/ImageTools.java
index 43faf9a..f7c6198 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/files/ImageTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/files/ImageTools.java
@@ -1,7 +1,7 @@
 package com.gitee.qdbp.tools.files;
 
 import java.awt.Color;
-import java.awt.Graphics;
+import java.awt.Graphics2D;
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -10,6 +10,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Hashtable;
 import java.util.Iterator;
+import java.util.List;
 import javax.imageio.ImageIO;
 import javax.imageio.ImageReader;
 import javax.imageio.stream.ImageInputStream;
@@ -32,7 +33,7 @@ public class ImageTools {
 
     /**
      * 给图片增加边距
-     * 
+     *
      * @param image 图片
      * @param top 上边距
      * @param bottom 下边距
@@ -46,37 +47,37 @@ public class ImageTools {
 
     /**
      * 给图片增加边距
-     * 
+     *
      * @param image 图片
-     * @param color 填充颜色
+     * @param bgColor 填充边距的背景色
      * @param top 上边距
      * @param bottom 下边距
      * @param left 左边距
      * @param right 右边距
      * @return 新图片
      */
-    public static BufferedImage paddingImage(BufferedImage image, Color color, int top, int bottom, int left,
-            int right) {
+    public static BufferedImage paddingImage(BufferedImage image, Color bgColor,
+            int top, int bottom, int left, int right) {
         int w = image.getWidth();
         int h = image.getHeight();
         int width = w + left + right;
         int height = h + top + bottom;
         BufferedImage background = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
         // 背景默认是黑色的, 填充为指定颜色
-        if (color != null) {
-            Graphics g = background.getGraphics();
-            g.setColor(color);
+        if (bgColor != null) {
+            Graphics2D g = (Graphics2D) background.getGraphics();
+            g.setBackground(bgColor);
             if (top > 0) {
-                g.fillRect(0, 0, width, top);
+                g.clearRect(0, 0, width, top);
             }
             if (bottom > 0) {
-                g.fillRect(0, height - bottom, width, bottom);
+                g.clearRect(0, height - bottom, width, bottom);
             }
             if (left > 0) {
-                g.fillRect(0, 0, left, height);
+                g.clearRect(0, 0, left, height);
             }
             if (right > 0) {
-                g.fillRect(0, width - right, right, height);
+                g.clearRect(width - right, 0, right, height);
             }
         }
         // 复制图片
@@ -84,6 +85,196 @@ public class ImageTools {
         return background;
     }
 
+    /**
+     * 纵向合并图片
+     *
+     * @param imageList 图片列表
+     */
+    public static BufferedImage mergeImage(List<BufferedImage> imageList) {
+        return mergeImage(imageList, Color.WHITE, 0, 0, 0, 0);
+    }
+
+    /**
+     * 纵向合并图片
+     *
+     * @param imageList 图片列表
+     * @param top 上边距
+     * @param bottom 下边距
+     * @param left 左边距
+     * @param right 右边距
+     */
+    public static BufferedImage mergeImage(List<BufferedImage> imageList, int top, int bottom, int left, int right) {
+        return mergeImage(imageList, Color.WHITE, top, bottom, left, right);
+    }
+
+    /**
+     * 纵向合并图片
+     *
+     * @param imageList 图片列表
+     * @param bgColor 填充边距的背景色
+     * @param top 上边距
+     * @param bottom 下边距
+     * @param left 左边距
+     * @param right 右边距
+     */
+    public static BufferedImage mergeImage(List<BufferedImage> imageList, Color bgColor,
+            int top, int bottom, int left, int right) {
+        int maxHeight = top + bottom;
+        int maxWidth = left + right;
+        for (BufferedImage image : imageList) {
+            maxHeight += image.getHeight();
+            if (maxWidth < image.getWidth()) {
+                maxWidth = image.getWidth();
+            }
+        }
+        BufferedImage merged = new BufferedImage(maxWidth, maxHeight, BufferedImage.TYPE_3BYTE_BGR);
+        if (bgColor != null) {
+            Graphics2D g = (Graphics2D) merged.getGraphics();
+            g.setBackground(bgColor);
+            if (top > 0) {
+                g.clearRect(0, 0, maxWidth, top);
+            }
+            if (bottom > 0) {
+                g.clearRect(0, maxHeight - bottom, maxWidth, bottom);
+            }
+            if (left > 0) {
+                g.clearRect(0, 0, left, maxHeight);
+            }
+            if (right > 0) {
+                g.clearRect(maxWidth - right, 0, right, maxHeight);
+            }
+        }
+        for (int i = 0; i < imageList.size(); i++) {
+            int usedHeight = top;
+            if (i > 0) {
+                int currentImageIndex = i;
+                while ((currentImageIndex - 1) >= 0) {
+                    usedHeight += imageList.get(currentImageIndex - 1).getHeight();
+                    currentImageIndex--;
+                }
+            }
+            BufferedImage image = imageList.get(i);
+            for (int h = 0; h < image.getHeight(); h++) {
+                for (int w = 0; w < image.getWidth(); w++) {
+                    merged.setRGB(w + left, h + usedHeight, image.getRGB(w, h));
+                }
+                Color bg = bgColor == null ? Color.WHITE : bgColor;
+                for (int w = image.getWidth(); w < maxWidth; w++) {
+                    merged.setRGB(w + left, h + usedHeight, bg.getRGB());
+                }
+            }
+        }
+        return merged;
+    }
+
+    /**
+     * 删除多余的白边
+     *
+     * @param image 原图片
+     * @return 修改后的图片
+     */
+    public static BufferedImage removeImagePadding(BufferedImage image) {
+        int width = image.getWidth();
+        int height = image.getHeight();
+        int maxX = width - 1;
+        int maxY = height - 1;
+
+        int rgb1 = image.getRGB(0, 0); // 左上角
+        int rgb2 = image.getRGB(maxX, 0); // 右上角
+        int rgb3 = image.getRGB(0, maxY); // 左下角
+        int rgb4 = image.getRGB(maxX, maxY); // 右下角
+
+        int leftX = getLeftPadding(image, rgb1);
+        if (leftX < 0 && rgb1 != rgb3) {
+            // 如果左上角的颜色不满足要求, 则取左下角的颜色重试
+            leftX = getLeftPadding(image, rgb3);
+        }
+        int topY = getTopPadding(image, rgb1);
+        if (topY < 0 && rgb1 != rgb2) {
+            // 如果左上角的颜色不满足要求, 则取左上角的颜色重试
+            topY = getLeftPadding(image, rgb2);
+        }
+        int rightX = getRightPadding(image, rgb4);
+        if (rightX < 0 && rgb4 != rgb2) {
+            // 如果右下角的颜色不满足要求, 则取左上角的颜色重试
+            rightX = getLeftPadding(image, rgb2);
+        }
+        int bottomY = getBottomPadding(image, rgb4);
+        if (bottomY < 0 && rgb4 != rgb3) {
+            // 如果右下角的颜色不满足要求, 则取左下角的颜色重试
+            bottomY = getLeftPadding(image, rgb3);
+        }
+
+        // 如果topY,leftX返回-1,说明上边或左边没有白边,此时坐标取0
+        int x1 = Math.max(0, leftX);
+        int y1 = Math.max(0, topY);
+
+        // bottomY,rightX返回-1,说明下边或右边没有白边,此时坐标分别取图片的高度和宽度
+        // 宽
+        int x2 = rightX < 0 ? maxX : rightX;
+        // 高
+        int y2 = bottomY < 0 ? maxY : bottomY;
+
+        // 如果四个边都没有白边, 就返回null
+        if (x1 == 0 && y1 == 0 && x2 == width && y2 == height) {
+            return image;
+        }
+
+        return image.getSubimage(x1, y1, x2 - x1, y2 - y1);
+    }
+
+    // 从上往下扫描,取上边最突出的点的Y坐标
+    private static int getTopPadding(BufferedImage originImage, int baseRgb) {
+        for (int y = 0; y < originImage.getHeight(); y++) {
+            for (int x = 0; x < originImage.getWidth(); x++) {
+                int rgb = originImage.getRGB(x, y);
+                if (rgb != baseRgb) {
+                    return y - 1;
+                }
+            }
+        }
+        return -1;
+    }
+
+    // 从左往右扫描,取左边最突出的点的X坐标
+    private static int getLeftPadding(BufferedImage originImage, int baseRgb) {
+        for (int x = 0; x < originImage.getWidth(); x++) {
+            for (int y = 0; y < originImage.getHeight(); y++) {
+                int rgb = originImage.getRGB(x, y);
+                if (rgb != baseRgb) {
+                    return x - 1;
+                }
+            }
+        }
+        return -1;
+    }
+
+    // 从下往上扫描,取下边最突出的点的Y坐标
+    private static int getBottomPadding(BufferedImage originImage, int baseRgb) {
+        for (int y = originImage.getHeight() - 1; y >= 0; y--) {
+            for (int x = originImage.getWidth() - 1; x >= 0; x--) {
+                int rgb = originImage.getRGB(x, y);
+                if (rgb != baseRgb) {
+                    return y + 1;
+                }
+            }
+        }
+        return -1;
+    }
+
+    // 从右往左扫描,取右边最突出的X坐标
+    private static int getRightPadding(BufferedImage originImage, int baseRgb) {
+        for (int x = originImage.getWidth() - 1; x >= 0; x--) {
+            for (int y = originImage.getHeight() - 1; y >= 0; y--) {
+                int rgb = originImage.getRGB(x, y);
+                if (rgb != baseRgb) {
+                    return x + 1;
+                }
+            }
+        }
+        return -1;
+    }
+
     /**
      * 生成缩略图<br>
      * 截取图片的中间部分(不产生变形)<br>
@@ -162,7 +353,7 @@ public class ImageTools {
 
     /**
      * 生成二维码
-     * 
+     *
      * @param content 内容
      * @param size 尺寸
      * @param os 输出流
@@ -173,7 +364,7 @@ public class ImageTools {
 
     /**
      * 生成二维码
-     * 
+     *
      * @param content 内容
      * @param size 尺寸
      * @param margin 边距
@@ -203,7 +394,7 @@ public class ImageTools {
 
     /**
      * 重设白边框宽度
-     * 
+     *
      * @param matrix BitMatrix
      * @param margin 边框宽度
      * @return 新的BitMatrix
-- 
Gitee


From 9994544845eebf2e2f3a8d79cbc644f8bca4e6fe Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 6 Jan 2022 23:22:19 +0800
Subject: [PATCH 010/160] =?UTF-8?q?Json=E6=B5=8B=E8=AF=95=E7=B1=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/utils/JsonTest.java  | 31 +++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/utils/JsonTest.java

diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/JsonTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/JsonTest.java
new file mode 100644
index 0000000..b52c9d0
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/JsonTest.java
@@ -0,0 +1,31 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JsonTest
+ *
+ * @author zhaohuihua
+ * @version 20211209
+ */
+public class JsonTest {
+    public static void main(String[] args) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("accountCid", "201601251326023230619fe78f6220eb");
+        data.put("registerApplyCid", "2016012920303240607176aec6733a32");
+        data.put("status", "1");
+        Map<String, Object> params = new HashMap<>();
+        params.put("jsonData", data);
+        params.put("account", "15500000006");
+        params.put("accountType", 1);
+        params.put("appCode", "A000");
+        params.put("digest", "6ea2604fa0204421b2579aac82d43214");
+        params.put("imeiuuid", "359596063773059");
+        params.put("sourceType", "android");
+        params.put("ts", 945700);
+        params.put("createTime", DateTools.parse("2016-1-20"));
+        System.out.println(JsonTools.toLogString(params));
+        System.out.println(JsonTools.toJsonString(params));
+    }
+}
-- 
Gitee


From 830c3bb784542f801b97f0869ee09b7735c0772c Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 9 Jan 2022 21:37:52 +0800
Subject: [PATCH 011/160] =?UTF-8?q?Jdk8=E6=97=A5=E6=9C=9F=E8=BD=AC?=
 =?UTF-8?q?=E6=8D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/ConvertTools.java  | 53 +++++++++++++++++++
 .../com/gitee/qdbp/tools/utils/DateTools.java | 23 ++++----
 2 files changed, 65 insertions(+), 11 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index 3c9c9ad..bfe8f8f 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -5,6 +5,12 @@ import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.text.DecimalFormat;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -1291,4 +1297,51 @@ public class ConvertTools {
         return MapTools.filterFields(data, fields);
     }
 
+    /** LocalDateTime转换为Date **/
+    public static Date toDate(LocalDateTime source) {
+        ZoneId zoneId = ZoneId.systemDefault();
+        // Combines this date-time with a time-zone to create a ZonedDateTime.
+        ZonedDateTime zdt = source.atZone(zoneId);
+        return Date.from(zdt.toInstant());
+    }
+
+    /** LocalDate转换为Date **/
+    public static Date toDate(LocalDate source) {
+        ZoneId zoneId = ZoneId.systemDefault();
+        // Combines this date-time with a time-zone to create a ZonedDateTime.
+        ZonedDateTime zdt = source.atStartOfDay().atZone(zoneId);
+        return Date.from(zdt.toInstant());
+    }
+
+    /** LocalTime转换为Date **/
+    public static Date toDate(LocalTime source) {
+        LocalDateTime localDateTime = LocalDateTime.of(LocalDate.now(), source);
+        ZoneId zoneId = ZoneId.systemDefault();
+        // Combines this date-time with a time-zone to create a ZonedDateTime.
+        ZonedDateTime zdt = localDateTime.atZone(zoneId);
+        return Date.from(zdt.toInstant());
+    }
+
+    /** Date转换为LocalDateTime **/
+    public static LocalDateTime toLocalDateTime(Date source) {
+        Instant instant = source.toInstant();
+        ZoneId zone = ZoneId.systemDefault();
+        return LocalDateTime.ofInstant(instant, zone);
+    }
+
+    /** Date转换为LocalDate **/
+    public static LocalDate toLocalDate(Date source) {
+        Instant instant = source.toInstant();
+        ZoneId zone = ZoneId.systemDefault();
+        LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
+        return localDateTime.toLocalDate();
+    }
+
+    /** Date转换为LocalTime **/
+    public static LocalTime toLocalTime(Date source) {
+        Instant instant = source.toInstant();
+        ZoneId zone = ZoneId.systemDefault();
+        LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
+        return localDateTime.toLocalTime();
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
index 7861788..1da539f 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
@@ -2,6 +2,7 @@ package com.gitee.qdbp.tools.utils;
 
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.time.Month;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Date;
@@ -423,8 +424,8 @@ public class DateTools {
             throw new IllegalArgumentException(desc);
         }
         int longCount = 0;
-        for (int i = 0; i < array.length; i++) {
-            if (array[i].length() > 2) {
+        for (String s : array) {
+            if (s.length() > 2) {
                 longCount++;
             }
         }
@@ -519,8 +520,8 @@ public class DateTools {
         if (array.length != 3) { // 不是三个数字
             throw new IllegalArgumentException(desc);
         }
-        for (int i = 0; i < array.length; i++) {
-            if (array[i].length() > 2) {
+        for (String s : array) {
+            if (s.length() > 2) {
                 throw new IllegalArgumentException(desc);
             }
         }
@@ -1071,13 +1072,13 @@ public class DateTools {
         return calendar.getTime();
     }
 
-    // /** 日期设置月份 **/
-    // public static Date setMonth(Date date, Month month) {
-    //     Calendar calendar = Calendar.getInstance();
-    //     calendar.setTime(date);
-    //     calendar.set(Calendar.MONTH, month.ordinal());
-    //     return calendar.getTime();
-    // }
+     /** 日期设置月份 **/
+     public static Date setMonth(Date date, Month month) {
+         Calendar calendar = Calendar.getInstance();
+         calendar.setTime(date);
+         calendar.set(Calendar.MONTH, month.ordinal());
+         return calendar.getTime();
+     }
 
     /** 日期设置日 **/
     public static Date setDay(Date date, int day) {
-- 
Gitee


From be84366b47868a2a7b22b4734109b9b2dcb71afa Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 9 Jan 2022 21:40:14 +0800
Subject: [PATCH 012/160] release profile jdk7

---
 pom.xml | 33 ++++++++++++++++++++++++---------
 1 file changed, 24 insertions(+), 9 deletions(-)

diff --git a/pom.xml b/pom.xml
index c4f8033..33e812d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
 	<groupId>com.gitee.qdbp</groupId>
 	<artifactId>qdbp-parent</artifactId>
-	<version>5.0.1</version>
+	<version>5.0.2</version>
 	<packaging>pom</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/</url>
@@ -35,15 +35,16 @@
 	<properties>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<project.build.locales>zh_CN</project.build.locales>
-		<project.build.jdk>1.7</project.build.jdk>
+		<maven.compiler.source>8</maven.compiler.source>
+		<maven.compiler.target>8</maven.compiler.target>
 	</properties>
 
 	<scm>
-        <connection>scm:git:https://gitee.com/qdbp/qdbp-able.git</connection>
-        <developerConnection>scm:git:https://gitee.com/qdbp/qdbp-able.git</developerConnection>
-        <url>https://gitee.com/qdbp/qdbp-able.git</url>
-        <tag>HEAD</tag>
-    </scm>
+		<connection>scm:git:https://gitee.com/qdbp/qdbp-able.git</connection>
+		<developerConnection>scm:git:https://gitee.com/qdbp/qdbp-able.git</developerConnection>
+		<url>https://gitee.com/qdbp/qdbp-able.git</url>
+		<tag>HEAD</tag>
+	</scm>
 
 	<distributionManagement>
 		<repository>
@@ -65,8 +66,8 @@
 				<artifactId>maven-compiler-plugin</artifactId>
 				<version>3.7.0</version>
 				<configuration>
-					<source>${project.build.jdk}</source>
-					<target>${project.build.jdk}</target>
+					<source>${maven.compiler.source}</source>
+					<target>${maven.compiler.target}</target>
 					<encoding>${project.build.sourceEncoding}</encoding>
 				</configuration>
 			</plugin>
@@ -113,14 +114,28 @@
 	</build>
 
 	<profiles>
+		<profile>
+			<id>jdk7</id>
+			<properties>
+				<maven.compiler.source>7</maven.compiler.source>
+				<maven.compiler.target>7</maven.compiler.target>
+			</properties>
+		</profile>
 		<profile>
 			<id>release</id>
+			<properties>
+				<maven.compiler.source>7</maven.compiler.source>
+				<maven.compiler.target>7</maven.compiler.target>
+			</properties>
 			<build>
 				<plugins>
 					<plugin>
 						<groupId>org.apache.maven.plugins</groupId>
 						<artifactId>maven-javadoc-plugin</artifactId>
 						<version>3.0.0</version>
+						<configuration>
+							<failOnError>false</failOnError>
+						</configuration>
 						<executions>
 							<execution>
 								<id>attach-javadocs</id>
-- 
Gitee


From 83a3f5ae2f5ab7bedec26b8e588b1461eb211e65 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 9 Jan 2022 21:40:31 +0800
Subject: [PATCH 013/160] 5.4.7

---
 able/pom.xml  | 4 ++--
 tools/pom.xml | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 80d5e70..9c7016f 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -5,11 +5,11 @@
 	<parent>	
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-parent</artifactId>
-		<version>5.0.1</version>
+		<version>5.0.2</version>
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.6</version>
+	<version>5.4.7</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index f453d73..f0e9c08 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -5,12 +5,12 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-parent</artifactId>
-		<version>5.0.1</version>
+		<version>5.0.2</version>
 	</parent>
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.6</version>
+	<version>5.4.7</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 92e4ca82da9e4544b50d5831330adcc272c67a15 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 20 Jan 2022 21:19:02 +0800
Subject: [PATCH 014/160] =?UTF-8?q?=E9=93=BE=E5=BC=8F=E6=AF=94=E8=BE=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/tools/compare/CompareStream.java     | 108 ++++++++++++++++++
 .../qdbp/tools/compare/CompareTools.java      |   7 +-
 .../gitee/qdbp/tools/compare/CompareTest.java |  94 ++++++++++++++-
 3 files changed, 204 insertions(+), 5 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java

diff --git a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java
new file mode 100644
index 0000000..798512b
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java
@@ -0,0 +1,108 @@
+package com.gitee.qdbp.tools.compare;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * CompareStream
+ *
+ * @author zhaohuihua
+ * @version 20220113
+ */
+public class CompareStream {
+    private final List<CompareItem<?>> items = new ArrayList<>();
+
+    protected CompareStream() {
+    }
+
+    /**
+     * 升序比较 (空值排最后)
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param <T> 对象类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T extends Comparable<T>> CompareStream asc(T o1, T o2) {
+        CompareItem<T> item = new CompareItem<T>(o1, o2, true, false);
+        this.items.add(item);
+        return this;
+    }
+
+    /**
+     * 升序比较
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param nullsLow 空值优先级: true=空值排最前, false=空值排最后
+     * @param <T> 对象类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T extends Comparable<T>> CompareStream asc(T o1, T o2, boolean nullsLow) {
+        CompareItem<T> item = new CompareItem<T>(o1, o2, true, nullsLow);
+        this.items.add(item);
+        return this;
+    }
+
+    /**
+     * 降序比较 (空值排最后)
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param <T> 对象类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T extends Comparable<T>> CompareStream desc(T o1, T o2) {
+        CompareItem<T> item = new CompareItem<T>(o1, o2, false, false);
+        this.items.add(item);
+        return this;
+    }
+
+    /**
+     * 降序比较
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param <T> 对象类型
+     * @param nullsLow 空值优先级: true=空值排最前, false=空值排最后
+     * @return 返回对象自身用于链式调用
+     */
+    public <T extends Comparable<T>> CompareStream desc(T o1, T o2, boolean nullsLow) {
+        CompareItem<T> item = new CompareItem<T>(o1, o2, false, false);
+        this.items.add(item);
+        return this;
+    }
+
+    /**
+     * 逐一比较对象, 遇到首个不为0的结果就返回
+     *
+     * @return 小于返回1/等于返回0/大于返回-1
+     */
+    public int compare() {
+        for (CompareItem<?> item : items) {
+            int result = item.compare();
+            if (result != 0) {
+                return result;
+            }
+        }
+        return 0;
+    }
+
+    private static class CompareItem<T extends Comparable<T>> {
+        private final T o1;
+        private final T o2;
+        private final boolean ascending;
+        private final boolean nullsLow;
+
+        private CompareItem(T o1, T o2, boolean ascending, boolean nullsLow) {
+            this.o1 = o1;
+            this.o2 = o2;
+            this.ascending = ascending;
+            this.nullsLow = nullsLow;
+        }
+
+        public int compare() {
+            return CompareTools.compare(o1, o2, ascending, nullsLow);
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
index 6a97147..eefb20b 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
@@ -72,6 +72,11 @@ public class CompareTools {
         return new MapFieldComparator<>(orderBy, ascending);
     }
 
+    /** 链式比较多个字段 **/
+    public static CompareStream stream() {
+        return new CompareStream();
+    }
+
     /**
      * 升序比较 (空值排最后)
      *
@@ -122,7 +127,7 @@ public class CompareTools {
         return compare(o1, o2, false, nullsLow);
     }
 
-    private static <T extends Comparable<T>> int compare(T o1, T o2, boolean ascending, boolean nullsLow) {
+    protected static <T extends Comparable<T>> int compare(T o1, T o2, boolean ascending, boolean nullsLow) {
         if (o1 == o2) {
             return 0;
         }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java b/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java
index 4482eac..bd89dd5 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java
@@ -1,9 +1,11 @@
 package com.gitee.qdbp.tools.compare;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import org.testng.Assert;
 import org.testng.annotations.Test;
 import com.gitee.qdbp.tools.utils.AssertTools;
 
@@ -18,7 +20,7 @@ public class CompareTest {
 
     @Test(priority = 1)
     public void testAscAndNullsToLast() {
-        List<Integer> list = Arrays.asList(3,7,2,3,9,6,null,1,3,0,2,7);
+        List<Integer> list = Arrays.asList(3, 7, 2, 3, 9, 6, null, 1, 3, 0, 2, 7);
         Collections.sort(list, new Comparator<Integer>() {
             public int compare(Integer a, Integer b) {
                 return CompareTools.ascCompare(a, b);
@@ -31,7 +33,7 @@ public class CompareTest {
 
     @Test(priority = 2)
     public void testAscAndNullsToFirst() {
-        List<Integer> list = Arrays.asList(3,7,2,3,9,6,null,1,3,0,2,7);
+        List<Integer> list = Arrays.asList(3, 7, 2, 3, 9, 6, null, 1, 3, 0, 2, 7);
         Collections.sort(list, new Comparator<Integer>() {
             public int compare(Integer a, Integer b) {
                 return CompareTools.ascCompare(a, b, true);
@@ -44,7 +46,7 @@ public class CompareTest {
 
     @Test(priority = 3)
     public void testDescAndNullsToLast() {
-        List<Integer> list = Arrays.asList(3,7,2,3,9,6,null,1,3,0,2,7);
+        List<Integer> list = Arrays.asList(3, 7, 2, 3, 9, 6, null, 1, 3, 0, 2, 7);
         Collections.sort(list, new Comparator<Integer>() {
             public int compare(Integer a, Integer b) {
                 return CompareTools.descCompare(a, b);
@@ -57,7 +59,7 @@ public class CompareTest {
 
     @Test(priority = 4)
     public void testDescAndNullsToFirst() {
-        List<Integer> list = Arrays.asList(3,7,2,3,9,6,null,1,3,0,2,7);
+        List<Integer> list = Arrays.asList(3, 7, 2, 3, 9, 6, null, 1, 3, 0, 2, 7);
         Collections.sort(list, new Comparator<Integer>() {
             public int compare(Integer a, Integer b) {
                 return CompareTools.descCompare(a, b, true);
@@ -67,4 +69,88 @@ public class CompareTest {
         System.out.println(list);
         AssertTools.assertDeepEquals(list, Arrays.asList(null, 9, 7, 7, 6, 3, 3, 3, 2, 2, 1, 0));
     }
+
+    @Test(priority = 5)
+    public void testObjects() {
+        List<Item1> items = new ArrayList<>();
+        items.add(new Item1(10, 15));
+        items.add(new Item2(null, 8));
+        items.add(new Item2(5, null));
+        items.add(new Item1(10, 13));
+        items.add(new Item2(5, 8));
+        items.add(new Item1(10, null));
+        items.add(new Item1(10, 8));
+        Collections.sort(items);
+        System.out.println(items);
+    }
+
+    @Test(priority = 6)
+    public void testAscCompares() {
+        {
+            int result = CompareTools.stream()
+                    .asc(2, 2)
+                    .asc("3", "4")
+                    .compare();
+            Assert.assertEquals(result, -1);
+        }
+        {
+            int result = CompareTools.stream()
+                    .asc(2, 2)
+                    .desc("3", "4")
+                    .asc(4, 4)
+                    .compare();
+            Assert.assertEquals(result, 1);
+        }
+        {
+            int result = CompareTools.stream()
+                    .asc(2, 2)
+                    .asc("3", "3")
+                    .asc(4, 4)
+                    .compare();
+            Assert.assertEquals(result, 0);
+        }
+    }
+
+    private static class Item1 implements Comparable<Item1> {
+        private final Integer start;
+        private final Integer end;
+
+        public Item1(Integer start, Integer end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        public Integer getStart() {
+            return start;
+        }
+
+        public Integer getEnd() {
+            return end;
+        }
+
+        @Override
+        public int compareTo(Item1 o) {
+            if (o == null) {
+                return -1;
+            }
+            return CompareTools.stream()
+                    .asc(start, o.getStart())
+                    .asc(end, o.getEnd())
+                    .compare();
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder buffer = new StringBuilder();
+            buffer.append(start).append('-').append(end);
+            return buffer.toString();
+        }
+    }
+
+    private static class Item2 extends Item1 {
+
+        public Item2(Integer start, Integer end) {
+            super(start, end);
+        }
+    }
 }
-- 
Gitee


From a9b997bcb7d1bf75c53edf0f2e7559806ccc0906 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 20 Jan 2022 21:21:38 +0800
Subject: [PATCH 015/160] =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=8C=B9?=
 =?UTF-8?q?=E9=85=8D=E8=A7=84=E5=88=99=E8=A7=A3=E6=9E=90=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/pom.xml                                  |   5 +
 .../gitee/qdbp/able/beans/ParamBuffer.java    |   4 +-
 .../qdbp/able/matches/StringMatcher.java      |  12 +-
 .../qdbp/able/matches/WrapStringMatcher.java  | 214 +++++++++++-------
 .../gitee/qdbp/tools/utils/ConvertTools.java  |  11 +
 .../gitee/qdbp/tools/utils/StringTools.java   |   2 +-
 .../able/matches/WrapStringMatcherTest.java   |  52 +++--
 7 files changed, 197 insertions(+), 103 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 9c7016f..ee7b4fb 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -20,4 +20,9 @@
         <url>https://gitee.com/qdbp/qdbp-able/</url>
     </organization>
 
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
 </project>
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java b/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java
index 50f8f89..25e6ab0 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java
@@ -9,7 +9,7 @@ import com.gitee.qdbp.tools.utils.StringTools;
  * @author zhaohuihua
  * @version 180814
  */
-public class ParamBuffer implements Cloneable {
+public class ParamBuffer implements Copyable {
 
     private static final Pattern TAB = Pattern.compile("\\t");
     private static final Pattern RETURN = Pattern.compile("\\r");
@@ -74,7 +74,7 @@ public class ParamBuffer implements Cloneable {
     }
 
     @Override
-    public ParamBuffer clone() {
+    public ParamBuffer copy() {
         ParamBuffer copy = new ParamBuffer();
         copy.separator = this.separator;
         copy.valueStringMaxLength = this.valueStringMaxLength;
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java
index 7fcdfc5..b6338f8 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java
@@ -36,7 +36,7 @@ public interface StringMatcher {
         /** 否定模式, 不符合条件为匹配 **/
         Negative
     }
-    
+
     /** 逻辑类型: 多个匹配规则使用and还是or关联 **/
     enum LogicType {
         AND, OR
@@ -51,4 +51,14 @@ public interface StringMatcher {
      *         <code>false</code> otherwise.
      */
     boolean matches(String source);
+
+    /** 规则解析器 **/
+    interface Parser {
+
+        /** 支持的类型 **/
+        String[] supportTypes();
+
+        /** 解析匹配规则 **/
+        StringMatcher parse(String pattern, String type, StringMatcher.Matches matchType);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java
index 55b2c75..3a96325 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java
@@ -2,7 +2,10 @@ package com.gitee.qdbp.able.matches;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.parse.StringAccess;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
@@ -126,16 +129,36 @@ public class WrapStringMatcher implements StringMatcher {
         this.logicType = logicType;
     }
 
+    private static final Map<String, List<Parser>> MATCHER_PARSER = new HashMap<>();
+
+    static {
+        addMatcherParser(new SimpleStringMatcherParser());
+    }
+
+    /** 增加字符串匹配规则解析器 **/
+    public static void addMatcherParser(Parser parser) {
+        String[] supports = parser.supportTypes();
+        for (String support : supports) {
+            if (MATCHER_PARSER.containsKey(support)) {
+                MATCHER_PARSER.get(support).add(parser);
+            } else {
+                List<Parser> parsers = new ArrayList<>();
+                parsers.add(parser);
+                MATCHER_PARSER.put(support, parsers);
+            }
+        }
+    }
+
     /**
      * 解析StringMatcher规则<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
+     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
      * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
+     * eq:或equals:开头的解析为EqualsStringMatcher<br>
+     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
+     * srt:或starts:开头的解析为StartsStringMatcher<br>
+     * end:或ends:开头的解析为EndsStringMatcher<br>
      * 其余的解析为EqualsStringMatcher<br>
-     * 
+     *
      * @param pattern 匹配规则
      * @return StringMatcher
      */
@@ -146,14 +169,14 @@ public class WrapStringMatcher implements StringMatcher {
 
     /**
      * 解析StringMatcher规则<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
+     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
      * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
+     * eq:或equals:开头的解析为EqualsStringMatcher<br>
+     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
+     * srt:或starts:开头的解析为StartsStringMatcher<br>
+     * end:或ends:开头的解析为EndsStringMatcher<br>
      * 其余的, 严格模式下解析为EqualsStringMatcher, 否则解析为ContainsStringMatcher<br>
-     * 
+     *
      * @param pattern 匹配规则
      * @param strict 是否使用严格格式
      * @return StringMatcher
@@ -163,16 +186,32 @@ public class WrapStringMatcher implements StringMatcher {
         return parseMatcher(pattern, strict ? "equals" : "contains");
     }
 
+    private static StringMatcher doParseMatcher(String pattern, String type, Matches matchType) {
+        if (type == null || !MATCHER_PARSER.containsKey(type)) {
+            return new EqualsStringMatcher(pattern, matchType);
+        }
+        List<Parser> parsers = MATCHER_PARSER.get(type);
+        // 从后往前匹配, 后加入的优先级最高
+        for (int i = parsers.size() - 1; i >= 0; i--) {
+            Parser parser = parsers.get(i);
+            StringMatcher matcher = parser.parse(pattern, type, matchType);
+            if (matcher != null) {
+                return matcher;
+            }
+        }
+        return new EqualsStringMatcher(pattern, matchType);
+    }
+
     /**
      * 解析StringMatcher规则<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
+     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
      * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
+     * eq:或equals:开头的解析为EqualsStringMatcher<br>
+     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
+     * srt:或starts:开头的解析为StartsStringMatcher<br>
+     * end:或ends:开头的解析为EndsStringMatcher<br>
      * 其余的, 使用defaultMode指定的匹配方式<br>
-     * 
+     *
      * @param pattern 匹配规则
      * @param defaultMode 默认匹配方式
      * @return StringMatcher
@@ -180,71 +219,35 @@ public class WrapStringMatcher implements StringMatcher {
      */
     public static StringMatcher parseMatcher(String pattern, String defaultMode) {
         VerifyTools.requireNotBlank(pattern, "pattern");
-        if (pattern.startsWith("regexp:")) {
-            String value = StringTools.removePrefix(pattern, "regexp:");
-            return new RegexpStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("regexp!:")) {
-            String value = StringTools.removePrefix(pattern, "regexp!:");
-            return new RegexpStringMatcher(value, Matches.Negative);
-        } else if (pattern.startsWith("ant:")) {
-            String value = StringTools.removePrefix(pattern, "ant:");
-            return new AntStringMatcher(value, true, Matches.Positive);
-        } else if (pattern.startsWith("ant!:")) {
-            String value = StringTools.removePrefix(pattern, "ant!:");
-            return new AntStringMatcher(value, true, Matches.Negative);
-        } else if (pattern.startsWith("equals:")) {
-            String value = StringTools.removePrefix(pattern, "equals:");
-            return new EqualsStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("equals!:")) {
-            String value = StringTools.removePrefix(pattern, "equals!:");
-            return new EqualsStringMatcher(value, Matches.Negative);
-        } else if (pattern.startsWith("contains:")) {
-            String value = StringTools.removePrefix(pattern, "contains:");
-            return new ContainsStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("contains!:")) {
-            String value = StringTools.removePrefix(pattern, "contains!:");
-            return new ContainsStringMatcher(value, Matches.Negative);
-        } else if (pattern.startsWith("starts:")) {
-            String value = StringTools.removePrefix(pattern, "starts:");
-            return new StartsStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("starts!:")) {
-            String value = StringTools.removePrefix(pattern, "starts!:");
-            return new StartsStringMatcher(value, Matches.Negative);
-        } else if (pattern.startsWith("ends:")) {
-            String value = StringTools.removePrefix(pattern, "ends:");
-            return new EndsStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("ends!:")) {
-            String value = StringTools.removePrefix(pattern, "contains!:");
-            return new EndsStringMatcher(value, Matches.Negative);
-        } else {
-            if ("ant".equals(defaultMode)) {
-                return new AntStringMatcher(pattern, true, Matches.Positive);
-            } else if ("regexp".equals(defaultMode)) {
-                return new RegexpStringMatcher(pattern, Matches.Positive);
-            } else if ("equals".equals(defaultMode)) {
-                return new EqualsStringMatcher(pattern, Matches.Positive);
-            } else if ("contains".equals(defaultMode)) {
-                return new ContainsStringMatcher(pattern, Matches.Positive);
-            } else if ("starts".equals(defaultMode)) {
-                return new StartsStringMatcher(pattern, Matches.Positive);
-            } else if ("ends".equals(defaultMode)) {
-                return new EndsStringMatcher(pattern, Matches.Positive);
-            } else {
-                return new EqualsStringMatcher(pattern, Matches.Positive);
-            }
+        StringAccess sa = new StringAccess(pattern);
+        String type = sa.skipWhitespace().readAscii();
+        if (type == null) {
+            return doParseMatcher(pattern, defaultMode, Matches.Positive);
+        }
+        Matches matchType = Matches.Positive;
+        char next = sa.skipWhitespace().readChar();
+        if (next == '!') {
+            matchType = Matches.Negative;
+            next = sa.skipWhitespace().readChar();
+        }
+        if (next != ':') {
+            return doParseMatcher(pattern, defaultMode, Matches.Positive);
         }
+        sa.skipWhitespace();
+        String realPattern = sa.readToEnd();
+        return doParseMatcher(realPattern, type, matchType);
     }
 
     /**
      * 解析StringMatcher规则列表, 以逗号或换行符分隔<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
+     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
      * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
+     * eq:或equals:开头的解析为EqualsStringMatcher<br>
+     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
+     * srt:或starts:开头的解析为StartsStringMatcher<br>
+     * end:或ends:开头的解析为EndsStringMatcher<br>
      * 其余的解析为EqualsStringMatcher<br>
-     * 
+     *
      * @param patterns 匹配规则列表
      * @param logicType 多个匹配规则使用and还是or关联
      * @return StringMatcher
@@ -259,14 +262,14 @@ public class WrapStringMatcher implements StringMatcher {
     /**
      * 解析StringMatcher规则列表<br>
      * 如: parseMatchers(pattern, Logic.OR, "ant", ',', '\n'); // 默认以ant规则匹配<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
+     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
      * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
+     * eq:或equals:开头的解析为EqualsStringMatcher<br>
+     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
+     * srt:或starts:开头的解析为StartsStringMatcher<br>
+     * end:或ends:开头的解析为EndsStringMatcher<br>
      * 其余的, 使用defaultMode指定的匹配方式<br>
-     * 
+     *
      * @param patterns 匹配规则列表
      * @param defaultMode 默认匹配方式
      * @param chars 分隔符
@@ -277,7 +280,7 @@ public class WrapStringMatcher implements StringMatcher {
     public static StringMatcher parseMatchers(String patterns, LogicType logicType, String defaultMode, char... chars) {
         VerifyTools.requireNotBlank(patterns, "patterns");
         if (chars == null || chars.length == 0) {
-            chars = new char[] { ',', '\n' };
+            chars = new char[] {',', '\n'};
         }
         String[] array = StringTools.split(patterns, chars);
         List<StringMatcher> matchers = new ArrayList<>();
@@ -295,4 +298,49 @@ public class WrapStringMatcher implements StringMatcher {
         }
     }
 
+    /**
+     * 基础字符串匹配规则解析器
+     *
+     * @author zhaohuihua
+     * @version 20220120
+     */
+    private static class SimpleStringMatcherParser implements StringMatcher.Parser {
+
+        public String[] supportTypes() {
+            List<String> types = new ArrayList<>();
+            types.add("regexp");
+            types.add("ant");
+            types.add("equals");
+            types.add("contains");
+            types.add("starts");
+            types.add("ends");
+
+            types.add("rgp");
+            types.add("eq");
+            types.add("ctn");
+            types.add("srt");
+            types.add("end");
+            return types.toArray(new String[] {});
+        }
+
+        @Override
+        public StringMatcher parse(String pattern, String type, StringMatcher.Matches matchType) {
+            VerifyTools.requireNotBlank(pattern, "pattern");
+            if ("ant".equals(type)) {
+                return new AntStringMatcher(pattern, true, matchType);
+            } else if ("rgp".equals(type) || "regexp".equals(type)) {
+                return new RegexpStringMatcher(pattern, matchType);
+            } else if ("eq".equals(type) || "equals".equals(type)) {
+                return new EqualsStringMatcher(pattern, matchType);
+            } else if ("ctn".equals(type) || "contains".equals(type)) {
+                return new ContainsStringMatcher(pattern, matchType);
+            } else if ("srt".equals(type) || "starts".equals(type)) {
+                return new StartsStringMatcher(pattern, matchType);
+            } else if ("end".equals(type) || "ends".equals(type)) {
+                return new EndsStringMatcher(pattern, matchType);
+            } else {
+                return null;
+            }
+        }
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index bfe8f8f..a1bff9d 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -662,6 +662,17 @@ public class ConvertTools {
         }
     }
 
+    /** 数字转换为BigDecimal **/
+    public static BigDecimal toBigDecimal(Number number) {
+        if (number == null) {
+            return null;
+        } else if (number instanceof BigDecimal) {
+            return (BigDecimal) number;
+        } else {
+            return new BigDecimal(String.valueOf(number));
+        }
+    }
+
     /**
      * 转换为Boolean
      * 
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index e377123..722d443 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -387,7 +387,7 @@ public class StringTools {
         }
 
         if (params.length % 2 != 0) {
-            throw new IllegalArgumentException("参数必须是键值对, 参数个数必须是2的倍数");
+            throw new IllegalArgumentException("The parameter must be a key value pair.");
         }
 
         Map<String, Object> map = new HashMap<>();
diff --git a/tools/src/test/java/com/gitee/qdbp/able/matches/WrapStringMatcherTest.java b/tools/src/test/java/com/gitee/qdbp/able/matches/WrapStringMatcherTest.java
index 2932e96..bb6abcb 100644
--- a/tools/src/test/java/com/gitee/qdbp/able/matches/WrapStringMatcherTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/able/matches/WrapStringMatcherTest.java
@@ -1,55 +1,75 @@
 package com.gitee.qdbp.able.matches;
 
+import java.util.ArrayList;
+import java.util.List;
+import org.testng.annotations.Test;
 import com.gitee.qdbp.able.matches.StringMatcher.LogicType;
+import com.gitee.qdbp.tools.utils.AssertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 
+@Test
 public class WrapStringMatcherTest {
 
-    public static void main(String[] args) {
-        // @formatter:off
-        String[] sources = new String[] {
+    private static final String[] sources = new String[] {
             "/home/files/202005/aaa.docx",
             "/home/files/202005/bbb.html",
             "/home/files/202005/aaa.docx.bak",
             "/home/files/temp/202005/aaa.docx",
             "/home/files/temp/202005/bbb.html",
             "/home/files/temp/202005/aaa.docx.bak"
-        };
-        // @formatter:on
-        test1(sources);
-        test2(sources);
-        test3(sources);
-    }
+    };
 
-    private static void test1(String[] sources) {
+    @Test
+    public void test1() {
         // 不在temp文件夹下的docx文件
         String rule1 = "contains!:/temp/";
         String rule2 = "ant:/**/*.docx";
         StringMatcher matcher = new WrapStringMatcher(LogicType.AND, rule1, rule2);
-        doMatches(matcher, sources);
+        List<Boolean> result = doMatches(matcher, sources);
+        assertEquals(result, "1,0,0,0,0,0");
     }
 
-    private static void test2(String[] sources) {
+    @Test
+    public void test2() {
         // temp文件夹下的bak文件
         String rule1 = "contains:/temp/";
         String rule2 = "ant:/**/*.bak";
         StringMatcher matcher = new WrapStringMatcher(LogicType.AND, rule1, rule2);
-        doMatches(matcher, sources);
+        List<Boolean> result = doMatches(matcher, sources);
+        assertEquals(result, "0,0,0,0,0,1");
     }
 
-    private static void test3(String[] sources) {
+    @Test
+    public void test3() {
         // temp文件或bak文件
         String rule1 = "contains:/temp/";
         String rule2 = "ant:/**/*.bak";
         StringMatcher matcher = new WrapStringMatcher(LogicType.OR, rule1, rule2);
-        doMatches(matcher, sources);
+        List<Boolean> result = doMatches(matcher, sources);
+        assertEquals(result, "0,0,1,1,1,1");
+    }
+
+    private static void assertEquals(List<Boolean> actual, String expected) {
+        List<Boolean> list = new ArrayList<>();
+        List<String> strings = StringTools.splits(expected, ',');
+        for (String item : strings) {
+            if ("0".equals(item)) {
+                list.add(false);
+            } else {
+                list.add(true);
+            }
+        }
+        AssertTools.assertDeepEquals(actual, list);
     }
 
-    private static void doMatches(StringMatcher matcher, String... strings) {
+    private static List<Boolean> doMatches(StringMatcher matcher, String... strings) {
         System.out.println(matcher.toString());
+        List<Boolean> result = new ArrayList<>();
         for (String string : strings) {
             boolean matches = matcher.matches(string);
             System.out.println(StringTools.pad(String.valueOf(matches), ' ', false, 8) + string);
+            result.add(matches);
         }
+        return result;
     }
 }
-- 
Gitee


From c79cc51dc743d2014f28d37d0cc91884ad172167 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 20 Jan 2022 21:22:36 +0800
Subject: [PATCH 016/160] =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=8D=95?=
 =?UTF-8?q?=E8=8E=B7=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/able/beans/SubString.java  | 83 +++++++++++++++++++
 .../gitee/qdbp/able/matches/GroupCatcher.java | 18 ++++
 .../qdbp/able/matches/RegexpGroupCatcher.java | 65 +++++++++++++++
 .../able/matches/RegexpStringCatcher.java     | 55 ++++++++++++
 .../qdbp/able/matches/StringCatcher.java      | 17 ++++
 .../gitee/qdbp/tools/parse/StringAccess.java  | 23 +++++
 .../qdbp/able/matches/StringCatcherTest.java  | 21 +++++
 7 files changed, 282 insertions(+)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/beans/SubString.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/GroupCatcher.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupCatcher.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringCatcher.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/StringCatcher.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/able/matches/StringCatcherTest.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java b/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java
new file mode 100644
index 0000000..5af918c
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java
@@ -0,0 +1,83 @@
+package com.gitee.qdbp.able.beans;
+
+import java.io.Serializable;
+import com.gitee.qdbp.tools.compare.CompareTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 子文本信息
+ *
+ * @author zhaohuihua
+ * @version 20210727
+ */
+public class SubString implements Serializable, Comparable<SubString> {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    /** 子文本内容 **/
+    private String content;
+    /** 子文本在原文本的开始位置 **/
+    private int startIndex;
+    /** 子文本在原文本的结束位置 **/
+    private int endIndex;
+
+    public SubString() {
+    }
+
+    public SubString(String content, int start, int end) {
+        this.content = content;
+        this.startIndex = start;
+        this.endIndex = end;
+    }
+
+    /** 子文本内容 **/
+    public String getContent() {
+        return content;
+    }
+
+    /** 子文本内容 **/
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    /** 子文本在原文本的开始位置 **/
+    public int getStartIndex() {
+        return startIndex;
+    }
+
+    /** 子文本在原文本的开始位置 **/
+    public void setStartIndex(int startIndex) {
+        this.startIndex = startIndex;
+    }
+
+    /** 子文本在原文本的结束位置 **/
+    public int getEndIndex() {
+        return endIndex;
+    }
+
+    /** 子文本在原文本的结束位置 **/
+    public void setEndIndex(int endIndex) {
+        this.endIndex = endIndex;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append(StringTools.pad(startIndex, 3));
+        buffer.append(':').append(StringTools.pad(endIndex, 3));
+        buffer.append("    ").append(content);
+        return buffer.toString();
+    }
+
+    @Override
+    public int compareTo(SubString o) {
+        if (o == null) {
+            return -1;
+        }
+        return CompareTools.stream()
+                .asc(startIndex, o.getStartIndex())
+                .asc(endIndex, o.getEndIndex())
+                .compare();
+    }
+}
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupCatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupCatcher.java
new file mode 100644
index 0000000..f77bbba
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupCatcher.java
@@ -0,0 +1,18 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.able.beans.SubString;
+
+/**
+ * 字符串组捕获接口
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public interface GroupCatcher {
+
+    Map<Integer, SubString> catchFirst(String string);
+
+    List<Map<Integer, SubString>> catchAll(String string);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupCatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupCatcher.java
new file mode 100644
index 0000000..3bdd260
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupCatcher.java
@@ -0,0 +1,65 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * 正则表达式字符串组捕获实现类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class RegexpGroupCatcher implements GroupCatcher {
+    private final Pattern pattern;
+    private final int[] groups;
+
+    public RegexpGroupCatcher(String pattern, int... groups) {
+        this(pattern == null ? null : Pattern.compile(pattern), groups);
+    }
+
+    public RegexpGroupCatcher(Pattern pattern, int... groups) {
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+
+        this.pattern = pattern;
+        this.groups = groups;
+    }
+
+    @Override
+    public Map<Integer, SubString> catchFirst(String string) {
+        Matcher matcher = pattern.matcher(string);
+        Map<Integer, SubString> result = new HashMap<>();
+        if (matcher.find()) {
+            for (int group : groups) {
+                String content = matcher.group(group);
+                int start = matcher.start(group);
+                int end = matcher.end(group);
+                result.put(group, new SubString(content, start, end));
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public List<Map<Integer, SubString>> catchAll(String string) {
+        List<Map<Integer, SubString>> list = new ArrayList<>();
+        Matcher matcher = pattern.matcher(string);
+        while (matcher.find()) {
+            Map<Integer, SubString> map = new HashMap<>();
+            list.add(map);
+            for (int group : groups) {
+                String content = matcher.group(group);
+                int start = matcher.start(group);
+                int end = matcher.end(group);
+                map.put(group, new SubString(content, start, end));
+            }
+        }
+        return list;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringCatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringCatcher.java
new file mode 100644
index 0000000..b7c49ba
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringCatcher.java
@@ -0,0 +1,55 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * 正则表达式字符串捕获实现类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class RegexpStringCatcher implements StringCatcher {
+    private final Pattern pattern;
+    private final int group;
+
+    public RegexpStringCatcher(String pattern, int group) {
+        this(pattern == null ? null : Pattern.compile(pattern), group);
+    }
+
+    public RegexpStringCatcher(Pattern pattern, int group) {
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        this.pattern = pattern;
+        this.group = group;
+    }
+
+    @Override
+    public SubString catchFirst(String string) {
+        Matcher matcher = pattern.matcher(string);
+        if (!matcher.find()) {
+            return null;
+        } else {
+            String content = matcher.group(group);
+            int start = matcher.start(group);
+            int end = matcher.end(group);
+            return new SubString(content, start, end);
+        }
+    }
+
+    @Override
+    public List<SubString> catchAll(String string) {
+        List<SubString> list = new ArrayList<>();
+        Matcher matcher = pattern.matcher(string);
+        while (matcher.find()) {
+            String content = matcher.group(group);
+            int start = matcher.start(group);
+            int end = matcher.end(group);
+            list.add(new SubString(content, start, end));
+        }
+        return list;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringCatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringCatcher.java
new file mode 100644
index 0000000..90c5768
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringCatcher.java
@@ -0,0 +1,17 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.List;
+import com.gitee.qdbp.able.beans.SubString;
+
+/**
+ * 字符串捕获接口
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public interface StringCatcher {
+
+    SubString catchFirst(String string);
+
+    List<SubString> catchAll(String string);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java b/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java
index f71f832..2fbd22f 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java
@@ -315,6 +315,29 @@ public class StringAccess {
         return originIndex == index ? null : string.substring(originIndex, index);
     }
 
+    /** 读取Ascii字符 **/
+    public String readAscii() {
+        if (index >= length) {
+            this.lastReadSuccess = true;
+            return null;
+        }
+        // 记录原始位置
+        int originIndex = index;
+        // 遍历获取结束位置
+        for (; index < length; index++) {
+            char c = string.charAt(index);
+            if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
+                continue;
+            } else {
+                break;
+            }
+        }
+
+        // 想要读取单词, 却不是单词, 则为读取失败
+        this.lastReadSuccess = index > originIndex;
+        return originIndex == index ? null : string.substring(originIndex, index);
+    }
+
     /** 读取单词 (数字字母下划线美元符) **/
     public String readWord() {
         if (index >= length) {
diff --git a/tools/src/test/java/com/gitee/qdbp/able/matches/StringCatcherTest.java b/tools/src/test/java/com/gitee/qdbp/able/matches/StringCatcherTest.java
new file mode 100644
index 0000000..e4bd8a6
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/able/matches/StringCatcherTest.java
@@ -0,0 +1,21 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * StringCatcherTest
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class StringCatcherTest {
+
+    public static void main(String[] args) {
+        Pattern pattern = Pattern.compile("(?<number>\\d+)");
+        Matcher matcher = pattern.matcher("aabbcc1122ddee33ff44");
+        if (matcher.find()) {
+            System.out.println(matcher.group("number"));
+        }
+    }
+}
-- 
Gitee


From ac6048d399fc3fabcac56a0f5f5988ee762a07a3 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 20 Jan 2022 21:22:56 +0800
Subject: [PATCH 017/160] =?UTF-8?q?=E8=BF=98=E5=8E=9Fpom?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/pom.xml | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index ee7b4fb..9c7016f 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -20,9 +20,4 @@
         <url>https://gitee.com/qdbp/qdbp-able/</url>
     </organization>
 
-	<properties>
-		<maven.compiler.source>7</maven.compiler.source>
-		<maven.compiler.target>7</maven.compiler.target>
-	</properties>
-
 </project>
-- 
Gitee


From d961205bceee9eac1db020d26e28c3215c836641 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Fri, 21 Jan 2022 08:54:44 +0800
Subject: [PATCH 018/160] =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=8F=90?=
 =?UTF-8?q?=E5=8F=96=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 ...{GroupCatcher.java => GroupExtractor.java} |  4 ++--
 ...Catcher.java => RegexpGroupExtractor.java} |  8 +++----
 ...atcher.java => RegexpStringExtractor.java} |  8 +++----
 ...tringCatcher.java => StringExtractor.java} |  4 ++--
 .../qdbp/able/matches/StringCatcherTest.java  | 21 -------------------
 .../able/matches/StringExtractorTest.java     | 21 +++++++++++++++++++
 6 files changed, 33 insertions(+), 33 deletions(-)
 rename able/src/main/java/com/gitee/qdbp/able/matches/{GroupCatcher.java => GroupExtractor.java} (82%)
 rename able/src/main/java/com/gitee/qdbp/able/matches/{RegexpGroupCatcher.java => RegexpGroupExtractor.java} (88%)
 rename able/src/main/java/com/gitee/qdbp/able/matches/{RegexpStringCatcher.java => RegexpStringExtractor.java} (85%)
 rename able/src/main/java/com/gitee/qdbp/able/matches/{StringCatcher.java => StringExtractor.java} (80%)
 delete mode 100644 tools/src/test/java/com/gitee/qdbp/able/matches/StringCatcherTest.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupCatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
similarity index 82%
rename from able/src/main/java/com/gitee/qdbp/able/matches/GroupCatcher.java
rename to able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
index f77bbba..f8671ce 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/GroupCatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
@@ -5,12 +5,12 @@ import java.util.Map;
 import com.gitee.qdbp.able.beans.SubString;
 
 /**
- * 字符串组捕获接口
+ * 字符串组提取接口
  *
  * @author zhaohuihua
  * @version 20220112
  */
-public interface GroupCatcher {
+public interface GroupExtractor {
 
     Map<Integer, SubString> catchFirst(String string);
 
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupCatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
similarity index 88%
rename from able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupCatcher.java
rename to able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
index 3bdd260..f884442 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupCatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
@@ -10,20 +10,20 @@ import com.gitee.qdbp.able.beans.SubString;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
- * 正则表达式字符串组捕获实现类
+ * 正则表达式字符串组提取实现类
  *
  * @author zhaohuihua
  * @version 20220112
  */
-public class RegexpGroupCatcher implements GroupCatcher {
+public class RegexpGroupExtractor implements GroupExtractor {
     private final Pattern pattern;
     private final int[] groups;
 
-    public RegexpGroupCatcher(String pattern, int... groups) {
+    public RegexpGroupExtractor(String pattern, int... groups) {
         this(pattern == null ? null : Pattern.compile(pattern), groups);
     }
 
-    public RegexpGroupCatcher(Pattern pattern, int... groups) {
+    public RegexpGroupExtractor(Pattern pattern, int... groups) {
         VerifyTools.requireNotBlank(pattern, "pattern");
         VerifyTools.requireNotBlank(groups, "groups");
 
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringCatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
similarity index 85%
rename from able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringCatcher.java
rename to able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
index b7c49ba..6dcadaf 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringCatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
@@ -8,20 +8,20 @@ import com.gitee.qdbp.able.beans.SubString;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
- * 正则表达式字符串捕获实现类
+ * 正则表达式字符串提取实现类
  *
  * @author zhaohuihua
  * @version 20220112
  */
-public class RegexpStringCatcher implements StringCatcher {
+public class RegexpStringExtractor implements StringExtractor {
     private final Pattern pattern;
     private final int group;
 
-    public RegexpStringCatcher(String pattern, int group) {
+    public RegexpStringExtractor(String pattern, int group) {
         this(pattern == null ? null : Pattern.compile(pattern), group);
     }
 
-    public RegexpStringCatcher(Pattern pattern, int group) {
+    public RegexpStringExtractor(Pattern pattern, int group) {
         VerifyTools.requireNotBlank(pattern, "pattern");
         this.pattern = pattern;
         this.group = group;
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringCatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
similarity index 80%
rename from able/src/main/java/com/gitee/qdbp/able/matches/StringCatcher.java
rename to able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
index 90c5768..9b9fd30 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringCatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
@@ -4,12 +4,12 @@ import java.util.List;
 import com.gitee.qdbp.able.beans.SubString;
 
 /**
- * 字符串捕获接口
+ * 字符串提取接口
  *
  * @author zhaohuihua
  * @version 20220112
  */
-public interface StringCatcher {
+public interface StringExtractor {
 
     SubString catchFirst(String string);
 
diff --git a/tools/src/test/java/com/gitee/qdbp/able/matches/StringCatcherTest.java b/tools/src/test/java/com/gitee/qdbp/able/matches/StringCatcherTest.java
deleted file mode 100644
index e4bd8a6..0000000
--- a/tools/src/test/java/com/gitee/qdbp/able/matches/StringCatcherTest.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.gitee.qdbp.able.matches;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * StringCatcherTest
- *
- * @author zhaohuihua
- * @version 20220112
- */
-public class StringCatcherTest {
-
-    public static void main(String[] args) {
-        Pattern pattern = Pattern.compile("(?<number>\\d+)");
-        Matcher matcher = pattern.matcher("aabbcc1122ddee33ff44");
-        if (matcher.find()) {
-            System.out.println(matcher.group("number"));
-        }
-    }
-}
diff --git a/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
new file mode 100644
index 0000000..67f6854
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
@@ -0,0 +1,21 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.List;
+import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+
+/**
+ * 字符串提取测试类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class StringExtractorTest {
+
+    public static void main(String[] args) {
+        RegexpStringExtractor catcher = new RegexpStringExtractor("(\\d+)", 1);
+        String string = "aabbcc1122ddee33ff44";
+        List<SubString> result = catcher.catchAll(string);
+        System.out.println(ConvertTools.joinToString(result, '\n'));
+    }
+}
-- 
Gitee


From dc900dcc710e1440809bfb2b86ab9c6b480633e1 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Fri, 21 Jan 2022 19:25:49 +0800
Subject: [PATCH 019/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/specialized/KeywordHandler.java   | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/specialized/KeywordHandler.java b/tools/src/main/java/com/gitee/qdbp/tools/specialized/KeywordHandler.java
index 910a3df..4e64d4c 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/specialized/KeywordHandler.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/specialized/KeywordHandler.java
@@ -4,12 +4,9 @@ import java.io.IOException;
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.TreeSet;
-import com.gitee.qdbp.tools.utils.ConvertTools;
-import com.gitee.qdbp.tools.utils.StringTools;
-import com.gitee.qdbp.tools.utils.VerifyTools;
 import org.lionsoul.jcseg.tokenizer.core.ADictionary;
 import org.lionsoul.jcseg.tokenizer.core.DictionaryFactory;
 import org.lionsoul.jcseg.tokenizer.core.ISegment;
@@ -17,6 +14,9 @@ import org.lionsoul.jcseg.tokenizer.core.IWord;
 import org.lionsoul.jcseg.tokenizer.core.JcsegException;
 import org.lionsoul.jcseg.tokenizer.core.JcsegTaskConfig;
 import org.lionsoul.jcseg.tokenizer.core.SegmentFactory;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
  * 关键字收集
@@ -26,7 +26,7 @@ import org.lionsoul.jcseg.tokenizer.core.SegmentFactory;
  */
 public class KeywordHandler {
 
-    private final Set<String> container = new TreeSet<>();
+    private final Set<String> container = new LinkedHashSet<>();
 
     private static final JcsegTaskConfig CONFIG = new JcsegTaskConfig(true);
     private static final ADictionary DICTIONARY = DictionaryFactory.createDefaultDictionary(CONFIG, true);
-- 
Gitee


From 270e23197cfe9ae1835e41c9c62397d4e3d6a365 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 24 Jan 2022 21:25:53 +0800
Subject: [PATCH 020/160] =?UTF-8?q?=E6=96=B9=E6=B3=95=E9=87=8D=E5=91=BD?=
 =?UTF-8?q?=E5=90=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/pom.xml                                  |   2 +-
 .../qdbp/able/matches/GroupExtractor.java     |   4 +-
 .../qdbp/able/matches/GroupExtractors.java    |  41 +++
 .../able/matches/RegexpGroupExtractor.java    |  89 ++++++-
 .../able/matches/RegexpStringExtractor.java   |  55 +++-
 .../qdbp/able/matches/StringExtractor.java    |   4 +-
 .../qdbp/able/matches/StringExtractors.java   |  40 +++
 .../gitee/qdbp/tools/utils/AssertTools.java   |   4 +-
 .../qdbp/tools/utils/DeepEqualsAssertion.java |   4 +-
 .../gitee/qdbp/tools/utils/ReflectTools.java  | 235 ++++++++++++++++++
 tools/pom.xml                                 |   2 +-
 .../able/matches/StringExtractorTest.java     |   4 +-
 12 files changed, 463 insertions(+), 21 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java

diff --git a/able/pom.xml b/able/pom.xml
index 9c7016f..240e26c 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.7</version>
+	<version>5.4.8</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
index f8671ce..9785c17 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
@@ -12,7 +12,7 @@ import com.gitee.qdbp.able.beans.SubString;
  */
 public interface GroupExtractor {
 
-    Map<Integer, SubString> catchFirst(String string);
+    Map<Integer, SubString> extractFirst(String string);
 
-    List<Map<Integer, SubString>> catchAll(String string);
+    List<Map<Integer, SubString>> extractAll(String string);
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
new file mode 100644
index 0000000..8a63363
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
@@ -0,0 +1,41 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.able.beans.SubString;
+
+/**
+ * 字符串组提取集合实现类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class GroupExtractors implements GroupExtractor {
+    private final List<? extends GroupExtractor> extractors;
+
+    public <T extends GroupExtractor> GroupExtractors(List<T> extractors) {
+        this.extractors = extractors;
+    }
+
+    @Override
+    public Map<Integer, SubString> extractFirst(String string) {
+        for (GroupExtractor extractor : extractors) {
+            Map<Integer, SubString> result = extractor.extractFirst(string);
+            if (result != null) {
+                return result;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public List<Map<Integer, SubString>> extractAll(String string) {
+        for (GroupExtractor extractor : extractors) {
+            List<Map<Integer, SubString>> result = extractor.extractAll(string);
+            if (result != null) {
+                return result;
+            }
+        }
+        return null;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
index f884442..e97132e 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
@@ -6,7 +6,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.parse.StringAccess;
+import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -17,7 +22,7 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  */
 public class RegexpGroupExtractor implements GroupExtractor {
     private final Pattern pattern;
-    private final int[] groups;
+    private final List<Integer> groups;
 
     public RegexpGroupExtractor(String pattern, int... groups) {
         this(pattern == null ? null : Pattern.compile(pattern), groups);
@@ -27,27 +32,44 @@ public class RegexpGroupExtractor implements GroupExtractor {
         VerifyTools.requireNotBlank(pattern, "pattern");
         VerifyTools.requireNotBlank(groups, "groups");
 
+        this.pattern = pattern;
+        this.groups = new ArrayList<>();
+        for (int group : groups) {
+            this.groups.add(group);
+        }
+    }
+
+    public RegexpGroupExtractor(String pattern, List<Integer> groups) {
+        this(pattern == null ? null : Pattern.compile(pattern), groups);
+    }
+
+    public RegexpGroupExtractor(Pattern pattern, List<Integer> groups) {
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+
         this.pattern = pattern;
         this.groups = groups;
     }
 
     @Override
-    public Map<Integer, SubString> catchFirst(String string) {
+    public Map<Integer, SubString> extractFirst(String string) {
         Matcher matcher = pattern.matcher(string);
-        Map<Integer, SubString> result = new HashMap<>();
-        if (matcher.find()) {
+        if (!matcher.find()) {
+            return null;
+        } else {
+            Map<Integer, SubString> result = new HashMap<>();
             for (int group : groups) {
                 String content = matcher.group(group);
                 int start = matcher.start(group);
                 int end = matcher.end(group);
                 result.put(group, new SubString(content, start, end));
             }
+            return result;
         }
-        return result;
     }
 
     @Override
-    public List<Map<Integer, SubString>> catchAll(String string) {
+    public List<Map<Integer, SubString>> extractAll(String string) {
         List<Map<Integer, SubString>> list = new ArrayList<>();
         Matcher matcher = pattern.matcher(string);
         while (matcher.find()) {
@@ -60,6 +82,59 @@ public class RegexpGroupExtractor implements GroupExtractor {
                 map.put(group, new SubString(content, start, end));
             }
         }
-        return list;
+        return list.isEmpty() ? null : list;
+    }
+
+    /**
+     * 根据字符串参数解析成RegexpGroupExtractor对象
+     *
+     * @param string 字符串, 格式为: [1,2] regexp
+     * @return RegexpGroupExtractor对象
+     * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+     */
+    public static RegexpGroupExtractor parse(String string) {
+        VerifyTools.requireNotBlank(string, "string");
+        StringAccess sa = new StringAccess(string);
+        sa.skipWhitespace();
+        String g = sa.readInBracket('[', ']');
+        if (!sa.isLastReadSuccess()) {
+            String desc = "Group format error:  " + string + "  , The correct format is [1,2] regexp";
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, desc);
+        }
+        List<Integer> groups = new ArrayList<>();
+        for (String group : StringTools.splits(g, ',')) {
+            if (StringTools.isDigit(group)) {
+                groups.add(Integer.parseInt(group));
+            } else {
+                String desc = "Group must be digits: " + group + " --> " + sa.peekFrom(0);
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, desc);
+            }
+        }
+        sa.skipWhitespace();
+        String regexp = sa.readToEnd();
+        try {
+            Pattern pattern = Pattern.compile(regexp);
+            return new RegexpGroupExtractor(pattern, groups);
+        } catch (PatternSyntaxException e) {
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, "Regexp format error, --> " + regexp, e);
+        }
+    }
+
+    /**
+     * 根据字符串参数解析成GroupExtractors对象
+     *
+     * @param strings 字符串数组, 格式为: [1,2] regexp
+     * @return GroupExtractors对象
+     * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+     */
+    public static GroupExtractors parse(List<String> strings) {
+        VerifyTools.requireNotBlank(strings, "strings");
+        List<RegexpGroupExtractor> extractors = new ArrayList<>();
+        for (String string : strings) {
+            if (VerifyTools.isNotBlank(string)) {
+                extractors.add(RegexpGroupExtractor.parse(string));
+            }
+        }
+        return new GroupExtractors(extractors);
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
index 6dcadaf..45c8ea4 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
@@ -4,7 +4,11 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.able.exception.FormatException;
+import com.gitee.qdbp.tools.parse.StringAccess;
+import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -28,7 +32,7 @@ public class RegexpStringExtractor implements StringExtractor {
     }
 
     @Override
-    public SubString catchFirst(String string) {
+    public SubString extractFirst(String string) {
         Matcher matcher = pattern.matcher(string);
         if (!matcher.find()) {
             return null;
@@ -41,7 +45,7 @@ public class RegexpStringExtractor implements StringExtractor {
     }
 
     @Override
-    public List<SubString> catchAll(String string) {
+    public List<SubString> extractAll(String string) {
         List<SubString> list = new ArrayList<>();
         Matcher matcher = pattern.matcher(string);
         while (matcher.find()) {
@@ -50,6 +54,51 @@ public class RegexpStringExtractor implements StringExtractor {
             int end = matcher.end(group);
             list.add(new SubString(content, start, end));
         }
-        return list;
+        return list.isEmpty() ? null : list;
+    }
+
+    /**
+     * 根据字符串参数解析成RegexpGroupExtractor对象
+     *
+     * @param string 字符串, 格式为: [1] regexp
+     * @return RegexpGroupExtractor对象
+     */
+    public static RegexpStringExtractor parse(String string) {
+        VerifyTools.requireNotBlank(string, "string");
+        StringAccess sa = new StringAccess(string);
+        sa.skipWhitespace();
+        String g = sa.readInBracket('[', ']');
+        if (!sa.isLastReadSuccess()) {
+            throw new FormatException("Group format error, The correct format is [1] regexp, --> " + string);
+        }
+        if (!StringTools.isDigit(g)) {
+            throw new FormatException("Group must be digits: " + g + " --> " + string);
+        }
+        int group = Integer.parseInt(g);
+        sa.skipWhitespace();
+        String regexp = sa.readToEnd();
+        try {
+            Pattern pattern = Pattern.compile(regexp);
+            return new RegexpStringExtractor(pattern, group);
+        } catch (PatternSyntaxException e) {
+            throw new FormatException("Regexp format error, --> " + regexp, e);
+        }
+    }
+
+    /**
+     * 根据字符串参数解析成StringExtractors对象
+     *
+     * @param strings 字符串数组, 格式为: [1] regexp
+     * @return StringExtractors对象
+     */
+    public static StringExtractors parse(List<String> strings) {
+        VerifyTools.requireNotBlank(strings, "strings");
+        List<RegexpStringExtractor> extractors = new ArrayList<>();
+        for (String string : strings) {
+            if (VerifyTools.isNotBlank(string)) {
+                extractors.add(parse(string));
+            }
+        }
+        return new StringExtractors(extractors);
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
index 9b9fd30..c23dc74 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
@@ -11,7 +11,7 @@ import com.gitee.qdbp.able.beans.SubString;
  */
 public interface StringExtractor {
 
-    SubString catchFirst(String string);
+    SubString extractFirst(String string);
 
-    List<SubString> catchAll(String string);
+    List<SubString> extractAll(String string);
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
new file mode 100644
index 0000000..1a20034
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
@@ -0,0 +1,40 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.List;
+import com.gitee.qdbp.able.beans.SubString;
+
+/**
+ * 字符串提取集合实现类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class StringExtractors implements StringExtractor {
+    private final List<? extends StringExtractor> extractors;
+
+    public <T extends StringExtractor> StringExtractors(List<T> extractors) {
+        this.extractors = extractors;
+    }
+
+    @Override
+    public SubString extractFirst(String string) {
+        for (StringExtractor extractor : extractors) {
+            SubString result = extractor.extractFirst(string);
+            if (result != null) {
+                return result;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public List<SubString> extractAll(String string) {
+        for (StringExtractor extractor : extractors) {
+            List<SubString> result = extractor.extractAll(string);
+            if (result != null) {
+                return result;
+            }
+        }
+        return null;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/AssertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/AssertTools.java
index bf44125..ab1cb6e 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/AssertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/AssertTools.java
@@ -119,7 +119,7 @@ public class AssertTools {
         for (int i = 0; i < aLines; i++) {
             String aString = actualLines[i];
             String eString = expectLines[i];
-            if (assertTextEquals(aString, eString)) {
+            if (checkTextEquals(aString, eString)) {
                 continue;
             }
 
@@ -185,7 +185,7 @@ public class AssertTools {
         assertTextLinesEquals(actual, eContent, desc);
     }
 
-    private static boolean assertTextEquals(String aString, String eString) {
+    protected static boolean checkTextEquals(String aString, String eString) {
         if (aString.length() != eString.length()) {
             return false;
         }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/DeepEqualsAssertion.java b/able/src/main/java/com/gitee/qdbp/tools/utils/DeepEqualsAssertion.java
index c1fa817..8ffe59b 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/DeepEqualsAssertion.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/DeepEqualsAssertion.java
@@ -340,8 +340,10 @@ class DeepEqualsAssertion {
     private static boolean equals(Object actual, Object expected) {
         if (actual == null && expected == null) {
             return true;
-        } else if (actual == null ^ expected == null) {
+        } else if (actual == null || expected == null) {
             return false;
+        } else if (actual instanceof String && expected instanceof String) {
+            return AssertTools.checkTextEquals((String) actual, (String) expected);
         } else {
             return expected.equals(actual) && actual.equals(expected);
         }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
index 112a834..8204c94 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
@@ -1129,6 +1129,241 @@ public abstract class ReflectTools {
         }
     }
 
+    /**
+     * 查找无参数的构造函数
+     *
+     * @param clazz 类
+     * @return 构造函数对象, 如果未找到则返回null
+     */
+    public static <T> Constructor<T> findConstructor(Class<T> clazz) {
+        return findConstructor(clazz, false, new Class<?>[0]);
+    }
+
+    /**
+     * 查找无参数的构造函数
+     *
+     * @param clazz 类
+     * @param throwOnNotFound 如果构造函数不存在是否抛出异常
+     * @return 构造函数对象
+     */
+    public static <T> Constructor<T> findConstructor(Class<T> clazz, boolean throwOnNotFound) {
+        return findConstructor(clazz, throwOnNotFound, new Class<?>[0]);
+    }
+
+    /**
+     * 优先选择参数类型完全相同的, 其次选择通过装箱/拆箱能匹配的或父子关系能匹配的<br>
+     * 总之:<br>
+     * 如果有一个构造函数Constructor(List&lt;?&gt; list, Integer i)<br>
+     * 通过eachFindConstructor(clazz, List.class, Integer.class);能找到<br>
+     * 通过eachFindConstructor(clazz, List.class, int.class);能找到<br>
+     * 通过eachFindConstructor(clazz, ArrayList.class, Integer.class);能找到<br>
+     * 通过eachFindConstructor(clazz, ArrayList.class, int.class);也能找到<br>
+     *
+     * @param clazz 类
+     * @param types 参数列表
+     * @return 构造函数对象, 如果未找到则返回null
+     */
+    public static <T> Constructor<T> findConstructor(Class<T> clazz, Class<?>... types) {
+        return findConstructor(clazz, false, types);
+    }
+
+    /**
+     * 优先选择参数类型完全相同的, 其次选择通过装箱/拆箱能匹配的或父子关系能匹配的<br>
+     * 总之:<br>
+     * 如果有一个构造函数Constructor(List&lt;?&gt; list, Integer i)<br>
+     * 通过eachFindConstructor(clazz, List.class, Integer.class);能找到<br>
+     * 通过eachFindConstructor(clazz, List.class, int.class);能找到<br>
+     * 通过eachFindConstructor(clazz, ArrayList.class, Integer.class);能找到<br>
+     * 通过eachFindConstructor(clazz, ArrayList.class, int.class);也能找到<br>
+     *
+     * @param clazz 类
+     * @param throwOnNotFound 如果构造函数不存在是否抛出异常
+     * @param types 参数列表, 如果其中某个参数为null, 则不检查类型
+     * @return 构造函数对象
+     */
+    public static <T> Constructor<T> findConstructor(Class<T> clazz, boolean throwOnNotFound, Class<?>... types) {
+        VerifyTools.requireNonNull(clazz, "clazz");
+
+        try {
+            return clazz.getConstructor(types);
+        } catch (NoSuchMethodException e) {
+            if (types != null && types.length > 0) {
+                return eachFindConstructor(clazz, throwOnNotFound, types);
+            } else {
+                if (throwOnNotFound) {
+                    String signature = getMethodLogSignature(clazz, clazz.getSimpleName(), types);
+                    throw new IllegalArgumentException(signature + " not found.");
+                } else {
+                    return null;
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings({ "unchecked" })
+    private static <T> Constructor<T> eachFindConstructor(Class<T> clazz, boolean throwOnNotFound, Class<?>... types) {
+        // 直接采用clazz.getConstructor(types)的方式
+        // 会出现根据int找不到clazzName(Integer), 根据Integer找不到clazzName(Object)的情况
+
+        List<Constructor<?>> accepted = new ArrayList<>(); // 可接受的(通过装箱/拆箱能匹配的或父子关系能匹配的)
+
+        int length = types.length;
+        Constructor<?>[] methods = clazz.getConstructors();
+
+        for (Constructor<?> m : methods) {
+            Class<?>[] actuals = m.getParameterTypes();
+            if (actuals.length != types.length) {
+                continue; // 参数个数不符
+            }
+            boolean equals = true;
+            boolean accept = true;
+            for (int i = 0; i < length; i++) {
+                Class<?> a = actuals[i];
+                Class<?> t = types[i];
+                if (t == null) {
+                    equals = false;
+                    continue;
+                }
+                if (a != t) {
+                    equals = false;
+                }
+                if (a != t && !a.isAssignableFrom(t) && !isCompatible(a, t)) {
+                    accept = false;
+                }
+            }
+            if (equals) {
+                return (Constructor<T>) m; // 参数类型完全相同, 直接返回
+            }
+            if (accept) {
+                accepted.add(m);
+            }
+        }
+
+        if (accepted.isEmpty()) {
+            if (throwOnNotFound) {
+                String signature = getMethodLogSignature(clazz, clazz.getSimpleName(), types);
+                throw new IllegalArgumentException(signature + " not found.");
+            } else {
+                return null;
+            }
+        } else { // 没有参数类型完全相同, 但有通过装箱/拆箱能匹配的或父子关系能匹配的
+            if (accepted.size() == 1) { // 如果只有一个这种构造函数, 就选择这个啦
+                return (Constructor<T>) accepted.get(0);
+            } else { // 如果不只一个, 那没办法, 报个错吧
+                String signature = getMethodLogSignature(clazz, clazz.getSimpleName(), types);
+                throw new IllegalArgumentException(signature + " is ambiguous.");
+            }
+        }
+    }
+
+    /**
+     * 执行无参数的构造函数
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param constructor 构造函数
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Constructor<T> constructor) {
+        VerifyTools.requireNonNull(constructor, "constructor");
+        return newInstance(constructor, new Object[0]);
+    }
+
+    /**
+     * 创建新对象实例
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param constructor 构造函数
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Constructor<T> constructor, Object... args) {
+        VerifyTools.requireNonNull(constructor, "constructor");
+
+        try {
+            return constructor.newInstance(args);
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw renewMethodRuntimeException(e);
+        }
+    }
+
+    /**
+     * 执行无参数的构造函数
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param clazz Class
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Class<T> clazz) {
+        return newInstance(clazz, true);
+    }
+
+    /**
+     * 执行无参数的构造函数
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param clazz Class
+     * @param throwOnMethodNotFound 如果默认构造函数不存在是否抛出异常
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Class<T> clazz, boolean throwOnMethodNotFound) {
+        VerifyTools.requireNotBlank(clazz, "clazz");
+
+        Class<?>[] types = new Class<?>[0];
+        Constructor<T> constructor = findConstructor(clazz, throwOnMethodNotFound, types);
+        if (constructor == null) {
+            return null;
+        }
+        return newInstance(constructor, new Object[0]);
+    }
+
+    /**
+     * 创建新对象实例<br>
+     * 如果根据构造函数名和参数类型找到多个构造函数将抛出IllegalArgumentException异常<br>
+     * 在args中如果包含null对象将不会判断参数类型, 如果有多个同名且参数个数相同的构造函数将有可能因无法确定目标构造函数而报错
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param clazz Class
+     * @param args 参数
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Class<T> clazz, Object... args) {
+        return newInstance(clazz, true, args);
+    }
+
+    /**
+     * 创建新对象实例<br>
+     * 如果根据构造函数名和参数类型找到多个构造函数将抛出IllegalArgumentException异常<br>
+     * 在args中如果包含null对象将不会判断参数类型, 如果有多个同名且参数个数相同的构造函数将有可能因无法确定目标构造函数而报错
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param clazz Class
+     * @param throwOnMethodNotFound 如果构造函数不存在是否抛出异常
+     * @param args 参数
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Class<T> clazz, boolean throwOnMethodNotFound, Object... args) {
+        VerifyTools.requireNotBlank(clazz, "clazz");
+
+        int size = args == null ? 0 : args.length;
+        Class<?>[] types = new Class<?>[size];
+        for (int i = 0; i < size; i++) {
+            types[i] = args[i] == null ? null : args[i].getClass();
+        }
+        Constructor<T> constructor = findConstructor(clazz, throwOnMethodNotFound, types);
+        if (constructor == null) {
+            return null;
+        }
+
+        try {
+            return constructor.newInstance(args);
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw renewMethodRuntimeException(e);
+        }
+    }
+
     /** 判断参数是否能够匹配 **/
     private static boolean isCompatible(Class<?> a, Class<?> b) {
         if (a == b) {
diff --git a/tools/pom.xml b/tools/pom.xml
index f0e9c08..d84a3c6 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.7</version>
+	<version>5.4.8</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
diff --git a/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
index 67f6854..7240325 100644
--- a/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
@@ -13,9 +13,9 @@ import com.gitee.qdbp.tools.utils.ConvertTools;
 public class StringExtractorTest {
 
     public static void main(String[] args) {
-        RegexpStringExtractor catcher = new RegexpStringExtractor("(\\d+)", 1);
+        RegexpStringExtractor extractor = new RegexpStringExtractor("(\\d+)", 1);
         String string = "aabbcc1122ddee33ff44";
-        List<SubString> result = catcher.catchAll(string);
+        List<SubString> result = extractor.extractAll(string);
         System.out.println(ConvertTools.joinToString(result, '\n'));
     }
 }
-- 
Gitee


From aa126d6a88c9ebe9a1cdf8b1c03d7a601505cd62 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 24 Jan 2022 21:26:11 +0800
Subject: [PATCH 021/160] UrlGetTest

---
 .../gitee/qdbp/tools/utils/UrlGetTest.java    | 27 +++++++++++++++++++
 1 file changed, 27 insertions(+)
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/utils/UrlGetTest.java

diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/UrlGetTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/UrlGetTest.java
new file mode 100644
index 0000000..ebd1328
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/UrlGetTest.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+public class UrlGetTest {
+
+    public static void main(String[] args) throws IOException {
+        System.out.println(getUrlContent("https://www.bing.com"));
+    }
+
+    public static String getUrlContent(String url) throws IOException {
+        try (InputStream input = new URL(url).openStream();
+                ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+            int length;
+            byte[] buffer = new byte[1024];
+            while ((length = input.read(buffer, 0, buffer.length)) > 0) {
+                output.write(buffer, 0, length);
+            }
+            byte[] bytes = output.toByteArray();
+            return new String(bytes, StandardCharsets.UTF_8);
+        }
+    }
+}
-- 
Gitee


From d835c07cc5176035afeb78f778c9b3e3458f088b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Fri, 11 Feb 2022 22:21:38 +0800
Subject: [PATCH 022/160] =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=8F=90?=
 =?UTF-8?q?=E5=8F=96=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/able/beans/KeyValue.java   |  18 +
 .../gitee/qdbp/able/beans/ParamBuffer.java    |  17 +-
 .../com/gitee/qdbp/able/beans/SubString.java  |   4 +-
 .../com/gitee/qdbp/able/beans/SubStrings.java |  71 +++
 .../able/instance/WithMatcherFileFilter.java  |  63 +-
 .../qdbp/able/matches/GroupExtractor.java     |   8 +-
 .../qdbp/able/matches/GroupExtractors.java    |  22 +-
 .../qdbp/able/matches/GroupSubString.java     |  77 +++
 .../qdbp/able/matches/GroupSubStrings.java    |  72 +++
 .../qdbp/able/matches/QuotationReplacer.java  |   2 +-
 .../able/matches/RegexpGroupExtractor.java    | 215 +++++--
 .../able/matches/RegexpStringExtractor.java   | 172 ++++--
 .../able/matches/RegexpStringReplacer.java    |  85 ++-
 .../qdbp/able/matches/StringExtractor.java    |   4 +-
 .../qdbp/able/matches/StringExtractors.java   |  17 +-
 .../qdbp/able/matches/StringMatcher.java      |  38 +-
 .../qdbp/able/matches/StringReplacer.java     |  22 +-
 .../qdbp/able/matches/WrapFileMatcher.java    | 108 ++--
 .../qdbp/able/matches/WrapStringMatcher.java  | 540 ++++++++++++------
 .../qdbp/able/matches/WrapStringReplacer.java | 450 +++++++++++++++
 .../qdbp/able/model/reusable/ExtraBase.java   |  33 ++
 .../qdbp/able/model/reusable/ExtraData.java   |  19 +-
 .../qdbp/eachfile/runner/FileEachRunner.java  |  87 +--
 .../com/gitee/qdbp/tools/files/PathTools.java |  44 ++
 .../gitee/qdbp/tools/utils/ConvertTools.java  |  41 ++
 .../gitee/qdbp/tools/utils/IndentTools.java   |  10 +
 .../com/gitee/qdbp/tools/utils/RuleTools.java | 216 +++++++
 .../gitee/qdbp/tools/utils/StringTools.java   |   2 +-
 .../qdbp/matches/StringMatcherParserTest.java |  25 +
 .../matches/StringReplacerParserTest.java     |  56 ++
 .../com/gitee/qdbp/matches/replace-rules.txt  |   7 +
 .../gitee/qdbp/tools/utils/CartesianTest.java |  24 +
 32 files changed, 2106 insertions(+), 463 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/beans/SubStrings.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/GroupSubStrings.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraBase.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/matches/StringMatcherParserTest.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/matches/StringReplacerParserTest.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/matches/replace-rules.txt
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/utils/CartesianTest.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java b/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java
index 0a62f92..96e607e 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java
@@ -5,6 +5,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -59,6 +60,23 @@ public class KeyValue<V> implements Comparable<KeyValue<V>>, Serializable {
         this.value = value;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        KeyValue<?> keyValue = (KeyValue<?>) o;
+        return Objects.equals(key, keyValue.key) && Objects.equals(value, keyValue.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(key, value);
+    }
+
     /**
      * List转换为Map
      *
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java b/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java
index 25e6ab0..1f0f5c1 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java
@@ -1,6 +1,5 @@
 package com.gitee.qdbp.able.beans;
 
-import java.util.regex.Pattern;
 import com.gitee.qdbp.tools.utils.StringTools;
 
 /**
@@ -11,10 +10,6 @@ import com.gitee.qdbp.tools.utils.StringTools;
  */
 public class ParamBuffer implements Copyable {
 
-    private static final Pattern TAB = Pattern.compile("\\t");
-    private static final Pattern RETURN = Pattern.compile("\\r");
-    private static final Pattern NEWLINE = Pattern.compile("\\n");
-
     private String separator;
     private int valueStringMaxLength;
     private StringBuffer buffer;
@@ -58,17 +53,7 @@ public class ParamBuffer implements Copyable {
         if (value == null) {
             return "";
         }
-        String string = value.toString();
-        // 替换掉\t\r\n
-        if (string.indexOf('\t') >= 0) {
-            string = TAB.matcher(string).replaceAll("\\t");
-        }
-        if (string.indexOf('\r') >= 0) {
-            string = RETURN.matcher(string).replaceAll("\\r");
-        }
-        if (string.indexOf('\n') >= 0) {
-            string = NEWLINE.matcher(string).replaceAll("\\n");
-        }
+        String string = StringTools.replace(value.toString(), "\t", "\\t", "\r", "\\r", "\n", "\\n");
         // 裁切过长的字符串
         return StringTools.ellipsis(string, valueStringMaxLength);
     }
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java b/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java
index 5af918c..7a79c51 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java
@@ -1,6 +1,6 @@
 package com.gitee.qdbp.able.beans;
 
-import java.io.Serializable;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
 import com.gitee.qdbp.tools.compare.CompareTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 
@@ -10,7 +10,7 @@ import com.gitee.qdbp.tools.utils.StringTools;
  * @author zhaohuihua
  * @version 20210727
  */
-public class SubString implements Serializable, Comparable<SubString> {
+public class SubString extends ExtraData implements Comparable<SubString> {
 
     /** serialVersionUID **/
     private static final long serialVersionUID = 1L;
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/SubStrings.java b/able/src/main/java/com/gitee/qdbp/able/beans/SubStrings.java
new file mode 100644
index 0000000..f1bc645
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/SubStrings.java
@@ -0,0 +1,71 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.ArrayList;
+import java.util.Map;
+import com.gitee.qdbp.able.model.reusable.ExtraBase;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+
+/**
+ * 子文本信息集合
+ *
+ * @author zhaohuihua
+ * @version 20220130
+ */
+public class SubStrings extends ArrayList<SubString> implements ExtraBase {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    private ExtraData extraData;
+
+    /** 获取附加数据 **/
+    @Override
+    public Map<String, Object> getExtra() {
+        return extraData == null ? null : extraData.getExtra();
+    }
+
+    /** 设置附加数据 **/
+    @Override
+    public void setExtra(Map<String, Object> extra) {
+        this.extraData = new ExtraData(extra);
+    }
+
+    /** 是否存在指定的附加数据 **/
+    @Override
+    public boolean containsExtra(String key) {
+        return extraData != null && extraData.containsExtra(key);
+    }
+
+    /** 获取指定的附加数据 **/
+    @Override
+    public Object getExtra(String key) {
+        return extraData == null ? null : extraData.getExtra(key);
+    }
+
+    /** 获取指定类型的附加数据 **/
+    @Override
+    public <T> T getExtra(String key, Class<T> clazz) {
+        return extraData == null ? null : extraData.getExtra(key, clazz);
+    }
+
+    /** 设置指定的附加数据 **/
+    @Override
+    public void putExtra(String key, Object value) {
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(key, value);
+    }
+
+    /** 设置附加信息 **/
+    @Override
+    public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(extra);
+    }
+}
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/instance/WithMatcherFileFilter.java b/able/src/main/java/com/gitee/qdbp/able/instance/WithMatcherFileFilter.java
index 4756118..aee256f 100644
--- a/able/src/main/java/com/gitee/qdbp/able/instance/WithMatcherFileFilter.java
+++ b/able/src/main/java/com/gitee/qdbp/able/instance/WithMatcherFileFilter.java
@@ -3,9 +3,8 @@ package com.gitee.qdbp.able.instance;
 import java.io.File;
 import java.io.FileFilter;
 import java.io.Serializable;
-import com.gitee.qdbp.able.matches.StringMatcher;
-import com.gitee.qdbp.able.matches.WrapStringMatcher;
-import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.able.matches.FileMatcher;
+import com.gitee.qdbp.able.matches.WrapFileMatcher;
 
 /**
  * 根据StringMatcher筛选文件的过滤器
@@ -18,9 +17,7 @@ public class WithMatcherFileFilter implements FileFilter, Serializable {
     /** serialVersionUID **/
     private static final long serialVersionUID = 1L;
     /** 匹配规则 **/
-    private StringMatcher matcher;
-    /** 匹配文件路径还是文件名 **/
-    private boolean usePath = true;
+    private FileMatcher matcher;
 
     /** 默认构造函数 **/
     public WithMatcherFileFilter() {
@@ -33,76 +30,44 @@ public class WithMatcherFileFilter implements FileFilter, Serializable {
      * equals:开头的解析为EqualsStringMatcher<br>
      * contains:开头的解析为ContainsStringMatcher<br>
      * 其余的也解析为ContainsStringMatcher<br>
-     * 
+     *
      * @param matcher 文件名匹配规则
      */
     public WithMatcherFileFilter(String matcher) {
-        this(WrapStringMatcher.parseMatcher(matcher, false));
+        this(WrapFileMatcher.parseMatcher(matcher, false));
     }
 
     /**
      * 构造函数
-     * 
+     *
      * @param matcher 文件名匹配规则
      */
-    public WithMatcherFileFilter(StringMatcher matcher) {
+    public WithMatcherFileFilter(FileMatcher matcher) {
         this.matcher = matcher;
     }
 
     @Override
     public boolean accept(File file) {
-        boolean isFolder = file.isDirectory();
-        if (isFolder) {
-            return false;
-        } else if (matcher == null) {
+        if (matcher == null) {
             // 未设置文件匹配规则就等于遍历所有文件, 因此返回true
             return true;
         }
-        String path;
-        if (!usePath) {
-            path = file.getName();
-        } else {
-            // 路径转换为/分隔符, 方便windows/linux统一处理
-            path = PathTools.formatPath(file.getAbsolutePath());
-            if (!path.startsWith("/") && path.charAt(1) == ':') {
-                // windows文件, 去掉盘符, 如D:/home/files, 只取/home/files
-                // 如果保留盘符, ant规则不好处理
-                path = path.substring(2);
-            }
-        }
-        return matcher.matches(path);
+        return matcher.matches(file);
     }
 
-    /** 文件名匹配规则 **/
-    public StringMatcher getMatcher() {
+    /** 文件匹配规则 **/
+    public FileMatcher getMatcher() {
         return matcher;
     }
 
-    /** 文件名匹配规则 **/
-    public void setMatcher(StringMatcher matcher) {
+    /** 文件匹配规则 **/
+    public void setMatcher(FileMatcher matcher) {
         this.matcher = matcher;
     }
 
-    /** 匹配文件路径还是文件名 **/
-    public boolean isUsePath() {
-        return usePath;
-    }
-
-    /** 匹配文件路径还是文件名 **/
-    public void setUsePath(boolean usePath) {
-        this.usePath = usePath;
-    }
-
     @Override
     public String toString() {
-        if (matcher == null) {
-            return "matcher=null";
-        }
-        StringBuilder buffer = new StringBuilder();
-        buffer.append("matcher").append('=').append(matcher);
-        buffer.append(',').append(' ');
-        buffer.append("usePath").append('=').append(usePath);
-        return buffer.toString();
+        return matcher == null ? "matcher=null" : matcher.toString();
     }
 
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
index 9785c17..0f8f817 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
@@ -1,9 +1,5 @@
 package com.gitee.qdbp.able.matches;
 
-import java.util.List;
-import java.util.Map;
-import com.gitee.qdbp.able.beans.SubString;
-
 /**
  * 字符串组提取接口
  *
@@ -12,7 +8,7 @@ import com.gitee.qdbp.able.beans.SubString;
  */
 public interface GroupExtractor {
 
-    Map<Integer, SubString> extractFirst(String string);
+    GroupSubString extractFirst(String string);
 
-    List<Map<Integer, SubString>> extractAll(String string);
+    GroupSubStrings extractAll(String string);
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
index 8a63363..78c9e46 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
@@ -1,8 +1,6 @@
 package com.gitee.qdbp.able.matches;
 
 import java.util.List;
-import java.util.Map;
-import com.gitee.qdbp.able.beans.SubString;
 
 /**
  * 字符串组提取集合实现类
@@ -18,10 +16,14 @@ public class GroupExtractors implements GroupExtractor {
     }
 
     @Override
-    public Map<Integer, SubString> extractFirst(String string) {
-        for (GroupExtractor extractor : extractors) {
-            Map<Integer, SubString> result = extractor.extractFirst(string);
+    public GroupSubString extractFirst(String string) {
+        for (int i = 0, z = extractors.size(); i < z; i++) {
+            GroupExtractor extractor = extractors.get(i);
+            GroupSubString result = extractor.extractFirst(string);
             if (result != null) {
+                if (!result.containsExtra("i")) {
+                    result.putExtra("i", i);
+                }
                 return result;
             }
         }
@@ -29,10 +31,14 @@ public class GroupExtractors implements GroupExtractor {
     }
 
     @Override
-    public List<Map<Integer, SubString>> extractAll(String string) {
-        for (GroupExtractor extractor : extractors) {
-            List<Map<Integer, SubString>> result = extractor.extractAll(string);
+    public GroupSubStrings extractAll(String string) {
+        for (int i = 0, z = extractors.size(); i < z; i++) {
+            GroupExtractor extractor = extractors.get(i);
+            GroupSubStrings result = extractor.extractAll(string);
             if (result != null) {
+                if (!result.containsExtra("i")) {
+                    result.putExtra("i", i);
+                }
                 return result;
             }
         }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
new file mode 100644
index 0000000..5a31f1c
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
@@ -0,0 +1,77 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.HashMap;
+import java.util.Map;
+import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.able.model.reusable.ExtraBase;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+
+/**
+ * 带分组的子字符串对象
+ *
+ * @author zhaohuihua
+ * @version 20220129
+ */
+public class GroupSubString extends HashMap<String, SubString> implements ExtraBase {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    private ExtraData extraData;
+
+    public SubString get(Integer key) {
+        return super.get(String.valueOf(key));
+    }
+
+    /** 获取附加数据 **/
+    @Override
+    public Map<String, Object> getExtra() {
+        return extraData == null ? null : extraData.getExtra();
+    }
+
+    /** 设置附加数据 **/
+    @Override
+    public void setExtra(Map<String, Object> extra) {
+        this.extraData = new ExtraData(extra);
+    }
+
+    /** 是否存在指定的附加数据 **/
+    @Override
+    public boolean containsExtra(String key) {
+        return extraData != null && extraData.containsExtra(key);
+    }
+
+    /** 获取指定的附加数据 **/
+    @Override
+    public Object getExtra(String key) {
+        return extraData == null ? null : extraData.getExtra(key);
+    }
+
+    /** 获取指定类型的附加数据 **/
+    @Override
+    public <T> T getExtra(String key, Class<T> clazz) {
+        return extraData == null ? null : extraData.getExtra(key, clazz);
+    }
+
+    /** 设置指定的附加数据 **/
+    @Override
+    public void putExtra(String key, Object value) {
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(key, value);
+    }
+
+    /** 设置附加信息 **/
+    @Override
+    public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(extra);
+    }
+
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubStrings.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubStrings.java
new file mode 100644
index 0000000..c8f616c
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubStrings.java
@@ -0,0 +1,72 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.Map;
+import com.gitee.qdbp.able.model.reusable.ExtraBase;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+
+/**
+ * 带分组的子字符串对象集合
+ *
+ * @author zhaohuihua
+ * @version 20220129
+ */
+public class GroupSubStrings extends ArrayList<GroupSubString> implements ExtraBase {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    private ExtraData extraData;
+
+    /** 获取附加数据 **/
+    @Override
+    public Map<String, Object> getExtra() {
+        return extraData == null ? null : extraData.getExtra();
+    }
+
+    /** 设置附加数据 **/
+    @Override
+    public void setExtra(Map<String, Object> extra) {
+        this.extraData = new ExtraData(extra);
+    }
+
+    /** 是否存在指定的附加数据 **/
+    @Override
+    public boolean containsExtra(String key) {
+        return extraData != null && extraData.containsExtra(key);
+    }
+
+    /** 获取指定的附加数据 **/
+    @Override
+    public Object getExtra(String key) {
+        return extraData == null ? null : extraData.getExtra(key);
+    }
+
+    /** 获取指定类型的附加数据 **/
+    @Override
+    public <T> T getExtra(String key, Class<T> clazz) {
+        return extraData == null ? null : extraData.getExtra(key, clazz);
+    }
+
+    /** 设置指定的附加数据 **/
+    @Override
+    public void putExtra(String key, Object value) {
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(key, value);
+    }
+
+    /** 设置附加信息 **/
+    @Override
+    public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(extra);
+    }
+
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/QuotationReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/QuotationReplacer.java
index a507355..7939b25 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/QuotationReplacer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/QuotationReplacer.java
@@ -38,7 +38,7 @@ public class QuotationReplacer implements StringReplacer {
     @Override
     public String toString() {
         StringBuilder buffer = new StringBuilder();
-        buffer.append("quotation: ").append(source).append(" -->");
+        buffer.append("quotation: ").append(source).append(" -- ");
         for (char c : targets) {
             buffer.append(' ').append(c);
         }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
index e97132e..f434755 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
@@ -1,16 +1,20 @@
 package com.gitee.qdbp.able.matches;
 
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
+import com.gitee.qdbp.able.beans.KeyString;
 import com.gitee.qdbp.able.beans.SubString;
 import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.parse.StringAccess;
+import com.gitee.qdbp.tools.utils.IndentTools;
+import com.gitee.qdbp.tools.utils.RuleTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
@@ -20,119 +24,236 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  * @author zhaohuihua
  * @version 20220112
  */
-public class RegexpGroupExtractor implements GroupExtractor {
+public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
+
+    private static final long serialVersionUID = 2523069377411632793L;
     private final Pattern pattern;
-    private final List<Integer> groups;
+    private final List<Integer> digitGroups;
+    private final List<String> namedGroups;
 
     public RegexpGroupExtractor(String pattern, int... groups) {
-        this(pattern == null ? null : Pattern.compile(pattern), groups);
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = Pattern.compile(pattern);
+        this.namedGroups = null;
+        this.digitGroups = toIntegerList(groups);
     }
 
     public RegexpGroupExtractor(Pattern pattern, int... groups) {
-        VerifyTools.requireNotBlank(pattern, "pattern");
+        VerifyTools.requireNonNull(pattern, "pattern");
         VerifyTools.requireNotBlank(groups, "groups");
-
         this.pattern = pattern;
-        this.groups = new ArrayList<>();
-        for (int group : groups) {
-            this.groups.add(group);
-        }
+        this.namedGroups = null;
+        this.digitGroups = toIntegerList(groups);
     }
 
-    public RegexpGroupExtractor(String pattern, List<Integer> groups) {
-        this(pattern == null ? null : Pattern.compile(pattern), groups);
+    public RegexpGroupExtractor(String pattern, String... groups) {
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = Pattern.compile(pattern);
+        this.digitGroups = null;
+        this.namedGroups = Arrays.asList(groups);
+    }
+
+    public RegexpGroupExtractor(Pattern pattern, String... groups) {
+        VerifyTools.requireNonNull(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = pattern;
+        this.digitGroups = null;
+        this.namedGroups = Arrays.asList(groups);
     }
 
-    public RegexpGroupExtractor(Pattern pattern, List<Integer> groups) {
+    public RegexpGroupExtractor(String pattern, List<?> groups) {
         VerifyTools.requireNotBlank(pattern, "pattern");
         VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = Pattern.compile(pattern);
 
+        List<Integer> digitGroups = new ArrayList<>();
+        List<String> namedGroups = new ArrayList<>();
+        parseGroups(groups, digitGroups, namedGroups);
+        this.digitGroups = digitGroups.isEmpty() ? null : digitGroups;
+        this.namedGroups = namedGroups.isEmpty() ? null : namedGroups;
+    }
+
+    public RegexpGroupExtractor(Pattern pattern, List<?> groups) {
+        VerifyTools.requireNonNull(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
         this.pattern = pattern;
-        this.groups = groups;
+        List<Integer> digitGroups = new ArrayList<>();
+        List<String> namedGroups = new ArrayList<>();
+        parseGroups(groups, digitGroups, namedGroups);
+        this.digitGroups = digitGroups.isEmpty() ? null : digitGroups;
+        this.namedGroups = namedGroups.isEmpty() ? null : namedGroups;
     }
 
     @Override
-    public Map<Integer, SubString> extractFirst(String string) {
+    public GroupSubString extractFirst(String string) {
         Matcher matcher = pattern.matcher(string);
         if (!matcher.find()) {
             return null;
         } else {
-            Map<Integer, SubString> result = new HashMap<>();
-            for (int group : groups) {
-                String content = matcher.group(group);
-                int start = matcher.start(group);
-                int end = matcher.end(group);
-                result.put(group, new SubString(content, start, end));
-            }
+            GroupSubString result = newGroupSubString(matcher);
+            result.putExtra(this.getExtra());
             return result;
         }
     }
 
     @Override
-    public List<Map<Integer, SubString>> extractAll(String string) {
-        List<Map<Integer, SubString>> list = new ArrayList<>();
+    public GroupSubStrings extractAll(String string) {
+        GroupSubStrings result = new GroupSubStrings();
         Matcher matcher = pattern.matcher(string);
         while (matcher.find()) {
-            Map<Integer, SubString> map = new HashMap<>();
-            list.add(map);
-            for (int group : groups) {
+            result.add(newGroupSubString(matcher));
+        }
+        if (result.isEmpty()) {
+            return null;
+        }
+        result.putExtra(this.getExtra());
+        return result;
+    }
+
+    private GroupSubString newGroupSubString(Matcher matcher) {
+        GroupSubString result = new GroupSubString();
+        if (digitGroups != null) {
+            for (int group : digitGroups) {
                 String content = matcher.group(group);
                 int start = matcher.start(group);
                 int end = matcher.end(group);
-                map.put(group, new SubString(content, start, end));
+                result.put(String.valueOf(group), new SubString(content, start, end));
             }
         }
-        return list.isEmpty() ? null : list;
+        if (namedGroups != null) {
+            for (String group : namedGroups) {
+                String content = matcher.group(group);
+                int start = matcher.start(group);
+                int end = matcher.end(group);
+                result.put(group, new SubString(content, start, end));
+            }
+        }
+        return result;
+    }
+
+    private static List<Integer> toIntegerList(int... numbers) {
+        List<Integer> list = new ArrayList<>();
+        for (int number : numbers) {
+            list.add(number);
+        }
+        return list;
+    }
+
+    private static void parseGroups(List<?> groups, List<Integer> digitGroups, List<String> namedGroups) {
+        for (int i = 0; i < groups.size(); i++) {
+            Object group = groups.get(i);
+            if (group instanceof Integer) {
+                digitGroups.add((Integer) group);
+            } else if (group instanceof String) {
+                namedGroups.add((String) group);
+            } else {
+                String type = group == null ? "null" : group.getClass().getSimpleName();
+                String msg = "Group must be integer or string, but type of groups[" + i + "] is " + type;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+            }
+        }
+    }
+
+    /**
+     * 根据字符串参数解析成RegexpGroupExtractor对象
+     *
+     * @param rule 规则字符串, 格式为: [1,2] regexp
+     * @return RegexpGroupExtractor对象
+     * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+     */
+    public static RegexpGroupExtractor parseExtractor(String rule) {
+        return parseExtractor(rule, null);
     }
 
     /**
      * 根据字符串参数解析成RegexpGroupExtractor对象
      *
-     * @param string 字符串, 格式为: [1,2] regexp
+     * @param rule 规则字符串, 格式为: [1,2] regexp
+     * @param extras 附加参数
      * @return RegexpGroupExtractor对象
      * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
      */
-    public static RegexpGroupExtractor parse(String string) {
-        VerifyTools.requireNotBlank(string, "string");
-        StringAccess sa = new StringAccess(string);
+    public static RegexpGroupExtractor parseExtractor(String rule, Map<String, Object> extras) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        StringAccess sa = new StringAccess(rule);
         sa.skipWhitespace();
         String g = sa.readInBracket('[', ']');
         if (!sa.isLastReadSuccess()) {
-            String desc = "Group format error:  " + string + "  , The correct format is [1,2] regexp";
-            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, desc);
+            String msg = "Group format error:  " + rule + "  , The correct format is [1,2] regexp";
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
         }
-        List<Integer> groups = new ArrayList<>();
+        g = g.trim();
+        if (g.length() == 0) {
+            String msg = "Group must must not be empty, --> " + rule;
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+        }
+        List<Object> groups = new ArrayList<>();
         for (String group : StringTools.splits(g, ',')) {
             if (StringTools.isDigit(group)) {
                 groups.add(Integer.parseInt(group));
+            } else if (group.indexOf('$') < 0 && StringTools.isWordString(group)) {
+                groups.add(group);
             } else {
-                String desc = "Group must be digits: " + group + " --> " + sa.peekFrom(0);
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, desc);
+                String msg = "Group must be digit or word characters: " + g + " --> " + rule;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
             }
         }
         sa.skipWhitespace();
         String regexp = sa.readToEnd();
         try {
             Pattern pattern = Pattern.compile(regexp);
-            return new RegexpGroupExtractor(pattern, groups);
+            RegexpGroupExtractor extractor = new RegexpGroupExtractor(pattern, groups);
+            if (extras != null) {
+                extractor.putExtra(extras);
+            }
+            return extractor;
         } catch (PatternSyntaxException e) {
-            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, "Regexp format error, --> " + regexp, e);
+            String msg = "Regexp format error, --> " + regexp;
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg, e);
         }
     }
 
     /**
-     * 根据字符串参数解析成GroupExtractors对象
+     * 根据字符串参数解析成GroupExtractors对象<br>
+     * 参数格式为: [1,2] regexp<br>
+     * 如果配置带额外属性的正则, 其格式为:
+     * <pre>
+     * |[1,2] regexp
+     * |    extraKey1 = extraValue1
+     * |    extraKey2 = extraValue2
+     * </pre>
      *
-     * @param strings 字符串数组, 格式为: [1,2] regexp
+     * @param rules 规则字符串数组
      * @return GroupExtractors对象
      * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
      */
-    public static GroupExtractors parse(List<String> strings) {
-        VerifyTools.requireNotBlank(strings, "strings");
+    public static GroupExtractors parseExtractors(List<String> rules) {
+        VerifyTools.requireNotBlank(rules, "rules");
         List<RegexpGroupExtractor> extractors = new ArrayList<>();
-        for (String string : strings) {
-            if (VerifyTools.isNotBlank(string)) {
-                extractors.add(RegexpGroupExtractor.parse(string));
+        List<String> cleared = RuleTools.clearCommentLines(rules);
+        for (int i = 0, z = cleared.size(); i < z; i++) {
+            String string = cleared.get(0);
+            String trimmed = string.trim();
+            if (extractors.isEmpty()) {
+                // 第1个正则之前的行, 判断是否缩进
+                int indent = IndentTools.countLeadingIndentSize(string);
+                if (indent > 0) {
+                    // 第1个正则之前的带缩进的行, 跳过
+                    continue;
+                }
+            }
+            // 解析正则表达式提取规则
+            RegexpGroupExtractor extractor = parseExtractor(trimmed);
+            extractors.add(extractor);
+            // 解析附加属性
+            List<String> extraLines = new ArrayList<>();
+            i += RuleTools.collectNextIndentLines(cleared, i + 1, extraLines);
+            if (!extraLines.isEmpty()) {
+                List<KeyString> keyStrings = RuleTools.splitRuleKeyValues(extraLines);
+                Map<String, Object> maps = KeyString.toMap(keyStrings);
+                extractor.putExtra(maps);
             }
         }
         return new GroupExtractors(extractors);
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
index 45c8ea4..014625d 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
@@ -2,12 +2,19 @@ package com.gitee.qdbp.able.matches;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
+import com.gitee.qdbp.able.beans.KeyString;
 import com.gitee.qdbp.able.beans.SubString;
-import com.gitee.qdbp.able.exception.FormatException;
+import com.gitee.qdbp.able.beans.SubStrings;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.parse.StringAccess;
+import com.gitee.qdbp.tools.utils.IndentTools;
+import com.gitee.qdbp.tools.utils.RuleTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
@@ -17,18 +24,39 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  * @author zhaohuihua
  * @version 20220112
  */
-public class RegexpStringExtractor implements StringExtractor {
+public class RegexpStringExtractor extends ExtraData implements StringExtractor {
+
+    private static final long serialVersionUID = 6340075034069358703L;
     private final Pattern pattern;
-    private final int group;
+    private final Integer digitGroup;
+    private final String namedGroup;
 
     public RegexpStringExtractor(String pattern, int group) {
-        this(pattern == null ? null : Pattern.compile(pattern), group);
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        this.pattern = Pattern.compile(pattern);
+        this.digitGroup = group;
+        this.namedGroup = null;
     }
 
     public RegexpStringExtractor(Pattern pattern, int group) {
+        VerifyTools.requireNonNull(pattern, "pattern");
+        this.pattern = pattern;
+        this.digitGroup = group;
+        this.namedGroup = null;
+    }
+
+    public RegexpStringExtractor(String pattern, String group) {
         VerifyTools.requireNotBlank(pattern, "pattern");
+        this.pattern = Pattern.compile(pattern);
+        this.digitGroup = null;
+        this.namedGroup = group;
+    }
+
+    public RegexpStringExtractor(Pattern pattern, String group) {
+        VerifyTools.requireNonNull(pattern, "pattern");
         this.pattern = pattern;
-        this.group = group;
+        this.digitGroup = null;
+        this.namedGroup = group;
     }
 
     @Override
@@ -37,66 +65,140 @@ public class RegexpStringExtractor implements StringExtractor {
         if (!matcher.find()) {
             return null;
         } else {
-            String content = matcher.group(group);
-            int start = matcher.start(group);
-            int end = matcher.end(group);
-            return new SubString(content, start, end);
+            SubString result = newSubString(matcher);
+            result.putExtra(this.getExtra());
+            return result;
         }
     }
 
     @Override
-    public List<SubString> extractAll(String string) {
-        List<SubString> list = new ArrayList<>();
+    public SubStrings extractAll(String string) {
+        SubStrings result = new SubStrings();
         Matcher matcher = pattern.matcher(string);
         while (matcher.find()) {
-            String content = matcher.group(group);
-            int start = matcher.start(group);
-            int end = matcher.end(group);
-            list.add(new SubString(content, start, end));
+            result.add(newSubString(matcher));
         }
-        return list.isEmpty() ? null : list;
+        if (result.isEmpty()) {
+            return null;
+        }
+        result.putExtra(this.getExtra());
+        return result;
+    }
+
+    private SubString newSubString(Matcher matcher) {
+        if (this.digitGroup != null) {
+            String content = matcher.group(this.digitGroup);
+            int start = matcher.start(this.digitGroup);
+            int end = matcher.end(this.digitGroup);
+            return new SubString(content, start, end);
+        } else {
+            String content = matcher.group(this.namedGroup);
+            int start = matcher.start(this.namedGroup);
+            int end = matcher.end(this.namedGroup);
+            return new SubString(content, start, end);
+        }
+    }
+
+    /**
+     * 根据字符串参数解析成RegexpGroupExtractor对象
+     *
+     * @param rule 规则字符串, 格式为: [1] regexp
+     * @return RegexpGroupExtractor对象
+     */
+    public static RegexpStringExtractor parseExtractor(String rule) {
+        return parseExtractor(rule, null);
     }
 
     /**
      * 根据字符串参数解析成RegexpGroupExtractor对象
      *
-     * @param string 字符串, 格式为: [1] regexp
+     * @param rule 规则字符串, 格式为: [1] regexp
+     * @param extras 附加参数
      * @return RegexpGroupExtractor对象
      */
-    public static RegexpStringExtractor parse(String string) {
-        VerifyTools.requireNotBlank(string, "string");
-        StringAccess sa = new StringAccess(string);
+    public static RegexpStringExtractor parseExtractor(String rule, Map<String, Object> extras) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        StringAccess sa = new StringAccess(rule);
         sa.skipWhitespace();
-        String g = sa.readInBracket('[', ']');
+        String group = sa.readInBracket('[', ']');
         if (!sa.isLastReadSuccess()) {
-            throw new FormatException("Group format error, The correct format is [1] regexp, --> " + string);
+            String msg = "Group format error, The correct format is [1] regexp, --> " + rule;
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+        }
+        group = group.trim();
+        if (group.length() == 0) {
+            String msg = "Group must must not be empty, --> " + rule;
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
         }
-        if (!StringTools.isDigit(g)) {
-            throw new FormatException("Group must be digits: " + g + " --> " + string);
+        Integer digitGroup = null;
+        String namedGroup = null;
+        if (StringTools.isDigit(group)) {
+            digitGroup = Integer.parseInt(group);
+        } else if (group.indexOf('$') < 0 && StringTools.isWordString(group)) {
+            namedGroup = group;
+        } else {
+            String msg = "Group must be digit or word characters: " + group + " --> " + rule;
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
         }
-        int group = Integer.parseInt(g);
         sa.skipWhitespace();
         String regexp = sa.readToEnd();
+        Pattern pattern;
         try {
-            Pattern pattern = Pattern.compile(regexp);
-            return new RegexpStringExtractor(pattern, group);
+            pattern = Pattern.compile(regexp);
         } catch (PatternSyntaxException e) {
-            throw new FormatException("Regexp format error, --> " + regexp, e);
+            String msg = "Regexp format error, --> " + regexp;
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg, e);
         }
+        RegexpStringExtractor extractor;
+        if (digitGroup != null) {
+            extractor = new RegexpStringExtractor(pattern, digitGroup);
+        } else {
+            extractor = new RegexpStringExtractor(pattern, namedGroup);
+        }
+        if (extras != null) {
+            extractor.putExtra(extras);
+        }
+        return extractor;
     }
 
     /**
-     * 根据字符串参数解析成StringExtractors对象
+     * 根据字符串参数解析成StringExtractors对象<br>
+     * 参数格式为: [1] regexp<br>
+     * 如果配置带额外属性的正则, 其格式为:
+     * <pre>
+     * |[1] regexp
+     * |    extraKey1 = extraValue1
+     * |    extraKey2 = extraValue2
+     * </pre>
      *
-     * @param strings 字符串数组, 格式为: [1] regexp
+     * @param rules 字符串数组
      * @return StringExtractors对象
      */
-    public static StringExtractors parse(List<String> strings) {
-        VerifyTools.requireNotBlank(strings, "strings");
+    public static StringExtractors parseExtractors(List<String> rules) {
+        VerifyTools.requireNotBlank(rules, "rules");
         List<RegexpStringExtractor> extractors = new ArrayList<>();
-        for (String string : strings) {
-            if (VerifyTools.isNotBlank(string)) {
-                extractors.add(parse(string));
+        List<String> cleared = RuleTools.clearCommentLines(rules);
+        for (int i = 0, z = cleared.size(); i < z; i++) {
+            String string = cleared.get(0);
+            String trimmed = string.trim();
+            if (extractors.isEmpty()) {
+                // 第1个正则之前的行, 判断是否缩进
+                int indent = IndentTools.countLeadingIndentSize(string);
+                if (indent > 0) {
+                    // 第1个正则之前的带缩进的行, 跳过
+                    continue;
+                }
+            }
+            // 解析正则表达式提取规则
+            RegexpStringExtractor extractor = parseExtractor(trimmed);
+            extractors.add(extractor);
+            // 解析附加属性
+            List<String> extraLines = new ArrayList<>();
+            i += RuleTools.collectNextIndentLines(cleared, i + 1, extraLines);
+            if (!extraLines.isEmpty()) {
+                List<KeyString> keyStrings = RuleTools.splitRuleKeyValues(extraLines);
+                Map<String, Object> maps = KeyString.toMap(keyStrings);
+                extractor.putExtra(maps);
             }
         }
         return new StringExtractors(extractors);
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringReplacer.java
index 5eb510a..e86b70f 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringReplacer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringReplacer.java
@@ -1,6 +1,10 @@
 package com.gitee.qdbp.able.matches;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.regex.Pattern;
+import com.gitee.qdbp.tools.utils.RuleTools;
+import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -18,7 +22,7 @@ public class RegexpStringReplacer implements StringReplacer {
 
     /**
      * 构造函数
-     * 
+     *
      * @param pattern 正则表达式
      * @param replacement 替换内容
      */
@@ -31,7 +35,7 @@ public class RegexpStringReplacer implements StringReplacer {
 
     /**
      * 构造函数
-     * 
+     *
      * @param pattern 正则表达式
      * @param replacement 替换内容
      */
@@ -55,7 +59,82 @@ public class RegexpStringReplacer implements StringReplacer {
 
     @Override
     public String toString() {
-        return "regexp:" + pattern + " --> " + replacement;
+        return "regexp:" + pattern + " -- " + replacement;
     }
 
+    /**
+     * 解析文本替换规则<br>
+     * // 年初现金替换为期初现金<br>
+     * 年([初末])现金 -- 期$1现金<br>
+     * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+     * (?<=所)支付的现金 -- 所支付的现金<br>
+     *
+     * @param pattern 规则内容
+     * @return 规则对象
+     */
+    public static RegexpStringReplacer parseReplacer(String pattern) {
+        return parseReplacer(pattern, " -- ");
+    }
+
+    /**
+     * 解析文本替换规则<br>
+     * // 年初现金替换为期初现金<br>
+     * 年([初末])现金 -- 期$1现金<br>
+     * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+     * (?<=所)支付的现金 -- 所支付的现金<br>
+     *
+     * @param pattern 规则内容
+     * @param delimiter 分隔符
+     * @return 规则对象
+     */
+    public static RegexpStringReplacer parseReplacer(String pattern, String delimiter) {
+        int splitIndex = pattern.indexOf(delimiter);
+        if (splitIndex < 0) {
+            return null;
+        }
+        String source = StringTools.trimRight(pattern.substring(0, splitIndex));
+        String target = StringTools.trimLeft(pattern.substring(splitIndex + delimiter.length()));
+        if ("{null}".equals(target)) {
+            target = "";
+        }
+        return new RegexpStringReplacer(source, target);
+    }
+
+    /**
+     * 解析文本替换规则<br>
+     * // 年初现金替换为期初现金<br>
+     * 年([初末])现金 -- 期$1现金<br>
+     * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+     * (?<=所)支付的现金 -- 所支付的现金<br>
+     *
+     * @param patterns 规则内容
+     * @return 规则对象
+     */
+    public static WrapStringReplacer parseReplacers(List<String> patterns) {
+        return parseReplacers(patterns, " -- ");
+    }
+
+    /**
+     * 解析文本替换规则<br>
+     * // 年初现金替换为期初现金<br>
+     * 年([初末])现金 -- 期$1现金<br>
+     * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+     * (?<=所)支付的现金 -- 所支付的现金<br>
+     *
+     * @param rules 规则内容
+     * @param delimiter 分隔符
+     * @return 规则对象
+     */
+    public static WrapStringReplacer parseReplacers(List<String> rules, String delimiter) {
+        List<StringReplacer> replacers = new ArrayList<>();
+        List<String> cleared = RuleTools.clearCommentLines(rules);
+        for (String rule : cleared) {
+            String trimmed = rule.trim();
+            RegexpStringReplacer replacer = parseReplacer(trimmed, delimiter);
+            if (replacer != null) {
+                replacers.add(replacer);
+            }
+        }
+        return new WrapStringReplacer(replacers);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
index c23dc74..463e6f6 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
@@ -1,7 +1,7 @@
 package com.gitee.qdbp.able.matches;
 
-import java.util.List;
 import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.able.beans.SubStrings;
 
 /**
  * 字符串提取接口
@@ -13,5 +13,5 @@ public interface StringExtractor {
 
     SubString extractFirst(String string);
 
-    List<SubString> extractAll(String string);
+    SubStrings extractAll(String string);
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
index 1a20034..9c43be2 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
@@ -2,6 +2,7 @@ package com.gitee.qdbp.able.matches;
 
 import java.util.List;
 import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.able.beans.SubStrings;
 
 /**
  * 字符串提取集合实现类
@@ -18,9 +19,13 @@ public class StringExtractors implements StringExtractor {
 
     @Override
     public SubString extractFirst(String string) {
-        for (StringExtractor extractor : extractors) {
+        for (int i = 0, z = extractors.size(); i < z; i++) {
+            StringExtractor extractor = extractors.get(i);
             SubString result = extractor.extractFirst(string);
             if (result != null) {
+                if (!result.containsExtra("i")) {
+                    result.putExtra("i", i);
+                }
                 return result;
             }
         }
@@ -28,10 +33,14 @@ public class StringExtractors implements StringExtractor {
     }
 
     @Override
-    public List<SubString> extractAll(String string) {
-        for (StringExtractor extractor : extractors) {
-            List<SubString> result = extractor.extractAll(string);
+    public SubStrings extractAll(String string) {
+        for (int i = 0, z = extractors.size(); i < z; i++) {
+            StringExtractor extractor = extractors.get(i);
+            SubStrings result = extractor.extractAll(string);
             if (result != null) {
+                if (!result.containsExtra("i")) {
+                    result.putExtra("i", i);
+                }
                 return result;
             }
         }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java
index b6338f8..d8ec4fc 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java
@@ -29,6 +29,16 @@ package com.gitee.qdbp.able.matches;
  */
 public interface StringMatcher {
 
+    /**
+     * Returns <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
+     * <code>false</code> otherwise.
+     *
+     * @param source the source to match
+     * @return <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
+     * <code>false</code> otherwise.
+     */
+    boolean matches(String source);
+
     /** 匹配模式 **/
     enum Matches {
         /** 肯定模式, 符合条件为匹配 **/
@@ -42,23 +52,23 @@ public interface StringMatcher {
         AND, OR
     }
 
-    /**
-     * Returns <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
-     * <code>false</code> otherwise.
-     *
-     * @param source  the source to match
-     * @return <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
-     *         <code>false</code> otherwise.
-     */
-    boolean matches(String source);
+    /** 基础的规则解析处理接口 **/
+    interface ParserByBase {
+
+        /** 是否支持解析 **/
+        boolean supports(String rule);
+
+        /** 解析匹配规则 **/
+        StringMatcher parse(String rule, String defaultMode);
+    }
 
-    /** 规则解析器 **/
-    interface Parser {
+    /** 按匹配模式解析的规则解析处理接口 **/
+    interface ParserByMode {
 
-        /** 支持的类型 **/
-        String[] supportTypes();
+        /** 支持的匹配模式 **/
+        String[] supportModes();
 
         /** 解析匹配规则 **/
-        StringMatcher parse(String pattern, String type, StringMatcher.Matches matchType);
+        StringMatcher parse(String rule, String mode, String defaultMode, Matches matchType);
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringReplacer.java
index e909124..1140724 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringReplacer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringReplacer.java
@@ -10,9 +10,29 @@ public interface StringReplacer {
 
     /**
      * 替换字符串
-     * 
+     *
      * @param string 待替换的源字符串
      * @return 替换后的字符串
      */
     String replace(String string);
+
+    /** 基础的规则解析处理接口 **/
+    interface ParserByBase {
+
+        /** 是否支持解析 **/
+        boolean supports(String rule);
+
+        /** 解析匹配规则 **/
+        StringReplacer parse(String rule, String defaultMode);
+    }
+
+    /** 按mode解析的规则解析处理接口 **/
+    interface ParserByMode {
+
+        /** 支持的匹配模式 **/
+        String[] supportModes();
+
+        /** 解析匹配规则 **/
+        StringReplacer parse(String rule, String mode, String defaultMode);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java
index ce572f9..9e46629 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java
@@ -78,7 +78,7 @@ public class WrapFileMatcher implements FileMatcher {
     @Override
     public String toString() {
         if (this.matchers == null || this.matchers.isEmpty()) {
-            return "NULL";
+            return "{null}";
         }
         StringBuilder buffer = new StringBuilder();
         String logicType = this.logicType == null ? "OR" : this.logicType.name();
@@ -139,16 +139,16 @@ public class WrapFileMatcher implements FileMatcher {
 
     /**
      * 解析FileMatcher规则<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的解析为EqualsStringMatcher<br>
+     * <br>
+     * 例如: name:ant:*.txt, 表示用fileName匹配ant规则*.txt<br>
+     * 例如: path:contains!:bak, 表示filePath不含有bak的<br>
+     * <br>
+     * 第1段, name或path表示匹配目标, 是根据文件名还是文件路径进行比对<br>
+     * 第2段, 匹配规则, 详见 StringMatcher<br>
      * 
      * @param pattern 匹配规则
      * @return FileMatcher
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      */
     public static FileMatcher parseMatcher(String pattern) {
         VerifyTools.requireNotBlank(pattern, "pattern");
@@ -162,18 +162,12 @@ public class WrapFileMatcher implements FileMatcher {
      * 例如: path:contains!:bak, 表示filePath不含有bak的<br>
      * <br>
      * 第1段, name或path表示匹配目标, 是根据文件名还是文件路径进行比对<br>
-     * 第2段, 匹配规则:<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 严格模式下解析为EqualsStringMatcher, 否则解析为ContainsStringMatcher<br>
+     * 第2段, 匹配规则, 详见 StringMatcher<br>
      * 
      * @param pattern 匹配规则
      * @param strict 是否使用严格格式
      * @return FileMatcher
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      */
     public static FileMatcher parseMatcher(String pattern, boolean strict) {
         VerifyTools.requireNotBlank(pattern, "pattern");
@@ -188,20 +182,14 @@ public class WrapFileMatcher implements FileMatcher {
      * 例如: path:contains!:bak, 表示filePath不含有bak的<br>
      * <br>
      * 第1段, name或path表示匹配目标, 是根据文件名还是文件路径进行比对<br>
-     * 第2段, 匹配规则:<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 使用defaultMode指定的匹配方式<br>
-     * 
+     * 第2段, 匹配规则, 详见 StringMatcher<br>
+     *
      * @param pattern 匹配规则
      * @param defaultTarget 默认匹配目标
      * @param defaultMode 默认匹配方式
      * @return FileMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String, String)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      * @since 5.2.0
      */
     public static FileMatcher parseMatcher(String pattern, String defaultTarget, String defaultMode) {
@@ -225,60 +213,58 @@ public class WrapFileMatcher implements FileMatcher {
     }
 
     /**
-     * 解析FileMatcher规则列表, 以逗号或换行符分隔<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的解析为EqualsStringMatcher<br>
+     * 解析FileMatcher规则列表, 以逗号或换行符分隔
      * 
-     * @param patterns 匹配规则列表
+     * @param rules 匹配规则列表
      * @param logicType 多个匹配规则使用and还是or关联
      * @return FileMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see WrapStringMatcher.Parsers#parseMatchers(String, LogicType)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static FileMatcher parseMatchers(String patterns, LogicType logicType) {
-        VerifyTools.requireNotBlank(patterns, "patterns");
-        return parseMatchers(patterns, logicType, "name", "equals", ',', '\n');
+    public static FileMatcher parseMatchers(String rules, LogicType logicType) {
+        VerifyTools.requireNotBlank(rules, "rules");
+        return parseMatchers(rules, logicType, "name", "equals", ',', '\n');
     }
 
     /**
-     * 解析FileMatcher规则列表<br>
-     * 如: parseMatcher(pattern, Logic.OR, "name", "ant", ',', '\n'); // 默认按文件名以ant规则匹配<br>
-     * <br>
-     * 例如: name:ant:*.txt, 表示用fileName匹配ant规则*.txt<br>
-     * 例如: path:contains!:bak, 表示filePath不含有bak的<br>
-     * <br>
-     * 第1段, name或path表示匹配目标, 是根据文件名还是文件路径进行比对<br>
-     * 第2段, 匹配规则:<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 使用defaultMode指定的匹配方式<br>
+     * 解析FileMatcher规则列表
      * 
-     * @param patterns 匹配规则列表
+     * @param rules 匹配规则列表
      * @param defaultTarget 默认匹配目标
      * @param defaultMode 默认匹配方式
      * @param chars 分隔符
      * @return FileMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see WrapStringMatcher.Parsers#parseMatchers(String, LogicType, String, char...)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static FileMatcher parseMatchers(String patterns, LogicType logicType, String defaultTarget,
+    public static FileMatcher parseMatchers(String rules, LogicType logicType, String defaultTarget,
             String defaultMode, char... chars) {
-        VerifyTools.requireNotBlank(patterns, "patterns");
+        VerifyTools.requireNotBlank(rules, "rules");
         if (chars == null || chars.length == 0) {
-            chars = new char[] { ',', '\n' };
+            chars = new char[] {',', '\n'};
         }
-        String[] array = StringTools.split(patterns, chars);
+        List<String> list = StringTools.splits(rules, chars);
+        return parseMatchers(list, logicType, defaultTarget, defaultMode);
+    }
+
+    /**
+     * 解析FileMatcher规则列表
+     *
+     * @param rules 匹配规则列表
+     * @param defaultTarget 默认匹配目标
+     * @param defaultMode 默认匹配方式
+     * @return FileMatcher
+     * @see WrapStringMatcher.Parsers#parseMatchers(List, LogicType, String)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
+     * @since 5.4.10
+     */
+    public static FileMatcher parseMatchers(List<String> rules, LogicType logicType, String defaultTarget,
+            String defaultMode) {
+        VerifyTools.requireNotBlank(rules, "rules");
         List<FileMatcher> matchers = new ArrayList<>();
-        for (String pattern : array) {
+        for (String pattern : rules) {
             if (VerifyTools.isNotBlank(pattern)) {
                 matchers.add(parseMatcher(pattern, defaultTarget, defaultMode));
             }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java
index 3a96325..550b0c8 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java
@@ -6,7 +6,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import com.gitee.qdbp.tools.parse.StringAccess;
-import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.RuleTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
@@ -18,37 +18,42 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  */
 public class WrapStringMatcher implements StringMatcher {
 
-    private List<StringMatcher> matchers;
     /** 多个匹配规则使用and还是or关联 **/
     private LogicType logicType;
+    private final List<StringMatcher> matchers = new ArrayList<>();
 
-    public WrapStringMatcher(String... patterns) {
+    public WrapStringMatcher(String... rules) {
         this.logicType = LogicType.AND;
-        this.addMatchers(patterns);
+        this.addMatchers(rules);
     }
 
-    public WrapStringMatcher(LogicType logicType, String... patterns) {
+    public WrapStringMatcher(LogicType logicType, String... rules) {
         this.logicType = logicType;
-        this.addMatchers(patterns);
+        this.addMatchers(rules);
     }
 
     public WrapStringMatcher(StringMatcher... matchers) {
         this.logicType = LogicType.AND;
-        this.addMatcher(matchers);
+        this.addMatchers(matchers);
     }
 
     public WrapStringMatcher(LogicType logicType, StringMatcher... matchers) {
         this.logicType = logicType;
-        this.addMatcher(matchers);
+        this.addMatchers(matchers);
+    }
+
+    public WrapStringMatcher(List<StringMatcher> matchers) {
+        this.logicType = LogicType.AND;
+        this.addMatchers(matchers);
+    }
+
+    public WrapStringMatcher(LogicType logicType, List<StringMatcher> matchers) {
+        this.logicType = logicType;
+        this.addMatchers(matchers);
     }
 
     @Override
     public boolean matches(String source) {
-        if (this.matchers == null) {
-            // matchers=null和matchers是空数组效果一致
-            return this.logicType == LogicType.AND;
-        }
-
         for (StringMatcher matcher : this.matchers) {
             boolean matches = matcher.matches(source);
             if (this.logicType == LogicType.AND) {
@@ -69,8 +74,8 @@ public class WrapStringMatcher implements StringMatcher {
 
     @Override
     public String toString() {
-        if (this.matchers == null || this.matchers.isEmpty()) {
-            return "NULL";
+        if (this.matchers.isEmpty()) {
+            return "{null}";
         }
         StringBuilder buffer = new StringBuilder();
         String logicType = this.logicType == null ? "OR" : this.logicType.name();
@@ -94,31 +99,33 @@ public class WrapStringMatcher implements StringMatcher {
 
     /** 设置匹配规则 **/
     public void setMatchers(List<StringMatcher> matchers) {
-        this.matchers = matchers;
+        this.matchers.clear();
+        this.addMatchers(matchers);
     }
 
     /** 增加匹配规则 **/
-    public void addMatchers(String... patterns) {
-        if (this.matchers == null) {
-            this.matchers = new ArrayList<>();
-        }
-        if (patterns != null && patterns.length > 0) {
-            for (String pattern : patterns) {
-                this.matchers.add(parseMatcher(pattern, true));
+    private void addMatchers(String... rules) {
+        if (rules != null && rules.length > 0) {
+            for (String rule : rules) {
+                this.matchers.add(parseMatcher(rule, true));
             }
         }
     }
 
     /** 增加匹配规则 **/
-    public void addMatcher(StringMatcher... matchers) {
-        if (this.matchers == null) {
-            this.matchers = new ArrayList<>();
-        }
+    public void addMatchers(StringMatcher... matchers) {
         if (matchers != null && matchers.length > 0) {
             Collections.addAll(this.matchers, matchers);
         }
     }
 
+    /** 增加匹配规则 **/
+    public void addMatchers(List<StringMatcher> matchers) {
+        if (matchers != null && matchers.size() > 0) {
+            this.matchers.addAll(matchers);
+        }
+    }
+
     /** 多个匹配规则使用and还是or关联 **/
     public LogicType getLogicType() {
         return logicType;
@@ -129,184 +136,366 @@ public class WrapStringMatcher implements StringMatcher {
         this.logicType = logicType;
     }
 
-    private static final Map<String, List<Parser>> MATCHER_PARSER = new HashMap<>();
-
-    static {
-        addMatcherParser(new SimpleStringMatcherParser());
-    }
-
-    /** 增加字符串匹配规则解析器 **/
-    public static void addMatcherParser(Parser parser) {
-        String[] supports = parser.supportTypes();
-        for (String support : supports) {
-            if (MATCHER_PARSER.containsKey(support)) {
-                MATCHER_PARSER.get(support).add(parser);
-            } else {
-                List<Parser> parsers = new ArrayList<>();
-                parsers.add(parser);
-                MATCHER_PARSER.put(support, parsers);
-            }
-        }
-    }
+    private static final Parsers MATCHER_PARSER = new Parsers();
 
     /**
-     * 解析StringMatcher规则<br>
-     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * eq:或equals:开头的解析为EqualsStringMatcher<br>
-     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
-     * srt:或starts:开头的解析为StartsStringMatcher<br>
-     * end:或ends:开头的解析为EndsStringMatcher<br>
-     * 其余的解析为EqualsStringMatcher<br>
+     * 解析StringMatcher规则
      *
-     * @param pattern 匹配规则
+     * @param rule 匹配规则
      * @return StringMatcher
+     * @see Parsers#parseMatcher(String)
      */
-    public static StringMatcher parseMatcher(String pattern) {
-        VerifyTools.requireNotBlank(pattern, "pattern");
-        return parseMatcher(pattern, "equals");
+    public static StringMatcher parseMatcher(String rule) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return parseMatcher(rule, "equals");
     }
 
     /**
-     * 解析StringMatcher规则<br>
-     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * eq:或equals:开头的解析为EqualsStringMatcher<br>
-     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
-     * srt:或starts:开头的解析为StartsStringMatcher<br>
-     * end:或ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 严格模式下解析为EqualsStringMatcher, 否则解析为ContainsStringMatcher<br>
+     * 解析StringMatcher规则
      *
-     * @param pattern 匹配规则
+     * @param rule 匹配规则
      * @param strict 是否使用严格格式
      * @return StringMatcher
+     * @see Parsers#parseMatcher(String, boolean)
+     * @see Parsers#parseMatcher(String)
      */
-    public static StringMatcher parseMatcher(String pattern, boolean strict) {
-        VerifyTools.requireNotBlank(pattern, "pattern");
-        return parseMatcher(pattern, strict ? "equals" : "contains");
-    }
-
-    private static StringMatcher doParseMatcher(String pattern, String type, Matches matchType) {
-        if (type == null || !MATCHER_PARSER.containsKey(type)) {
-            return new EqualsStringMatcher(pattern, matchType);
-        }
-        List<Parser> parsers = MATCHER_PARSER.get(type);
-        // 从后往前匹配, 后加入的优先级最高
-        for (int i = parsers.size() - 1; i >= 0; i--) {
-            Parser parser = parsers.get(i);
-            StringMatcher matcher = parser.parse(pattern, type, matchType);
-            if (matcher != null) {
-                return matcher;
-            }
-        }
-        return new EqualsStringMatcher(pattern, matchType);
+    public static StringMatcher parseMatcher(String rule, boolean strict) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return MATCHER_PARSER.parseMatcher(rule, strict);
     }
 
     /**
-     * 解析StringMatcher规则<br>
-     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * eq:或equals:开头的解析为EqualsStringMatcher<br>
-     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
-     * srt:或starts:开头的解析为StartsStringMatcher<br>
-     * end:或ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 使用defaultMode指定的匹配方式<br>
+     * 解析StringMatcher规则
      *
-     * @param pattern 匹配规则
+     * @param rule 匹配规则
      * @param defaultMode 默认匹配方式
      * @return StringMatcher
+     * @see Parsers#parseMatcher(String, String)
+     * @see Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static StringMatcher parseMatcher(String pattern, String defaultMode) {
-        VerifyTools.requireNotBlank(pattern, "pattern");
-        StringAccess sa = new StringAccess(pattern);
-        String type = sa.skipWhitespace().readAscii();
-        if (type == null) {
-            return doParseMatcher(pattern, defaultMode, Matches.Positive);
-        }
-        Matches matchType = Matches.Positive;
-        char next = sa.skipWhitespace().readChar();
-        if (next == '!') {
-            matchType = Matches.Negative;
-            next = sa.skipWhitespace().readChar();
-        }
-        if (next != ':') {
-            return doParseMatcher(pattern, defaultMode, Matches.Positive);
-        }
-        sa.skipWhitespace();
-        String realPattern = sa.readToEnd();
-        return doParseMatcher(realPattern, type, matchType);
+    public static StringMatcher parseMatcher(String rule, String defaultMode) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return MATCHER_PARSER.parseMatcher(rule, defaultMode);
     }
 
     /**
-     * 解析StringMatcher规则列表, 以逗号或换行符分隔<br>
-     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * eq:或equals:开头的解析为EqualsStringMatcher<br>
-     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
-     * srt:或starts:开头的解析为StartsStringMatcher<br>
-     * end:或ends:开头的解析为EndsStringMatcher<br>
-     * 其余的解析为EqualsStringMatcher<br>
+     * 解析StringMatcher规则列表, 以逗号或换行符分隔
      *
-     * @param patterns 匹配规则列表
+     * @param rules 匹配规则列表
      * @param logicType 多个匹配规则使用and还是or关联
      * @return StringMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see Parsers#parseMatchers(String, LogicType)
+     * @see Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static StringMatcher parseMatchers(String patterns, LogicType logicType) {
-        VerifyTools.requireNotBlank(patterns, "patterns");
-        return parseMatchers(patterns, logicType, "equals", ',', '\n');
+    public static StringMatcher parseMatchers(String rules, LogicType logicType) {
+        VerifyTools.requireNotBlank(rules, "rules");
+        return MATCHER_PARSER.parseMatchers(rules, logicType);
     }
 
     /**
-     * 解析StringMatcher规则列表<br>
-     * 如: parseMatchers(pattern, Logic.OR, "ant", ',', '\n'); // 默认以ant规则匹配<br>
-     * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * eq:或equals:开头的解析为EqualsStringMatcher<br>
-     * ctn:或contains:开头的解析为ContainsStringMatcher<br>
-     * srt:或starts:开头的解析为StartsStringMatcher<br>
-     * end:或ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 使用defaultMode指定的匹配方式<br>
+     * 解析StringMatcher规则列表
      *
-     * @param patterns 匹配规则列表
+     * @param rules 匹配规则列表
      * @param defaultMode 默认匹配方式
      * @param chars 分隔符
      * @return StringMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see Parsers#parseMatchers(String, LogicType, String, char...)
+     * @see Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static StringMatcher parseMatchers(String patterns, LogicType logicType, String defaultMode, char... chars) {
-        VerifyTools.requireNotBlank(patterns, "patterns");
-        if (chars == null || chars.length == 0) {
-            chars = new char[] {',', '\n'};
+    public static StringMatcher parseMatchers(String rules, LogicType logicType, String defaultMode, char... chars) {
+        return MATCHER_PARSER.parseMatchers(rules, logicType, defaultMode, chars);
+    }
+
+    /**
+     * 解析StringMatcher规则列表
+     *
+     * @param rules 匹配规则列表
+     * @param defaultMode 默认匹配方式
+     * @return StringMatcher
+     * @see Parsers#parseMatchers(List, LogicType, String)
+     * @see Parsers#parseMatcher(String)
+     * @since 5.4.10
+     */
+    public static StringMatcher parseMatchers(List<String> rules, StringMatcher.LogicType logicType,
+            String defaultMode) {
+        return MATCHER_PARSER.parseMatchers(rules, logicType, defaultMode);
+    }
+
+    /**
+     * StringMatcher规则解析类
+     *
+     * @author zhaohuihua
+     * @version 20220206
+     */
+    public static class Parsers {
+
+        /** 按匹配模式解析的规则解析处理类 **/
+        private final Map<String, List<ParserByMode>> modeParsers = new HashMap<>();
+        /** 基础的规则解析处理类 **/
+        private final List<ParserByBase> baseParsers = new ArrayList<>();
+
+        public Parsers() {
+            this(true);
+        }
+
+        public Parsers(boolean addDefaultParsers) {
+            if (addDefaultParsers) {
+                addParser(new SimpleParser());
+            }
+        }
+
+        /** 增加规则解析处理类 **/
+        public void addParser(ParserByBase parser) {
+            this.baseParsers.add(parser);
+            if (parser instanceof RecursiveParser) {
+                ((RecursiveParser) parser).parsers = this;
+            }
+        }
+
+        /** 增加规则解析处理类 **/
+        public void addParser(ParserByMode parser) {
+            String[] supports = parser.supportModes();
+            if (supports == null || supports.length == 0) {
+                return;
+            }
+            if (parser instanceof RecursiveParser) {
+                ((RecursiveParser) parser).parsers = this;
+            }
+            for (String support : supports) {
+                if (this.modeParsers.containsKey(support)) {
+                    this.modeParsers.get(support).add(parser);
+                } else {
+                    List<ParserByMode> parsers = new ArrayList<>();
+                    parsers.add(parser);
+                    this.modeParsers.put(support, parsers);
+                }
+            }
         }
-        String[] array = StringTools.split(patterns, chars);
-        List<StringMatcher> matchers = new ArrayList<>();
-        for (String pattern : array) {
-            if (VerifyTools.isNotBlank(pattern)) {
-                matchers.add(parseMatcher(pattern, defaultMode));
+
+        /**
+         * 解析StringMatcher规则<br>
+         * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
+         * ant:开头的解析为AntStringMatcher<br>
+         * eq:或equals:开头的解析为EqualsStringMatcher<br>
+         * ctn:或contains:开头的解析为ContainsStringMatcher<br>
+         * srt:或starts:开头的解析为StartsStringMatcher<br>
+         * end:或ends:开头的解析为EndsStringMatcher<br>
+         * 其余的解析为EqualsStringMatcher<br>
+         *
+         * @param rule 匹配规则
+         * @return StringMatcher
+         */
+        public StringMatcher parseMatcher(String rule) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseMatcher(rule, "equals");
+        }
+
+        /**
+         * 解析StringMatcher规则
+         *
+         * @param rule 匹配规则
+         * @param strict 是否使用严格格式
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatcher(String rule, boolean strict) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseMatcher(rule, strict ? "equals" : "contains");
+        }
+
+        /**
+         * 解析StringMatcher规则
+         *
+         * @param rule 匹配规则
+         * @param defaultMode 默认匹配方式
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatcher(String rule, String defaultMode) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseMatcherWithExcludeParser(rule, defaultMode, null);
+        }
+
+        protected StringMatcher parseMatcherWithExcludeParser(String rule, String defaultMode, Object excludeParser) {
+            StringMatcher matcher = doParseMatcherWithExcludeParser(rule, defaultMode, excludeParser);
+            return matcher != null ? matcher : new FixedStringMatcher(false);
+        }
+
+        private StringMatcher doParseMatcherWithExcludeParser(String rule, String defaultMode, Object excludeParser) {
+            // 先遍历判断解析器是否支持
+            for (ParserByBase parser : this.baseParsers) {
+                if (parser != null && parser != excludeParser && parser.supports(rule)) {
+                    return parser.parse(rule, defaultMode);
+                }
+            }
+
+            // 再按mode遍历解析器
+            RuleInfo ruleInfo = parseRuleInfo(rule, defaultMode);
+            String realRule = ruleInfo.rule;
+            String realMode = ruleInfo.mode;
+            Matches matchType = ruleInfo.matchType;
+            if (realMode != null && modeParsers.containsKey(realMode)) {
+                List<ParserByMode> parsers = modeParsers.get(realMode);
+                // 从后往前匹配, 同一类型下, 后加入的优先级最高
+                for (int i = parsers.size() - 1; i >= 0; i--) {
+                    ParserByMode parser = parsers.get(i);
+                    if (parser != null && parser != excludeParser) {
+                        StringMatcher matcher = parser.parse(realRule, realMode, defaultMode, matchType);
+                        if (matcher != null) {
+                            return matcher;
+                        }
+                    }
+                }
+            }
+
+            // 未找到解析器, 按EqualsStringMatcher处理
+            return new EqualsStringMatcher(rule);
+        }
+
+        private RuleInfo parseRuleInfo(String rule, String defaultMode) {
+            StringMatcher.Matches matchType = StringMatcher.Matches.Positive;
+            StringAccess sa = new StringAccess(rule);
+            String realMode = sa.skipWhitespace().readAscii();
+            if (realMode == null) {
+                return new RuleInfo(rule, defaultMode, matchType);
+            }
+            sa.skipWhitespace();
+            if (!sa.isReadable()) {
+                // 只有一个单词就到结尾了
+                if (modeParsers.containsKey(realMode)) {
+                    // 按无参数规则解析
+                    return new RuleInfo(null, realMode, matchType);
+                } else {
+                    // realMode作为参数解析
+                    return new RuleInfo(rule, defaultMode, matchType);
+                }
+            }
+            char next = sa.readChar();
+            if (next == '!') {
+                matchType = StringMatcher.Matches.Negative;
+                next = sa.skipWhitespace().readChar();
+            }
+            if (next != ':') {
+                return new RuleInfo(rule, defaultMode, matchType);
+            }
+            sa.skipWhitespace();
+            String realRule = sa.readToEnd();
+            return new RuleInfo(realRule, realMode, matchType);
+        }
+
+        /**
+         * 解析StringMatcher规则列表, 以逗号或换行符分隔
+         *
+         * @param rules 匹配规则列表
+         * @param logicType 多个匹配规则使用and还是or关联
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatchers(String rules, StringMatcher.LogicType logicType) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            return parseMatchers(rules, logicType, "equals", ',', '\n');
+        }
+
+        /**
+         * 解析StringMatcher规则列表<br>
+         * 如: parseMatchers(rule, Logic.OR, "ant", '\n'); // 默认以ant规则匹配
+         *
+         * @param rules 匹配规则列表
+         * @param defaultMode 默认匹配方式
+         * @param chars 分隔符
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatchers(String rules, StringMatcher.LogicType logicType, String defaultMode,
+                char... chars) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            if (chars == null || chars.length == 0) {
+                chars = new char[] {'\n'};
+            }
+            List<String> list = StringTools.splits(rules, chars);
+            return parseMatchers(list, logicType, defaultMode);
+        }
+
+        /**
+         * 解析StringMatcher规则列表<br>
+         * 如: parseMatchers(rule, Logic.OR, "ant", '\n'); // 默认以ant规则匹配
+         *
+         * @param rules 匹配规则列表
+         * @param defaultMode 默认匹配方式
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatchers(List<String> rules, StringMatcher.LogicType logicType,
+                String defaultMode) {
+            List<StringMatcher> matchers = new ArrayList<>();
+            List<String> cleared = RuleTools.clearCommentLines(rules);
+            for (String rule : cleared) {
+                StringMatcher matcher = parseMatcher(rule, defaultMode);
+                if (matcher != null) {
+                    matchers.add(matcher);
+                }
             }
+            if (matchers.size() == 0) {
+                return null;
+            } else if (matchers.size() == 1) {
+                return matchers.get(0);
+            } else {
+                return new WrapStringMatcher(logicType, matchers);
+            }
+        }
+    }
+
+    public static class FixedStringMatcher implements StringMatcher {
+
+        private final boolean result;
+
+        public FixedStringMatcher(boolean result) {
+            this.result = result;
+        }
+
+        @Override
+        public boolean matches(String source) {
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return "Always" + (result ? "True" : "False");
         }
-        if (matchers.size() == 0) {
-            return null;
-        } else if (matchers.size() == 1) {
-            return matchers.get(0);
-        } else {
-            return new WrapStringMatcher(logicType, ConvertTools.toArray(matchers, StringMatcher.class));
+    }
+
+    public static abstract class RecursiveParser {
+        private Parsers parsers;
+
+        public StringMatcher recursiveParse(String rule, String defaultMode) {
+            return parsers.parseMatcherWithExcludeParser(rule, defaultMode, this);
+        }
+
+        public List<StringMatcher> recursiveParse(List<String> rules, String defaultMode) {
+            List<StringMatcher> matchers = new ArrayList<>();
+            for (String item : rules) {
+                StringMatcher matcher = recursiveParse(item, defaultMode);
+                if (matcher != null) {
+                    matchers.add(matcher);
+                }
+            }
+            return matchers;
         }
     }
 
     /**
-     * 基础字符串匹配规则解析器
+     * 字符串匹配规则基础解析器
      *
      * @author zhaohuihua
      * @version 20220120
      */
-    private static class SimpleStringMatcherParser implements StringMatcher.Parser {
+    private static class SimpleParser implements ParserByMode {
 
-        public String[] supportTypes() {
+        @Override
+        public String[] supportModes() {
             List<String> types = new ArrayList<>();
             types.add("regexp");
             types.add("ant");
@@ -324,23 +513,34 @@ public class WrapStringMatcher implements StringMatcher {
         }
 
         @Override
-        public StringMatcher parse(String pattern, String type, StringMatcher.Matches matchType) {
-            VerifyTools.requireNotBlank(pattern, "pattern");
-            if ("ant".equals(type)) {
-                return new AntStringMatcher(pattern, true, matchType);
-            } else if ("rgp".equals(type) || "regexp".equals(type)) {
-                return new RegexpStringMatcher(pattern, matchType);
-            } else if ("eq".equals(type) || "equals".equals(type)) {
-                return new EqualsStringMatcher(pattern, matchType);
-            } else if ("ctn".equals(type) || "contains".equals(type)) {
-                return new ContainsStringMatcher(pattern, matchType);
-            } else if ("srt".equals(type) || "starts".equals(type)) {
-                return new StartsStringMatcher(pattern, matchType);
-            } else if ("end".equals(type) || "ends".equals(type)) {
-                return new EndsStringMatcher(pattern, matchType);
+        public StringMatcher parse(String rule, String mode, String defaultMode, Matches matchType) {
+            if ("ant".equals(mode)) {
+                return new AntStringMatcher(rule, true, matchType);
+            } else if ("rgp".equals(mode) || "regexp".equals(mode)) {
+                return new RegexpStringMatcher(rule, matchType);
+            } else if ("eq".equals(mode) || "equals".equals(mode)) {
+                return new EqualsStringMatcher(rule, matchType);
+            } else if ("ctn".equals(mode) || "contains".equals(mode)) {
+                return new ContainsStringMatcher(rule, matchType);
+            } else if ("srt".equals(mode) || "starts".equals(mode)) {
+                return new StartsStringMatcher(rule, matchType);
+            } else if ("end".equals(mode) || "ends".equals(mode)) {
+                return new EndsStringMatcher(rule, matchType);
             } else {
                 return null;
             }
         }
     }
+
+    private static class RuleInfo {
+        private final String rule;
+        private final String mode;
+        private final Matches matchType;
+
+        public RuleInfo(String rule, String mode, Matches matchType) {
+            this.rule = rule;
+            this.mode = mode;
+            this.matchType = matchType;
+        }
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java
new file mode 100644
index 0000000..a98b467
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java
@@ -0,0 +1,450 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.parse.StringAccess;
+import com.gitee.qdbp.tools.utils.IndentTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * StringReplacer多规则的包装类
+ *
+ * @author zhaohuihua
+ * @version 20220206
+ * @since 5.4.10
+ */
+public class WrapStringReplacer implements StringReplacer {
+
+    private final List<StringReplacer> replacers = new ArrayList<>();
+
+    public WrapStringReplacer(List<StringReplacer> replacers) {
+        this.addReplacers(replacers);
+    }
+
+    public WrapStringReplacer(StringReplacer... replacers) {
+        this.addReplacers(replacers);
+    }
+
+    @Override
+    public String replace(String string) {
+        String target = string;
+        for (StringReplacer replacer : replacers) {
+            if (replacer != null) {
+                target = replacer.replace(target);
+            }
+        }
+        return target;
+    }
+
+    @Override
+    public String toString() {
+        if (this.replacers.isEmpty()) {
+            return "{null}";
+        }
+        StringBuilder buffer = new StringBuilder();
+        for (StringReplacer matcher : this.replacers) {
+            if (buffer.length() > 0) {
+                buffer.append('\n');
+            }
+            if (matcher instanceof WrapStringReplacer) {
+                String inner = IndentTools.space.tabAll(matcher.toString(), 1);
+                buffer.append("(\n").append(inner).append("\n)");
+            } else {
+                buffer.append(matcher.toString());
+            }
+        }
+        return buffer.toString();
+    }
+
+    /** 获取替换规则 **/
+    public List<StringReplacer> getReplacers() {
+        return replacers;
+    }
+
+    /** 设置替换规则 **/
+    public void setReplacers(List<StringReplacer> replacers) {
+        this.replacers.clear();
+        this.addReplacers(replacers);
+    }
+
+    /** 增加替换规则 **/
+    public void addReplacers(String... rules) {
+        if (rules != null && rules.length > 0) {
+            for (String rule : rules) {
+                this.replacers.add(parseReplacer(rule));
+            }
+        }
+    }
+
+    /** 增加替换规则 **/
+    public void addReplacers(StringReplacer... replacers) {
+        if (replacers != null && replacers.length > 0) {
+            Collections.addAll(this.replacers, replacers);
+        }
+    }
+
+    /** 增加替换规则 **/
+    public void addReplacers(List<StringReplacer> replacers) {
+        if (replacers != null && replacers.size() > 0) {
+            this.replacers.addAll(replacers);
+        }
+    }
+
+    private static final Parsers MATCHER_PARSER = new Parsers();
+
+    /**
+     * 解析文本替换规则
+     *
+     * @param rule 匹配规则
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacer(String rule) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return parseReplacer(rule, "regexp");
+    }
+
+    /**
+     * 解析文本替换规则
+     *
+     * @param rule 匹配规则
+     * @param defaultMode 默认替换方式
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacer(String, String)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacer(String rule, String defaultMode) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return MATCHER_PARSER.parseReplacer(rule, defaultMode);
+    }
+
+    /**
+     * 解析文本替换规则列表, 以逗号或换行符分隔
+     *
+     * @param rules 匹配规则列表
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacers(String)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacers(String rules) {
+        VerifyTools.requireNotBlank(rules, "rules");
+        return MATCHER_PARSER.parseReplacers(rules);
+    }
+
+    /**
+     * 解析文本替换规则列表
+     *
+     * @param rules 匹配规则列表
+     * @param defaultMode 默认替换方式
+     * @param chars 分隔符
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacers(String, String, char...)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacers(String rules, String defaultMode, char... chars) {
+        return MATCHER_PARSER.parseReplacers(rules, defaultMode, chars);
+    }
+
+    /**
+     * 解析文本替换规则列表
+     *
+     * @param rules 匹配规则列表
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacers(List)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacers(List<String> rules) {
+        return MATCHER_PARSER.parseReplacers(rules);
+    }
+
+    /**
+     * 解析文本替换规则列表
+     *
+     * @param rules 匹配规则列表
+     * @param defaultMode 默认替换方式
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacers(List, String)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacers(List<String> rules, String defaultMode) {
+        return MATCHER_PARSER.parseReplacers(rules, defaultMode);
+    }
+
+    /**
+     * StringReplacer规则解析类
+     *
+     * @author zhaohuihua
+     * @version 20220206
+     */
+    public static class Parsers {
+
+        /** 按mode解析的规则解析处理类 **/
+        private final Map<String, List<ParserByMode>> modeParsers = new HashMap<>();
+        /** 基础的规则解析处理类 **/
+        private final List<ParserByBase> baseParsers = new ArrayList<>();
+
+        public Parsers() {
+            this(true);
+        }
+
+        public Parsers(boolean addDefaultParsers) {
+            if (addDefaultParsers) {
+                addParser(new SimpleParser());
+            }
+        }
+
+        /** 增加规则解析处理类 **/
+        public void addParser(ParserByBase parser) {
+            this.baseParsers.add(parser);
+            if (parser instanceof RecursiveParser) {
+                ((RecursiveParser) parser).parsers = this;
+            }
+        }
+
+        /** 增加规则解析处理类 **/
+        public void addParser(ParserByMode parser) {
+            String[] supports = parser.supportModes();
+            if (supports == null || supports.length == 0) {
+                return;
+            }
+            if (parser instanceof RecursiveParser) {
+                ((RecursiveParser) parser).parsers = this;
+            }
+            for (String support : supports) {
+                if (this.modeParsers.containsKey(support)) {
+                    this.modeParsers.get(support).add(parser);
+                } else {
+                    List<ParserByMode> parsers = new ArrayList<>();
+                    parsers.add(parser);
+                    this.modeParsers.put(support, parsers);
+                }
+            }
+        }
+
+        /**
+         * 解析文本替换规则<br>
+         * // 年初现金替换为期初现金<br>
+         * 年([初末])现金 -- 期$1现金<br>
+         * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+         * (?<=所)支付的现金 -- 所支付的现金<br>
+         *
+         * @param rule 规则内容
+         * @return 规则对象
+         */
+        public StringReplacer parseReplacer(String rule) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseReplacer(rule, "regexp");
+        }
+
+        /**
+         * 解析文本替换规则
+         *
+         * @param rule 替换规则
+         * @param defaultMode 默认替换方式
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacer(String rule, String defaultMode) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseReplacerWithExcludeParser(rule, defaultMode, null);
+        }
+
+        protected StringReplacer parseReplacerWithExcludeParser(String rule, String defaultMode, Object excludeParser) {
+            StringReplacer Replacer = doParseReplacerWithExcludeParser(rule, defaultMode, excludeParser);
+            return Replacer != null ? Replacer : new NothingStringReplacer();
+        }
+
+        private StringReplacer doParseReplacerWithExcludeParser(String rule, String defaultMode, Object excludeParser) {
+            // 先遍历判断解析器是否支持
+            for (ParserByBase parser : this.baseParsers) {
+                if (parser != null && parser != excludeParser && parser.supports(rule)) {
+                    return parser.parse(rule, defaultMode);
+                }
+            }
+
+            // 再按mode遍历解析器
+            RuleInfo ruleInfo = parseRuleInfo(rule, defaultMode);
+            String realRule = ruleInfo.rule;
+            String realMode = ruleInfo.mode;
+            if (realMode != null && modeParsers.containsKey(realMode)) {
+                List<ParserByMode> parsers = modeParsers.get(realMode);
+                // 从后往前匹配, 同一类型下, 后加入的优先级最高
+                for (int i = parsers.size() - 1; i >= 0; i--) {
+                    ParserByMode parser = parsers.get(i);
+                    if (parser != null && parser != excludeParser) {
+                        StringReplacer Replacer = parser.parse(realRule, realMode, defaultMode);
+                        if (Replacer != null) {
+                            return Replacer;
+                        }
+                    }
+                }
+            }
+
+            // 未找到解析器, 按RegexpStringReplacer处理
+            return RegexpStringReplacer.parseReplacer(rule);
+        }
+
+        private RuleInfo parseRuleInfo(String rule, String defaultMode) {
+            StringAccess sa = new StringAccess(rule);
+            String realMode = sa.skipWhitespace().readAscii();
+            if (realMode == null) {
+                return new RuleInfo(rule, defaultMode);
+            }
+            sa.skipWhitespace();
+            if (!sa.isReadable()) {
+                // 只有一个单词就到结尾了
+                if (modeParsers.containsKey(realMode)) {
+                    // 按无参数规则解析
+                    return new RuleInfo(null, realMode);
+                } else {
+                    // realMode作为参数解析
+                    return new RuleInfo(rule, defaultMode);
+                }
+            }
+            char next = sa.readChar();
+            if (next != ':') {
+                return new RuleInfo(rule, defaultMode);
+            }
+            sa.skipWhitespace();
+            String realRule = sa.readToEnd();
+            return new RuleInfo(realRule, realMode);
+        }
+
+        /**
+         * 解析文本替换规则列表, 以逗号或换行符分隔
+         *
+         * @param rules 替换规则列表
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacers(String rules) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            return parseReplacers(rules, "regexp", '\n');
+        }
+
+        /**
+         * 解析文本替换规则列表
+         *
+         * @param rules 替换规则列表
+         * @param defaultMode 默认替换方式
+         * @param chars 分隔符
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacers(String rules, String defaultMode, char... chars) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            if (chars == null || chars.length == 0) {
+                chars = new char[] {'\n'};
+            }
+            List<String> list = StringTools.splits(rules, chars);
+            return parseReplacers(list, defaultMode);
+        }
+
+        /**
+         * 解析文本替换规则列表
+         *
+         * @param rules 替换规则列表
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacers(List<String> rules) {
+            return parseReplacers(rules, "regexp");
+        }
+
+        /**
+         * 解析文本替换规则列表
+         *
+         * @param rules 替换规则列表
+         * @param defaultMode 默认替换方式
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacers(List<String> rules, String defaultMode) {
+            List<StringReplacer> replacers = new ArrayList<>();
+            for (String rule : rules) {
+                if (VerifyTools.isNotBlank(rule)) {
+                    StringReplacer replacer = parseReplacer(rule, defaultMode);
+                    if (replacer != null) {
+                        replacers.add(replacer);
+                    }
+                }
+            }
+            if (replacers.size() == 0) {
+                return null;
+            } else if (replacers.size() == 1) {
+                return replacers.get(0);
+            } else {
+                return new WrapStringReplacer(replacers);
+            }
+        }
+    }
+
+    public static class NothingStringReplacer implements StringReplacer {
+
+        @Override
+        public String replace(String string) {
+            return string;
+        }
+
+        @Override
+        public String toString() {
+            return "Nothing";
+        }
+    }
+
+    public static abstract class RecursiveParser {
+        private Parsers parsers;
+
+        public StringReplacer recursiveParse(String rule, String defaultMode) {
+            return parsers.parseReplacerWithExcludeParser(rule, defaultMode, this);
+        }
+
+        public List<StringReplacer> recursiveParse(List<String> rules, String defaultMode) {
+            List<StringReplacer> Replacers = new ArrayList<>();
+            for (String item : rules) {
+                StringReplacer Replacer = recursiveParse(item, defaultMode);
+                if (Replacer != null) {
+                    Replacers.add(Replacer);
+                }
+            }
+            return Replacers;
+        }
+    }
+
+    /**
+     * 字符串替换规则基础解析器
+     *
+     * @author zhaohuihua
+     * @version 20220206
+     */
+    private static class SimpleParser implements ParserByMode {
+
+        @Override
+        public String[] supportModes() {
+            return new String[] {"rgp", "regexp"};
+        }
+
+        @Override
+        public StringReplacer parse(String rule, String mode, String defaultMode) {
+            if ("rgp".equals(mode) || "regexp".equals(mode)) {
+                return RegexpStringReplacer.parseReplacer(rule);
+            } else {
+                return null;
+            }
+        }
+    }
+
+    private static class RuleInfo {
+        private final String rule;
+        private final String mode;
+
+        public RuleInfo(String rule, String mode) {
+            this.rule = rule;
+            this.mode = mode;
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraBase.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraBase.java
new file mode 100644
index 0000000..2fdf49c
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraBase.java
@@ -0,0 +1,33 @@
+package com.gitee.qdbp.able.model.reusable;
+
+import java.util.Map;
+
+/**
+ * 附加数据
+ *
+ * @author zhaohuihua
+ * @version 20220130
+ */
+public interface ExtraBase {
+
+    /** 获取附加数据 **/
+    Map<String, Object> getExtra();
+
+    /** 设置附加数据 **/
+    void setExtra(Map<String, Object> extra);
+
+    /** 是否存在指定的附加数据 **/
+    boolean containsExtra(String key);
+
+    /** 获取指定的附加数据 **/
+    Object getExtra(String key);
+
+    /** 获取指定类型的附加数据 **/
+    <T> T getExtra(String key, Class<T> clazz);
+
+    /** 设置指定的附加数据 **/
+    void putExtra(String key, Object value);
+
+    /** 设置附加信息 **/
+    void putExtra(Map<String, ?> extra);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraData.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraData.java
index c671b03..144c235 100644
--- a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraData.java
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraData.java
@@ -11,7 +11,7 @@ import com.gitee.qdbp.tools.utils.MapTools;
  * @author zhaohuihua
  * @version 151020
  */
-public class ExtraData implements Serializable {
+public class ExtraData implements Serializable, ExtraBase {
 
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
@@ -19,32 +19,45 @@ public class ExtraData implements Serializable {
     /** 附加数据 **/
     private Map<String, Object> extra;
 
+    public ExtraData() {
+    }
+
+    public ExtraData(Map<String, Object> extra) {
+        this.extra = extra;
+    }
+
     /** 获取附加数据 **/
+    @Override
     public Map<String, Object> getExtra() {
         return extra;
     }
 
     /** 设置附加数据 **/
+    @Override
     public void setExtra(Map<String, Object> extra) {
         this.extra = extra;
     }
 
     /** 是否存在指定的附加数据 **/
+    @Override
     public boolean containsExtra(String key) {
         return this.extra != null && this.extra.containsKey(key);
     }
 
     /** 获取指定的附加数据 **/
+    @Override
     public Object getExtra(String key) {
         return this.extra == null ? null : this.extra.get(key);
     }
 
     /** 获取指定类型的附加数据 **/
+    @Override
     public <T> T getExtra(String key, Class<T> clazz) {
         return this.extra == null ? null : MapTools.getValue(this.extra, key, clazz);
     }
 
     /** 设置指定的附加数据 **/
+    @Override
     public void putExtra(String key, Object value) {
         if (this.extra == null) {
             this.extra = new HashMap<>();
@@ -53,7 +66,11 @@ public class ExtraData implements Serializable {
     }
 
     /** 设置附加信息 **/
+    @Override
     public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
         if (this.extra == null) {
             this.extra = new HashMap<>();
         }
diff --git a/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java b/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java
index 3b2f21e..9a5f4a4 100644
--- a/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java
+++ b/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java
@@ -2,14 +2,10 @@ package com.gitee.qdbp.eachfile.runner;
 
 import java.io.File;
 import java.io.FileFilter;
-import java.io.UnsupportedEncodingException;
-import java.net.URL;
-import java.net.URLDecoder;
 import com.gitee.qdbp.able.debug.ConsoleDebugger;
 import com.gitee.qdbp.able.debug.Debugger;
 import com.gitee.qdbp.eachfile.process.FileProcessor;
 import com.gitee.qdbp.tools.files.PathTools;
-import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.wait.SingleLock;
 
 /**
@@ -25,6 +21,7 @@ public class FileEachRunner extends Thread {
     private final FileFilter fileFilter;
     private final Debugger debugger;
     private SingleLock lock;
+    private boolean alive = true;
 
     public FileEachRunner(String root, FileProcessor processor) {
         this(root, null, processor, null);
@@ -65,9 +62,11 @@ public class FileEachRunner extends Thread {
         } finally {
             try {
                 processor.destroy(root);
+                debugger.debug("Success to each folder, the task has been finished!");
             } catch (Exception e) {
                 debugger.debug("Failed to call destroy()", e);
             } finally {
+                this.alive = false;
                 if (lock != null) {
                     lock.signal();
                 }
@@ -82,7 +81,24 @@ public class FileEachRunner extends Thread {
         }
     }
 
+    /** 取消任务 **/
+    public void cancel(long delay) {
+        // 标记为结束
+        this.alive = false;
+        // 延时强杀
+        new StopTask(this, delay, debugger).start();
+    }
+
+    /** 任务是否仍在运行中 **/
+    public boolean isRunning() {
+        return this.alive;
+    }
+
     protected void each(File folder) {
+        if (!alive) {
+            debugger.debug("The task has been canceled!");
+            return;
+        }
         File[] files;
         if (fileFilter == null) {
             files = folder.listFiles();
@@ -93,6 +109,10 @@ public class FileEachRunner extends Thread {
             return;
         }
         for (File file : files) {
+            if (!alive) {
+                debugger.debug("The task has been canceled!");
+                return;
+            }
             if (file.isFile()) {
                 processor.fileProcess(root, file);
             } else if (file.isDirectory()) {
@@ -106,7 +126,7 @@ public class FileEachRunner extends Thread {
 
     protected boolean checkRoot(File root) {
         if (root.isFile()) {
-            debugger.debug("根路径必须是文件夹");
+            debugger.debug("The root path must be a folder");
             return false;
         } else {
             return true;
@@ -129,45 +149,28 @@ public class FileEachRunner extends Thread {
         return debugger;
     }
 
-    /**
-     * 获取指定类所在的classpath
-     * 
-     * @param clazz 指定类
-     * @return classpath
-     */
-    public static String getClasspath(Class<?> clazz) {
-        // 获取与JAR包自身所在的文件夹
-        // project-path/target/classes/
-        URL url = clazz.getResource("/");
-        if (url != null) {
-            return PathTools.toUriPath(url);
-        } else { // jar包中获取为空
-            // project-path/target/classes/com/gitee/qdbp/.../
-            // jar:file:/F:/project-path/xxx.jar!/com/gitee/qdbp/.../
-            url = clazz.getResource("");
-            assert url != null;
-            String p = PathTools.toUriPath(url);
-            p = StringTools.removePrefix(p, "jar://");
-            p = StringTools.removePrefix(p, "jar:");
-            p = StringTools.removePrefix(p, "file:");
-            int index = p.indexOf('!');
-            if (index > 0) {
-                p = PathTools.removeFileName(p.substring(0, index));
-            }
-            if ((p.length() > 2) && (p.charAt(2) == ':')) {
-                // "/c:/foo/" --> "c:/foo/"
-                p = p.substring(1);
+    private static class StopTask extends Thread {
+        private final Thread thread;
+        private final long delay;
+        private final Debugger debugger;
+
+        private StopTask(Thread thread, long delay, Debugger debugger) {
+            this.thread = thread;
+            this.delay = delay;
+            this.debugger = debugger;
+        }
+
+        @Override
+        @SuppressWarnings("deprecation")
+        public void run() {
+            try {
+                Thread.sleep(delay);
+            } catch (InterruptedException ignore) {
             }
-            if (!new File(p).exists() && p.contains("%")) {
-                try {
-                    p = URLDecoder.decode(p, "UTF-8");
-                    if (!new File(p).exists()) {
-                        p = URLDecoder.decode(p, "GBK");
-                    }
-                } catch (UnsupportedEncodingException ignore) {
-                }
+            if (thread.isAlive()) {
+                thread.stop();
+                debugger.debug("The task {} has been killed!", thread.getName());
             }
-            return p;
         }
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
index f5bbe04..ed6b34d 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
@@ -8,12 +8,14 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
 import java.net.JarURLConnection;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
+import java.net.URLDecoder;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -876,6 +878,48 @@ public abstract class PathTools {
         return toUriPath(root);
     }
 
+    /**
+     * 获取指定类所在的classpath
+     *
+     * @param clazz 指定类
+     * @return classpath
+     */
+    public static String getClassPath(Class<?> clazz) {
+        // 获取与JAR包自身所在的文件夹
+        // project-path/target/classes/
+        URL url = clazz.getResource("/");
+        if (url != null) {
+            return PathTools.toUriPath(url);
+        } else { // jar包中获取为空
+            // project-path/target/classes/com/gitee/qdbp/.../
+            // jar:file:/F:/project-path/xxx.jar!/com/gitee/qdbp/.../
+            url = clazz.getResource("");
+            assert url != null;
+            String p = PathTools.toUriPath(url);
+            p = StringTools.removePrefix(p, "jar://");
+            p = StringTools.removePrefix(p, "jar:");
+            p = StringTools.removePrefix(p, "file:");
+            int index = p.indexOf('!');
+            if (index > 0) {
+                p = PathTools.removeFileName(p.substring(0, index));
+            }
+            if ((p.length() > 2) && (p.charAt(2) == ':')) {
+                // "/c:/foo/" --> "c:/foo/"
+                p = p.substring(1);
+            }
+            if (!new File(p).exists() && p.contains("%")) {
+                try {
+                    p = URLDecoder.decode(p, "UTF-8");
+                    if (!new File(p).exists()) {
+                        p = URLDecoder.decode(p, "GBK");
+                    }
+                } catch (UnsupportedEncodingException ignore) {
+                }
+            }
+            return p;
+        }
+    }
+
     /**
      * 计算输出文件夹路径<br>
      * 如果root不是文件, 则一律返回classpath对应文件夹
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index a1bff9d..e4112e8 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -1150,6 +1150,47 @@ public class ConvertTools {
         return compressed;
     }
 
+    /**
+     * 生成迪卡尔积
+     *
+     * @param sources 原二维数组
+     * @return 迪卡尔积数组
+     */
+    // http://blog.sina.com.cn/s/blog_a6ae0b3b0101624x.html
+    public static <T> List<List<T>> toCartesianList(List<List<T>> sources) {
+        // 计算出笛卡尔积行数
+        int rows = sources.size() > 0 ? 1 : 0;
+        for (Collection<T> data : sources) {
+            rows *= data.size();
+        }
+
+        // 笛卡尔积索引记录
+        int[] record = new int[sources.size()];
+
+        List<List<T>> results = new ArrayList<>();
+        // 产生笛卡尔积
+        for (int i = 0; i < rows; i++) {
+            List<T> row = new ArrayList<>();
+            // 生成笛卡尔积的每组数据
+            for (int index = 0; index < record.length; index++) {
+                row.add(sources.get(index).get(record[index]));
+            }
+
+            results.add(row);
+            toCartesianList(sources, record, sources.size() - 1);
+        }
+        return results;
+    }
+
+    private static <T> void toCartesianList(List<List<T>> source, int[] record, int level) {
+        record[level] = record[level] + 1;
+
+        if (record[level] >= source.get(level).size() && level > 0) {
+            record[level] = 0;
+            toCartesianList(source, record, level - 1);
+        }
+    }
+
     /**
      * 清除Map中的空值
      * 
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java
index 6106a10..72d5402 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java
@@ -186,6 +186,16 @@ public class IndentTools {
         return size;
     }
 
+    /**
+     * 统计开头的缩进数
+     *
+     * @param string 字符串
+     * @return 缩进数量, -1表示未找到缩进
+     */
+    public static int countLeadingIndentSize(CharSequence string) {
+        return calcSpacesToTabSize(string);
+    }
+
     /**
      * 统计最小的缩进数
      * 
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
new file mode 100644
index 0000000..ca1eec7
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
@@ -0,0 +1,216 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import com.gitee.qdbp.able.beans.KeyString;
+
+/**
+ * 规则配置解析工具类
+ *
+ * @author zhaohuihua
+ * @version 20220129
+ */
+public class RuleTools {
+
+    private RuleTools() {
+    }
+
+    /**
+     * 按换行拆分字符串, 并清除注释行
+     *
+     * @param string 原字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<String> splitRuleLines(String string) {
+        if (string == null) {
+            return null;
+        }
+        List<String> list = StringTools.splits(string, false,'\n');
+        return clearCommentLines(list);
+    }
+
+    /**
+     * 清除注释行
+     *
+     * @param strings 原字符串数组
+     * @return 清除后的字符串数组
+     */
+    public static List<String> clearCommentLines(List<String> strings) {
+        if (strings == null) {
+            return null;
+        }
+        List<String> list = new ArrayList<>();
+        for (String string : strings) {
+            if (string == null || string.length() == 0) {
+                continue;
+            }
+            String trimmed = string.trim();
+            if (trimmed.length() == 0 || trimmed.charAt(0) == '#' || trimmed.startsWith("//")) {
+                // 遇到注释行, 跳过
+                continue;
+            }
+            list.add(string);
+        }
+        return list;
+    }
+
+    /**
+     * 按指定的分隔符将字符串拆分为片段<br>
+     * 例如: 将一大段带换行的文本按######拆分成多个小段<pre>
+     * |######
+     * |aaa
+     * |bbb
+     * |######
+     * |ccc
+     * |######
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<String> splitRuleFragments(String rules) {
+        return splitRuleFragments(rules, '#');
+    }
+
+    /**
+     * 按指定的分隔符将字符串拆分为片段<br>
+     * 例如: 将一大段带换行的文本按######拆分成多个小段<pre>
+     * |######
+     * |aaa
+     * |bbb
+     * |######
+     * |ccc
+     * |######
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @param delimiter 分隔符
+     * @return 拆分后的字符串数组
+     */
+    public static List<String> splitRuleFragments(String rules, char delimiter) {
+        List<String> strings = StringTools.splits(rules, false, '\n');
+        List<String> results = new ArrayList<>();
+        List<String> buffer = new ArrayList<>();
+        for (int i = 0, z = strings.size() - 1; i <= z; i++) {
+            String string = strings.get(i);
+            if (isRuleLineDelimiter(string, delimiter)) {
+                // 遇到分隔符
+                results.add(ConvertTools.joinToString(buffer, '\n'));
+                buffer.clear();
+            } else {
+                buffer.add(string);
+            }
+        }
+        if (!buffer.isEmpty()) {
+            results.add(ConvertTools.joinToString(buffer, '\n'));
+        }
+        return results;
+    }
+
+    private static boolean isRuleLineDelimiter(String string, char delimiter) {
+        String t = string.trim();
+        int len = t.length();
+        int min = 6;
+        if (len > min && t.charAt(0) == delimiter && t.charAt(1) == delimiter && t.charAt(len - 1) == delimiter) {
+            if (StringTools.countCharacter(t, delimiter) >= min) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 按指定的分隔符将字符串拆分为键值对, 且按缩进合并行 (缩进的行与其上一行合并)<br>
+     * 如下文本, 返回三项, 分别为 [ name, class, params ], 其中 params = Value1\nValue2\nValue3
+     * <pre>
+     * |name = 披露渠道检查
+     * |class = StringMatcherExecutor
+     * |params =
+     * |    Value1
+     * |    Value2
+     * |    Value3
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<KeyString> splitRuleKeyValues(String rules) {
+        List<String> strings = StringTools.splits(rules, false, '\n');
+        return splitRuleKeyValues(strings);
+    }
+
+    /**
+     * 按指定的分隔符将字符串拆分为键值对, 且按缩进合并行 (缩进的行与其上一行合并)<br>
+     * 如下文本, 返回三项, 分别为 [ name, class, params ], 其中 params = Value1\nValue2\nValue3
+     * <pre>
+     * |name = 披露渠道检查
+     * |class = StringMatcherExecutor
+     * |params =
+     * |    Value1
+     * |    Value2
+     * |    Value3
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<KeyString> splitRuleKeyValues(List<String> rules) {
+        List<KeyString> results = new ArrayList<>();
+        for (int i = 0, z = rules.size(); i < z; i++) {
+            String string = rules.get(i).trim();
+            if (string.length() == 0) {
+                continue;
+            }
+            if (string.charAt(0) == '#' || string.startsWith("//")) {
+                // 遇到注释行, 跳过
+                continue;
+            }
+            int index = string.indexOf('=');
+            String key = null;
+            List<String> values = new ArrayList<>();
+            if (index >= 0) {
+                if (index > 0) {
+                    key = StringTools.trimRight(string.substring(0, index));
+                }
+                String value = StringTools.trimLeft(string.substring(index + 1));
+                if (value.length() > 0) {
+                    values.add(value);
+                }
+            }
+            i += collectNextIndentLines(rules, i + 1, values);
+            results.add(new KeyString(key, ConvertTools.joinToString(values, '\n')));
+        }
+        return results;
+    }
+
+    public static int collectNextIndentLines(List<String> rules, int start, List<String> collectors) {
+        int count = 0;
+        for (int i = start, z = rules.size(); i < z; i++, count++) {
+            String next = rules.get(i);
+            String trimmed = next.trim();
+            if (trimmed.length() == 0) {
+                continue;
+            }
+            if (trimmed.charAt(0) == '#' || trimmed.startsWith("//")) {
+                // 遇到注释行, 跳过
+                continue;
+            }
+            // 不是缩进, 但是以右括号开头, 也与上一行合并
+            char first = next.charAt(0);
+            if (first == '}' || first == ']' || first == ')') {
+                collectors.add(next);
+                // 遇到未缩进的行, 说明已经结束了
+                break;
+            }
+            // 计算缩进量
+            int indent = IndentTools.countLeadingIndentSize(next);
+            if (indent > 0) {
+                collectors.add(IndentTools.tabs.tabAll(next, -1));
+            } else {
+                // 遇到未缩进的行, 说明已经结束了
+                break;
+            }
+        }
+        return count;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index 722d443..fd1ff48 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -718,7 +718,7 @@ public class StringTools {
      * @return 省略中间部分的字符
      */
     public static String ellipsis(String string, int length) {
-        if (string == null || string.length() <= length || length == 0) {
+        if (string == null || string.length() <= length || length <= 0) {
             return string;
         }
         String flag = " ... ";
diff --git a/tools/src/test/java/com/gitee/qdbp/matches/StringMatcherParserTest.java b/tools/src/test/java/com/gitee/qdbp/matches/StringMatcherParserTest.java
new file mode 100644
index 0000000..e4eb1a2
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/matches/StringMatcherParserTest.java
@@ -0,0 +1,25 @@
+package com.gitee.qdbp.matches;
+
+import org.testng.annotations.Test;
+import com.gitee.qdbp.able.matches.StringMatcher;
+import com.gitee.qdbp.able.matches.WrapStringMatcher;
+
+/**
+ * StringMatcherParserTest
+ *
+ * @author zhaohuihua
+ * @version 20220206
+ */
+@Test
+public class StringMatcherParserTest {
+
+    @Test
+    public void test() {
+        String rules = "中国货币网.{0,5}上海清算所网站(向.{0,10})?披露\n" +
+                "上海清算所(网站)?.{0,5}中国货币网(向.{0,10})?披露\n" +
+                "上海清算所(网站)?(向.{0,5})?披露\n" +
+                "中国货币网(向.{0,10})?披露";
+        StringMatcher matcher = WrapStringMatcher.parseMatchers(rules, StringMatcher.LogicType.OR, "regexp");
+        System.out.println(matcher.toString());
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/matches/StringReplacerParserTest.java b/tools/src/test/java/com/gitee/qdbp/matches/StringReplacerParserTest.java
new file mode 100644
index 0000000..67ab00a
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/matches/StringReplacerParserTest.java
@@ -0,0 +1,56 @@
+package com.gitee.qdbp.matches;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.matches.StringReplacer;
+import com.gitee.qdbp.able.matches.WrapStringReplacer;
+import com.gitee.qdbp.tools.files.PathTools;
+
+/**
+ * StringMatcherParserTest
+ *
+ * @author zhaohuihua
+ * @version 20220206
+ */
+@Test
+public class StringReplacerParserTest {
+
+    private static final Logger log = LoggerFactory.getLogger(StringReplacerParserTest.class);
+
+    @Test
+    public void test() {
+        List<KeyString> sources = new ArrayList<>();
+        sources.add(new KeyString("负债和所有者权益", "负债及所有者权益"));
+        sources.add(new KeyString("分配股利、利润或偿付利息支付的现金", "分配股利、利润或偿付利息所支付的现金"));
+        sources.add(new KeyString("年初现金及现金等价物余额", "期初现金及现金等价物余额"));
+        sources.add(new KeyString("年末现金及现金等价物余额", "期末现金及现金等价物余额"));
+        sources.add(new KeyString("盈余公积金", "盈余公积"));
+        sources.add(new KeyString("资本公积", "资本公积"));
+        StringReplacer replacer = WrapStringReplacer.parseReplacers(getReplaceRules());
+        System.out.println(replacer.toString());
+        System.out.println();
+        for (KeyString source : sources) {
+            String target = replacer.replace(source.getKey());
+            System.out.println(source + "\t\t" + target);
+            Assert.assertEquals(target, source.getValue());
+        }
+    }
+
+    private static String getReplaceRules() {
+        String filePath = "replace-rules.txt";
+        URL url = PathTools.findClassResource(StringReplacerParserTest.class, filePath);
+        try {
+            return PathTools.downloadString(url);
+        } catch (IOException e) {
+            log.error("FailedToReadTitleClearSymbols", e);
+            return null;
+        }
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/matches/replace-rules.txt b/tools/src/test/java/com/gitee/qdbp/matches/replace-rules.txt
new file mode 100644
index 0000000..04e7056
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/matches/replace-rules.txt
@@ -0,0 +1,7 @@
+负债和所有者权益 -- 负债及所有者权益
+// 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字
+(?<!所)支付的现金 -- 所支付的现金
+// 年初现金替换为期初现金
+年([初末])现金 -- 期$1现金
+// 盈余公积金替换为盈余公积
+(盈余|资本)公积金 -- $1公积
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/CartesianTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/CartesianTest.java
new file mode 100644
index 0000000..41f6e4a
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/CartesianTest.java
@@ -0,0 +1,24 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 迪卡尔积测试类
+ *
+ * @author zhaohuihua
+ * @version 20220206
+ */
+public class CartesianTest {
+
+    public static void main(String[] args) {
+        List<List<String>> sources = new ArrayList<>();
+        sources.add(StringTools.splits("A1|A2|A3"));
+        sources.add(StringTools.splits("B1|B2"));
+        sources.add(StringTools.splits("C1|C2|C3|C4"));
+        List<List<String>> targets = ConvertTools.toCartesianList(sources);
+        for (List<String> list : targets) {
+            System.out.println(ConvertTools.joinToString(list));
+        }
+    }
+}
-- 
Gitee


From 6951f2afde24ce7365851ac0661dd76dca27c229 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Fri, 11 Feb 2022 22:22:04 +0800
Subject: [PATCH 023/160] 5.4.11

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 240e26c..f59f250 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.8</version>
+	<version>5.4.11</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index d84a3c6..66f7f76 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.8</version>
+	<version>5.4.11</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
@@ -140,7 +140,7 @@
 			<version>1.2.3</version>
 			<scope>test</scope>
 		</dependency>
-		
+
 		<dependency>
 			<groupId>org.testng</groupId>
 			<artifactId>testng</artifactId>
-- 
Gitee


From c194a04956a36f11f8a72cdb4e6128077e7edfe7 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 23 Feb 2022 23:41:01 +0800
Subject: [PATCH 024/160] =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=8F=90?=
 =?UTF-8?q?=E5=8F=96=E5=B7=A5=E5=85=B7=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/able/matches/GroupExtractors.java    |  18 +-
 .../able/matches/RegexpGroupExtractor.java    | 272 ++++++++++++-----
 .../able/matches/RegexpStringExtractor.java   | 285 ++++++++++++------
 .../qdbp/able/matches/StringExtractors.java   |  18 +-
 .../com/gitee/qdbp/tools/utils/RuleTools.java |   4 +-
 5 files changed, 426 insertions(+), 171 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
index 78c9e46..b550096 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
@@ -15,14 +15,21 @@ public class GroupExtractors implements GroupExtractor {
         this.extractors = extractors;
     }
 
+    public List<? extends GroupExtractor> getExtractors() {
+        return extractors;
+    }
+
     @Override
     public GroupSubString extractFirst(String string) {
         for (int i = 0, z = extractors.size(); i < z; i++) {
             GroupExtractor extractor = extractors.get(i);
             GroupSubString result = extractor.extractFirst(string);
             if (result != null) {
-                if (!result.containsExtra("i")) {
-                    result.putExtra("i", i);
+                if (!result.containsExtra("index")) {
+                    result.putExtra("index", i);
+                }
+                if (!result.containsExtra("rule")) {
+                    result.putExtra("rule", extractor);
                 }
                 return result;
             }
@@ -36,8 +43,11 @@ public class GroupExtractors implements GroupExtractor {
             GroupExtractor extractor = extractors.get(i);
             GroupSubStrings result = extractor.extractAll(string);
             if (result != null) {
-                if (!result.containsExtra("i")) {
-                    result.putExtra("i", i);
+                if (!result.containsExtra("index")) {
+                    result.putExtra("index", i);
+                }
+                if (!result.containsExtra("rule")) {
+                    result.putExtra("rule", extractor);
                 }
                 return result;
             }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
index f434755..f24a992 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
@@ -2,6 +2,7 @@ package com.gitee.qdbp.able.matches;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
@@ -27,9 +28,17 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
 public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
 
     private static final long serialVersionUID = 2523069377411632793L;
-    private final Pattern pattern;
-    private final List<Integer> digitGroups;
-    private final List<String> namedGroups;
+    /** 提取规则 **/
+    private Pattern pattern;
+    /** 排除规则 **/
+    private StringMatcher exclude;
+    /** 提取组序号 **/
+    private List<Integer> digitGroups;
+    /** 提取组名称 **/
+    private List<String> namedGroups;
+
+    public RegexpGroupExtractor() {
+    }
 
     public RegexpGroupExtractor(String pattern, int... groups) {
         VerifyTools.requireNotBlank(pattern, "pattern");
@@ -86,16 +95,62 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
         this.namedGroups = namedGroups.isEmpty() ? null : namedGroups;
     }
 
+    /** 提取规则 **/
+    public Pattern getPattern() {
+        return pattern;
+    }
+
+    /** 提取规则 **/
+    public void setPattern(Pattern pattern) {
+        this.pattern = pattern;
+    }
+
+    /** 提取规则 **/
+    public void setPattern(String pattern) {
+        this.pattern = Pattern.compile(pattern);
+    }
+
+    /** 排除规则 **/
+    public StringMatcher getExclude() {
+        return exclude;
+    }
+
+    /** 排除规则 **/
+    public void setExclude(StringMatcher exclude) {
+        this.exclude = exclude;
+    }
+
+    /** 提取组序号 **/
+    public List<Integer> getDigitGroups() {
+        return digitGroups;
+    }
+
+    /** 提取组序号 **/
+    public void setDigitGroups(List<Integer> digitGroups) {
+        this.digitGroups = digitGroups;
+    }
+
+    /** 提取组名称 **/
+    public List<String> getNamedGroups() {
+        return namedGroups;
+    }
+
+    /** 提取组名称 **/
+    public void setNamedGroups(List<String> namedGroups) {
+        this.namedGroups = namedGroups;
+    }
+
     @Override
     public GroupSubString extractFirst(String string) {
         Matcher matcher = pattern.matcher(string);
-        if (!matcher.find()) {
-            return null;
-        } else {
-            GroupSubString result = newGroupSubString(matcher);
-            result.putExtra(this.getExtra());
-            return result;
+        if (matcher.find()) {
+            if (exclude == null || !exclude.matches(matcher.group())) {
+                GroupSubString result = newGroupSubString(matcher);
+                result.putExtra(this.getExtra());
+                return result;
+            }
         }
+        return null;
     }
 
     @Override
@@ -103,7 +158,9 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
         GroupSubStrings result = new GroupSubStrings();
         Matcher matcher = pattern.matcher(string);
         while (matcher.find()) {
-            result.add(newGroupSubString(matcher));
+            if (exclude == null || !exclude.matches(matcher.group())) {
+                result.add(newGroupSubString(matcher));
+            }
         }
         if (result.isEmpty()) {
             return null;
@@ -156,15 +213,18 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
         }
     }
 
+    private static final Parsers PARSER = new Parsers();
+
     /**
      * 根据字符串参数解析成RegexpGroupExtractor对象
      *
      * @param rule 规则字符串, 格式为: [1,2] regexp
      * @return RegexpGroupExtractor对象
+     * @see Parsers#parseExtractor(String, Map)
      * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
      */
     public static RegexpGroupExtractor parseExtractor(String rule) {
-        return parseExtractor(rule, null);
+        return PARSER.parseExtractor(rule, null);
     }
 
     /**
@@ -173,89 +233,145 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
      * @param rule 规则字符串, 格式为: [1,2] regexp
      * @param extras 附加参数
      * @return RegexpGroupExtractor对象
+     * @see Parsers#parseExtractor(String, Map)
      * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
      */
     public static RegexpGroupExtractor parseExtractor(String rule, Map<String, Object> extras) {
-        VerifyTools.requireNotBlank(rule, "rule");
-        StringAccess sa = new StringAccess(rule);
-        sa.skipWhitespace();
-        String g = sa.readInBracket('[', ']');
-        if (!sa.isLastReadSuccess()) {
-            String msg = "Group format error:  " + rule + "  , The correct format is [1,2] regexp";
-            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
-        }
-        g = g.trim();
-        if (g.length() == 0) {
-            String msg = "Group must must not be empty, --> " + rule;
-            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
-        }
-        List<Object> groups = new ArrayList<>();
-        for (String group : StringTools.splits(g, ',')) {
-            if (StringTools.isDigit(group)) {
-                groups.add(Integer.parseInt(group));
-            } else if (group.indexOf('$') < 0 && StringTools.isWordString(group)) {
-                groups.add(group);
-            } else {
-                String msg = "Group must be digit or word characters: " + g + " --> " + rule;
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
-            }
-        }
-        sa.skipWhitespace();
-        String regexp = sa.readToEnd();
-        try {
-            Pattern pattern = Pattern.compile(regexp);
-            RegexpGroupExtractor extractor = new RegexpGroupExtractor(pattern, groups);
-            if (extras != null) {
-                extractor.putExtra(extras);
-            }
-            return extractor;
-        } catch (PatternSyntaxException e) {
-            String msg = "Regexp format error, --> " + regexp;
-            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg, e);
-        }
+        return PARSER.parseExtractor(rule, extras);
     }
 
     /**
-     * 根据字符串参数解析成GroupExtractors对象<br>
-     * 参数格式为: [1,2] regexp<br>
-     * 如果配置带额外属性的正则, 其格式为:
-     * <pre>
-     * |[1,2] regexp
-     * |    extraKey1 = extraValue1
-     * |    extraKey2 = extraValue2
-     * </pre>
+     * 根据字符串参数解析成GroupExtractors对象
      *
      * @param rules 规则字符串数组
      * @return GroupExtractors对象
+     * @see Parsers#parseExtractors(List)
      * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
      */
     public static GroupExtractors parseExtractors(List<String> rules) {
-        VerifyTools.requireNotBlank(rules, "rules");
-        List<RegexpGroupExtractor> extractors = new ArrayList<>();
-        List<String> cleared = RuleTools.clearCommentLines(rules);
-        for (int i = 0, z = cleared.size(); i < z; i++) {
-            String string = cleared.get(0);
-            String trimmed = string.trim();
-            if (extractors.isEmpty()) {
-                // 第1个正则之前的行, 判断是否缩进
-                int indent = IndentTools.countLeadingIndentSize(string);
-                if (indent > 0) {
-                    // 第1个正则之前的带缩进的行, 跳过
-                    continue;
+        return PARSER.parseExtractors(rules);
+    }
+
+    public static class Parsers {
+
+        /**
+         * 根据字符串参数解析成RegexpGroupExtractor对象
+         *
+         * @param rule 规则字符串, 格式为: [1,2] regexp
+         * @param extras 附加参数
+         * @return RegexpGroupExtractor对象
+         * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+         */
+        public RegexpGroupExtractor parseExtractor(String rule, Map<String, Object> extras) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            StringAccess sa = new StringAccess(rule);
+            sa.skipWhitespace();
+            String g = sa.readInBracket('[', ']');
+            if (!sa.isLastReadSuccess()) {
+                String msg = "Group format error:  " + rule + "  , The correct format is [1,2] regexp";
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+            }
+            g = g.trim();
+            if (g.length() == 0) {
+                String msg = "Group must must not be empty, --> " + rule;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+            }
+            List<Object> groups = new ArrayList<>();
+            for (String group : StringTools.splits(g, ',')) {
+                if (StringTools.isDigit(group)) {
+                    groups.add(Integer.parseInt(group));
+                } else if (group.indexOf('$') < 0 && StringTools.isWordString(group)) {
+                    groups.add(group);
+                } else {
+                    String msg = "Group must be digit or word characters: " + g + " --> " + rule;
+                    throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+                }
+            }
+            sa.skipWhitespace();
+            String regexp = sa.readToEnd();
+            try {
+                Pattern pattern = Pattern.compile(regexp);
+                RegexpGroupExtractor extractor = new RegexpGroupExtractor(pattern, groups);
+                if (extras != null) {
+                    extractor.putExtra(extras);
+                }
+                return extractor;
+            } catch (PatternSyntaxException e) {
+                String msg = "Regexp format error, --> " + regexp;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg, e);
+            }
+        }
+
+        /**
+         * 根据字符串参数解析成GroupExtractors对象<br>
+         * 参数格式为: [1,2] regexp<br>
+         * 如果配置带额外属性的正则, 其格式为:
+         * <pre>
+         * |[1,2] regexp
+         * |    extraKey1 = extraValue1
+         * |    extraKey2 = extraValue2
+         * |    exclude = 排除规则
+         * </pre>
+         *
+         * @param rules 规则字符串数组
+         * @return GroupExtractors对象
+         * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+         */
+        public GroupExtractors parseExtractors(List<String> rules) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            List<RegexpGroupExtractor> extractors = new ArrayList<>();
+            List<String> cleared = RuleTools.clearCommentLines(rules);
+            for (int i = 0, z = cleared.size(); i < z; i++) {
+                String string = cleared.get(i);
+                String trimmed = string.trim();
+                if (extractors.isEmpty()) {
+                    // 第1个正则之前的行, 判断是否缩进
+                    int indent = IndentTools.countLeadingIndentSize(string);
+                    if (indent > 0) {
+                        // 第1个正则之前的带缩进的行, 跳过
+                        continue;
+                    }
+                }
+                // 解析附加属性
+                List<String> extraLines = new ArrayList<>();
+                i += RuleTools.collectNextIndentLines(cleared, i + 1, extraLines);
+                Map<String, Object> extras = null;
+                if (!extraLines.isEmpty()) {
+                    List<KeyString> keyStrings = RuleTools.splitRuleKeyValues(extraLines);
+                    extras = KeyString.toMap(keyStrings);
+                }
+                // 解析正则表达式提取规则
+                List<RegexpGroupExtractor> list = doParseExtractors(trimmed, extras);
+                if (list != null) {
+                    for (RegexpGroupExtractor item : list) {
+                        doHandleExcludeMatcher(item);
+                        extractors.add(item);
+                    }
                 }
             }
+            return new GroupExtractors(extractors);
+        }
+
+        protected List<RegexpGroupExtractor> doParseExtractors(String rule, Map<String, Object> extras) {
             // 解析正则表达式提取规则
-            RegexpGroupExtractor extractor = parseExtractor(trimmed);
-            extractors.add(extractor);
-            // 解析附加属性
-            List<String> extraLines = new ArrayList<>();
-            i += RuleTools.collectNextIndentLines(cleared, i + 1, extraLines);
-            if (!extraLines.isEmpty()) {
-                List<KeyString> keyStrings = RuleTools.splitRuleKeyValues(extraLines);
-                Map<String, Object> maps = KeyString.toMap(keyStrings);
-                extractor.putExtra(maps);
+            RegexpGroupExtractor extractor = parseExtractor(rule, extras);
+            doHandleExcludeMatcher(extractor);
+            return Collections.singletonList(extractor);
+        }
+
+        protected void doHandleExcludeMatcher(RegexpGroupExtractor extractor) {
+            if (extractor.containsExtra("exclude")) {
+                Object exclude = extractor.getExtra("exclude");
+                if (exclude instanceof String) {
+                    extractor.setExclude(doParseExcludeMatcher((String) exclude));
+                } else if (exclude instanceof StringMatcher) {
+                    extractor.setExclude((StringMatcher) exclude);
+                }
             }
         }
-        return new GroupExtractors(extractors);
+
+        protected StringMatcher doParseExcludeMatcher(String exclude) {
+            return WrapStringMatcher.parseMatcher(exclude, "regexp");
+        }
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
index 014625d..d548ef7 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
@@ -1,6 +1,7 @@
 package com.gitee.qdbp.able.matches;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
@@ -27,13 +28,22 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
 public class RegexpStringExtractor extends ExtraData implements StringExtractor {
 
     private static final long serialVersionUID = 6340075034069358703L;
-    private final Pattern pattern;
-    private final Integer digitGroup;
-    private final String namedGroup;
+    /** 提取规则 **/
+    private Pattern pattern;
+    /** 排除规则 **/
+    private StringMatcher exclude;
+    /** 提取组序号 **/
+    private Integer digitGroup;
+    /** 提取组名称 **/
+    private String namedGroup;
+
+    public RegexpStringExtractor() {
+    }
 
     public RegexpStringExtractor(String pattern, int group) {
         VerifyTools.requireNotBlank(pattern, "pattern");
         this.pattern = Pattern.compile(pattern);
+        this.exclude = null;
         this.digitGroup = group;
         this.namedGroup = null;
     }
@@ -41,6 +51,7 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
     public RegexpStringExtractor(Pattern pattern, int group) {
         VerifyTools.requireNonNull(pattern, "pattern");
         this.pattern = pattern;
+        this.exclude = null;
         this.digitGroup = group;
         this.namedGroup = null;
     }
@@ -48,6 +59,7 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
     public RegexpStringExtractor(String pattern, String group) {
         VerifyTools.requireNotBlank(pattern, "pattern");
         this.pattern = Pattern.compile(pattern);
+        this.exclude = null;
         this.digitGroup = null;
         this.namedGroup = group;
     }
@@ -55,20 +67,67 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
     public RegexpStringExtractor(Pattern pattern, String group) {
         VerifyTools.requireNonNull(pattern, "pattern");
         this.pattern = pattern;
+        this.exclude = null;
         this.digitGroup = null;
         this.namedGroup = group;
     }
 
+    /** 提取规则 **/
+    public Pattern getPattern() {
+        return pattern;
+    }
+
+    /** 提取规则 **/
+    public void setPattern(Pattern pattern) {
+        this.pattern = pattern;
+    }
+
+    /** 提取规则 **/
+    public void setPattern(String pattern) {
+        this.pattern = Pattern.compile(pattern);
+    }
+
+    /** 排除规则 **/
+    public StringMatcher getExclude() {
+        return exclude;
+    }
+
+    /** 排除规则 **/
+    public void setExclude(StringMatcher exclude) {
+        this.exclude = exclude;
+    }
+
+    /** 提取组序号 **/
+    public Integer getDigitGroup() {
+        return digitGroup;
+    }
+
+    /** 提取组序号 **/
+    public void setDigitGroup(Integer digitGroup) {
+        this.digitGroup = digitGroup;
+    }
+
+    /** 提取组名称 **/
+    public String getNamedGroup() {
+        return namedGroup;
+    }
+
+    /** 提取组名称 **/
+    public void setNamedGroup(String namedGroup) {
+        this.namedGroup = namedGroup;
+    }
+
     @Override
     public SubString extractFirst(String string) {
         Matcher matcher = pattern.matcher(string);
-        if (!matcher.find()) {
-            return null;
-        } else {
+        if (matcher.find()) {
             SubString result = newSubString(matcher);
-            result.putExtra(this.getExtra());
-            return result;
+            if (exclude == null || !exclude.matches(matcher.group())) {
+                result.putExtra(this.getExtra());
+                return result;
+            }
         }
+        return null;
     }
 
     @Override
@@ -76,7 +135,9 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
         SubStrings result = new SubStrings();
         Matcher matcher = pattern.matcher(string);
         while (matcher.find()) {
-            result.add(newSubString(matcher));
+            if (exclude == null || !exclude.matches(matcher.group())) {
+                result.add(newSubString(matcher));
+            }
         }
         if (result.isEmpty()) {
             return null;
@@ -99,14 +160,17 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
         }
     }
 
+    private static final Parsers PARSER = new Parsers();
+
     /**
      * 根据字符串参数解析成RegexpGroupExtractor对象
      *
      * @param rule 规则字符串, 格式为: [1] regexp
+     * @see Parsers#parseExtractor(String, Map)
      * @return RegexpGroupExtractor对象
      */
-    public static RegexpStringExtractor parseExtractor(String rule) {
-        return parseExtractor(rule, null);
+    public RegexpStringExtractor parseExtractor(String rule) {
+        return PARSER.parseExtractor(rule, null);
     }
 
     /**
@@ -114,93 +178,146 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
      *
      * @param rule 规则字符串, 格式为: [1] regexp
      * @param extras 附加参数
+     * @see Parsers#parseExtractor(String, Map)
      * @return RegexpGroupExtractor对象
      */
-    public static RegexpStringExtractor parseExtractor(String rule, Map<String, Object> extras) {
-        VerifyTools.requireNotBlank(rule, "rule");
-        StringAccess sa = new StringAccess(rule);
-        sa.skipWhitespace();
-        String group = sa.readInBracket('[', ']');
-        if (!sa.isLastReadSuccess()) {
-            String msg = "Group format error, The correct format is [1] regexp, --> " + rule;
-            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
-        }
-        group = group.trim();
-        if (group.length() == 0) {
-            String msg = "Group must must not be empty, --> " + rule;
-            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
-        }
-        Integer digitGroup = null;
-        String namedGroup = null;
-        if (StringTools.isDigit(group)) {
-            digitGroup = Integer.parseInt(group);
-        } else if (group.indexOf('$') < 0 && StringTools.isWordString(group)) {
-            namedGroup = group;
-        } else {
-            String msg = "Group must be digit or word characters: " + group + " --> " + rule;
-            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
-        }
-        sa.skipWhitespace();
-        String regexp = sa.readToEnd();
-        Pattern pattern;
-        try {
-            pattern = Pattern.compile(regexp);
-        } catch (PatternSyntaxException e) {
-            String msg = "Regexp format error, --> " + regexp;
-            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg, e);
-        }
-        RegexpStringExtractor extractor;
-        if (digitGroup != null) {
-            extractor = new RegexpStringExtractor(pattern, digitGroup);
-        } else {
-            extractor = new RegexpStringExtractor(pattern, namedGroup);
-        }
-        if (extras != null) {
-            extractor.putExtra(extras);
-        }
-        return extractor;
+    public RegexpStringExtractor parseExtractor(String rule, Map<String, Object> extras) {
+        return PARSER.parseExtractor(rule, extras);
     }
 
     /**
-     * 根据字符串参数解析成StringExtractors对象<br>
-     * 参数格式为: [1] regexp<br>
-     * 如果配置带额外属性的正则, 其格式为:
-     * <pre>
-     * |[1] regexp
-     * |    extraKey1 = extraValue1
-     * |    extraKey2 = extraValue2
-     * </pre>
+     * 根据字符串参数解析成StringExtractors对象
      *
      * @param rules 字符串数组
+     * @see Parsers#parseExtractors(List)
      * @return StringExtractors对象
      */
     public static StringExtractors parseExtractors(List<String> rules) {
-        VerifyTools.requireNotBlank(rules, "rules");
-        List<RegexpStringExtractor> extractors = new ArrayList<>();
-        List<String> cleared = RuleTools.clearCommentLines(rules);
-        for (int i = 0, z = cleared.size(); i < z; i++) {
-            String string = cleared.get(0);
-            String trimmed = string.trim();
-            if (extractors.isEmpty()) {
-                // 第1个正则之前的行, 判断是否缩进
-                int indent = IndentTools.countLeadingIndentSize(string);
-                if (indent > 0) {
-                    // 第1个正则之前的带缩进的行, 跳过
-                    continue;
+        return PARSER.parseExtractors(rules);
+    }
+
+    public static class Parsers {
+
+        /**
+         * 根据字符串参数解析成RegexpGroupExtractor对象
+         *
+         * @param rule 规则字符串, 格式为: [1] regexp
+         * @param extras 附加参数
+         * @return RegexpGroupExtractor对象
+         */
+        public RegexpStringExtractor parseExtractor(String rule, Map<String, Object> extras) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            StringAccess sa = new StringAccess(rule);
+            sa.skipWhitespace();
+            String group = sa.readInBracket('[', ']');
+            if (!sa.isLastReadSuccess()) {
+                String msg = "Group format error, The correct format is [1] regexp, --> " + rule;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+            }
+            group = group.trim();
+            if (group.length() == 0) {
+                String msg = "Group must must not be empty, --> " + rule;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+            }
+            Integer digitGroup = null;
+            String namedGroup = null;
+            if (StringTools.isDigit(group)) {
+                digitGroup = Integer.parseInt(group);
+            } else if (group.indexOf('$') < 0 && StringTools.isWordString(group)) {
+                namedGroup = group;
+            } else {
+                String msg = "Group must be digit or word characters: " + group + " --> " + rule;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+            }
+            sa.skipWhitespace();
+            String regexp = sa.readToEnd();
+            Pattern pattern;
+            try {
+                pattern = Pattern.compile(regexp);
+            } catch (PatternSyntaxException e) {
+                String msg = "Regexp format error, --> " + regexp;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg, e);
+            }
+            RegexpStringExtractor extractor;
+            if (digitGroup != null) {
+                extractor = new RegexpStringExtractor(pattern, digitGroup);
+            } else {
+                extractor = new RegexpStringExtractor(pattern, namedGroup);
+            }
+            if (extras != null) {
+                extractor.putExtra(extras);
+            }
+            return extractor;
+        }
+
+        /**
+         * 根据字符串参数解析成StringExtractors对象<br>
+         * 参数格式为: [1] regexp<br>
+         * 如果配置带额外属性的正则, 其格式为:
+         * <pre>
+         * |[1] regexp
+         * |    extraKey1 = extraValue1
+         * |    extraKey2 = extraValue2
+         * |    exclude = 排除规则
+         * </pre>
+         *
+         * @param rules 字符串数组
+         * @return StringExtractors对象
+         */
+        public StringExtractors parseExtractors(List<String> rules) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            List<RegexpStringExtractor> extractors = new ArrayList<>();
+            List<String> cleared = RuleTools.clearCommentLines(rules);
+            for (int i = 0, z = cleared.size(); i < z; i++) {
+                String string = cleared.get(i);
+                String trimmed = string.trim();
+                if (extractors.isEmpty()) {
+                    // 第1个正则之前的行, 判断是否缩进
+                    int indent = IndentTools.countLeadingIndentSize(string);
+                    if (indent > 0) {
+                        // 第1个正则之前的带缩进的行, 跳过
+                        continue;
+                    }
+                }
+                // 解析附加属性
+                List<String> extraLines = new ArrayList<>();
+                i += RuleTools.collectNextIndentLines(cleared, i + 1, extraLines);
+                Map<String, Object> extras = null;
+                if (!extraLines.isEmpty()) {
+                    List<KeyString> keyStrings = RuleTools.splitRuleKeyValues(extraLines);
+                    extras = KeyString.toMap(keyStrings);
+                }
+                // 解析正则表达式提取规则
+                List<RegexpStringExtractor> list = doParseExtractors(trimmed, extras);
+                if (list != null) {
+                    for (RegexpStringExtractor item : list) {
+                        doHandleExcludeMatcher(item);
+                        extractors.add(item);
+                    }
                 }
             }
+            return new StringExtractors(extractors);
+        }
+
+        protected List<RegexpStringExtractor> doParseExtractors(String rule, Map<String, Object> extras) {
             // 解析正则表达式提取规则
-            RegexpStringExtractor extractor = parseExtractor(trimmed);
-            extractors.add(extractor);
-            // 解析附加属性
-            List<String> extraLines = new ArrayList<>();
-            i += RuleTools.collectNextIndentLines(cleared, i + 1, extraLines);
-            if (!extraLines.isEmpty()) {
-                List<KeyString> keyStrings = RuleTools.splitRuleKeyValues(extraLines);
-                Map<String, Object> maps = KeyString.toMap(keyStrings);
-                extractor.putExtra(maps);
+            RegexpStringExtractor extractor = parseExtractor(rule, extras);
+            return Collections.singletonList(extractor);
+        }
+
+        protected StringMatcher doParseExcludeMatcher(String exclude) {
+            return WrapStringMatcher.parseMatcher(exclude, "regexp");
+        }
+
+        protected void doHandleExcludeMatcher(RegexpStringExtractor extractor) {
+            if (extractor.containsExtra("exclude")) {
+                Object exclude = extractor.getExtra("exclude");
+                if (exclude instanceof String) {
+                    extractor.setExclude(doParseExcludeMatcher((String) exclude));
+                } else if (exclude instanceof StringMatcher) {
+                    extractor.setExclude((StringMatcher) exclude);
+                }
             }
         }
-        return new StringExtractors(extractors);
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
index 9c43be2..b168295 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
@@ -17,14 +17,21 @@ public class StringExtractors implements StringExtractor {
         this.extractors = extractors;
     }
 
+    public List<? extends StringExtractor> getExtractors() {
+        return extractors;
+    }
+
     @Override
     public SubString extractFirst(String string) {
         for (int i = 0, z = extractors.size(); i < z; i++) {
             StringExtractor extractor = extractors.get(i);
             SubString result = extractor.extractFirst(string);
             if (result != null) {
-                if (!result.containsExtra("i")) {
-                    result.putExtra("i", i);
+                if (!result.containsExtra("index")) {
+                    result.putExtra("index", i);
+                }
+                if (!result.containsExtra("rule")) {
+                    result.putExtra("rule", extractor);
                 }
                 return result;
             }
@@ -38,8 +45,11 @@ public class StringExtractors implements StringExtractor {
             StringExtractor extractor = extractors.get(i);
             SubStrings result = extractor.extractAll(string);
             if (result != null) {
-                if (!result.containsExtra("i")) {
-                    result.putExtra("i", i);
+                if (!result.containsExtra("index")) {
+                    result.putExtra("index", i);
+                }
+                if (!result.containsExtra("rule")) {
+                    result.putExtra("rule", extractor);
                 }
                 return result;
             }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
index ca1eec7..da812f2 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
@@ -168,7 +168,9 @@ public class RuleTools {
             int index = string.indexOf('=');
             String key = null;
             List<String> values = new ArrayList<>();
-            if (index >= 0) {
+            if (index < 0) {
+                values.add(string);
+            } else {
                 if (index > 0) {
                     key = StringTools.trimRight(string.substring(0, index));
                 }
-- 
Gitee


From 06f58cea79c3ece206e09ce9c98f2749b3b47a62 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 23 Feb 2022 23:41:29 +0800
Subject: [PATCH 025/160] qdbp-able-5.4.12

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index f59f250..aec0f4c 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.11</version>
+	<version>5.4.12</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index 66f7f76..ed75d07 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.11</version>
+	<version>5.4.12</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From dbe48ca1caac884ec5de300aa11659f886931bc3 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 6 Mar 2022 22:53:34 +0800
Subject: [PATCH 026/160] =?UTF-8?q?StringExtractor=E8=BF=94=E5=9B=9E?=
 =?UTF-8?q?=E7=BB=93=E6=9E=9C=E6=94=B9=E4=B8=BALocatedSubString?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/able/matches/GroupSubString.java     |  37 ++++++
 .../qdbp/able/matches/LocatedSubString.java   | 113 ++++++++++++++++++
 .../qdbp/able/matches/LocatedSubStrings.java  |  71 +++++++++++
 .../able/matches/RegexpGroupExtractor.java    |   3 +
 .../able/matches/RegexpStringExtractor.java   |  33 ++---
 .../qdbp/able/matches/StringExtractor.java    |  19 ++-
 .../qdbp/able/matches/StringExtractors.java   |  10 +-
 .../able/matches/StringExtractorTest.java     |   4 +-
 .../qdbp/tools/utils/OgnlCharacterTest.java   |  29 +++++
 9 files changed, 289 insertions(+), 30 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubStrings.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlCharacterTest.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
index 5a31f1c..dee353a 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
@@ -19,6 +19,43 @@ public class GroupSubString extends HashMap<String, SubString> implements ExtraB
 
     private ExtraData extraData;
 
+    /** 子字符串内容[即group(0)] **/
+    private String textContent;
+    /** 子字符串在原文本的开始位置 **/
+    private int textStartIndex;
+    /** 子字符串在原文本的结束位置 **/
+    private int textEndIndex;
+
+    /** 子文本内容[即group(0)] **/
+    public String getTextContent() {
+        return textContent;
+    }
+
+    /** 子文本内容[即group(0)] **/
+    public void setTextContent(String textContent) {
+        this.textContent = textContent;
+    }
+
+    /** 子文本在原文本的开始位置 **/
+    public int getTextStartIndex() {
+        return textStartIndex;
+    }
+
+    /** 子文本在原文本的开始位置 **/
+    public void setTextStartIndex(int textStartIndex) {
+        this.textStartIndex = textStartIndex;
+    }
+
+    /** 子文本在原文本的结束位置 **/
+    public int getTextEndIndex() {
+        return textEndIndex;
+    }
+
+    /** 子文本在原文本的结束位置 **/
+    public void setTextEndIndex(int textEndIndex) {
+        this.textEndIndex = textEndIndex;
+    }
+
     public SubString get(Integer key) {
         return super.get(String.valueOf(key));
     }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
new file mode 100644
index 0000000..64adb77
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
@@ -0,0 +1,113 @@
+package com.gitee.qdbp.able.matches;
+
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+import com.gitee.qdbp.tools.compare.CompareTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 带有位置描述信息的子字符串
+ *
+ * @author zhaohuihua
+ * @version 20220306
+ */
+public class LocatedSubString extends ExtraData implements Comparable<com.gitee.qdbp.able.beans.SubString> {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    /** 提取项内容 **/
+    private String groupContent;
+    /** 提取项在原文本的开始位置 **/
+    private int groupStartIndex;
+    /** 提取项在原文本的结束位置 **/
+    private int groupEndIndex;
+    /** 子文本内容[即group(0)] **/
+    private String textContent;
+    /** 子文本在原文本的开始位置 **/
+    private int textStartIndex;
+    /** 子文本在原文本的结束位置 **/
+    private int textEndIndex;
+
+    public LocatedSubString() {
+    }
+
+    /** 提取项内容 **/
+    public String getGroupContent() {
+        return groupContent;
+    }
+
+    /** 提取项内容 **/
+    public void setGroupContent(String groupContent) {
+        this.groupContent = groupContent;
+    }
+
+    /** 提取项在原文本的开始位置 **/
+    public int getGroupStartIndex() {
+        return groupStartIndex;
+    }
+
+    /** 提取项在原文本的结束位置 **/
+    public void setGroupStartIndex(int groupStartIndex) {
+        this.groupStartIndex = groupStartIndex;
+    }
+
+    /** 提取项在原文本的结束位置 **/
+    public int getGroupEndIndex() {
+        return groupEndIndex;
+    }
+
+    /** 提取项在原文本的结束位置 **/
+    public void setGroupEndIndex(int groupEndIndex) {
+        this.groupEndIndex = groupEndIndex;
+    }
+
+    /** 子文本内容[即group(0)] **/
+    public String getTextContent() {
+        return textContent;
+    }
+
+    /** 子文本内容[即group(0)] **/
+    public void setTextContent(String textContent) {
+        this.textContent = textContent;
+    }
+
+    /** 子文本在原文本的开始位置 **/
+    public int getTextStartIndex() {
+        return textStartIndex;
+    }
+
+    /** 子文本在原文本的开始位置 **/
+    public void setTextStartIndex(int textStartIndex) {
+        this.textStartIndex = textStartIndex;
+    }
+
+    /** 子文本在原文本的结束位置 **/
+    public int getTextEndIndex() {
+        return textEndIndex;
+    }
+
+    /** 子文本在原文本的结束位置 **/
+    public void setTextEndIndex(int textEndIndex) {
+        this.textEndIndex = textEndIndex;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append(StringTools.pad(groupStartIndex, 3));
+        buffer.append(':').append(StringTools.pad(groupEndIndex, 3));
+        buffer.append("    ").append(groupContent);
+        return buffer.toString();
+    }
+
+    @Override
+    public int compareTo(com.gitee.qdbp.able.beans.SubString o) {
+        if (o == null) {
+            return -1;
+        }
+        return CompareTools.stream()
+                .asc(groupStartIndex, o.getStartIndex())
+                .asc(groupEndIndex, o.getEndIndex())
+                .compare();
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubStrings.java b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubStrings.java
new file mode 100644
index 0000000..af9919e
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubStrings.java
@@ -0,0 +1,71 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.Map;
+import com.gitee.qdbp.able.model.reusable.ExtraBase;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+
+/**
+ * 带有位置信息的子文本集合
+ *
+ * @author zhaohuihua
+ * @version 20220306
+ */
+public class LocatedSubStrings extends ArrayList<LocatedSubString> implements ExtraBase {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    private ExtraData extraData;
+
+    /** 获取附加数据 **/
+    @Override
+    public Map<String, Object> getExtra() {
+        return extraData == null ? null : extraData.getExtra();
+    }
+
+    /** 设置附加数据 **/
+    @Override
+    public void setExtra(Map<String, Object> extra) {
+        this.extraData = new ExtraData(extra);
+    }
+
+    /** 是否存在指定的附加数据 **/
+    @Override
+    public boolean containsExtra(String key) {
+        return extraData != null && extraData.containsExtra(key);
+    }
+
+    /** 获取指定的附加数据 **/
+    @Override
+    public Object getExtra(String key) {
+        return extraData == null ? null : extraData.getExtra(key);
+    }
+
+    /** 获取指定类型的附加数据 **/
+    @Override
+    public <T> T getExtra(String key, Class<T> clazz) {
+        return extraData == null ? null : extraData.getExtra(key, clazz);
+    }
+
+    /** 设置指定的附加数据 **/
+    @Override
+    public void putExtra(String key, Object value) {
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(key, value);
+    }
+
+    /** 设置附加信息 **/
+    @Override
+    public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(extra);
+    }
+}
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
index f24a992..3a0277b 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
@@ -171,6 +171,9 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
 
     private GroupSubString newGroupSubString(Matcher matcher) {
         GroupSubString result = new GroupSubString();
+        result.setTextContent(matcher.group());
+        result.setTextStartIndex(matcher.start());
+        result.setTextEndIndex(matcher.end());
         if (digitGroups != null) {
             for (int group : digitGroups) {
                 String content = matcher.group(group);
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
index d548ef7..11d582e 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
@@ -8,8 +8,6 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 import com.gitee.qdbp.able.beans.KeyString;
-import com.gitee.qdbp.able.beans.SubString;
-import com.gitee.qdbp.able.beans.SubStrings;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.model.reusable.ExtraData;
 import com.gitee.qdbp.able.result.ResultCode;
@@ -118,10 +116,10 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
     }
 
     @Override
-    public SubString extractFirst(String string) {
+    public LocatedSubString extractFirst(String string) {
         Matcher matcher = pattern.matcher(string);
         if (matcher.find()) {
-            SubString result = newSubString(matcher);
+            LocatedSubString result = newLocationString(matcher);
             if (exclude == null || !exclude.matches(matcher.group())) {
                 result.putExtra(this.getExtra());
                 return result;
@@ -131,12 +129,12 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
     }
 
     @Override
-    public SubStrings extractAll(String string) {
-        SubStrings result = new SubStrings();
+    public LocatedSubStrings extractAll(String string) {
+        LocatedSubStrings result = new LocatedSubStrings();
         Matcher matcher = pattern.matcher(string);
         while (matcher.find()) {
             if (exclude == null || !exclude.matches(matcher.group())) {
-                result.add(newSubString(matcher));
+                result.add(newLocationString(matcher));
             }
         }
         if (result.isEmpty()) {
@@ -146,18 +144,21 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
         return result;
     }
 
-    private SubString newSubString(Matcher matcher) {
+    private LocatedSubString newLocationString(Matcher matcher) {
+        LocatedSubString result = new LocatedSubString();
+        result.setTextContent(matcher.group());
+        result.setTextEndIndex(matcher.start());
+        result.setTextEndIndex(matcher.end());
         if (this.digitGroup != null) {
-            String content = matcher.group(this.digitGroup);
-            int start = matcher.start(this.digitGroup);
-            int end = matcher.end(this.digitGroup);
-            return new SubString(content, start, end);
+            result.setGroupContent(matcher.group(this.digitGroup));
+            result.setGroupEndIndex(matcher.start(this.digitGroup));
+            result.setGroupEndIndex(matcher.end(this.digitGroup));
         } else {
-            String content = matcher.group(this.namedGroup);
-            int start = matcher.start(this.namedGroup);
-            int end = matcher.end(this.namedGroup);
-            return new SubString(content, start, end);
+            result.setGroupContent(matcher.group(this.namedGroup));
+            result.setGroupEndIndex(matcher.start(this.namedGroup));
+            result.setGroupEndIndex(matcher.end(this.namedGroup));
         }
+        return result;
     }
 
     private static final Parsers PARSER = new Parsers();
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
index 463e6f6..cb26184 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
@@ -1,8 +1,5 @@
 package com.gitee.qdbp.able.matches;
 
-import com.gitee.qdbp.able.beans.SubString;
-import com.gitee.qdbp.able.beans.SubStrings;
-
 /**
  * 字符串提取接口
  *
@@ -11,7 +8,19 @@ import com.gitee.qdbp.able.beans.SubStrings;
  */
 public interface StringExtractor {
 
-    SubString extractFirst(String string);
+    /**
+     * 提取第1个匹配项
+     *
+     * @param string 原文本
+     * @return 截取到的文本以及在原文中的位置
+     */
+    LocatedSubString extractFirst(String string);
 
-    SubStrings extractAll(String string);
+    /**
+     * 提取所有匹配项
+     *
+     * @param string 原文本
+     * @return 截取到的文本以及在原文中的位置, 通过get(1)方式取指定组的匹配值
+     */
+    LocatedSubStrings extractAll(String string);
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
index b168295..98c2536 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
@@ -1,8 +1,6 @@
 package com.gitee.qdbp.able.matches;
 
 import java.util.List;
-import com.gitee.qdbp.able.beans.SubString;
-import com.gitee.qdbp.able.beans.SubStrings;
 
 /**
  * 字符串提取集合实现类
@@ -22,10 +20,10 @@ public class StringExtractors implements StringExtractor {
     }
 
     @Override
-    public SubString extractFirst(String string) {
+    public LocatedSubString extractFirst(String string) {
         for (int i = 0, z = extractors.size(); i < z; i++) {
             StringExtractor extractor = extractors.get(i);
-            SubString result = extractor.extractFirst(string);
+            LocatedSubString result = extractor.extractFirst(string);
             if (result != null) {
                 if (!result.containsExtra("index")) {
                     result.putExtra("index", i);
@@ -40,10 +38,10 @@ public class StringExtractors implements StringExtractor {
     }
 
     @Override
-    public SubStrings extractAll(String string) {
+    public LocatedSubStrings extractAll(String string) {
         for (int i = 0, z = extractors.size(); i < z; i++) {
             StringExtractor extractor = extractors.get(i);
-            SubStrings result = extractor.extractAll(string);
+            LocatedSubStrings result = extractor.extractAll(string);
             if (result != null) {
                 if (!result.containsExtra("index")) {
                     result.putExtra("index", i);
diff --git a/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
index 7240325..195e466 100644
--- a/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
@@ -1,7 +1,5 @@
 package com.gitee.qdbp.able.matches;
 
-import java.util.List;
-import com.gitee.qdbp.able.beans.SubString;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 
 /**
@@ -15,7 +13,7 @@ public class StringExtractorTest {
     public static void main(String[] args) {
         RegexpStringExtractor extractor = new RegexpStringExtractor("(\\d+)", 1);
         String string = "aabbcc1122ddee33ff44";
-        List<SubString> result = extractor.extractAll(string);
+        LocatedSubStrings result = extractor.extractAll(string);
         System.out.println(ConvertTools.joinToString(result, '\n'));
     }
 }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlCharacterTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlCharacterTest.java
new file mode 100644
index 0000000..3cb3ae6
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlCharacterTest.java
@@ -0,0 +1,29 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import com.gitee.qdbp.able.exception.FormatException;
+
+/**
+ * OgnlCharapterTest
+ *
+ * @author zhaohuihua
+ * @version 20220306
+ */
+public class OgnlCharacterTest {
+
+    public static void main(String[] args) throws FormatException {
+        System.out.println();
+        Map<String, Object> map = new HashMap<>();
+        map.put("char1", 'A');
+        map.put("number1", (int) 'A');
+        map.put("string1", "A");
+        // 报错: Object result = OgnlTools.evaluate("string1==number1", map);
+        Object result = OgnlTools.evaluate("string1.equals(number1)", map);
+        System.out.println(result);
+        Object result2 = OgnlTools.evaluate("number1==char1", map);
+        System.out.println(result2);
+        Object result3 = OgnlTools.evaluate("number1==string1", map);
+        System.out.println(result3);
+    }
+}
-- 
Gitee


From 70bbe66556a9a6d9a8e8c75743031b25df5897c8 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 6 Mar 2022 22:58:51 +0800
Subject: [PATCH 027/160] 5.4.13

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index aec0f4c..edd6fa6 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.12</version>
+	<version>5.4.13</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index ed75d07..6ce6a98 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.12</version>
+	<version>5.4.13</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From dada19b89a17c6cacc869486d056f2f3c156f757 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 19 Mar 2022 12:01:14 +0800
Subject: [PATCH 028/160] =?UTF-8?q?=E6=8F=90=E5=8F=96=E8=A7=84=E5=88=99?=
 =?UTF-8?q?=E4=BC=98=E5=8C=96,=20=E4=BD=8D=E7=BD=AE=E6=8B=86=E5=88=86?=
 =?UTF-8?q?=E4=B8=BA=E6=8F=90=E5=8F=96=E9=A1=B9=E4=BD=8D=E7=BD=AE=E5=92=8C?=
 =?UTF-8?q?=E5=8C=B9=E9=85=8D=E9=A1=B9=E4=BD=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/able/matches/GroupSubString.java     |  50 ++---
 .../qdbp/able/matches/LocatedSubString.java   |  48 ++---
 .../able/matches/RegexpGroupExtractor.java    |   6 +-
 .../able/matches/RegexpStringExtractor.java   |  10 +-
 .../model/reusable/ExpressionContext.java     |   2 +-
 .../model/reusable/ExpressionExecutor.java    | 183 ++++++++++++++++++
 .../able/model/reusable/ExpressionMap.java    | 106 ++++++++++
 .../com/gitee/qdbp/tools/utils/MapTools.java  |  11 +-
 .../gitee/qdbp/tools/utils/ReflectTools.java  |   9 +-
 9 files changed, 362 insertions(+), 63 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionMap.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
index dee353a..a87087f 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
@@ -19,41 +19,41 @@ public class GroupSubString extends HashMap<String, SubString> implements ExtraB
 
     private ExtraData extraData;
 
-    /** 子字符串内容[即group(0)] **/
-    private String textContent;
-    /** 子字符串在原文本的开始位置 **/
-    private int textStartIndex;
-    /** 子字符串在原文本的结束位置 **/
-    private int textEndIndex;
-
-    /** 子文本内容[即group(0)] **/
-    public String getTextContent() {
-        return textContent;
+    /** 匹配项内容[即group(0)] **/
+    private String matchContent;
+    /** 匹配项在原文本的开始位置 **/
+    private int matchStartIndex;
+    /** 匹配项在原文本的结束位置 **/
+    private int matchEndIndex;
+
+    /** 匹配项内容[即group(0)] **/
+    public String getMatchContent() {
+        return matchContent;
     }
 
-    /** 子文本内容[即group(0)] **/
-    public void setTextContent(String textContent) {
-        this.textContent = textContent;
+    /** 匹配项内容[即group(0)] **/
+    public void setMatchContent(String matchContent) {
+        this.matchContent = matchContent;
     }
 
-    /** 子文本在原文本的开始位置 **/
-    public int getTextStartIndex() {
-        return textStartIndex;
+    /** 匹配项在原文本的开始位置 **/
+    public int getMatchStartIndex() {
+        return matchStartIndex;
     }
 
-    /** 子文本在原文本的开始位置 **/
-    public void setTextStartIndex(int textStartIndex) {
-        this.textStartIndex = textStartIndex;
+    /** 匹配项在原文本的开始位置 **/
+    public void setMatchStartIndex(int matchStartIndex) {
+        this.matchStartIndex = matchStartIndex;
     }
 
-    /** 子文本在原文本的结束位置 **/
-    public int getTextEndIndex() {
-        return textEndIndex;
+    /** 匹配项在原文本的结束位置 **/
+    public int getMatchEndIndex() {
+        return matchEndIndex;
     }
 
-    /** 子文本在原文本的结束位置 **/
-    public void setTextEndIndex(int textEndIndex) {
-        this.textEndIndex = textEndIndex;
+    /** 匹配项在原文本的结束位置 **/
+    public void setMatchEndIndex(int matchEndIndex) {
+        this.matchEndIndex = matchEndIndex;
     }
 
     public SubString get(Integer key) {
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
index 64adb77..9cf65af 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
@@ -21,12 +21,12 @@ public class LocatedSubString extends ExtraData implements Comparable<com.gitee.
     private int groupStartIndex;
     /** 提取项在原文本的结束位置 **/
     private int groupEndIndex;
-    /** 子文本内容[即group(0)] **/
-    private String textContent;
-    /** 子文本在原文本的开始位置 **/
-    private int textStartIndex;
-    /** 子文本在原文本的结束位置 **/
-    private int textEndIndex;
+    /** 匹配项内容[即group(0)] **/
+    private String matchContent;
+    /** 匹配项在原文本的开始位置 **/
+    private int matchStartIndex;
+    /** 匹配项在原文本的结束位置 **/
+    private int matchEndIndex;
 
     public LocatedSubString() {
     }
@@ -46,7 +46,7 @@ public class LocatedSubString extends ExtraData implements Comparable<com.gitee.
         return groupStartIndex;
     }
 
-    /** 提取项在原文本的结束位置 **/
+    /** 提取项在原文本的开始位置 **/
     public void setGroupStartIndex(int groupStartIndex) {
         this.groupStartIndex = groupStartIndex;
     }
@@ -62,33 +62,33 @@ public class LocatedSubString extends ExtraData implements Comparable<com.gitee.
     }
 
     /** 子文本内容[即group(0)] **/
-    public String getTextContent() {
-        return textContent;
+    public String getMatchContent() {
+        return matchContent;
     }
 
-    /** 子文本内容[即group(0)] **/
-    public void setTextContent(String textContent) {
-        this.textContent = textContent;
+    /** 匹配项内容[即group(0)] **/
+    public void setMatchContent(String matchContent) {
+        this.matchContent = matchContent;
     }
 
-    /** 子文本在原文本的开始位置 **/
-    public int getTextStartIndex() {
-        return textStartIndex;
+    /** 匹配项在原文本的开始位置 **/
+    public int getMatchStartIndex() {
+        return matchStartIndex;
     }
 
-    /** 子文本在原文本的开始位置 **/
-    public void setTextStartIndex(int textStartIndex) {
-        this.textStartIndex = textStartIndex;
+    /** 匹配项在原文本的开始位置 **/
+    public void setMatchStartIndex(int matchStartIndex) {
+        this.matchStartIndex = matchStartIndex;
     }
 
-    /** 子文本在原文本的结束位置 **/
-    public int getTextEndIndex() {
-        return textEndIndex;
+    /** 匹配项在原文本的结束位置 **/
+    public int getMatchEndIndex() {
+        return matchEndIndex;
     }
 
-    /** 子文本在原文本的结束位置 **/
-    public void setTextEndIndex(int textEndIndex) {
-        this.textEndIndex = textEndIndex;
+    /** 匹配项在原文本的结束位置 **/
+    public void setMatchEndIndex(int matchEndIndex) {
+        this.matchEndIndex = matchEndIndex;
     }
 
     @Override
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
index 3a0277b..21004dc 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
@@ -171,9 +171,9 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
 
     private GroupSubString newGroupSubString(Matcher matcher) {
         GroupSubString result = new GroupSubString();
-        result.setTextContent(matcher.group());
-        result.setTextStartIndex(matcher.start());
-        result.setTextEndIndex(matcher.end());
+        result.setMatchContent(matcher.group());
+        result.setMatchStartIndex(matcher.start());
+        result.setMatchEndIndex(matcher.end());
         if (digitGroups != null) {
             for (int group : digitGroups) {
                 String content = matcher.group(group);
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
index 11d582e..66f1e5b 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
@@ -146,16 +146,16 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
 
     private LocatedSubString newLocationString(Matcher matcher) {
         LocatedSubString result = new LocatedSubString();
-        result.setTextContent(matcher.group());
-        result.setTextEndIndex(matcher.start());
-        result.setTextEndIndex(matcher.end());
+        result.setMatchContent(matcher.group());
+        result.setMatchStartIndex(matcher.start());
+        result.setMatchEndIndex(matcher.end());
         if (this.digitGroup != null) {
             result.setGroupContent(matcher.group(this.digitGroup));
-            result.setGroupEndIndex(matcher.start(this.digitGroup));
+            result.setGroupStartIndex(matcher.start(this.digitGroup));
             result.setGroupEndIndex(matcher.end(this.digitGroup));
         } else {
             result.setGroupContent(matcher.group(this.namedGroup));
-            result.setGroupEndIndex(matcher.start(this.namedGroup));
+            result.setGroupStartIndex(matcher.start(this.namedGroup));
             result.setGroupEndIndex(matcher.end(this.namedGroup));
         }
         return result;
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionContext.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionContext.java
index 02edc1d..86a459a 100644
--- a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionContext.java
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionContext.java
@@ -23,7 +23,7 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
 public class ExpressionContext {
 
     /** 预置环境变量, 通过#{xxx}取值 **/
-    private final Map<String, Object> preset = new HashMap<>();
+    private final ExpressionMap preset = new ExpressionMap();
     /** 类的导入信息, key=&#64;类名简称, value=类名全称 **/
     private final Map<String, String> imports = new HashMap<>();
     /** 值栈包装类的构造函数 **/
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java
new file mode 100644
index 0000000..2041d1e
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java
@@ -0,0 +1,183 @@
+package com.gitee.qdbp.able.model.reusable;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.Map;
+import com.gitee.qdbp.able.exception.FormatException;
+
+/**
+ * ExpressionExecutor
+ *
+ * @author zhaohuihua
+ * @version 20210808
+ */
+public abstract class ExpressionExecutor extends ExpressionContext {
+
+    private static final Integer RESULT_SCALE = null;
+    private static final Integer CALC_SCALE = 18;
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression) throws FormatException {
+        return doEvaluate(expression, RESULT_SCALE, CALC_SCALE, null);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression, Map<String, Object> root) throws FormatException {
+        return doEvaluate(expression, RESULT_SCALE, CALC_SCALE, root);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param scale 结果保留几位小数 (只在结果是数字时生效)
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression, int scale, Map<String, Object> root) throws FormatException {
+        return doEvaluate(expression, scale, CALC_SCALE, root);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param resultScale 结果保留几位小数 (只在结果是数字时生效)
+     * @param calcScale 计算过程中的数字保留几位小数
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression, int resultScale, int calcScale, Map<String, Object> root)
+            throws FormatException {
+        return doEvaluate(expression, resultScale, calcScale, root);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param useBigDecimal 是否转换为BigDecimal以解决浮点数计算精度的问题
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression, boolean useBigDecimal, Map<String, Object> root) throws FormatException {
+        return doEvaluate(expression, null, useBigDecimal ? CALC_SCALE : null, root);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param resultScale 结果保留几位小数 (只在结果是数字时生效)
+     * @param calcScale 计算过程中的数字保留几位小数
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    protected Object doEvaluate(String expression, Integer resultScale, Integer calcScale, Map<String, Object> root)
+            throws FormatException {
+        // 替换静态方法及自定义方法名
+        String newExpression = replaceCustomizedFunctions(expression);
+        ExpressionMap map = new ExpressionMap();
+        if (root != null && !root.isEmpty()) {
+            map.putAll(root);
+        }
+
+        // 将值栈自身以ThisStack为名保存至栈中, 以实现在业务侧自定义值栈自身的函数, 如@contains(user.name)
+        // BaseContext.parseStackFunction将@contains(user.name)替换为ThisStack.contains('user.name')
+        StackWrapper thisStack = createStackWrapperInstance(map);
+        map.put("ThisStack", thisStack);
+
+        return evaluate(newExpression, resultScale, calcScale, map);
+    }
+
+    protected abstract Object evaluate(String expression, Integer resultScale, Integer calcScale,
+            Map<String, Object> root);
+
+    /**
+     * 如果对象是数字, 则转换为BigDecimal
+     * @param object 目标对象
+     * @param scale 小数位数
+     * @return 最终结果
+     */
+    public static Object tryConvertToDecimal(Object object, Integer scale) {
+        if (object instanceof BigDecimal) {
+            BigDecimal number = (BigDecimal) object;
+            if (number.scale() >= scale) {
+                return number;
+            } else {
+                return number.setScale(scale, BigDecimal.ROUND_HALF_UP);
+            }
+        } else if (object instanceof BigInteger) {
+            return new BigDecimal(object.toString()).setScale(scale, BigDecimal.ROUND_HALF_UP);
+        } else if (object instanceof Number) {
+            return new BigDecimal(String.valueOf(object)).setScale(scale, BigDecimal.ROUND_HALF_UP);
+        } else {
+            return object;
+        }
+    }
+
+    /**
+     * 如果对象是数字, 则设置数字的精度
+     * @param object 目标对象
+     * @param scale 小数位数
+     * @return 最终结果
+     */
+    public static Object trySetDecimalScale(Object object, Integer scale) {
+        if (object instanceof BigDecimal) {
+            BigDecimal number = (BigDecimal) object;
+            if (scale == null) {
+                return clearTrailingZeros(number);
+            } else if (number.scale() > scale) {
+                return number.setScale(scale, BigDecimal.ROUND_HALF_UP);
+            }
+        } else if (object instanceof Double) {
+            BigDecimal number = new BigDecimal(String.valueOf(object));
+            if (scale == null) {
+                return clearTrailingZeros(number);
+            } else if (number.scale() > scale) {
+                return number.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
+            }
+        } else if (object instanceof Float) {
+            BigDecimal number = new BigDecimal(String.valueOf(object));
+            if (scale == null) {
+                return clearTrailingZeros(number);
+            } else if (number.scale() > scale) {
+                return number.setScale(scale, BigDecimal.ROUND_HALF_UP).floatValue();
+            }
+        }
+        return object;
+    }
+
+    /**
+     * 清除科学计数法
+     * @param number 目标数字
+     * @return 最终结果
+     */
+    public static BigDecimal clearTrailingZeros(BigDecimal number) {
+        number = number.stripTrailingZeros();
+        // new BigDecimal(20000).stripTrailingZeros(); = 2E+4 (科学计数法)
+        // 此时precision=1,scale=-4; 需要将scale设置为0
+        if (number.scale() < 0) {
+            number = number.setScale(0, BigDecimal.ROUND_HALF_UP);
+        }
+        return number;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionMap.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionMap.java
new file mode 100644
index 0000000..2b611d4
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionMap.java
@@ -0,0 +1,106 @@
+package com.gitee.qdbp.able.model.reusable;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 表达式Map, 获取数字时会转换为BigDecimal, 解决浮点数计算的精度问题
+ *
+ * @author zhaohuihua
+ * @version 20220317
+ */
+public class ExpressionMap extends HashMap<String, Object> {
+    private static final long serialVersionUID = -6647493913834307155L;
+    private static final Integer DECIMAL_CALC_SCALE = 18;
+
+    private Integer decimalCalcScale = DECIMAL_CALC_SCALE;
+
+    /** 构造函数 **/
+    public ExpressionMap() {
+    }
+
+    /** 构造函数 **/
+    public ExpressionMap(Map<String, Object> map) {
+        this.putAll(map);
+    }
+
+    /** 构造函数 **/
+    public ExpressionMap(String key, Object value) {
+        this.put(key, value);
+    }
+
+    /** 获取值, 如果值是数字, 将会转换为BigDecimal **/
+    @Override
+    public Object get(Object key) {
+        Object value = super.get(key);
+        return ExpressionExecutor.tryConvertToDecimal(value, decimalCalcScale);
+    }
+
+    /** 获取原值 **/
+    public Object getOriginal(Object key) {
+        return super.get(key);
+    }
+
+    @Override
+    public Object put(String key, Object value) {
+        return super.put(key, convertFieldToExpressionMap(value, decimalCalcScale));
+    }
+
+    @Override
+    public void putAll(Map<? extends String, ?> map) {
+        for (Map.Entry<? extends String, ?> entry : map.entrySet()) {
+            this.put(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /** 数字在计算过程中保留的小数位数 **/
+    public Integer getDecimalCalcScale() {
+        return decimalCalcScale;
+    }
+
+    /** 数字在计算过程中保留的小数位数 **/
+    public void setDecimalCalcScale(Integer decimalCalcScale) {
+        this.decimalCalcScale = decimalCalcScale;
+    }
+
+    private static Object convertFieldToExpressionMap(Object object, int scale) {
+        if (object == null) {
+            return null;
+        }
+        if (object instanceof ExpressionMap) {
+            return object;
+        }
+        if (object instanceof Map) {
+            Map<?, ?> map = (Map<?, ?>) object;
+            ExpressionMap result = new ExpressionMap();
+            result.setDecimalCalcScale(scale);
+            for (Map.Entry<?, ?> entry : map.entrySet()) {
+                String key = entry.getKey() == null ? null : entry.getKey().toString();
+                result.put(key, convertFieldToExpressionMap(entry.getValue(), scale));
+            }
+            return result;
+        } else if (object.getClass().isArray()) {
+            List<Object> result = new ArrayList<>();
+            int size = Array.getLength(object);
+            for (int i = 0; i < size; i++) {
+                Object item = Array.get(object, i);
+                result.add(convertFieldToExpressionMap(item, scale));
+            }
+            return result.toArray();
+        } else if (object instanceof Iterable) {
+            Iterable<?> iterable = (Iterable<?>) object;
+            List<Object> result = new ArrayList<>();
+            for (Object item : iterable) {
+                result.add(convertFieldToExpressionMap(item, scale));
+            }
+            return result;
+        } else {
+            // 普通对象不能自动转换为map, 因为EL表达式中有可能会调用对象的方法
+            // Map<String, Object> map = JsonTools.beanToMap(object, true, false);
+            return object;
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index d706004..78dd397 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -11,6 +11,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import com.gitee.qdbp.able.beans.DepthMap;
+import com.gitee.qdbp.able.model.reusable.ExpressionMap;
 
 /**
  * Map工具类<br>
@@ -265,7 +266,11 @@ public class MapTools {
      */
     public static Object getValue(Map<?, ?> map, String key) {
         if (key == null || map.containsKey(key)) {
-            return map.get(key);
+            if (map instanceof ExpressionMap) {
+                return ((ExpressionMap)map).getOriginal(key);
+            } else {
+                return map.get(key);
+            }
         } else if (key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0) {
             return ReflectTools.getDepthValue(map, key);
         } else {
@@ -275,7 +280,7 @@ public class MapTools {
 
     /**
      * 获取指定类型的Map值<br>
-     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getValue("xxx", Boolean.class)将返回null
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
      * 
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
@@ -290,7 +295,7 @@ public class MapTools {
 
     /**
      * 获取指定类型的Map值<br>
-     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getValue("xxx", Boolean.class)将返回null
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
      * 
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
index 8204c94..4da9775 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
@@ -15,6 +15,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import com.gitee.qdbp.able.model.reusable.ExpressionMap;
 import com.gitee.qdbp.tools.parse.StringParser;
 
 /**
@@ -93,7 +94,11 @@ public abstract class ReflectTools {
     }
 
     private static Object getMapFieldValue(Map<?, ?> map, String fieldName) {
-        return map.get(fieldName);
+        if (map instanceof ExpressionMap) {
+            return ((ExpressionMap) map).getOriginal(fieldName);
+        } else {
+            return map.get(fieldName);
+        }
     }
 
     private static Object getArrayFieldValue(Object array, String fieldName) {
@@ -1200,7 +1205,7 @@ public abstract class ReflectTools {
         }
     }
 
-    @SuppressWarnings({ "unchecked" })
+    @SuppressWarnings({"unchecked"})
     private static <T> Constructor<T> eachFindConstructor(Class<T> clazz, boolean throwOnNotFound, Class<?>... types) {
         // 直接采用clazz.getConstructor(types)的方式
         // 会出现根据int找不到clazzName(Integer), 根据Integer找不到clazzName(Object)的情况
-- 
Gitee


From a81ee8f86fb35daf132cfc8507655edb7935ea9e Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 19 Mar 2022 12:03:05 +0800
Subject: [PATCH 029/160] =?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F=E8=AE=A1?=
 =?UTF-8?q?=E7=AE=97=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/OgnlEvaluator.java | 99 +------------------
 .../com/gitee/qdbp/tools/utils/OgnlTools.java | 99 ++-----------------
 .../able/matches/StringExtractorTest.java     | 62 +++++++++++-
 .../qdbp/tools/utils/OgnlEvaluatorTest.java   |  9 +-
 .../qdbp/tools/utils/OgnlExpressionTest.java  | 21 +++-
 .../gitee/qdbp/tools/utils/OgnlToolsTest.java | 10 +-
 6 files changed, 103 insertions(+), 197 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlEvaluator.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlEvaluator.java
index 57e4c27..88f575e 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlEvaluator.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlEvaluator.java
@@ -1,10 +1,7 @@
 package com.gitee.qdbp.tools.utils;
 
-import java.util.HashMap;
 import java.util.Map;
-import com.gitee.qdbp.able.exception.FormatException;
-import com.gitee.qdbp.able.model.reusable.ExpressionContext;
-import com.gitee.qdbp.able.model.reusable.StackWrapper;
+import com.gitee.qdbp.able.model.reusable.ExpressionExecutor;
 
 /**
  * Ognl计算处理类<br>
@@ -14,96 +11,10 @@ import com.gitee.qdbp.able.model.reusable.StackWrapper;
  * @author zhaohuihua
  * @version 20210808
  */
-public class OgnlEvaluator extends ExpressionContext {
+public class OgnlEvaluator extends ExpressionExecutor {
 
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression) throws FormatException {
-        return doEvaluate(expression, OgnlTools.RESULT_SCALE, OgnlTools.CALC_SCALE, null);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression, Map<String, Object> root) throws FormatException {
-        return doEvaluate(expression, OgnlTools.RESULT_SCALE, OgnlTools.CALC_SCALE, root);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param scale 结果保留几位小数 (只在结果是数字时生效)
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression, int scale, Map<String, Object> root) throws FormatException {
-        return doEvaluate(expression, scale, OgnlTools.CALC_SCALE, root);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param resultScale 结果保留几位小数 (只在结果是数字时生效)
-     * @param calcScale 计算过程中的数字保留几位小数
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression, int resultScale, int calcScale, Map<String, Object> root)
-            throws FormatException {
-        return doEvaluate(expression, resultScale, calcScale, root);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param useBigDecimal 是否转换为BigDecimal以解决浮点数计算精度的问题
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression, boolean useBigDecimal, Map<String, Object> root) throws FormatException {
-        return doEvaluate(expression, null, useBigDecimal ? OgnlTools.CALC_SCALE : null, root);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param resultScale 结果保留几位小数 (只在结果是数字时生效)
-     * @param calcScale 计算过程中的数字保留几位小数
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    protected Object doEvaluate(String expression, Integer resultScale, Integer calcScale, Map<String, Object> root)
-            throws FormatException {
-        // 替换静态方法及自定义方法名
-        String newExpression = replaceCustomizedFunctions(expression);
-        Map<String, Object> map = new HashMap<>();
-        if (root != null && !root.isEmpty()) {
-            map.putAll(root);
-        }
-
-        // 将值栈自身以ThisStack为名保存至栈中, 以实现在业务侧自定义值栈自身的函数, 如@contains(user.name)
-        // BaseContext.parseStackFunction将@contains(user.name)替换为ThisStack.contains('user.name')
-        StackWrapper thisStack = createStackWrapperInstance(map);
-        map.put("ThisStack", thisStack);
-
-        return OgnlTools.doEvaluate(newExpression, resultScale, calcScale, map);
+    @Override
+    protected Object evaluate(String expression, Integer resultScale, Integer calcScale, Map<String, Object> root) {
+        return OgnlTools.doEvaluate(expression, resultScale, calcScale, root);
     }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlTools.java
index 3212578..51f18d6 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlTools.java
@@ -1,13 +1,10 @@
 package com.gitee.qdbp.tools.utils;
 
-import java.lang.reflect.Array;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import com.gitee.qdbp.able.exception.FormatException;
+import com.gitee.qdbp.able.model.reusable.ExpressionExecutor;
+import com.gitee.qdbp.able.model.reusable.ExpressionMap;
 import ognl.ExpressionSyntaxException;
 import ognl.InappropriateExpressionException;
 import ognl.MethodFailedException;
@@ -112,14 +109,18 @@ public class OgnlTools {
         }
 
         Object vars = root;
-        if (root != null && calcScale != null) {
-            vars = convertFieldToBigDecimal(root, calcScale);
+        if (root != null && calcScale != null && !(root instanceof ExpressionMap)) {
+            Map<String, Object> json = JsonTools.beanToMap(root);
+            ExpressionMap map = new ExpressionMap();
+            map.setDecimalCalcScale(calcScale);
+            map.putAll(json);
+            vars = map;
         }
 
         OgnlContext context = new OgnlContext(null, null, OgnlMemberAccess.ALLOW_ALL_ACCESS);
         try {
             Object result = Ognl.getValue(expression, context, vars);
-            return setResultScale(result, resultScale);
+            return ExpressionExecutor.trySetDecimalScale(result, resultScale);
         } catch (ExpressionSyntaxException e) {
             throw new FormatException("ExpressionSyntaxException: " + expression, e);
         } catch (MethodFailedException e) {
@@ -139,88 +140,6 @@ public class OgnlTools {
         }
     }
 
-    private static Object setResultScale(Object result, Integer scale) {
-        if (result instanceof BigDecimal) {
-            BigDecimal number = (BigDecimal) result;
-            if (scale == null) {
-                return clearTrailingZeros(number);
-            } else if (number.scale() > scale) {
-                return number.setScale(scale, BigDecimal.ROUND_HALF_UP);
-            }
-        } else if (result instanceof Double) {
-            BigDecimal number = new BigDecimal(String.valueOf(result));
-            if (scale == null) {
-                return clearTrailingZeros(number);
-            } else if (number.scale() > scale) {
-                return number.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
-            }
-        } else if (result instanceof Float) {
-            BigDecimal number = new BigDecimal(String.valueOf(result));
-            if (scale == null) {
-                return clearTrailingZeros(number);
-            } else if (number.scale() > scale) {
-                return number.setScale(scale, BigDecimal.ROUND_HALF_UP).floatValue();
-            }
-        }
-        return result;
-    }
-
-    private static BigDecimal clearTrailingZeros(BigDecimal number) {
-        number = number.stripTrailingZeros();
-        // new BigDecimal(20000).stripTrailingZeros(); = 2E+4 (科学计数法)
-        // 此时precision=1,scale=-4; 需要将scale设置为0
-        if (number.scale() < 0) {
-            number = number.setScale(0, BigDecimal.ROUND_HALF_UP);
-        }
-        return number;
-    }
-
-    private static Object convertFieldToBigDecimal(Object object, int scale) {
-        if (object == null) {
-            return null;
-        }
-        if (object instanceof BigDecimal) {
-            BigDecimal number = (BigDecimal) object;
-            if (number.scale() >= scale) {
-                return number;
-            } else {
-                return number.setScale(scale, BigDecimal.ROUND_HALF_UP);
-            }
-        } else if (object instanceof BigInteger) {
-            return new BigDecimal(object.toString()).setScale(scale, BigDecimal.ROUND_HALF_UP);
-        } else if (object instanceof Number) {
-            return new BigDecimal(String.valueOf(object)).setScale(scale, BigDecimal.ROUND_HALF_UP);
-        } else if (ReflectTools.isPrimitive(object.getClass(), false)) {
-            return object;
-        } else if (object instanceof Map) {
-            Map<?, ?> map = (Map<?, ?>) object;
-            Map<Object, Object> result = new HashMap<>();
-            for (Map.Entry<?, ?> entry : map.entrySet()) {
-                result.put(entry.getKey(), convertFieldToBigDecimal(entry.getValue(), scale));
-            }
-            return result;
-        } else if (object.getClass().isArray()) {
-            List<Object> result = new ArrayList<>();
-            int size = Array.getLength(object);
-            for (int i = 0; i < size; i++) {
-                Object item = Array.get(object, i);
-                result.add(convertFieldToBigDecimal(item, scale));
-            }
-            return result.toArray();
-        } else if (object instanceof Iterable) {
-            Iterable<?> iterable = (Iterable<?>) object;
-            List<Object> result = new ArrayList<>();
-            for (Object item : iterable) {
-                result.add(convertFieldToBigDecimal(item, scale));
-            }
-            return result;
-        } else {
-            // 普通对象不能自动转换为map, 因为EL表达式中有可能会调用对象的方法
-            // Map<String, Object> map = JsonTools.beanToMap(object, true, false);
-            return object;
-        }
-    }
-
     /**
      * 从容器中获取Ognl表达式的值
      *
diff --git a/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
index 195e466..46e88dd 100644
--- a/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
@@ -1,5 +1,7 @@
 package com.gitee.qdbp.able.matches;
 
+import org.testng.Assert;
+import org.testng.annotations.Test;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 
 /**
@@ -8,12 +10,62 @@ import com.gitee.qdbp.tools.utils.ConvertTools;
  * @author zhaohuihua
  * @version 20220112
  */
+@Test
 public class StringExtractorTest {
 
-    public static void main(String[] args) {
-        RegexpStringExtractor extractor = new RegexpStringExtractor("(\\d+)", 1);
-        String string = "aabbcc1122ddee33ff44";
-        LocatedSubStrings result = extractor.extractAll(string);
-        System.out.println(ConvertTools.joinToString(result, '\n'));
+    @Test
+    public void testStringExtractor() {
+        RegexpStringExtractor extractor = new RegexpStringExtractor("[+-]?(\\d+)", 1);
+        String string = "aabbcc+1122ddee-33ff44";
+        LocatedSubString first = extractor.extractFirst(string);
+        System.out.println(first);
+        Assert.assertEquals(first.getGroupContent(), "1122");
+        Assert.assertEquals(first.getGroupStartIndex(), 7);
+        Assert.assertEquals(first.getGroupEndIndex(), 11);
+        Assert.assertEquals(first.getMatchContent(), "+1122");
+        Assert.assertEquals(first.getMatchStartIndex(), 6);
+        Assert.assertEquals(first.getMatchEndIndex(), 11);
+        System.out.println("------------");
+        LocatedSubStrings list = extractor.extractAll(string);
+        System.out.println(ConvertTools.joinToString(list, '\n'));
+        Assert.assertEquals(list.size(), 3);
+        LocatedSubString second = list.get(1);
+        Assert.assertEquals(second.getGroupContent(), "33");
+        Assert.assertEquals(second.getGroupStartIndex(), 16);
+        Assert.assertEquals(second.getGroupEndIndex(), 18);
+        Assert.assertEquals(second.getMatchContent(), "-33");
+        Assert.assertEquals(second.getMatchStartIndex(), 15);
+        Assert.assertEquals(second.getMatchEndIndex(), 18);
+    }
+
+    @Test
+    public void testGroupExtractor() {
+        RegexpGroupExtractor extractor = new RegexpGroupExtractor("([+-])?(\\d+)", 1, 2);
+        String string = "aabbcc+1122ddee-33ff44";
+        GroupSubString first = extractor.extractFirst(string);
+        System.out.println(first);
+        Assert.assertEquals(first.get(1).getContent(), "+");
+        Assert.assertEquals(first.get(1).getStartIndex(), 6);
+        Assert.assertEquals(first.get(1).getEndIndex(), 7);
+        Assert.assertEquals(first.get(2).getContent(), "1122");
+        Assert.assertEquals(first.get(2).getStartIndex(), 7);
+        Assert.assertEquals(first.get(2).getEndIndex(), 11);
+        Assert.assertEquals(first.getMatchContent(), "+1122");
+        Assert.assertEquals(first.getMatchStartIndex(), 6);
+        Assert.assertEquals(first.getMatchEndIndex(), 11);
+        System.out.println("------------");
+        GroupSubStrings list = extractor.extractAll(string);
+        System.out.println(ConvertTools.joinToString(list, '\n'));
+        Assert.assertEquals(list.size(), 3);
+        GroupSubString second = list.get(1);
+        Assert.assertEquals(second.get(1).getContent(), "-");
+        Assert.assertEquals(second.get(1).getStartIndex(), 15);
+        Assert.assertEquals(second.get(1).getEndIndex(), 16);
+        Assert.assertEquals(second.get(2).getContent(), "33");
+        Assert.assertEquals(second.get(2).getStartIndex(), 16);
+        Assert.assertEquals(second.get(2).getEndIndex(), 18);
+        Assert.assertEquals(second.getMatchContent(), "-33");
+        Assert.assertEquals(second.getMatchStartIndex(), 15);
+        Assert.assertEquals(second.getMatchEndIndex(), 18);
     }
 }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlEvaluatorTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlEvaluatorTest.java
index 700476b..9270ea9 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlEvaluatorTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlEvaluatorTest.java
@@ -3,6 +3,8 @@ package com.gitee.qdbp.tools.utils;
 import java.math.BigDecimal;
 import java.util.HashMap;
 import java.util.Map;
+import org.testng.Assert;
+import org.testng.annotations.Test;
 import com.gitee.qdbp.able.model.reusable.StackWrapper;
 
 /**
@@ -11,9 +13,11 @@ import com.gitee.qdbp.able.model.reusable.StackWrapper;
  * @author zhaohuihua
  * @version 20210808
  */
+@Test
 public class OgnlEvaluatorTest {
 
-    public static void main(String[] args) {
+    @Test
+    public void test() {
         Map<String, Object> user = new HashMap<>();
         user.put("email", "abc@dd.com");
         Map<String, Object> map = new HashMap<>();
@@ -26,6 +30,7 @@ public class OgnlEvaluatorTest {
         // 再在表达式中使用
         Object isEmail = evaluator.evaluate("@StringTools.isEmail(user.email)", map);
         System.out.println("isEmail=" + isEmail);
+        Assert.assertEquals(isEmail, Boolean.TRUE, "isEmail");
 
         user.put("x", 100);
         user.put("y", 200);
@@ -34,9 +39,11 @@ public class OgnlEvaluatorTest {
         // 自定义函数sum来自CustomizedFunctions
         Object sum = evaluator.evaluate("@sum(user.x,user.y,user.z,300)", map);
         System.out.println("sum=" + sum);
+        Assert.assertEquals(sum, new BigDecimal("600"), "sum result");
         // 自定义函数has来自StackWrapper, 判断KEY是否存在
         Object has = evaluator.evaluate("@has(user.a.b.c)", map);
         System.out.println("has=" + has);
+        Assert.assertEquals(has, Boolean.FALSE, "@has(user.a.b.c)");
     }
 
     protected static class CustomizedFunctions extends StackWrapper {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlExpressionTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlExpressionTest.java
index 35ad50f..9edad6e 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlExpressionTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlExpressionTest.java
@@ -13,7 +13,7 @@ public class OgnlExpressionTest {
     public void test() throws Exception {
         { // Java精度问题: 0.9-1=-0.09999999999999998
             double result = 0.9 - 1;
-            System.out.printf("%-10s %s%n", "Java:", result);
+            System.out.printf("%-10s %s=%s%n", "Java:", "0.9-1", result);
         }
         { // Ognl支持BigDecimal: 0.9-1=-0.1
             String expression = "收入-支出";
@@ -21,7 +21,18 @@ public class OgnlExpressionTest {
             map.put("收入", 0.9);
             map.put("支出", 1);
             Object result = OgnlTools.evaluate(expression, map);
-            System.out.printf("%-10s %s=%s%n", "Ognl:", "0.9-1", result);
+            System.out.printf("%-10s %s=%s \t%s%n", "Ognl:", "0.9-1", result, expression);
+            Assert.assertEquals(result, new BigDecimal("-0.1"));
+        }
+        { // Ognl支持BigDecimal: 0.9-1=-0.1
+            String expression = "$2020年.收入-$2020年.支出";
+            Map<String, Number> map = new HashMap<>();
+            map.put("收入", 0.9);
+            map.put("支出", 1);
+            Map<String, Object> root = new HashMap<>();
+            root.put("$2020年", map);
+            Object result = OgnlTools.evaluate(expression, root);
+            System.out.printf("%-10s %s=%s \t%s%n", "Ognl:", "0.9-1", result, expression);
             Assert.assertEquals(result, new BigDecimal("-0.1"));
         }
         { // 加法
@@ -30,7 +41,7 @@ public class OgnlExpressionTest {
             map.put("x", 1);
             map.put("y", 3);
             Object result = OgnlTools.evaluate(expression, map);
-            System.out.printf("%-10s %s=%s%n", "加法:", "1+3", result);
+            System.out.printf("%-10s %s=%s \t%s%n", "加法:", "1+3", result, expression);
             Assert.assertEquals(result, new BigDecimal("4"));
         }
         { // 除法
@@ -39,7 +50,7 @@ public class OgnlExpressionTest {
             map.put("x", 1);
             map.put("y", 3);
             Object result = OgnlTools.evaluate(expression, 4, map);
-            System.out.printf("%-10s %s=%s%n", "除法:", "1/3", result);
+            System.out.printf("%-10s %s=%s \t%s%n", "除法:", "1/3", result, expression);
             Assert.assertEquals(result, new BigDecimal("0.3333"));
         }
         { // 复杂表达式: 速动比率=(流动资产-存货-预付账款)/流动负债
@@ -50,7 +61,7 @@ public class OgnlExpressionTest {
             map.put("预付账款", 98);
             map.put("流动负债", 289);
             Object result = OgnlTools.evaluate(expression, 4, map);
-            System.out.printf("%-10s %s%n", "速动比率:", result);
+            System.out.printf("%-10s %s \t%s%n", "速动比率:", result, expression);
             Assert.assertEquals(result, new BigDecimal("2.2042"));
         }
         { // 结果不能是科学计数法
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java
index 1156878..090c634 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java
@@ -5,6 +5,7 @@ import java.util.List;
 import java.util.Map;
 import org.testng.Assert;
 import org.testng.annotations.Test;
+import com.alibaba.fastjson.JSONObject;
 
 @Test
 public class OgnlToolsTest {
@@ -30,13 +31,18 @@ public class OgnlToolsTest {
 
         String result1 = JsonTools.toLogString(map);
         System.out.println(result1);
-        Assert.assertEquals(result1, "{userBean:{address:{id:\"A0001\",name:\"Home\"}}}");
+        Map<String, Object> actual1 = JsonTools.beanToMap(map);
+        Map<String, Object> expected1 = (JSONObject) JSONObject.parse("{userBean:{address:{id:\"A0001\",name:\"Home\"}}}");
+        AssertTools.assertDeepEquals(actual1, expected1);
         OgnlTools.setValue(map, "userBean.address.details", "Beijing China");
         OgnlTools.setValue(map, "test.address.id", "A0009");
         OgnlTools.setValue(map, "test.address.details", "Nanjing China");
         String result2 = JsonTools.toLogString(map);
         System.out.println(result2);
-        Assert.assertEquals(result2, "{test:{address:{id:\"A0009\",details:\"Nanjing China\"}},userBean:{address:{details:\"Beijing China\",id:\"A0001\",name:\"Home\"}}}");
+        Map<String, Object> actual2 = JsonTools.beanToMap(map);
+        Map<String, Object> expected2 = (JSONObject) JSONObject.parse(
+                "{test:{address:{id:\"A0009\",details:\"Nanjing China\"}},userBean:{address:{details:\"Beijing China\",id:\"A0001\",name:\"Home\"}}}");
+        AssertTools.assertDeepEquals(actual2, expected2);
     }
 
     protected static class Address {
-- 
Gitee


From 6a9dae1a052721a681dd1a84314c22ad7cc02b40 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 19 Mar 2022 12:05:08 +0800
Subject: [PATCH 030/160] 5.4.15

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index edd6fa6..f5726cd 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.13</version>
+	<version>5.4.15</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index 6ce6a98..a2fb34b 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.13</version>
+	<version>5.4.15</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 5724ad15e47e900dec30a0ea846289191c90611a Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 19 Mar 2022 14:36:02 +0800
Subject: [PATCH 031/160] =?UTF-8?q?=E9=9A=8F=E6=9C=BA=E6=95=B0=E5=B7=A5?=
 =?UTF-8?q?=E5=85=B7=E7=B1=BB=E5=A2=9E=E5=8A=A0generateIntNumber/generateL?=
 =?UTF-8?q?ongNumber=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/RandomTools.java   | 36 +++++++++++++++++--
 1 file changed, 34 insertions(+), 2 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java
index deba9f6..93b40a5 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java
@@ -141,7 +141,39 @@ public final class RandomTools {
      * @return 随机数
      */
     public static long generateNumber(final long min, final long max) {
-        return min + RANDOM.nextLong() % (max - min + 1);
+        return min + Math.abs(RANDOM.nextLong()) % (max - min + 1);
+    }
+
+    /**
+     * 生成指定长度的随机数
+     *
+     * @param length 随机数长度 (最大9位)
+     * @return 随机数
+     * @author zhaohuihua
+     */
+    public static int generateIntNumber(final int length) {
+        if (length >= 10) {
+            throw new IllegalArgumentException("length must be less than 10.");
+        }
+        int min = (int) Math.pow(10, length - 1);
+        int max = (int) Math.pow(10, length) - 1;
+        return generateNumber(min, max);
+    }
+
+    /**
+     * 生成指定长度的随机数
+     *
+     * @param length 随机数长度 (最大18位)
+     * @return 随机数
+     * @author zhaohuihua
+     */
+    public static long generateLongNumber(final int length) {
+        if (length >= 19) {
+            throw new IllegalArgumentException("length must be less than 19.");
+        }
+        long min = (long) Math.pow(10, length - 1);
+        long max = (long) Math.pow(10, length) - 1;
+        return generateNumber(min, max);
     }
 
     /**
@@ -150,6 +182,6 @@ public final class RandomTools {
      * @return 序列号
      */
     public static String generateUuid() {
-        return UUID.randomUUID().toString().replace("-", "").toUpperCase();
+        return StringTools.removeChars(UUID.randomUUID().toString(), '-').toUpperCase();
     }
 }
-- 
Gitee


From 0cddf00daef4052f1192ad4323ebdec7eef6dff6 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 19 Mar 2022 14:36:54 +0800
Subject: [PATCH 032/160] =?UTF-8?q?=E6=95=B0=E5=AD=97=E8=BD=AC=E6=8D=A2?=
 =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/ConvertTools.java  | 20 +++++++++++++++----
 1 file changed, 16 insertions(+), 4 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index e4112e8..c572617 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -476,8 +476,10 @@ public class ConvertTools {
             throw new NumberFormatException("null");
         }
         Long number = toLong(value, null);
-        if (number == null || number > Integer.MAX_VALUE) {
+        if (number == null) {
             throw new NumberFormatException(value);
+        } else if (number > Integer.MAX_VALUE) {
+            throw new NumberFormatException(value + " greater than Integer.MAX_VALUE");
         } else {
             return number.intValue();
         }
@@ -492,7 +494,11 @@ public class ConvertTools {
      */
     public static Integer toInteger(String value, Integer defaults) {
         Long number = toLong(value, defaults == null ? null : Long.valueOf(defaults));
-        return number == null || number > Integer.MAX_VALUE ? null : number.intValue();
+        if (number == null || number > Integer.MAX_VALUE) {
+            return defaults;
+        } else {
+            return number.intValue();
+        }
     }
 
     /**
@@ -575,8 +581,10 @@ public class ConvertTools {
             throw new NumberFormatException("null");
         }
         Double number = toDouble(value, null);
-        if (number == null || number > Float.MAX_VALUE) {
+        if (number == null) {
             throw new NumberFormatException(value);
+        } else if (number > Float.MAX_VALUE) {
+            throw new NumberFormatException(value + " greater than Float.MAX_VALUE");
         } else {
             return number.floatValue();
         }
@@ -591,7 +599,11 @@ public class ConvertTools {
      */
     public static Float toFloat(String value, Float defaults) {
         Double number = toDouble(value, defaults == null ? null : Double.valueOf(defaults));
-        return number == null || number > Float.MAX_VALUE ? null : number.floatValue();
+        if (number == null || number > Float.MAX_VALUE) {
+            return defaults;
+        } else {
+            return number.floatValue();
+        }
     }
 
     /**
-- 
Gitee


From 8b5519e40ca1b532649cb2fd6bd02605599d486e Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 26 Mar 2022 15:01:09 +0800
Subject: [PATCH 033/160] =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=B1=BB=E4=BC=98?=
 =?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/able/matches/LocatedSubString.java  | 10 ++++++----
 .../com/gitee/qdbp/tools/utils/RandomTools.java    | 14 ++++++++------
 2 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
index 9cf65af..fa26de3 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
@@ -10,7 +10,7 @@ import com.gitee.qdbp.tools.utils.StringTools;
  * @author zhaohuihua
  * @version 20220306
  */
-public class LocatedSubString extends ExtraData implements Comparable<com.gitee.qdbp.able.beans.SubString> {
+public class LocatedSubString extends ExtraData implements Comparable<LocatedSubString> {
 
     /** serialVersionUID **/
     private static final long serialVersionUID = 1L;
@@ -101,13 +101,15 @@ public class LocatedSubString extends ExtraData implements Comparable<com.gitee.
     }
 
     @Override
-    public int compareTo(com.gitee.qdbp.able.beans.SubString o) {
+    public int compareTo(LocatedSubString o) {
         if (o == null) {
             return -1;
         }
         return CompareTools.stream()
-                .asc(groupStartIndex, o.getStartIndex())
-                .asc(groupEndIndex, o.getEndIndex())
+                .asc(groupStartIndex, o.getGroupEndIndex())
+                .asc(groupEndIndex, o.getGroupEndIndex())
+                .asc(matchStartIndex, o.getMatchStartIndex())
+                .asc(matchEndIndex, o.getMatchEndIndex())
                 .compare();
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java
index 93b40a5..5e01b8a 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java
@@ -98,14 +98,16 @@ public final class RandomTools {
      * @return 随机数
      */
     public static String generateNumber(final int length) {
-        StringBuilder buffer = new StringBuilder();
-        if (length > 0) {
-            // 确保第1位不是0
-            buffer.append(generateNumber(1, 9));
+        if (length <= 0) {
+            return "";
         }
-        if (length > 1) {
-            buffer.append(generateString(NUMBER_SOURCE, length - 1));
+        if (length <= 18) {
+            return String.valueOf(generateLongNumber(length));
         }
+        StringBuilder buffer = new StringBuilder();
+        // 确保第1位不是0
+        buffer.append(generateNumber(1, 9));
+        buffer.append(generateString(NUMBER_SOURCE, length - 1));
         return buffer.toString();
     }
 
-- 
Gitee


From cbc39aaf7973a7a0f883399d0a0d38fdfdfbebe7 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 26 Mar 2022 15:01:43 +0800
Subject: [PATCH 034/160] 5.4.15

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index f5726cd..dd16f70 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.15</version>
+	<version>5.4.16</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index a2fb34b..92d6e9d 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.15</version>
+	<version>5.4.16</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From dfce236dd3f54cb5c8ddb77b2ded20c7d2e8aa14 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 9 Apr 2022 10:52:29 +0800
Subject: [PATCH 035/160] =?UTF-8?q?=E5=8D=95=E8=AF=8D=E5=86=99=E9=94=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/able/jdbc/condition/DbWhere.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/jdbc/condition/DbWhere.java b/able/src/main/java/com/gitee/qdbp/able/jdbc/condition/DbWhere.java
index 3fd1695..4e0bfae 100644
--- a/able/src/main/java/com/gitee/qdbp/able/jdbc/condition/DbWhere.java
+++ b/able/src/main/java/com/gitee/qdbp/able/jdbc/condition/DbWhere.java
@@ -30,7 +30,7 @@ import com.gitee.qdbp.able.jdbc.base.DbCondition;
     where.on("userName").like(entity.getUserName());
     // [ORACLE/DB2] AND USER_NAME NOT LIKE('%'||:$1||'%')
     // [MYSQL] AND USER_NAME NOT LIKE CONCAT('%',:$1,'%')
-    where.on("userName").notLkie(entity.getUserName());
+    where.on("userName").notLike(entity.getUserName());
     // [ORACLE/DB2] AND PHONE LIKE(:$1||'%')
     // [MYSQL] AND PHONE LIKE CONCAT(:$1,'%')
     where.on("phone").startsWith("139");
-- 
Gitee


From 35baa2bc1addc10c6d07389c32c0d6a961711437 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 12 May 2022 21:01:06 +0800
Subject: [PATCH 036/160] =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=97=A5=E6=8E=A5?=
 =?UTF-8?q?=E5=8F=A3=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/able/time/BaseWorkdayResolver.java   | 26 +++++++++++++++++++
 .../able/time/SettingWorkdayResolver.java     | 18 +++++++++----
 .../gitee/qdbp/able/time/WorkdayResolver.java |  2 ++
 3 files changed, 41 insertions(+), 5 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/time/BaseWorkdayResolver.java b/able/src/main/java/com/gitee/qdbp/able/time/BaseWorkdayResolver.java
index c898e5c..594d2c3 100644
--- a/able/src/main/java/com/gitee/qdbp/able/time/BaseWorkdayResolver.java
+++ b/able/src/main/java/com/gitee/qdbp/able/time/BaseWorkdayResolver.java
@@ -86,4 +86,30 @@ public abstract class BaseWorkdayResolver implements WorkdayResolver {
         }
         throw new IllegalStateException("Try " + max + " times and not found working day.");
     }
+
+    /** 统计两个日期之间存在多少个工作日 **/
+    public int countWorkday(Date startDate, Date endDate) {
+        Date start = DateTools.toStartTime(startDate);
+        Date end = DateTools.toStartTime(endDate);
+        if (start.getTime() == end.getTime()) {
+            return 0;
+        }
+        Calendar curr = Calendar.getInstance();
+        Calendar last = Calendar.getInstance();
+        if (start.before(end)) {
+            curr.setTime(start);
+            last.setTime(end);
+        } else {
+            curr.setTime(end);
+            last.setTime(start);
+        }
+        int total = 0;
+        while (curr.before(last)) {
+            curr.add(Calendar.DAY_OF_MONTH, 1);
+            if (isWorkday(curr)) {
+                total++;
+            }
+        }
+        return total;
+    }
 }
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/time/SettingWorkdayResolver.java b/able/src/main/java/com/gitee/qdbp/able/time/SettingWorkdayResolver.java
index d95f5f1..fa59779 100644
--- a/able/src/main/java/com/gitee/qdbp/able/time/SettingWorkdayResolver.java
+++ b/able/src/main/java/com/gitee/qdbp/able/time/SettingWorkdayResolver.java
@@ -41,7 +41,7 @@ public abstract class SettingWorkdayResolver extends BaseWorkdayResolver {
         if (!isCached(day)) {
             findSettings(date.getTime());
         }
-        return workdays.containsKey(day) || !isWeekend(date) && !isHoliday(date);
+        return isWorkday(day, date);
     }
 
     @Override
@@ -57,10 +57,18 @@ public abstract class SettingWorkdayResolver extends BaseWorkdayResolver {
         if (!isCached(day)) {
             findSettings(date.getTime());
         }
+        return isHoliday(day, date);
+    }
+
+    protected boolean isWorkday(int day, Calendar date) {
+        return workdays.containsKey(day) || !isWeekend(date) && !isHoliday(day, date);
+    }
+
+    protected boolean isHoliday(int day, Calendar date) {
         return holidays.containsKey(day);
     }
 
-    private void findSettings(Date date) {
+    protected void findSettings(Date date) {
         // 整月的读取节假日, 并前后各多取1个月的数据
         Date minDate = DateTools.toFirstTime(DateTools.addMonth(date, -1), Calendar.MONTH);
         Date maxDate = DateTools.toLastTime(DateTools.addMonth(date, 1), Calendar.MONTH);
@@ -104,20 +112,20 @@ public abstract class SettingWorkdayResolver extends BaseWorkdayResolver {
         return months.containsKey(day / 100);
     }
 
-    private static int getDateValue(Date date) {
+    protected int getDateValue(Date date) {
         Calendar calendar = Calendar.getInstance();
         calendar.setTime(date);
         return getDateValue(calendar);
     }
 
-    private static int getDateValue(Calendar calendar) {
+    private int getDateValue(Calendar calendar) {
         int year = calendar.get(Calendar.YEAR);
         int month = calendar.get(Calendar.MONTH) + 1;
         int day = calendar.get(Calendar.DAY_OF_MONTH);
         return year * 10000 + month * 100 + day;
     }
 
-    protected static class Setting implements Serializable {
+    public static class Setting implements Serializable {
         private static final long serialVersionUID = -4206801241818872929L;
 
         private List<Date> holidays;
diff --git a/able/src/main/java/com/gitee/qdbp/able/time/WorkdayResolver.java b/able/src/main/java/com/gitee/qdbp/able/time/WorkdayResolver.java
index f0a793b..b2c779b 100644
--- a/able/src/main/java/com/gitee/qdbp/able/time/WorkdayResolver.java
+++ b/able/src/main/java/com/gitee/qdbp/able/time/WorkdayResolver.java
@@ -28,4 +28,6 @@ public interface WorkdayResolver {
     /** 加几个工作日 (返回的日期必定是工作日) **/
     Date addWorkday(Date date, int offset);
 
+    /** 统计两个日期之间存在多少个工作日 **/
+    int countWorkday(Date start, Date end);
 }
-- 
Gitee


From aeb160ecdcf598e9173e359e5a81a74791ddb917 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 12 May 2022 21:09:42 +0800
Subject: [PATCH 037/160] 5.4.17

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index dd16f70..fc3ee57 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.16</version>
+	<version>5.4.17</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index 92d6e9d..13525a7 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.16</version>
+	<version>5.4.17</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 3c124328672e54c1d2054e421bb0f86f94574e88 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 12 Jun 2022 20:42:58 +0800
Subject: [PATCH 038/160] =?UTF-8?q?=E4=BC=98=E5=8C=96,=20=E5=8E=BB?=
 =?UTF-8?q?=E9=99=A4=E6=AD=A3=E5=88=99(=E7=AE=80=E5=8D=95=E8=A7=84?=
 =?UTF-8?q?=E5=88=99=E4=B8=8D=E9=9C=80=E8=A6=81=E6=AD=A3=E5=88=99)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../excel/condition/IndexListCondition.java   | 67 +++++++++----------
 .../excel/condition/IndexRangeCondition.java  | 50 +++++++-------
 .../excel/condition/NameListCondition.java    | 19 +++---
 .../qdbp/tools/excel/condition/Required.java  | 23 ++++---
 .../qdbp/tools/excel/utils/ExcelHelper.java   | 27 +++++---
 5 files changed, 90 insertions(+), 96 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexListCondition.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexListCondition.java
index a818c7b..1d2c2b5 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexListCondition.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexListCondition.java
@@ -3,13 +3,11 @@ package com.gitee.qdbp.tools.excel.condition;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 序号列表配置类<br>
@@ -29,12 +27,6 @@ public class IndexListCondition implements Serializable {
     /** 日志对象 **/
     private static final Logger log = LoggerFactory.getLogger(IndexListCondition.class);
 
-    /** 以!开头的是排除法配置 **/
-    private static final Pattern EXCLUDE = Pattern.compile("^\\s*!\\s*");
-
-    /** 开始序号至结束序号 **/
-    private static final Pattern BETWEEN = Pattern.compile("^\\s*(\\d+)\\s*-\\s*(\\d+)\\s*$");
-
     /** 是不是全部允许 **/
     private boolean all = false;
 
@@ -84,46 +76,47 @@ public class IndexListCondition implements Serializable {
     }
 
     private void parse(String text, int startBy) {
-
-        if (VerifyTools.isBlank(text) || VerifyTools.isBlank(text.trim())) {
+        if (VerifyTools.isBlank(text)) {
             return;
         }
-
-        if ("*".equals(text.trim())) {
+        text = text.trim();
+        if (VerifyTools.isBlank(text)) {
+            return;
+        }
+        if ("*".equals(text)) {
             this.all = true;
             return;
         }
 
-        Matcher em = EXCLUDE.matcher(text);
-        if (em.find()) {
+        // 以感叹号开头的, 是排除规则
+        if (text.charAt(0) == '!') {
             exclude = true;
-            text = em.replaceFirst("").trim();
+            text = text.substring(1).trim();
         }
         String[] digits = StringTools.split(text);
-        List<Integer> indexs = new ArrayList<>();
+        List<Integer> indexes = new ArrayList<>();
         for (String digit : digits) {
-            digit = digit.trim();
             if (StringTools.isDigit(digit)) {
-                indexs.add(ConvertTools.toInteger(digit) - startBy);
-            } else {
-                Matcher bm = BETWEEN.matcher(digit);
-                if (bm.matches()) {
-                    int start = ConvertTools.toInteger(bm.group(1)) - startBy;
-                    int end = ConvertTools.toInteger(bm.group(2)) - startBy;
-                    if (start > end) {
-                        int temp = start;
-                        start = end;
-                        end = temp;
-                    }
-                    for (int i = start; i <= end; i++) {
-                        indexs.add(i);
-                    }
-                } else {
-                    log.warn("ExcelIndexError: " + digit);
-                }
+                indexes.add(ConvertTools.toInteger(digit) - startBy);
+                continue;
+            }
+            String[] ranges = StringTools.split(digit, '-');
+            if (ranges.length != 2 || !StringTools.isDigit(ranges[0]) || !StringTools.isDigit(ranges[1])) {
+                log.warn("ExcelIndexError: " + digit);
+                continue;
+            }
+            int start = ConvertTools.toInteger(ranges[0]) - startBy;
+            int end = ConvertTools.toInteger(ranges[1]) - startBy;
+            if (start > end) {
+                int temp = start;
+                start = end;
+                end = temp;
+            }
+            for (int i = start; i <= end; i++) {
+                indexes.add(i);
             }
         }
-        this.indexs = indexs;
+        this.indexs = indexes;
     }
 
     public String toString() {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexRangeCondition.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexRangeCondition.java
index 7b187a4..00e37c6 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexRangeCondition.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexRangeCondition.java
@@ -3,13 +3,11 @@ package com.gitee.qdbp.tools.excel.condition;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 序号范围配置类<br>
@@ -26,9 +24,6 @@ public class IndexRangeCondition implements Serializable {
     /** 日志对象 **/
     private static final Logger log = LoggerFactory.getLogger(IndexRangeCondition.class);
 
-    /** 开始序号至结束序号 **/
-    private static final Pattern BETWEEN = Pattern.compile("^\\s*(\\d+)\\s*-\\s*(\\d+)\\s*$");
-
     /** Index列表 **/
     private List<Integer> indexs;
 
@@ -72,33 +67,34 @@ public class IndexRangeCondition implements Serializable {
     }
 
     private void parse(String text, int startBy) {
-
-        if (VerifyTools.isBlank(text) || VerifyTools.isBlank(text.trim())) {
+        if (VerifyTools.isBlank(text)) {
+            return;
+        }
+        text = text.trim();
+        if (VerifyTools.isBlank(text)) {
             return;
         }
-
         String[] digits = StringTools.split(text);
         this.indexs = new ArrayList<>();
         for (String digit : digits) {
-            digit = digit.trim();
             if (StringTools.isDigit(digit)) {
                 addIndex(ConvertTools.toInteger(digit) - startBy);
-            } else {
-                Matcher bm = BETWEEN.matcher(digit);
-                if (bm.matches()) {
-                    int start = ConvertTools.toInteger(bm.group(1)) - startBy;
-                    int end = ConvertTools.toInteger(bm.group(2)) - startBy;
-                    if (start > end) {
-                        int temp = start;
-                        start = end;
-                        end = temp;
-                    }
-                    for (int i = start; i <= end; i++) {
-                        addIndex(i);
-                    }
-                } else {
-                    log.warn("ExcelIndexError: " + digit);
-                }
+                continue;
+            }
+            String[] ranges = StringTools.split(digit, '-');
+            if (ranges.length != 2 || !StringTools.isDigit(ranges[0]) || !StringTools.isDigit(ranges[1])) {
+                log.warn("ExcelIndexError: " + digit);
+                continue;
+            }
+            int start = ConvertTools.toInteger(ranges[0]) - startBy;
+            int end = ConvertTools.toInteger(ranges[1]) - startBy;
+            if (start > end) {
+                int temp = start;
+                start = end;
+                end = temp;
+            }
+            for (int i = start; i <= end; i++) {
+                addIndex(i);
             }
         }
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/NameListCondition.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/NameListCondition.java
index c3466db..c778a45 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/NameListCondition.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/NameListCondition.java
@@ -4,8 +4,6 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
@@ -24,9 +22,6 @@ public class NameListCondition implements Serializable {
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
 
-    /** 以!开头的是排除法配置 **/
-    private static final Pattern EXCLUDE = Pattern.compile("^\\s*!\\s*");
-
     /** 是不是全部允许 **/
     private boolean all = false;
 
@@ -65,8 +60,11 @@ public class NameListCondition implements Serializable {
     }
 
     private void parse(String text) {
-
-        if (VerifyTools.isBlank(text) || VerifyTools.isBlank(text.trim())) {
+        if (VerifyTools.isBlank(text)) {
+            return;
+        }
+        text = text.trim();
+        if (VerifyTools.isBlank(text)) {
             return;
         }
 
@@ -75,10 +73,10 @@ public class NameListCondition implements Serializable {
             return;
         }
 
-        Matcher em = EXCLUDE.matcher(text);
-        if (em.find()) {
+        // 以感叹号开头的, 是排除规则
+        if (text.charAt(0) == '!') {
             exclude = true;
-            text = em.replaceFirst("").trim();
+            text = text.substring(1).trim();
         }
 
         String[] digits = StringTools.split(text);
@@ -89,7 +87,6 @@ public class NameListCondition implements Serializable {
         this.names = names;
     }
 
-
     public String toString() {
         if (all) {
             return "*";
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/Required.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/Required.java
index 0c11450..d049d68 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/Required.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/Required.java
@@ -1,8 +1,6 @@
 package com.gitee.qdbp.tools.excel.condition;
 
 import java.io.Serializable;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -16,9 +14,6 @@ public class Required implements Serializable {
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
 
-    /** 星号开头或(*)结尾的字段为必填字段: [* 姓名] or [姓名 (*)] **/
-    private static final Pattern REQUIRED = Pattern.compile("(^\\s*\\*\\s*|\\s*\\(\\*\\)\\s*$)");
-
     /** 字段名 **/
     private final String name;
 
@@ -43,14 +38,20 @@ public class Required implements Serializable {
     public static Required of(String text) {
         VerifyTools.requireNotBlank(text, "text");
 
-        String string = text.trim();
         // 星号开头或(*)结尾的字段为必填字段
-        Matcher matcher = REQUIRED.matcher(string);
-        if (matcher.find()) {
-            return new Required(matcher.replaceAll(""), true);
-        } else {
-            return new Required(string, false);
+        // * 姓名
+        // 姓名 (*)
+        text = text.trim();
+        if (text.charAt(0) == '*') {
+            String field = text.substring(1);
+            return new Required(field.trim(), true);
+        }
+        String suffix = "(*)";
+        if (text.endsWith(suffix)) {
+            String field = text.substring(0, text.length() - suffix.length());
+            return new Required(field.trim(), true);
         }
+        return new Required(text, false);
     }
 
     public String toString() {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
index 00dd25e..ae15feb 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
@@ -4,7 +4,12 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.regex.Pattern;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Row.MissingCellPolicy;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.SheetFillCallback;
@@ -17,12 +22,6 @@ import com.gitee.qdbp.tools.excel.model.FieldInfo;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Cell;
-import org.apache.poi.ss.usermodel.Row;
-import org.apache.poi.ss.usermodel.Row.MissingCellPolicy;
-import org.apache.poi.ss.usermodel.Sheet;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Excel处理类
@@ -38,8 +37,6 @@ public class ExcelHelper {
 
     private static final Logger log = LoggerFactory.getLogger(ExcelHelper.class);
 
-    private static final Pattern IGNORE_SHEET_NAME = Pattern.compile("sheet\\d*", Pattern.CASE_INSENSITIVE);
-
     public static void parse(Sheet sheet, XMetadata metadata, SheetParseCallback cb) {
 
         String sheetName = sheet.getSheetName();
@@ -119,7 +116,7 @@ public class ExcelHelper {
 
         // Sheet名称填充至指定字段
         if (VerifyTools.isNotBlank(metadata.getSheetNameFillTo())) {
-            if (!IGNORE_SHEET_NAME.matcher(sheetName).matches()) {
+            if (!isIgnoreSheetName(sheetName)) {
                 map.put(metadata.getSheetNameFillTo(), sheetName);
             }
         }
@@ -173,6 +170,16 @@ public class ExcelHelper {
         cb.callback(data, rowInfo);
     }
 
+    private static boolean isIgnoreSheetName(String string) {
+        if (string.startsWith("Sheet") || string.startsWith("sheet")) {
+            String suffix = string.substring(5).trim();
+            if (suffix.length() == 0 || StringTools.isDigit(suffix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public static void fill(List<?> list, Sheet sheet, XMetadata metadata, SheetFillCallback cb) {
 
         String sheetName = sheet.getSheetName();
-- 
Gitee


From 4cec2b7d76fe3963151a4ffbf45939399d109359 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 12 Jun 2022 20:43:41 +0800
Subject: [PATCH 039/160] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/parse/StringAccess.java  | 75 ++++++++++++-------
 1 file changed, 49 insertions(+), 26 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java b/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java
index 2fbd22f..e0c576e 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java
@@ -34,11 +34,11 @@ public class StringAccess {
 
     /**
      * 构造函数
-     * 
+     *
      * @param string 源字符串
      * @param index 开始位置
      * @param multiline 是否支持多行模式: false=最多读取至行尾; true=最多读取至文本结束<br>
-     *            如 读取括号中的内容, (1\n2) 在多行模式下正确; 在单行模式下会报错(因为读取到行尾都没找到结束的括号)
+     * 如 读取括号中的内容, (1\n2) 在多行模式下正确; 在单行模式下会报错(因为读取到行尾都没找到结束的括号)
      */
     public StringAccess(String string, int index, boolean multiline) {
         this.string = string;
@@ -52,7 +52,7 @@ public class StringAccess {
      * 最后一次操作是否成功<br>
      * 遇到结尾导致没读取到内容也算成功(因为结尾可以通过isReadable判断)<br>
      * 只有想读数字却不是数字这种类型的失败才算失败<br>
-     * 
+     *
      * @return 是否成功
      */
     public boolean isLastReadSuccess() {
@@ -69,7 +69,7 @@ public class StringAccess {
 
     /**
      * 获取当前位置
-     * 
+     *
      * @return 当前位置
      */
     public int readerIndex() {
@@ -89,7 +89,7 @@ public class StringAccess {
 
     /**
      * 设置当前位置
-     * 
+     *
      * @param index 指定位置
      * @return 当前对象
      * @throws IndexOutOfBoundsException 位置超出源字符串范围
@@ -122,10 +122,24 @@ public class StringAccess {
         }
     }
 
+    /**
+     * 跳过1个字符<br>
+     * 即使已到末尾没得读取, 也会成功<br>
+     * 所有skip操作lastReadSuccess=true, 在这点上与read/peek等读取操作不一样
+     *
+     * @return 返回对象自身用于链式操作
+     */
     public StringAccess skipChar() {
         return skipChars(1);
     }
 
+    /**
+     * 跳过n个字符<br>
+     * 即使到末尾不够数量, 也会成功, 此时index将停留在最后<br>
+     * 所有skip操作lastReadSuccess=true, 在这点上与read/peek等读取操作不一样
+     *
+     * @return 返回对象自身用于链式操作
+     */
     public StringAccess skipChars(int size) {
         this.index = Math.min(index + size, length);
         this.lastReadSuccess = true;
@@ -135,7 +149,7 @@ public class StringAccess {
     /**
      * 读取几个字符<br>
      * 如果不够读取的数量, 将设置lastReadSuccess=false并返回null
-     * 
+     *
      * @param size 期望读取的数量
      * @return 字符串
      */
@@ -157,8 +171,8 @@ public class StringAccess {
 
     /**
      * 查看几个字符<br>
-     * 如果不够查看的数量, 将返回null
-     * 
+     * 如果不够查看的数量, 将设置lastReadSuccess=false并返回null
+     *
      * @param size 期望查看的数量
      * @return 字符串
      */
@@ -179,7 +193,7 @@ public class StringAccess {
     /**
      * 读取连续的指定字符<br>
      * 如果一个字符都没有读取到, 将设置lastReadSuccess=false并返回null
-     * 
+     *
      * @param chars 指定字符
      * @return 字符串
      */
@@ -204,12 +218,22 @@ public class StringAccess {
         }
     }
 
-    /** 跳过空白字符 (空格/TAB/换行) **/
+    /**
+     * 跳过空白字符 (空格/TAB/换行)<br>
+     * 所有skip操作lastReadSuccess=true, 在这点上与read/peek等读取操作不一样
+     *
+     * @return 返回对象自身用于链式操作
+     */
     public StringAccess skipWhitespace() {
         return this.doSkipWhitespace(true);
     }
 
-    /** 跳过空格和TAB字符 **/
+    /**
+     * 跳过空格和TAB字符 (不会跳过换行符)<br>
+     * 所有skip操作lastReadSuccess=true, 在这点上与read/peek等读取操作不一样
+     *
+     * @return 返回对象自身用于链式操作
+     */
     public StringAccess skipSpaceChars() {
         return this.doSkipWhitespace(false);
     }
@@ -365,7 +389,7 @@ public class StringAccess {
      * 从当前位置开始读取, 直到遇到指定字符<br>
      * 返回字符串不会包含指定字符<br>
      * 如果到最后也没遇到指定字符, 将还原位置/设置lastReadSuccess=false/返回null<br>
-     * 
+     *
      * @param chars 指定字符
      * @return 子字符串
      */
@@ -404,7 +428,7 @@ public class StringAccess {
      * 从当前位置开始读取, 直到遇到指定字符串<br>
      * 返回字符串不会包含指定字符串<br>
      * 如果到最后也没遇到指定字符串, 将还原位置/设置lastReadSuccess=false/返回null<br>
-     * 
+     *
      * @param strings 指定字符串
      * @return 子字符串
      */
@@ -422,7 +446,6 @@ public class StringAccess {
             }
             char c = string.charAt(newIndex);
             if (!multiline && (c == '\r' || c == '\n')) {
-                found = false;
                 break; // 单行模式下遇到换行符, 也是结束
             } else {
                 newIndex++;
@@ -444,7 +467,7 @@ public class StringAccess {
      * 从当前位置开始读取, 直到行尾<br>
      * 返回字符串不会包含指定字符<br>
      * 如果到最后也没遇到换行符, 将读取到最后<br>
-     * 
+     *
      * @return 子字符串
      */
     public String readToLineEnd() {
@@ -476,7 +499,7 @@ public class StringAccess {
         }
         return false;
     }
-    
+
     private static boolean isStartsWith(String string, int index, String... prefixes) {
         for (String prefix : prefixes) {
             if (index + prefix.length() > string.length()) {
@@ -521,7 +544,7 @@ public class StringAccess {
 
     /**
      * 查看从指定位置到当前位置的字符串
-     * 
+     *
      * @param startIndex 开始位置
      * @return 子字符串
      * @throws IndexOutOfBoundsException 位置超出源字符串范围
@@ -536,7 +559,7 @@ public class StringAccess {
 
     /**
      * 查看子字符串
-     * 
+     *
      * @param startIndex 开始位置
      * @param endIndex 结束位置
      * @return 子字符串
@@ -562,7 +585,7 @@ public class StringAccess {
      * 返回内容不会包含外层括号<br>
      * 如果没有找到匹配的括号, 将还原位置/设置lastReadSuccess=false/返回null<br>
      * 支持括号嵌套, 如 new StringAccess("(1/(a+b))").readInBracket('(', ')')将返回1/(a+b)
-     * 
+     *
      * @param leftBracket 左括号
      * @param rightBracket 右括号
      * @return 括号中的内容
@@ -582,7 +605,7 @@ public class StringAccess {
             return null;
         }
         // userName (pinyin)
-        char[] stops = multiline ? new char[] { ' ', '\t', '\r', '\n', ',' } : new char[] { ' ', '\t', ',' };
+        char[] stops = multiline ? new char[] {' ', '\t', '\r', '\n', ','} : new char[] {' ', '\t', ','};
         String result = readUniterm(stops); // (pinyin)
         if (!isLastReadSuccess() || result.charAt(0) != leftBracket
                 || result.charAt(result.length() - 1) != rightBracket) {
@@ -604,9 +627,9 @@ public class StringAccess {
      * Math.round(user.height), Math.round( Math.abs(user.height) )<br>
      * StringTools.removeChars('8,000.00(元)', ',')<br>
      * {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] }
-     * 
+     *
      * @param stopChars 停止符<br>
-     *            如 readUniterm(' ', '\t', '\r', '\n', ';')表示读取至空白或分号结束
+     * 如 readUniterm(' ', '\t', '\r', '\n', ';')表示读取至空白或分号结束
      * @return 单项内容
      */
     public String readUniterm(char... stopChars) {
@@ -647,9 +670,9 @@ public class StringAccess {
      * Math.round(user.height), Math.round( Math.abs(user.height) )<br>
      * StringTools.removeChars('8,000.00(元)', ',')<br>
      * {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] }
-     * 
+     *
      * @param separators 分隔符<br>
-     *            如 readUniterms(' ', '\t', '\r', '\n', ';')表示读取至空白或分号结束
+     * 如 readUniterms(' ', '\t', '\r', '\n', ';')表示读取至空白或分号结束
      * @return 多个单项内容列表
      */
     public List<String> readUniterms(char... separators) {
@@ -727,7 +750,7 @@ public class StringAccess {
      * ( Math.round(user.height), Math.round( Math.abs(user.height) ) )<br>
      * ( StringTools.removeChars('8,000.00(元)', ',') )<br>
      * ( {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] } )
-     * 
+     *
      * @return 方法参数列表
      */
     public List<String> readMethodParams() {
@@ -743,7 +766,7 @@ public class StringAccess {
      * ( Math.round(user.height), Math.round( Math.abs(user.height) ) )<br>
      * ( StringTools.removeChars('8,000.00(元)', ',') )<br>
      * ( {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] } )
-     * 
+     *
      * @param separators 分隔符
      * @return 方法参数列表
      */
-- 
Gitee


From 9c3d9a47154c965670edeacba2e7c5c865433dcc Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 12 Jun 2022 20:44:29 +0800
Subject: [PATCH 040/160] BigDecimal toNumber clearTrailingZeros

---
 .../model/reusable/ExpressionExecutor.java    |  23 +---
 .../gitee/qdbp/tools/utils/ConvertTools.java  | 115 +++++++++++++++++-
 2 files changed, 118 insertions(+), 20 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java
index 2041d1e..7009e2a 100644
--- a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java
@@ -2,9 +2,9 @@ package com.gitee.qdbp.able.model.reusable;
 
 import java.math.BigDecimal;
 import java.math.BigInteger;
-import java.util.HashMap;
 import java.util.Map;
 import com.gitee.qdbp.able.exception.FormatException;
+import com.gitee.qdbp.tools.utils.ConvertTools;
 
 /**
  * ExpressionExecutor
@@ -144,40 +144,25 @@ public abstract class ExpressionExecutor extends ExpressionContext {
         if (object instanceof BigDecimal) {
             BigDecimal number = (BigDecimal) object;
             if (scale == null) {
-                return clearTrailingZeros(number);
+                return ConvertTools.clearTrailingZeros(number);
             } else if (number.scale() > scale) {
                 return number.setScale(scale, BigDecimal.ROUND_HALF_UP);
             }
         } else if (object instanceof Double) {
             BigDecimal number = new BigDecimal(String.valueOf(object));
             if (scale == null) {
-                return clearTrailingZeros(number);
+                return ConvertTools.clearTrailingZeros(number);
             } else if (number.scale() > scale) {
                 return number.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
             }
         } else if (object instanceof Float) {
             BigDecimal number = new BigDecimal(String.valueOf(object));
             if (scale == null) {
-                return clearTrailingZeros(number);
+                return ConvertTools.clearTrailingZeros(number);
             } else if (number.scale() > scale) {
                 return number.setScale(scale, BigDecimal.ROUND_HALF_UP).floatValue();
             }
         }
         return object;
     }
-
-    /**
-     * 清除科学计数法
-     * @param number 目标数字
-     * @return 最终结果
-     */
-    public static BigDecimal clearTrailingZeros(BigDecimal number) {
-        number = number.stripTrailingZeros();
-        // new BigDecimal(20000).stripTrailingZeros(); = 2E+4 (科学计数法)
-        // 此时precision=1,scale=-4; 需要将scale设置为0
-        if (number.scale() < 0) {
-            number = number.setScale(0, BigDecimal.ROUND_HALF_UP);
-        }
-        return number;
-    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index c572617..38f2929 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -464,6 +464,115 @@ public class ConvertTools {
         }
     }
 
+    /**
+     * 转换为数字, 尽可能按照期望类型输出
+     *
+     * @param number 源数字
+     * @param expect 期望类型
+     * @param maxScale 最大的小数位
+     * @return 数字
+     */
+    public static Number toNumber(BigDecimal number, Class<? extends Number> expect, int maxScale) {
+        if (number == null) {
+            return null;
+        }
+        BigDecimal value = clearTrailingZeros(number);
+        if (value.scale() > maxScale) {
+            value = value.setScale(maxScale, BigDecimal.ROUND_HALF_UP);
+            value = clearTrailingZeros(value);
+        }
+        if (expect == BigDecimal.class) {
+            return value;
+        }
+        if (value.scale() == 0) {
+            if (expect == BigInteger.class) {
+                return value.toBigInteger();
+            }
+            if (expect == Integer.class || expect == int.class) {
+                if (isWithinRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE)) {
+                    // 在Integer数值范围内, 返回intValue
+                    return value.intValue();
+                }
+            } else if (expect == Long.class || expect == long.class) {
+                if (isWithinRange(value, Long.MIN_VALUE, Long.MAX_VALUE)) {
+                    // 在Long数值范围内, 返回longValue
+                    return value.longValue();
+                }
+            } else if (expect == Double.class || expect == double.class) {
+                if (isWithinRange(value, Double.MIN_VALUE, Double.MAX_VALUE)) {
+                    // 在Double数值范围内, 返回doubleValue
+                    return value.doubleValue();
+                }
+            } else if (expect == Float.class || expect == float.class) {
+                if (isWithinRange(value, Float.MIN_VALUE, Float.MAX_VALUE)) {
+                    // 在Float数值范围内, 返回floatValue
+                    return value.floatValue();
+                }
+            } else if (expect == Byte.class || expect == byte.class) {
+                if (isWithinRange(value, Byte.MIN_VALUE, Byte.MAX_VALUE)) {
+                    // 在Byte数值范围内, 返回byteValue
+                    return value.byteValue();
+                }
+            } else if (expect == Short.class || expect == short.class) {
+                if (isWithinRange(value, Short.MIN_VALUE, Short.MAX_VALUE)) {
+                    // 在Short数值范围内, 返回byteValue
+                    return value.shortValue();
+                }
+            }
+            if (isWithinRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE)) {
+                // 在Integer数值范围内, 返回intValue
+                return value.intValue();
+            } else if (isWithinRange(value, Long.MIN_VALUE, Long.MAX_VALUE)) {
+                // 在Long数值范围内, 返回longValue
+                return value.longValue();
+            } else {
+                // 超过Long的范围, 返回BigDecimal
+                return value;
+            }
+        } else {
+            if (expect == Double.class || expect == double.class) {
+                if (isWithinRange(value, Double.MIN_VALUE, Double.MAX_VALUE)) {
+                    // 在Double数值范围内, 返回doubleValue
+                    return value.doubleValue();
+                }
+            } else if (expect == Float.class || expect == float.class) {
+                if (isWithinRange(value, Float.MIN_VALUE, Float.MAX_VALUE)) {
+                    // 在Float数值范围内, 返回floatValue
+                    return value.floatValue();
+                }
+            }
+            if (isWithinRange(value, Float.MIN_VALUE, Float.MAX_VALUE)) {
+                // 在Float数值范围内, 返回floatValue
+                return value.floatValue();
+            } else if (isWithinRange(value, Double.MIN_VALUE, Double.MAX_VALUE)) {
+                // 在Double数值范围内, 返回doubleValue
+                return value.doubleValue();
+            } else {
+                // 超过Double的范围, 返回BigDecimal
+                return value;
+            }
+        }
+    }
+
+    private static boolean isWithinRange(BigDecimal number, double min, double max) {
+        return number.compareTo(BigDecimal.valueOf(min)) >= 0 && number.compareTo(BigDecimal.valueOf(max)) <= 0;
+    }
+
+    /**
+     * 清除科学计数法
+     * @param number 目标数字
+     * @return 最终结果
+     */
+    public static BigDecimal clearTrailingZeros(BigDecimal number) {
+        number = number.stripTrailingZeros();
+        // new BigDecimal(20000).stripTrailingZeros(); = 2E+4 (科学计数法)
+        // 此时precision=1,scale=-4; 需要将scale设置为0
+        if (number.scale() < 0) {
+            number = number.setScale(0, BigDecimal.ROUND_HALF_UP);
+        }
+        return number;
+    }
+
     /**
      * 转换为数字
      *
@@ -480,6 +589,8 @@ public class ConvertTools {
             throw new NumberFormatException(value);
         } else if (number > Integer.MAX_VALUE) {
             throw new NumberFormatException(value + " greater than Integer.MAX_VALUE");
+        } else if (number < Integer.MIN_VALUE) {
+            throw new NumberFormatException(value + " less than Integer.MIN_VALUE");
         } else {
             return number.intValue();
         }
@@ -494,7 +605,7 @@ public class ConvertTools {
      */
     public static Integer toInteger(String value, Integer defaults) {
         Long number = toLong(value, defaults == null ? null : Long.valueOf(defaults));
-        if (number == null || number > Integer.MAX_VALUE) {
+        if (number == null || number < Integer.MIN_VALUE || number > Integer.MAX_VALUE) {
             return defaults;
         } else {
             return number.intValue();
@@ -585,6 +696,8 @@ public class ConvertTools {
             throw new NumberFormatException(value);
         } else if (number > Float.MAX_VALUE) {
             throw new NumberFormatException(value + " greater than Float.MAX_VALUE");
+        } else if (number < Float.MIN_VALUE) {
+            throw new NumberFormatException(value + " less than Float.MIN_VALUE");
         } else {
             return number.floatValue();
         }
-- 
Gitee


From d19594d21133c469312895284a71ace8a55366a6 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 13 Jun 2022 23:20:36 +0800
Subject: [PATCH 041/160] =?UTF-8?q?Excel=E5=AF=BC=E5=85=A5=E5=AF=BC?=
 =?UTF-8?q?=E5=87=BA=E7=AE=80=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/able/exception/ServiceException.java |   3 +-
 .../gitee/qdbp/able/result/BatchResult.java   |   8 +-
 .../gitee/qdbp/able/result/IBatchResult.java  |   4 +-
 .../gitee/qdbp/tools/excel/ImportAsBean.java  |  51 ++++
 .../gitee/qdbp/tools/excel/ImportAsMap.java   |  33 +++
 .../gitee/qdbp/tools/excel/ImportResult.java  |  46 ++++
 .../qdbp/tools/excel/SheetFillCallback.java   |  13 +-
 .../qdbp/tools/excel/SheetParseCallback.java  |  39 +---
 .../qdbp/tools/excel/SheetParseResult.java    |  58 +++++
 .../qdbp/tools/excel/XExcelExporter.java      | 115 ++++++---
 .../gitee/qdbp/tools/excel/XExcelParser.java  | 219 ++++++++++++++++--
 .../com/gitee/qdbp/tools/excel/XMetadata.java |  31 ++-
 .../qdbp/tools/excel/utils/ExcelHelper.java   |  12 +-
 .../qdbp/tools/excel/utils/MetadataTools.java |  62 +++--
 .../gitee/qdbp/tools/excel/EmployeeInfo.java  |  79 +++++++
 .../qdbp/tools/excel/ExcelInOutTest.java      |  96 +-------
 ...nOutTest.txt => ExcelInOutTest.properties} |   0
 .../qdbp/tools/excel/ExcelSheetTest.java      |  16 +-
 .../qdbp/tools/excel/ExcelSimpleTest.java     |  57 +++++
 .../tools/excel/ExcelSimpleTest.properties    |  44 ++++
 .../com/gitee/qdbp/tools/excel/Gender.java    |   5 +
 .../qdbp/tools/specialized/PinyinChecker.java |   8 +-
 22 files changed, 771 insertions(+), 228 deletions(-)
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsMap.java
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/excel/ImportResult.java
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseResult.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/excel/EmployeeInfo.java
 rename tools/src/test/java/com/gitee/qdbp/tools/excel/{ExcelInOutTest.txt => ExcelInOutTest.properties} (100%)
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/excel/Gender.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
index 2c74398..b62e0de 100644
--- a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
+++ b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
@@ -103,8 +103,9 @@ public class ServiceException extends RuntimeException implements IResultMessage
      * 
      * @param details 错误详情
      */
-    public void setDetails(String details) {
+    public ServiceException setDetails(String details) {
         this.details = details;
+        return this;
     }
 
     /**
diff --git a/able/src/main/java/com/gitee/qdbp/able/result/BatchResult.java b/able/src/main/java/com/gitee/qdbp/able/result/BatchResult.java
index 7e16a5d..27c8c1a 100644
--- a/able/src/main/java/com/gitee/qdbp/able/result/BatchResult.java
+++ b/able/src/main/java/com/gitee/qdbp/able/result/BatchResult.java
@@ -10,7 +10,7 @@ import java.util.List;
  * @author zhaohuihua
  * @version 170527
  */
-public class BatchResult implements IBatchResult, Serializable {
+public class BatchResult implements IBatchResult<BatchResult.FailedItem>, Serializable {
 
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
@@ -19,7 +19,7 @@ public class BatchResult implements IBatchResult, Serializable {
     private Integer total = 0;
 
     /** 失败列表 **/
-    private final List<Failed> failed = new ArrayList<>();
+    private final List<FailedItem> failed = new ArrayList<>();
 
     /** 整条记录失败 **/
     public void addFailed(int row, IResultMessage result) {
@@ -33,7 +33,7 @@ public class BatchResult implements IBatchResult, Serializable {
 
     /** 失败列表 **/
     @Override
-    public List<Failed> getFailed() {
+    public List<FailedItem> getFailed() {
         return failed;
     }
 
@@ -64,7 +64,7 @@ public class BatchResult implements IBatchResult, Serializable {
      * @author zhaohuihua
      * @version 160302
      */
-    public static class FailedItem implements Failed, Serializable {
+    public static class FailedItem implements IBatchResult.Failed, Serializable {
 
         /** 版本序列号 **/
         private static final long serialVersionUID = 1L;
diff --git a/able/src/main/java/com/gitee/qdbp/able/result/IBatchResult.java b/able/src/main/java/com/gitee/qdbp/able/result/IBatchResult.java
index bcd67d1..6939092 100644
--- a/able/src/main/java/com/gitee/qdbp/able/result/IBatchResult.java
+++ b/able/src/main/java/com/gitee/qdbp/able/result/IBatchResult.java
@@ -8,7 +8,7 @@ import java.util.List;
  * @author zhaohuihua
  * @version 150305
  */
-public interface IBatchResult {
+public interface IBatchResult<T extends IBatchResult.Failed> {
 
     /**
      * 失败记录
@@ -38,5 +38,5 @@ public interface IBatchResult {
      *
      * @return 失败列表
      */
-    List<Failed> getFailed();
+    List<T> getFailed();
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java
new file mode 100644
index 0000000..4e5737d
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java
@@ -0,0 +1,51 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import com.alibaba.fastjson.JSONException;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.tools.excel.model.RowInfo;
+import com.gitee.qdbp.tools.utils.JsonTools;
+
+/**
+ * Excel内容导入为JavaBean
+ *
+ * @author zhaohuihua
+ * @version 20220612
+ */
+public class ImportAsBean<T> extends ImportCallback {
+
+    /** 版本序列号 **/
+    private static final long serialVersionUID = 1L;
+
+    private final Class<T> beanClass;
+    private final List<T> contents = new ArrayList<>();
+
+    /**
+     * 构造函数
+     *
+     * @param beanClass Bean的类型
+     */
+    public ImportAsBean(Class<T> beanClass) {
+        this.beanClass = beanClass;
+    }
+
+    /**
+     * 从Excel导入的内容
+     * @return JavaBean列表
+     */
+    public List<T> getContents() {
+        return contents;
+    }
+
+    @Override
+    public void callback(Map<String, Object> map, RowInfo row) throws ServiceException {
+        try {
+            T bean = JsonTools.mapToBean(map, beanClass);
+            contents.add(bean);
+        } catch (JSONException e) {
+            throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
+        }
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsMap.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsMap.java
new file mode 100644
index 0000000..f044699
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsMap.java
@@ -0,0 +1,33 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.excel.model.RowInfo;
+
+/**
+ * Excel内容导入为JavaBean
+ *
+ * @author zhaohuihua
+ * @version 20220612
+ */
+public class ImportAsMap extends ImportCallback {
+
+    /** 版本序列号 **/
+    private static final long serialVersionUID = 1L;
+
+    private final List<Map<String, Object>> contents = new ArrayList<>();
+
+    /**
+     * 从Excel导入的内容
+     * @return JavaBean列表
+     */
+    public List<Map<String, Object>> getContents() {
+        return contents;
+    }
+
+    @Override
+    public void callback(Map<String, Object> map, RowInfo row) {
+        contents.add(map);
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportResult.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportResult.java
new file mode 100644
index 0000000..5df1d82
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportResult.java
@@ -0,0 +1,46 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.io.Serializable;
+import java.util.List;
+import com.gitee.qdbp.able.result.IBatchResult;
+import com.gitee.qdbp.tools.excel.model.FailedInfo;
+
+/**
+ * 导入结果
+ *
+ * @author zhaohuihua
+ * @version 160302
+ */
+public class ImportResult<T> implements IBatchResult<FailedInfo>, Serializable {
+
+    /** 版本序列号 **/
+    private static final long serialVersionUID = 1L;
+
+    /** 导入成功的内容(成功列表) **/
+    private final List<T> contents;
+    /** 导入实际结果 **/
+    private final SheetParseResult callback;
+
+    protected ImportResult(List<T> contents, SheetParseResult callback) {
+        this.contents = contents;
+        this.callback = callback;
+    }
+
+    /** 获取记录总数 **/
+    @Override
+    public Integer getTotal() {
+        return callback.getTotal();
+    }
+
+    /** 导入成功的内容(成功列表) **/
+    public List<T> getContents() {
+        return contents;
+    }
+
+    /** 失败列表 **/
+    @Override
+    public List<FailedInfo> getFailed() {
+        return callback.getFailed();
+    }
+
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java
index e3ec798..e418820 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java
@@ -4,8 +4,10 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
@@ -13,11 +15,8 @@ import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.excel.rule.CellRule;
 import com.gitee.qdbp.tools.excel.rule.IgnoreIllegalValue;
 import com.gitee.qdbp.tools.excel.utils.ExcelTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Cell;
-import org.apache.poi.ss.usermodel.Row;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Sheet填充回调函数
@@ -49,7 +48,7 @@ public class SheetFillCallback implements Serializable {
 
     /** JavaBean转换为Map **/
     public Map<String, Object> toMap(Object value) {
-        return (JSONObject) JSON.toJSON(value);
+        return JsonTools.beanToMap(value);
     }
 
     /** 单元格字段转换, data.put(cellInfo.getField(), cellInfo.getValue()); **/
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java
index 4015eed..fffdae6 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java
@@ -4,21 +4,19 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import org.apache.poi.ss.usermodel.Cell;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.exception.ServiceException;
-import com.gitee.qdbp.able.result.IBatchResult;
 import com.gitee.qdbp.able.result.IResultMessage;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
-import com.gitee.qdbp.tools.excel.model.FailedInfo;
 import com.gitee.qdbp.tools.excel.model.FieldInfo;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.excel.rule.CellRule;
 import com.gitee.qdbp.tools.excel.rule.IgnoreIllegalValue;
 import com.gitee.qdbp.tools.excel.utils.ExcelTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Cell;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 解析回调函数
@@ -26,7 +24,7 @@ import org.slf4j.LoggerFactory;
  * @author zhaohuihua
  * @version 160302
  */
-public abstract class SheetParseCallback implements IBatchResult, Serializable {
+public abstract class SheetParseCallback extends SheetParseResult implements Serializable {
 
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
@@ -34,41 +32,28 @@ public abstract class SheetParseCallback implements IBatchResult, Serializable {
     /** 日志对象 **/
     private final Logger log = LoggerFactory.getLogger(this.getClass());
 
-    /** 记录总数 **/
-    private Integer total = 0;
-    /** 失败列表 **/
-    private final List<Failed> failed = new ArrayList<>();
-
     /** 整行失败 **/
+    @Override
     public void addFailed(String sheetName, int row, IResultMessage result) {
-        failed.add(new FailedInfo(sheetName, row, result));
+        super.addFailed(sheetName, row, result);
     }
 
     /** 具体某一列失败(注意:字段名和字段值都填在field这里, value的值不用于前端显示) **/
-    public void addFailed(String sheetName, int row, String field, Object value, IResultMessage result) {
-        failed.add(new FailedInfo(sheetName, row, field, value, result));
-    }
-
-    /** 失败列表 **/
     @Override
-    public List<Failed> getFailed() {
-        return failed;
+    public void addFailed(String sheetName, int row, String field, Object value, IResultMessage result) {
+        super.addFailed(sheetName, row, field, value, result);
     }
 
     /** 增加记录总数 **/
+    @Override
     public void addTotal(int number) {
-        total += number;
+        super.addTotal(number);
     }
 
     /** 设置记录总数 **/
-    public void setTotal(int total) {
-        this.total = total;
-    }
-
-    /** 获取记录总数 **/
     @Override
-    public Integer getTotal() {
-        return total;
+    public void setTotal(int total) {
+        super.setTotal(total);
     }
 
     /** 具体的业务处理逻辑, 一般是插入数据库之类的持久化操作 **/
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseResult.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseResult.java
new file mode 100644
index 0000000..7e380d4
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseResult.java
@@ -0,0 +1,58 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import com.gitee.qdbp.able.result.IBatchResult;
+import com.gitee.qdbp.able.result.IResultMessage;
+import com.gitee.qdbp.tools.excel.model.FailedInfo;
+
+/**
+ * 解析结果
+ *
+ * @author zhaohuihua
+ * @version 160302
+ */
+public class SheetParseResult implements IBatchResult<FailedInfo>, Serializable {
+
+    /** 版本序列号 **/
+    private static final long serialVersionUID = 1L;
+
+    /** 记录总数 **/
+    private Integer total = 0;
+    /** 失败列表 **/
+    private final List<FailedInfo> failed = new ArrayList<>();
+
+    /** 整行失败 **/
+    protected void addFailed(String sheetName, int row, IResultMessage result) {
+        failed.add(new FailedInfo(sheetName, row, result));
+    }
+
+    /** 具体某一列失败(注意:字段名和字段值都填在field这里, value的值不用于前端显示) **/
+    protected void addFailed(String sheetName, int row, String field, Object value, IResultMessage result) {
+        failed.add(new FailedInfo(sheetName, row, field, value, result));
+    }
+
+    /** 失败列表 **/
+    @Override
+    public List<FailedInfo> getFailed() {
+        return failed;
+    }
+
+    /** 增加记录总数 **/
+    protected void addTotal(int number) {
+        total += number;
+    }
+
+    /** 设置记录总数 **/
+    protected void setTotal(int total) {
+        this.total = total;
+    }
+
+    /** 获取记录总数 **/
+    @Override
+    public Integer getTotal() {
+        return total;
+    }
+
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
index 5a3f7ea..c75c684 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
@@ -1,9 +1,13 @@
 package com.gitee.qdbp.tools.excel;
 
+import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.URL;
 import java.util.List;
 import org.apache.poi.POIXMLException;
 import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
@@ -15,11 +19,12 @@ import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.enums.FileErrorCode;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.utils.ExcelHelper;
-import com.gitee.qdbp.tools.utils.VerifyTools;
+import com.gitee.qdbp.tools.files.FileTools;
+import com.gitee.qdbp.tools.files.PathTools;
 
 /**
  * Excel导出
- * 
+ *
  * @author zhaohuihua
  * @version 160302
  */
@@ -29,84 +34,118 @@ public class XExcelExporter {
 
     private final XMetadata metadata;
 
-    private ExportCallback callback;
-
     public XExcelExporter(XMetadata metadata) {
         this.metadata = metadata;
-        this.callback = new ExportCallback();
     }
 
     /**
      * Excel导出
-     * 
+     *
      * @param data 待导出的数据
-     * @param templatePath 模板文件的路径
-     * @param output 输出流
+     * @param templateFile 模板文件路径
+     * @param saveFile 保存的文件路径
      * @throws ServiceException 处理失败
      */
-    public void export(List<?> data, String templatePath, OutputStream output) throws ServiceException {
-        this.export(data, templatePath, output, null);
-    }
-
-    public void export(List<?> data, InputStream template, OutputStream output) throws ServiceException {
-        this.export(data, template, output, null);
+    public void export(List<?> data, File templateFile, File saveFile) throws ServiceException {
+        FileTools.mkdirsIfNotExists(saveFile);
+        try (InputStream input = new FileInputStream(templateFile)) {
+            try (OutputStream output = new FileOutputStream(saveFile)) {
+                export(data, input, output, null);
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(saveFile.getPath());
+            } catch (IOException e) {
+                log.error("write excel error.", e);
+                throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR, e);
+            }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(templateFile.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
     }
 
-    public void export(List<?> data, String templatePath, OutputStream output, ExportCallback cb)
-            throws ServiceException {
-        try (InputStream input = new FileInputStream(templatePath)) {
-            export(data, input, output, cb);
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param saveFile 保存的文件路径
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, URL templateFile, File saveFile) throws ServiceException {
+        FileTools.mkdirsIfNotExists(saveFile);
+        try (InputStream input = templateFile.openStream()) {
+            try (OutputStream output = new FileOutputStream(saveFile)) {
+                export(data, input, output, null);
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(saveFile.getPath());
+            } catch (IOException e) {
+                log.error("write excel error.", e);
+                throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR, e);
+            }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(templateFile));
         } catch (IOException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
         }
     }
 
-    public void export(List<?> data, InputStream template, OutputStream output, ExportCallback cb)
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param template 模板文件输入流
+     * @param output 输出流
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, InputStream template, OutputStream output) throws ServiceException {
+        this.export(data, template, output, null);
+    }
+
+    public void export(List<?> data, InputStream template, OutputStream output, ExportCallback callback)
             throws ServiceException {
 
-        if (cb == null) cb = this.callback;
+        if (callback == null) {
+            callback = new ExportCallback();
+        }
 
         try (Workbook wb = WorkbookFactory.create(template)) {
 
-            cb.init(wb, metadata);
+            callback.init(wb, metadata);
             Sheet sheet = wb.getSheetAt(0);
 
-            if (cb.onSheetStart(sheet, metadata, data)) {
-                ExcelHelper.fill(data, sheet, metadata, cb);
-                cb.onSheetFinished(sheet, metadata, data);
+            if (callback.onSheetStart(sheet, metadata, data)) {
+                ExcelHelper.fill(data, sheet, metadata, callback);
+                callback.onSheetFinished(sheet, metadata, data);
             }
 
             // 计算所有公式
             // sheet.setForceFormulaRecalculation(true); // 设置Excel打开的时候计算
             wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); // 事先计算
 
-            cb.finish(wb, metadata);
+            callback.finish(wb, metadata);
 
             try {
                 wb.write(output);
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
             } catch (IOException e) {
                 log.error("write excel error.", e);
                 throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR);
             }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
         } catch (IOException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
         } catch (POIXMLException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
+            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR, e);
         } catch (InvalidFormatException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
+            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
         }
     }
-
-    public void setExportCallback(ExportCallback callback) {
-        VerifyTools.requireNotBlank(callback, "callback");
-        this.callback = callback;
-    }
-
-    public ExportCallback getExportCallback() {
-        return this.callback;
-    }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java
index 82bd3bd..3203d08 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java
@@ -1,7 +1,12 @@
 package com.gitee.qdbp.tools.excel;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URL;
+import java.util.Map;
 import org.apache.poi.POIXMLException;
 import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Sheet;
@@ -12,6 +17,7 @@ import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.enums.FileErrorCode;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.utils.ExcelHelper;
+import com.gitee.qdbp.tools.files.PathTools;
 
 /**
  * excel2007版解析器
@@ -29,43 +35,226 @@ public class XExcelParser {
         this.metadata = metadata;
     }
 
-    public void parse(InputStream is, ImportCallback cb) throws ServiceException {
-        try (Workbook wb = WorkbookFactory.create(is)) {
-            this.parse(wb, cb);
+    /**
+     * 导入Excel, 解析为JavaBean
+     *
+     * @param file Excel文件
+     * @param beanType JavaBean类型
+     * @param <T> 泛型
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public <T> ImportResult<T> parseAsBean(File file, Class<T> beanType) throws ServiceException {
+        try (InputStream input = new FileInputStream(file)) {
+            return parseAsBean(input, beanType);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(file.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(file.getPath());
+        }
+    }
+
+    /**
+     * 导入Excel, 解析为JavaBean
+     *
+     * @param file Excel文件
+     * @param beanType JavaBean类型
+     * @param <T> 泛型
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public <T> ImportResult<T> parseAsBean(URL file, Class<T> beanType) throws ServiceException {
+        try (InputStream input = file.openStream()) {
+            return parseAsBean(input, beanType);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(file));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * 导入Excel, 解析为JavaBean
+     *
+     * @param input Excel文件输入流
+     * @param beanType JavaBean类型
+     * @param <T> 泛型
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public <T> ImportResult<T> parseAsBean(InputStream input, Class<T> beanType) throws ServiceException {
+        ImportAsBean<T> callback = new ImportAsBean<>(beanType);
+        parse(input, callback);
+        return new ImportResult<>(callback.getContents(), callback);
+    }
+
+    /**
+     * 导入Excel, 解析为JavaBean
+     *
+     * @param workbook Excel对象
+     * @param beanType JavaBean类型
+     * @param <T> 泛型
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public <T> ImportResult<T> parseAsBean(Workbook workbook, Class<T> beanType) throws ServiceException {
+        ImportAsBean<T> callback = new ImportAsBean<>(beanType);
+        parse(workbook, callback);
+        return new ImportResult<>(callback.getContents(), callback);
+    }
+
+    /**
+     * 导入Excel, 解析为Map数据
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public ImportResult<Map<String, Object>> parseAsMap(File file) throws ServiceException {
+        try (InputStream input = new FileInputStream(file)) {
+            return parseAsMap(input);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(file.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+    /**
+     * 导入Excel, 解析为Map数据
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public ImportResult<Map<String, Object>> parseAsMap(URL file) throws ServiceException {
+        try (InputStream input = file.openStream()) {
+            return parseAsMap(input);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(file));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * 导入Excel, 解析为Map数据
+     *
+     * @param input Excel文件输入流
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public ImportResult<Map<String, Object>> parseAsMap(InputStream input) throws ServiceException {
+        ImportAsMap callback = new ImportAsMap();
+        parse(input, callback);
+        return new ImportResult<>(callback.getContents(), callback);
+    }
+
+    /**
+     * 导入Excel, 解析为Map数据
+     *
+     * @param workbook Excel对象
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public ImportResult<Map<String, Object>> parseAsMap(Workbook workbook) throws ServiceException {
+        ImportAsMap callback = new ImportAsMap();
+        parse(workbook, callback);
+        return new ImportResult<>(callback.getContents(), callback);
+    }
+
+    /**
+     * 解析Excel内容
+     *
+     * @param file Excel文件
+     * @param callback 解析的回调处理类
+     * @throws ServiceException 解析失败
+     */
+    public void parse(File file, ImportCallback callback) throws ServiceException {
+        try (InputStream input = new FileInputStream(file)) {
+            parse(input, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(file.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * 解析Excel内容
+     *
+     * @param file Excel文件
+     * @param callback 解析的回调处理类
+     * @throws ServiceException 解析失败
+     */
+    public void parse(URL file, ImportCallback callback) throws ServiceException {
+        try (InputStream input = file.openStream()) {
+            parse(input, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(file));
         } catch (IOException e) {
             log.error("read excel error.", e);
             throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
+        }
+    }
+
+    /**
+     * 解析Excel内容
+     *
+     * @param input Excel文件输入流
+     * @param callback 解析的回调处理类
+     * @throws ServiceException 解析失败
+     */
+    public void parse(InputStream input, ImportCallback callback) throws ServiceException {
+        try (Workbook workbook = WorkbookFactory.create(input)) {
+            this.parse(workbook, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
         } catch (POIXMLException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
+            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR, e);
         } catch (InvalidFormatException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
+            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
         }
     }
 
-    public void parse(Workbook wb, ImportCallback cb) {
+    /**
+     * 解析Excel内容
+     *
+     * @param workbook Excel对象
+     * @param callback 解析的回调处理类
+     * @throws ServiceException 解析失败
+     */
+    public void parse(Workbook workbook, ImportCallback callback) {
         try {
-            cb.init(wb, metadata);
-            for (int i = 0, total = wb.getNumberOfSheets(); i < total; i++) {
-                if (metadata.isEnableSheet(i, wb.getSheetName(i))) {
-                    Sheet sheet = wb.getSheetAt(i);
-                    if (!cb.onSheetStart(sheet, metadata)) {
+            callback.init(workbook, metadata);
+            for (int i = 0, total = workbook.getNumberOfSheets(); i < total; i++) {
+                if (metadata.isEnableSheet(i, workbook.getSheetName(i))) {
+                    Sheet sheet = workbook.getSheetAt(i);
+                    if (!callback.onSheetStart(sheet, metadata)) {
                         continue; // 返回false跳过该Sheet 
                     }
 
                     // 解析Sheet
-                    ExcelHelper.parse(sheet, metadata, cb);
+                    ExcelHelper.parse(sheet, metadata, callback);
 
-                    if (!cb.onSheetFinished(sheet, metadata)) {
+                    if (!callback.onSheetFinished(sheet, metadata)) {
                         break; // 返回false跳过后面的所有Sheet 
                     }
                 }
             }
-            cb.finish(wb, metadata);
+            callback.finish(workbook, metadata);
         } catch (POIXMLException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
+            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR, e);
         }
     }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
index c4922d5..64a5edf 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
@@ -7,6 +7,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import org.apache.poi.ss.usermodel.Row;
 import com.gitee.qdbp.tools.excel.condition.IndexListCondition;
 import com.gitee.qdbp.tools.excel.condition.IndexRangeCondition;
 import com.gitee.qdbp.tools.excel.condition.MatchesRowCondition;
@@ -17,7 +18,6 @@ import com.gitee.qdbp.tools.excel.rule.CellRule;
 import com.gitee.qdbp.tools.excel.utils.MetadataTools;
 import com.gitee.qdbp.tools.utils.Config;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Row;
 
 /**
  * excel配置数据<br>
@@ -52,6 +52,8 @@ public class XMetadata implements Serializable {
     private IndexRangeCondition footerRows;
     /** Sheet名称填充至哪个字段 **/
     private String sheetNameFillTo;
+    /** 行序号填充至哪个字段 **/
+    private String rowIndexFillTo;
     /** Sheet序号配置, 默认读取第1个Sheet **/
     private IndexListCondition sheetIndexs = new IndexListCondition(0);
     /** Sheet名称配置, 默认全部匹配 **/
@@ -67,7 +69,7 @@ public class XMetadata implements Serializable {
 
     /**
      * 构造函数
-     * 
+     *
      * @param config 配置项
      * @deprecated 改为{@link MetadataTools#parseProperties(Properties)}
      */
@@ -82,6 +84,7 @@ public class XMetadata implements Serializable {
         this.setHeaderRows(metadata.getHeaderRows()); // 表头所在的行号
         this.setFooterRows(metadata.getFooterRows()); // 页脚所在的行号
         this.setSheetNameFillTo(metadata.getSheetNameFillTo()); // Sheet名称填充至哪个字段
+        this.setRowIndexFillTo(metadata.getRowIndexFillTo()); // 行序号填充至哪个字段
         this.setSheetIndexs(metadata.getSheetIndexs()); // Sheet序号配置
         this.setSheetNames(metadata.getSheetNames()); // Sheet名称配置
     }
@@ -233,6 +236,16 @@ public class XMetadata implements Serializable {
         this.sheetNameFillTo = sheetNameFillTo;
     }
 
+    /** 行序号填充至哪个字段 **/
+    public String getRowIndexFillTo() {
+        return rowIndexFillTo;
+    }
+
+    /** 行序号填充至哪个字段 **/
+    public void setRowIndexFillTo(String rowIndexFillTo) {
+        this.rowIndexFillTo = rowIndexFillTo;
+    }
+
     /** 字段与转换规则的映射表 **/
     public Map<String, List<CellRule>> getRules() {
         return rules;
@@ -244,11 +257,20 @@ public class XMetadata implements Serializable {
     }
 
     /** 增加转换规则 **/
-    public void addRule(String column, List<CellRule> rule) {
+    public void addRule(String column, CellRule rule) {
+        addRule(column, Collections.singletonList(rule));
+    }
+
+    /** 增加转换规则 **/
+    public void addRule(String column, List<CellRule> rules) {
         if (this.rules == null) {
             this.rules = new HashMap<>();
+            this.rules.put(column, new ArrayList<>(rules));
+        } else if (this.rules.containsKey(column)) {
+            this.rules.get(column).addAll(rules);
+        } else {
+            this.rules.put(column, new ArrayList<>(rules));
         }
-        this.rules.put(column, rule);
     }
 
     /** 获取指定列的转换规则 **/
@@ -299,6 +321,7 @@ public class XMetadata implements Serializable {
         instance.setHeaderRows(this.getHeaderRows()); // 表头所在的行号
         instance.setFooterRows(this.getFooterRows()); // 页脚所在的行号
         instance.setSheetNameFillTo(this.getSheetNameFillTo()); // Sheet名称填充至哪个字段
+        instance.setRowIndexFillTo(this.getRowIndexFillTo()); // 行序号填充至哪个字段
         instance.setSheetIndexs(this.getSheetIndexs()); // Sheet序号配置
         instance.setSheetNames(this.getSheetNames()); // Sheet名称配置
         instance.setCopyConcatFields(this.getCopyConcatFields()); // 字段复制合并参数 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
index ae15feb..3aea11e 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
@@ -114,15 +114,19 @@ public class ExcelHelper {
 
         cb.addTotal(1); // 总行数加1
 
+        // 生成单元格信息
+        Map<String, Object> data = new HashMap<>();
         // Sheet名称填充至指定字段
         if (VerifyTools.isNotBlank(metadata.getSheetNameFillTo())) {
             if (!isIgnoreSheetName(sheetName)) {
-                map.put(metadata.getSheetNameFillTo(), sheetName);
+                data.put(metadata.getSheetNameFillTo(), sheetName);
             }
         }
-
-        // 生成单元格信息
-        Map<String, Object> data = new HashMap<>();
+        // 行序号填充至哪个字段
+        if (VerifyTools.isNotBlank(metadata.getRowIndexFillTo())) {
+            data.put(metadata.getRowIndexFillTo(), rowIndex);
+        }
+        // 生成CellInfo列表
         List<CellInfo> cellInfos = newCellInfos(columnInfos, metadata);
         for (CellInfo info : cellInfos) {
             if (info == null) {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
index 43bf02a..aa8047b 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
@@ -1,5 +1,7 @@
 package com.gitee.qdbp.tools.excel.utils;
 
+import java.io.InputStream;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -55,17 +57,28 @@ public class MetadataTools {
 
     private static final Logger log = LoggerFactory.getLogger(MetadataTools.class);
 
+    /**
+     * 解析XMetadata<br>
+     *
+     * @param url 配置文件路径
+     * @return XMetadata
+     * @see #parseProperties(Properties)
+     */
+    public static XMetadata parseProperties(URL url) {
+        return parseProperties(PropertyTools.load(url, "UTF-8"));
+    }
+
     /**
      * 解析XMetadata<br>
      * MetadataTools.parseProperties(PropertyTools.load(filePath)); <pre>
      * ## SheetIndex从1开始, 行号从1开始, 列号从A开始
-     * 
+     *
      * ## 字段对应关系, field.names/field.rows必填其一
      * ## 字段列表
      * # field.names = *id|*name|positive|height||birthday|gender|subsidy
      * ## 字段行
      * field.rows = 5
-     * 
+     *
      * ## 跳过的行数(不配置时默认取字段行或标题行的下一行)
      * # skip.rows = 2
      * ## 标题行号,从1开始
@@ -77,7 +90,7 @@ public class MetadataTools {
      * ## 包含指定关键字时跳过此行
      * ## A列为空, 或B列包含小计且H列包含元, 或B列包含总计且H列包含元
      * # skip.row.when.contains = { A:"NULL" }, { B:"小计", H:"元" }, { B:"总计", H:"元" }
-     * 
+     *
      * ## 加载哪些Sheet, sheet.index/sheet.name必填其一
      * ## 配置规则: * 表示全部
      * ## 配置规则: 1|2|5-8|12
@@ -90,18 +103,20 @@ public class MetadataTools {
      * sheet.name = !说明
      * ## 页签名称填充至哪个字段
      * sheet.name.fill.to = dept
-     * 
+     * ## 行序号填充至哪个字段
+     * row.index.fill.to = index
+     *
      * ## 字段转换规则(支持多个转换规则)
      * rules.rate = { clear:"[^\\.\\d]" }, { number:"int" }, { ignoreIllegalValue:true }, { rate:100 }
      * rules.tags = { split:"|" }
      * rules.positive = { map:{ true:"已转正|是|Y", false:"未转正|否|N" } }
      * rules.gender = { map:{ UNKNOWN:"未知|0", MALE:"男|1", FEMALE:"女|2" } }
      * rules.birthday = { date:"yyyy/MM/dd" }
-     * 
+     *
      * ## 将多个字段复制合并到一个字段
      * copy.concat = { keywords:"userName,nickName,deptName" }
      * </pre>
-     * 
+     *
      * @param properties 配置内容
      * @return XMetadata
      */
@@ -241,6 +256,8 @@ public class MetadataTools {
 
         // Sheet名称填充至哪个字段
         String sheetNameFillTo = PropertyTools.getString(properties, "sheet.name.fill.to", false);
+        // 行序号填充至哪个字段
+        String rowIndexFillTo = PropertyTools.getString(properties, "row.index.fill.to", false);
 
         // 解析字段复制合并参数
         String sCopyConcat = PropertyTools.getString(properties, "copy.concat", false);
@@ -253,6 +270,7 @@ public class MetadataTools {
         metadata.setHeaderRows(headerRows); // 表头所在的行号
         metadata.setFooterRows(footerRows); // 页脚所在的行号
         metadata.setSheetNameFillTo(sheetNameFillTo); // Sheet名称填充至哪个字段
+        metadata.setRowIndexFillTo(rowIndexFillTo); // 行序号填充至哪个字段
         metadata.setSheetIndexs(sheetIndexs); // Sheet序号配置
         metadata.setSheetNames(sheetNames); // Sheet名称配置
         if (!skipRowWhen.isEmpty()) {
@@ -269,7 +287,7 @@ public class MetadataTools {
 
     /**
      * 解析单元格规则
-     * 
+     *
      * @param jsonString JSON字符串
      * @return 单元格规则
      */
@@ -320,7 +338,7 @@ public class MetadataTools {
 
     /**
      * 解析单元格值的判断条件
-     * 
+     *
      * @param jsonString JSON字符串: { A:"NULL" }, { B:"小计", H:"元" }, { B:"总计", H:"元" }
      * @return 条件对象
      */
@@ -430,7 +448,7 @@ public class MetadataTools {
     /**
      * 解析字段列表配置<br>
      * 星号开头或(*)结尾的字段为必填字段: [*name] or [name(*)]
-     * 
+     *
      * @param text 配置内容
      * @return 字段配置对象
      */
@@ -455,7 +473,7 @@ public class MetadataTools {
     /**
      * 解析字段列表配置<br>
      * 星号开头或(*)结尾的字段为必填字段: [*name] or [name(*)]
-     * 
+     *
      * @param sheet Sheet
      * @param fieldRows 字段数据所在的行
      * @return 字段配置对象
@@ -506,7 +524,7 @@ public class MetadataTools {
     /**
      * 解析表头数据<br>
      * 星号开头或(*)结尾的字段为必填字段: [* 姓名] or [姓名 (*)]
-     * 
+     *
      * @param sheet Sheet
      * @param headerRows 表头数据所在的行
      * @param fieldInfos 字段数据
@@ -564,17 +582,17 @@ public class MetadataTools {
      * 从excel中读取转换规则<br>
      * 4列, 顺序为: 名称|KEY|类型|规则, 例如:<br>
      * <pre>
-        名称    KEY     类型    规则
-        整数    int     number  int
-        长整数  long    number  long
-        浮点数  double  number  double
-        布尔值  boolean map     true:是|Y|1, false:否|N|0
-        日期    date    date    yyyy-MM-dd
-        时间    time    date    HH:mm:ss
-        时分    hhmm    date    HH:mm
-        性别    gender  map     0:未知, 1:男, 2:女
+     * 名称    KEY     类型    规则
+     * 整数    int     number  int
+     * 长整数  long    number  long
+     * 浮点数  double  number  double
+     * 布尔值  boolean map     true:是|Y|1, false:否|N|0
+     * 日期    date    date    yyyy-MM-dd
+     * 时间    time    date    HH:mm:ss
+     * 时分    hhmm    date    HH:mm
+     * 性别    gender  map     0:未知, 1:男, 2:女
      * </pre>
-     * 
+     *
      * @param wb Excel文件对象
      * @param sheetName Sheet名称
      * @return 转换规则
@@ -586,7 +604,7 @@ public class MetadataTools {
 
     /**
      * 从excel中读取转换规则
-     * 
+     *
      * @param wb Excel文件对象
      * @param sheetName Sheet名称
      * @param columnFields 列字段顺序, 其中key|type|options三列必不可少
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/EmployeeInfo.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/EmployeeInfo.java
new file mode 100644
index 0000000..30d19c6
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/EmployeeInfo.java
@@ -0,0 +1,79 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.util.Date;
+
+class EmployeeInfo {
+
+    private Long id;
+    private String dept;
+    private String name;
+    private Boolean positive;
+    private Integer height;
+    private Date birthday;
+    private Gender gender;
+    private Integer subsidy;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getDept() {
+        return dept;
+    }
+
+    public void setDept(String dept) {
+        this.dept = dept;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Boolean getPositive() {
+        return positive;
+    }
+
+    public void setPositive(Boolean positive) {
+        this.positive = positive;
+    }
+
+    public Integer getHeight() {
+        return height;
+    }
+
+    public void setHeight(Integer height) {
+        this.height = height;
+    }
+
+    public Date getBirthday() {
+        return birthday;
+    }
+
+    public void setBirthday(Date birthday) {
+        this.birthday = birthday;
+    }
+
+    public Gender getGender() {
+        return gender;
+    }
+
+    public void setGender(Gender gender) {
+        this.gender = gender;
+    }
+
+    public Integer getSubsidy() {
+        return subsidy;
+    }
+
+    public void setSubsidy(Integer subsidy) {
+        this.subsidy = subsidy;
+    }
+}
\ No newline at end of file
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java
index d4b0ca6..8a5083d 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java
@@ -11,9 +11,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
-import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONException;
-import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.excel.utils.MetadataTools;
@@ -39,11 +37,9 @@ public class ExcelInOutTest {
 
         @Override
         public void callback(Map<String, Object> map, RowInfo row) throws ServiceException {
-            JSONObject json = new JSONObject();
-            json.putAll(map);
             EmployeeInfo model;
             try {
-                model = JSON.toJavaObject(json, EmployeeInfo.class);
+                model = JsonTools.mapToBean(map, EmployeeInfo.class);
                 employees.add(model);
             } catch (JSONException e) {
                 throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
@@ -52,8 +48,8 @@ public class ExcelInOutTest {
         }
     }
 
-    public static void main(String[] args) {
-        URL path = PathTools.findClassResource(ExcelInOutTest.class, "ExcelInOutTest.txt");
+    public static void main(String[] args) throws Exception {
+        URL path = PathTools.findClassResource(ExcelInOutTest.class, "ExcelInOutTest.properties");
         System.out.println(path);
         Properties original = PropertyTools.load(path, "UTF-8");
         Properties common = PropertyTools.filter(original, "common."); // 公共配置
@@ -63,7 +59,7 @@ public class ExcelInOutTest {
         test(PropertyTools.concat(new Properties(), common, test2), 2);
     }
 
-    private static void test(Properties properties, int index) {
+    private static void test(Properties properties, int index) throws Exception {
 
         URL xlsx = PathTools.findClassResource(ExcelInOutTest.class, "员工信息导入." + index + ".xlsx");
         XMetadata metadata = MetadataTools.parseProperties(properties);
@@ -82,9 +78,6 @@ public class ExcelInOutTest {
             if (succ != 8 || fail != 2) {
                 throw new Exception("不符合预期结果: index=" + index + ", succ!=8, fail!=2");
             }
-        } catch (Exception e) {
-            e.printStackTrace();
-            return;
         }
 
         URL tpl = PathTools.findClassResource(ExcelInOutTest.class, "员工信息(模板)." + index + ".xlsx");
@@ -135,85 +128,4 @@ public class ExcelInOutTest {
             System.out.println(index + "\t" + this.getName() + " done --> " + ConvertTools.toDuration(start, true));
         }
     }
-
-    public enum Gender {
-        UNKNOWN, MALE, FEMALE
-    }
-
-    public static class EmployeeInfo {
-
-        private Long id;
-        private String dept;
-        private String name;
-        private Boolean positive;
-        private Integer height;
-        private Date birthday;
-        private Gender gender;
-        private Integer subsidy;
-
-        public Long getId() {
-            return id;
-        }
-
-        public void setId(Long id) {
-            this.id = id;
-        }
-
-        public String getDept() {
-            return dept;
-        }
-
-        public void setDept(String dept) {
-            this.dept = dept;
-        }
-
-        public String getName() {
-            return name;
-        }
-
-        public void setName(String name) {
-            this.name = name;
-        }
-
-        public Boolean getPositive() {
-            return positive;
-        }
-
-        public void setPositive(Boolean positive) {
-            this.positive = positive;
-        }
-
-        public Integer getHeight() {
-            return height;
-        }
-
-        public void setHeight(Integer height) {
-            this.height = height;
-        }
-
-        public Date getBirthday() {
-            return birthday;
-        }
-
-        public void setBirthday(Date birthday) {
-            this.birthday = birthday;
-        }
-
-        public Gender getGender() {
-            return gender;
-        }
-
-        public void setGender(Gender gender) {
-            this.gender = gender;
-        }
-
-        public Integer getSubsidy() {
-            return subsidy;
-        }
-
-        public void setSubsidy(Integer subsidy) {
-            this.subsidy = subsidy;
-        }
-
-    }
 }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.txt b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.properties
similarity index 100%
rename from tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.txt
rename to tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.properties
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSheetTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSheetTest.java
index 7bc01a7..2e5648c 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSheetTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSheetTest.java
@@ -1,15 +1,17 @@
 package com.gitee.qdbp.tools.excel;
 
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
-import com.gitee.qdbp.tools.excel.utils.ExcelTools;
-import com.gitee.qdbp.tools.files.PathTools;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import com.gitee.qdbp.tools.excel.utils.ExcelTools;
+import com.gitee.qdbp.tools.files.PathTools;
 
 /**
  * Sheet页复制(前景色和背景色复制有问题)
@@ -19,19 +21,19 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook;
  */
 public class ExcelSheetTest {
 
-    public static void main(String[] args) {
+    public static void main(String[] args) throws IOException, InvalidFormatException {
         String outFile = "D:/ExcelSheet.xlsx";
 
         URL xlsx = PathTools.findClassResource(ExcelInOutTest.class, "员工信息导入.1.xlsx");
 
-        try (InputStream is = xlsx.openStream(); OutputStream os = new FileOutputStream(outFile);
-                Workbook src = WorkbookFactory.create(is); Workbook wb = new XSSFWorkbook()) {
+        try (InputStream is = xlsx.openStream();
+                OutputStream os = new FileOutputStream(outFile);
+                Workbook src = WorkbookFactory.create(is);
+                Workbook wb = new XSSFWorkbook()) {
             Sheet srcSheet = src.getSheetAt(0);
             Sheet targetSheet = wb.createSheet(srcSheet.getSheetName());
             ExcelTools.copySheet(srcSheet, targetSheet, true);
             wb.write(os);
-        } catch (Exception e) {
-            e.printStackTrace();
         }
 
     }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.java
new file mode 100644
index 0000000..1ca7033
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.java
@@ -0,0 +1,57 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.io.File;
+import java.net.URL;
+import java.util.Date;
+import java.util.List;
+import com.gitee.qdbp.tools.excel.model.FailedInfo;
+import com.gitee.qdbp.tools.excel.utils.MetadataTools;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
+
+public class ExcelSimpleTest {
+
+    public static void main(String[] args) throws Exception {
+        URL configPath = PathTools.findClassResource(ExcelSimpleTest.class, "ExcelSimpleTest.properties");
+        URL sourceExcel = PathTools.findClassResource(ExcelSimpleTest.class, "员工信息导入.1.xlsx");
+        System.out.println("configPath = " + PathTools.toUriPath(configPath));
+        System.out.println("sourceExcel = " + PathTools.toUriPath(sourceExcel));
+
+        XMetadata metadata = MetadataTools.parseProperties(configPath);
+        XExcelParser parser = new XExcelParser(metadata);
+        ImportResult<EmployeeInfo> result = parser.parseAsBean(sourceExcel, EmployeeInfo.class);
+
+        int fail = result.getFailed().size();
+        int succ = result.getTotal() - fail;
+        System.out.printf("finished: succ=%s, fail=%s%n", succ, fail);
+        if (succ != 8 || fail != 2) {
+            throw new Exception("不符合预期结果: succ!=8, fail!=2");
+        }
+        System.out.println("---------------");
+        System.out.println("fail: " + fail);
+        for (FailedInfo item : result.getFailed()) {
+            System.out.println(JsonTools.toLogString(item));
+        }
+        List<EmployeeInfo> employees = result.getContents();
+        System.out.println("---------------");
+        System.out.println("succ: " + succ);
+        for (EmployeeInfo item : employees) {
+            System.out.println(JsonTools.toLogString(item));
+        }
+        System.out.println("---------------");
+
+        URL templatePath = PathTools.findClassResource(ExcelSimpleTest.class, "员工信息(模板).1.xlsx");
+        String saveFolder = PathTools.getOutputFolder(sourceExcel, "./");
+        String targetExcel = PathTools.concat(saveFolder, "员工信息导出.1.xlsx");
+        System.out.println("templatePath = " + PathTools.toUriPath(templatePath));
+        System.out.println("targetExcel = " + targetExcel);
+
+        // 导出
+        XExcelExporter exporter = new XExcelExporter(metadata);
+        Date start = new Date();
+        exporter.export(employees, templatePath, new File(targetExcel));
+
+        System.out.println("export successful, " + ConvertTools.toDuration(start));
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties
new file mode 100644
index 0000000..6b4b9b5
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties
@@ -0,0 +1,44 @@
+## 字段列表
+columns = *id|*name|positive|height||birthday|gender|subsidy
+
+## 跳过的行数(不配置时默认取字段行或标题行的下一行)
+# skip.rows = 2
+## 标题行号,从1开始
+header.rows = 2-4
+## 页脚行号,从1开始,只对导出生效
+## 导入时每个excel的页脚位置有可能不一样, 所以导入不能指定页脚行, 只能在导入之前把页脚删掉
+## 如果页脚有公式, 那么数据行必须至少2行, 公式的范围必须包含这两行数据, 如SUM($E$5:E6), 而不能是SUM($E$5:E5), 否则公式不会计算
+footer.rows = 7-9
+## 包含指定关键字时跳过此行
+## A列为空, 或B列包含小计且H列包含元, 或B列包含总计且H列包含元
+# skip.row.when = { A:"NULL" }, { B:"小计", H:"元" }, { B:"总计", H:"元" }
+
+
+## 按索引号配置Sheet页
+## 配置规则: * 表示全部
+## 配置规则: 1|2|5-8|12
+## 配置规则: !1 表示排除第1个
+## 配置规则: !1|3|5 表示排除第1/3/5个
+# sheet.index = !1
+## 按索名称配置Sheet页(sheet1这种未命名的sheet是默认排除的)
+## 配置规则: * 表示全部
+## 配置规则: 开发|测试
+## 配置规则: !说明|描述 表示排除
+sheet.name = !说明
+## 页签名称填充至哪个字段
+sheet.name.fill.to = dept
+## 行序号填充至哪个字段
+row.index.fill.to = index
+
+## 转换规则
+rule.date.birthday = yyyy/MM/dd
+rule.map.positive = { true:"已转正|是|Y", false:"未转正|否|N" }
+# 多个规则
+rules.height = { number:"int" }, { ignoreIllegalValue:true }
+# 简写规则
+# rule.map.gender = { UNKNOWN:"未知|0", MALE:"男|1", FEMALE:"女|2" }
+# rule.map.gender = UNKNOWN:未知|0, MALE:男|1, FEMALE:女|2
+# rules.gender = { map:{ UNKNOWN:"未知|0", MALE:"男|1", FEMALE:"女|2" } }, { ignoreIllegalValue:true }
+# rules.gender = { map:"UNKNOWN:未知|0, MALE:男|1, FEMALE:女|2" }, { ignoreIllegalValue:true }
+rules.gender = { map:"UNKNOWN:未知|0, MALE:男|1, FEMALE:女|2" }
+
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/Gender.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/Gender.java
new file mode 100644
index 0000000..37f8837
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/Gender.java
@@ -0,0 +1,5 @@
+package com.gitee.qdbp.tools.excel;
+
+enum Gender {
+    UNKNOWN, MALE, FEMALE
+}
\ No newline at end of file
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/specialized/PinyinChecker.java b/tools/src/test/java/com/gitee/qdbp/tools/specialized/PinyinChecker.java
index 28b88ae..55a563d 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/specialized/PinyinChecker.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/specialized/PinyinChecker.java
@@ -13,6 +13,8 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.tools.files.FileTools;
 import com.gitee.qdbp.tools.files.PathTools;
 import com.gitee.qdbp.tools.http.BaseHttpHandler;
@@ -23,8 +25,6 @@ import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 import com.github.stuxuhai.jpinyin.PinyinFormat;
 import com.github.stuxuhai.jpinyin.PinyinHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 拼音正确性检查
@@ -36,8 +36,6 @@ public class PinyinChecker {
 
     private static final Logger log = LoggerFactory.getLogger(PinyinChecker.class);
 
-    private static final Pattern SEPARATOR = Pattern.compile("/");
-
     public static void main(String[] args) throws IOException {
         PinyinChecker checker = new PinyinChecker();
         // System.out.println(checker.findPinyinByBaiduHanyu("觉"));
@@ -68,7 +66,7 @@ public class PinyinChecker {
                     writeLine(writer, line);
                     continue;
                 }
-                String[] items = SEPARATOR.split(line);
+                String[] items = StringTools.split(line, '/');
                 if (items.length < 3) {
                     writeLine(writer, line);
                     continue;
-- 
Gitee


From 630432c1de25a35c96790f39dc84e40f93e75402 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 14 Jun 2022 00:20:07 +0800
Subject: [PATCH 042/160] =?UTF-8?q?Excel=E5=AF=BC=E5=85=A5=E5=AF=BC?=
 =?UTF-8?q?=E5=87=BA=E7=AE=80=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/excel/XMetadata.java | 18 ++++++-
 .../qdbp/tools/excel/utils/MetadataTools.java | 13 +++--
 .../gitee/qdbp/tools/excel/ExcelMapTest.java  | 52 +++++++++++++++++++
 3 files changed, 75 insertions(+), 8 deletions(-)
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelMapTest.java

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
index 64a5edf..0bcf233 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
@@ -20,7 +20,7 @@ import com.gitee.qdbp.tools.utils.Config;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
- * excel配置数据<br>
+ * Excel配置数据<br>
  * 有哪些配置项详见{@link MetadataTools#parseProperties(Properties)}<br>
  *
  * @author zhaohuihua
@@ -328,4 +328,20 @@ public class XMetadata implements Serializable {
         return instance;
     }
 
+    /**
+     * 创建最简单的XMetadata
+     *
+     * @param skipRows 跳过几行, field.rows = 5
+     * @param fieldNames 字段名配置, field.names = *id|*name|positive|height||birthday|gender|subsidy
+     * @return XMetadata
+     */
+    public static XMetadata of(int skipRows, String fieldNames) {
+        Properties properties = new Properties();
+        properties.put("skip.rows", String.valueOf(skipRows));
+        properties.put("field.names", fieldNames);
+        properties.put("sheet.name", "*");
+        properties.put("sheet.name.fill.to", "sheetName");
+        properties.put("row.index.fill.to", "rowIndex");
+        return MetadataTools.parseProperties(properties);
+    }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
index aa8047b..4c5ed12 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
@@ -1,6 +1,5 @@
 package com.gitee.qdbp.tools.excel.utils;
 
-import java.io.InputStream;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -10,6 +9,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
@@ -36,12 +41,6 @@ import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.PropertyTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Cell;
-import org.apache.poi.ss.usermodel.Row;
-import org.apache.poi.ss.usermodel.Sheet;
-import org.apache.poi.ss.usermodel.Workbook;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 元数据解析工具类
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelMapTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelMapTest.java
new file mode 100644
index 0000000..87ebd51
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelMapTest.java
@@ -0,0 +1,52 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.io.File;
+import java.net.URL;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.excel.model.FailedInfo;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
+
+public class ExcelMapTest {
+
+    public static void main(String[] args) throws Exception {
+        URL sourceExcel = PathTools.findClassResource(ExcelMapTest.class, "员工信息导入.1.xlsx");
+        System.out.println("sourceExcel = " + PathTools.toUriPath(sourceExcel));
+
+        XMetadata metadata = XMetadata.of(4, "*id|*name|positive|height||birthday|gender|subsidy");
+        XExcelParser parser = new XExcelParser(metadata);
+        ImportResult<Map<String, Object>> result = parser.parseAsMap(sourceExcel);
+
+        int fail = result.getFailed().size();
+        int succ = result.getTotal() - fail;
+        System.out.printf("finished: succ=%s, fail=%s%n", succ, fail);
+        System.out.println("---------------");
+        System.out.println("fail: " + fail);
+        for (FailedInfo item : result.getFailed()) {
+            System.out.println(JsonTools.toLogString(item));
+        }
+        List<Map<String, Object>> employees = result.getContents();
+        System.out.println("---------------");
+        System.out.println("succ: " + succ);
+        for (Map<String, Object> item : employees) {
+            System.out.println(JsonTools.toLogString(item));
+        }
+        System.out.println("---------------");
+
+        URL templatePath = PathTools.findClassResource(ExcelMapTest.class, "员工信息(模板).1.xlsx");
+        String saveFolder = PathTools.getOutputFolder(sourceExcel, "./");
+        String targetExcel = PathTools.concat(saveFolder, "员工信息导出.1.xlsx");
+        System.out.println("templatePath = " + PathTools.toUriPath(templatePath));
+        System.out.println("targetExcel = " + targetExcel);
+
+        // 导出
+        XExcelExporter exporter = new XExcelExporter(metadata);
+        Date start = new Date();
+        exporter.export(employees, templatePath, new File(targetExcel));
+
+        System.out.println("export successful, " + ConvertTools.toDuration(start));
+    }
+}
-- 
Gitee


From 6639b2dcd6704a37ce99401577bb98e1bb1150cf Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 15 Jun 2022 21:46:42 +0800
Subject: [PATCH 043/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/parse/StringParser.java | 16 ++++++++++++++++
 .../com/gitee/qdbp/tools/excel/XMetadata.java    |  2 +-
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/parse/StringParser.java b/able/src/main/java/com/gitee/qdbp/tools/parse/StringParser.java
index f7bcb65..a51f137 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/parse/StringParser.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/parse/StringParser.java
@@ -10,6 +10,22 @@ import java.util.List;
  */
 public class StringParser {
 
+    /**
+     * 解析方法参数, 支持的参数形式如下:<br>
+     * ( user.name, 'xxx', 'xxx,yyy', ['aaa', 'bbb'] )<br>
+     * ( user.name + ', hello!', 'Hello ' + user.name + ', welcome!' )<br>
+     * ( 'Let\'s go', "Let's go", '8,000.00', '8,000.00(元)' )<br>
+     * ( Math.round(user.height), Math.round( Math.abs(user.height) ) )<br>
+     * ( StringTools.removeChars('8,000.00(元)', ',') )<br>
+     * ( {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] } )
+     *
+     * @param string 字符串
+     * @return 解析结果
+     */
+    public static MethodParamItems parseMethodParams(String string) {
+        return MethodParamParser.parse(string, 0);
+    }
+
     /**
      * 解析方法参数, 支持的参数形式如下:<br>
      * ( user.name, 'xxx', 'xxx,yyy', ['aaa', 'bbb'] )<br>
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
index 0bcf233..c7854b4 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
@@ -331,7 +331,7 @@ public class XMetadata implements Serializable {
     /**
      * 创建最简单的XMetadata
      *
-     * @param skipRows 跳过几行, field.rows = 5
+     * @param skipRows 跳过几行, skip.rows = 5
      * @param fieldNames 字段名配置, field.names = *id|*name|positive|height||birthday|gender|subsidy
      * @return XMetadata
      */
-- 
Gitee


From ce04d7d88c43fd515487b4f1f81a0b76ff1bf58e Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Jun 2022 00:15:59 +0800
Subject: [PATCH 044/160] =?UTF-8?q?Excel=E5=B7=A5=E5=85=B7=E7=B1=BB?=
 =?UTF-8?q?=E6=94=AF=E6=8C=81poi-4.x?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/tools/excel/XExcelExporter.java      | 17 ++---
 .../gitee/qdbp/tools/excel/XExcelParser.java  | 25 ++++---
 .../qdbp/tools/excel/json/ExcelBeans.java     | 22 +++---
 .../qdbp/tools/excel/utils/ExcelTools.java    | 31 ++++----
 .../com/gitee/qdbp/tools/utils/JsonTools.java | 71 ++++++++++++++++---
 .../tools/excel/beans/ExcelBeansTest.java     | 20 +++---
 6 files changed, 127 insertions(+), 59 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
index c75c684..1b975fc 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
@@ -9,8 +9,6 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
 import java.util.List;
-import org.apache.poi.POIXMLException;
-import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
@@ -131,7 +129,7 @@ public class XExcelExporter {
                 wb.write(output);
             } catch (FileNotFoundException e) {
                 throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
-            } catch (IOException e) {
+            } catch (Exception e) {
                 log.error("write excel error.", e);
                 throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR);
             }
@@ -140,12 +138,15 @@ public class XExcelExporter {
         } catch (IOException e) {
             log.error("read excel error.", e);
             throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
-        } catch (POIXMLException e) {
-            log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR, e);
-        } catch (InvalidFormatException e) {
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         }
     }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java
index 3203d08..14aabb1 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java
@@ -7,8 +7,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
 import java.util.Map;
-import org.apache.poi.POIXMLException;
-import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
@@ -217,12 +215,15 @@ public class XExcelParser {
         } catch (IOException e) {
             log.error("read excel error.", e);
             throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
-        } catch (POIXMLException e) {
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR, e);
-        } catch (InvalidFormatException e) {
-            log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         }
     }
 
@@ -252,9 +253,15 @@ public class XExcelParser {
                 }
             }
             callback.finish(workbook, metadata);
-        } catch (POIXMLException e) {
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR, e);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         }
     }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelBeans.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelBeans.java
index e5647d6..01940b4 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelBeans.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelBeans.java
@@ -1,5 +1,6 @@
 package com.gitee.qdbp.tools.excel.json;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -12,8 +13,6 @@ import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.apache.poi.POIXMLException;
-import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
@@ -120,12 +119,9 @@ public class ExcelBeans {
         } catch (IOException e) {
             log.error("read excel error.", e);
             throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
-        } catch (POIXMLException e) {
+        } catch (Exception e) {
             log.error("read excel error.", e);
             throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
-        } catch (InvalidFormatException e) {
-            log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
         } finally {
             this.lock.unlock();
         }
@@ -144,7 +140,7 @@ public class ExcelBeans {
             this.init();
             this.doParseSheet(wb, sheetName, 0);
             return this.container;
-        } catch (POIXMLException e) {
+        } catch (Exception e) {
             log.error("read sheet error, sheetName=" + sheetName, e);
             throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
         } finally {
@@ -163,9 +159,15 @@ public class ExcelBeans {
             this.init();
             this.doParseSheet(sheet, 0);
             return this.container;
-        } catch (POIXMLException e) {
-            log.error("read sheet error, sheetName=" + sheet.getSheetName(), e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
+            log.error("read excel error.", e);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         } finally {
             this.lock.unlock();
         }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java
index 00a1487..09e9199 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java
@@ -265,25 +265,26 @@ public abstract class ExcelTools {
 
         // 单元格类型
         CellType cellType = src.getCellTypeEnum();
-        target.setCellType(cellType);
 
         if (cellType == CellType.FORMULA) { // 公式
             target.setCellFormula(src.getCellFormula());
-        } else if (copyValue) { // 复制内容
-
-            if (cellType == CellType.ERROR) { // 错误
-                target.setCellErrorValue(src.getErrorCellValue());
-            } else {
-                Object value = getCellValue(src);
-                setCellValue(target, value);
-            }
+        } else {
+            target.setCellType(cellType);
+            if (copyValue) { // 复制内容
+                if (cellType == CellType.ERROR) { // 错误
+                    target.setCellErrorValue(src.getErrorCellValue());
+                } else {
+                    Object value = getCellValue(src);
+                    setCellValue(target, value);
+                }
 
-            // 评论
-            Comment comment = src.getCellComment();
-            if (comment == null) {
-                target.removeCellComment();
-            } else {
-                target.setCellComment(comment);
+                // 评论
+                Comment comment = src.getCellComment();
+                if (comment == null) {
+                    target.removeCellComment();
+                } else {
+                    target.setCellComment(comment);
+                }
             }
         }
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
index 5d144b8..064810e 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
@@ -46,17 +46,17 @@ public class JsonTools {
     }
 
     public static String getMapString(Map<String, Object> map, String key) {
-        Object value = map.get(key);
+        Object value = MapTools.getValue(map, key);
         return TypeUtils.castToJavaBean(value, String.class);
     }
 
     public static <T> T getMapValue(Map<String, Object> map, String key, Class<T> clazz) {
-        Object value = map.get(key);
+        Object value = MapTools.getValue(map, key);
         return TypeUtils.castToJavaBean(value, clazz);
     }
 
     public static <T> T getMapValue(Map<String, Object> map, String key, T defaults, Class<T> clazz) {
-        Object value = map.get(key);
+        Object value = MapTools.getValue(map, key);
         return TypeUtils.castToJavaBean(value == null ? defaults : value, clazz);
     }
 
@@ -65,7 +65,7 @@ public class JsonTools {
     }
 
     public static <T> List<T> getMapValues(Map<String, Object> map, String key, boolean nullToEmpty, Class<T> clazz) {
-        Object value = map.get(key);
+        Object value = MapTools.getValue(map, key);
         if (!(value instanceof Collection)) {
             return nullToEmpty ? new ArrayList<T>() : null;
         }
@@ -80,7 +80,7 @@ public class JsonTools {
     /**
      * 将对象转换为以换行符分隔的日志文本<br>
      * 如 newlineLogs(params, operator) 返回 \n\t{paramsJson}\n\t{operatorJson}<br>
-     * 
+     *
      * @param objects 对象
      * @return 日志文本
      */
@@ -101,6 +101,7 @@ public class JsonTools {
     }
 
     private static final SerializeConfig JSON_CONFIG = new SerializeConfig();
+
     static {
         JSON_CONFIG.put(Double.class, new DoubleSerializer("#.##################"));
     }
@@ -158,7 +159,7 @@ public class JsonTools {
     /**
      * 将Map内容设置到Java对象中<br>
      * copy from JavaBeanDeserializer.createInstance(Map<String, Object>, ParserConfig);
-     * 
+     *
      * @param map Map
      * @param bean 目标Java对象
      */
@@ -269,13 +270,16 @@ public class JsonTools {
 
     /**
      * 将Map转换为Java对象
-     * 
+     *
      * @param <T> 目标类型
      * @param map Map
      * @param clazz 目标Java类
      * @return Java对象
      */
     public static <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
+        if (map == null) {
+            return null;
+        }
         @SuppressWarnings("unchecked")
         Map<String, Object> json = (Map<String, Object>) map;
         return TypeUtils.castToJavaBean(json, clazz, ParserConfig.getGlobalInstance());
@@ -284,7 +288,7 @@ public class JsonTools {
     /**
      * 将Java对象转换为Map<br>
      * copy from fastjson JSON.toJSON(), 保留enum和date
-     * 
+     *
      * @param bean JavaBean对象
      * @return Map
      */
@@ -297,7 +301,7 @@ public class JsonTools {
 
     /**
      * 将Java对象转换为Map
-     * 
+     *
      * @param object Java对象
      * @param deep 是否递归转换子对象
      * @param clearBlankValue 是否清除空值
@@ -314,6 +318,55 @@ public class JsonTools {
         return map;
     }
 
+    /**
+     * 将Map数组转换为Java对象列表
+     *
+     * @param <T> 目标类型
+     * @param maps Map数组
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    public static <T> List<T> mapToBeans(Collection<Map<String, ?>> maps, Class<T> clazz) {
+        if (maps == null) {
+            return null;
+        }
+        List<T> result = new ArrayList<>();
+        for (Map<String, ?> map : maps) {
+            result.add(mapToBean(map, clazz));
+        }
+        return result;
+    }
+
+    /**
+     * 将Java对象转换为Map列表<br>
+     * copy from fastjson JSON.toJSON(), 保留enum和date
+     *
+     * @param beans JavaBean对象列表
+     * @return Map列表
+     */
+    public static List<Map<String, Object>> beanToMaps(Collection<?> beans) {
+        return beanToMaps(beans, true, true);
+    }
+
+    /**
+     * 将Java对象转换为Map列表
+     *
+     * @param beans Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map列表
+     */
+    public static List<Map<String, Object>> beanToMaps(Collection<?> beans, boolean deep, boolean clearBlankValue) {
+        if (beans == null) {
+            return null;
+        }
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (Object bean : beans) {
+            result.add(beanToMap(bean, true, true));
+        }
+        return result;
+    }
+
     protected static JSONObject getBeanFieldValuesMap(Object bean, boolean deep, SerializeConfig config) {
         if (bean == null) {
             return null;
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/beans/ExcelBeansTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/beans/ExcelBeansTest.java
index ee30eb9..336ec58 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/excel/beans/ExcelBeansTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/beans/ExcelBeansTest.java
@@ -1,5 +1,6 @@
 package com.gitee.qdbp.tools.excel.beans;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
@@ -9,8 +10,6 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.apache.poi.POIXMLException;
-import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
 import org.slf4j.Logger;
@@ -66,15 +65,20 @@ public class ExcelBeansTest {
             compareHolidays(config);
             // 比较还本计划
             comparePrincipalSchedules(container, firstRepayPrincipalDate);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
         } catch (IOException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
-        } catch (POIXMLException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
-        } catch (InvalidFormatException e) {
-            log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         }
     }
 
-- 
Gitee


From 359c4cd2892386b23b322b4b20a441f46e1e8702 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Jun 2022 00:17:14 +0800
Subject: [PATCH 045/160] =?UTF-8?q?=E4=BF=AE=E6=AD=A3parseByteString?=
 =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/tools/utils/ConvertTools.java  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index 38f2929..bbbd79a 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -1005,11 +1005,11 @@ public class ConvertTools {
         String unit = null;
         char[] chars = number.toCharArray();
         for (int i = 0; i < chars.length; i++) {
-            int p = chars.length - 1 - i;
-            if (Character.isDigit(chars[p])) {
+            int p = chars.length - i;
+            if (Character.isDigit(chars[p - 1])) {
                 if (i > 0) {
                     number = string.substring(0, p);
-                    unit = string.substring(p + 1);
+                    unit = string.substring(p);
                 }
                 break;
             }
-- 
Gitee


From cd84e13fe7922049a6f8fd5b284575bda2740c72 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Jun 2022 00:18:20 +0800
Subject: [PATCH 046/160] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B3=A8=E9=87=8A?=
 =?UTF-8?q?=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/tools/utils/NamingTools.java | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/NamingTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/NamingTools.java
index e9fb3ea..ee4aa1a 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/NamingTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/NamingTools.java
@@ -141,10 +141,10 @@ public class NamingTools {
      * A {@code null} input String returns {@code null}.
      *
      * <pre>
-     * StringTools.capitalize(null)  = null
-     * StringTools.capitalize("")    = ""
-     * StringTools.capitalize("cat") = "Cat"
-     * StringTools.capitalize("cAt") = "CAt"
+     * NamingTools.capitalize(null)  = null
+     * NamingTools.capitalize("")    = ""
+     * NamingTools.capitalize("cat") = "Cat"
+     * NamingTools.capitalize("cAt") = "CAt"
      * </pre>
      *
      * @param str the String to capitalize, may be null
-- 
Gitee


From 2feef0acbbc142333fa50286d665928ed70a3f03 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Jun 2022 00:18:54 +0800
Subject: [PATCH 047/160] 5.4.19

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index fc3ee57..72c5346 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.17</version>
+	<version>5.4.19</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index 13525a7..84e0f38 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.17</version>
+	<version>5.4.19</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 99188af5d1420a6aafb803e0172e5c3060123db3 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 10 Jul 2022 11:47:43 +0800
Subject: [PATCH 048/160] =?UTF-8?q?Excel=E5=AF=BC=E5=85=A5=E9=94=99?=
 =?UTF-8?q?=E8=AF=AF=E4=BF=A1=E6=81=AF=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/able/exception/ServiceException.java | 50 +++++++++++++++++--
 .../able/matches/RegexpGroupExtractor.java    | 10 ++--
 .../able/matches/RegexpStringExtractor.java   |  8 +--
 .../qdbp/able/result/IResultException.java    | 16 ++++++
 .../gitee/qdbp/tools/utils/VerifyTools.java   | 10 ++--
 .../qdbp/tools/excel/SheetFillCallback.java   | 10 ++--
 .../qdbp/tools/excel/SheetParseCallback.java  | 14 ++++--
 .../com/gitee/qdbp/tools/excel/XMetadata.java | 14 +++---
 .../qdbp/tools/excel/model/FailedInfo.java    |  8 ++-
 .../qdbp/tools/excel/utils/ExcelHelper.java   | 26 ++++++----
 .../qdbp/tools/excel/utils/MetadataTools.java | 10 ++--
 11 files changed, 125 insertions(+), 51 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/result/IResultException.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
index b62e0de..d5bea23 100644
--- a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
+++ b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
@@ -2,6 +2,7 @@ package com.gitee.qdbp.able.exception;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.UndeclaredThrowableException;
+import com.gitee.qdbp.able.result.IResultException;
 import com.gitee.qdbp.able.result.IResultMessage;
 
 /**
@@ -10,7 +11,7 @@ import com.gitee.qdbp.able.result.IResultMessage;
  * @author zhaohuihua
  * @version 150915
  */
-public class ServiceException extends RuntimeException implements IResultMessage {
+public class ServiceException extends RuntimeException implements IResultException {
 
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
@@ -34,12 +35,25 @@ public class ServiceException extends RuntimeException implements IResultMessage
         this.code = result.getCode();
     }
 
+    /**
+     * 构造函数
+     *
+     * @param result 错误返回码
+     */
+    public ServiceException(IResultException result) {
+        super(result.getSimpleMessage());
+        this.code = result.getCode();
+        this.details = result.getDetails();
+    }
+
     /**
      * 构造函数
      *
      * @param result 错误返回码
      * @param details 错误详情
+     * @deprecated 第2个参数含义容易误解为Message, 改为throw new ServiceException(...).setDetails(...);
      */
+    @Deprecated
     public ServiceException(IResultMessage result, String details) {
         super(result.getMessage());
         this.code = result.getCode();
@@ -57,13 +71,27 @@ public class ServiceException extends RuntimeException implements IResultMessage
         this.code = result.getCode();
     }
 
+    /**
+     * 构造函数
+     *
+     * @param result 错误返回码
+     * @param cause 引发异常的原因
+     */
+    public ServiceException(IResultException result, Throwable cause) {
+        super(result.getSimpleMessage(), cause);
+        this.code = result.getCode();
+        this.details = result.getDetails();
+    }
+
     /**
      * 构造函数
      *
      * @param result 错误返回码
      * @param details 错误详情
      * @param cause 引发异常的原因
+     * @deprecated 第2个参数含义容易误解为Message, 改为throw new ServiceException(...).setDetails(...);
      */
+    @Deprecated
     public ServiceException(IResultMessage result, String details, Throwable cause) {
         super(result.getMessage(), cause);
         this.code = result.getCode();
@@ -94,6 +122,7 @@ public class ServiceException extends RuntimeException implements IResultMessage
      *
      * @return 错误详情
      */
+    @Override
     public String getDetails() {
         return details;
     }
@@ -113,6 +142,7 @@ public class ServiceException extends RuntimeException implements IResultMessage
      *
      * @return 错误消息
      */
+    @Override
     public String getSimpleMessage() {
         return super.getMessage();
     }
@@ -146,10 +176,18 @@ public class ServiceException extends RuntimeException implements IResultMessage
         }
     }
 
-    /** 从exception.cause中查找ServiceException **/
+    /** 从exception.cause中查找ServiceException, 如果存在则抛出该异常 **/
     public static void throwWhenServiceException(Throwable e) throws ServiceException {
+        ServiceException exception = findWhenServiceException(e);
+        if (exception != null) {
+            throw exception;
+        }
+    }
+
+    /** 从exception.cause中查找ServiceException, 如果不存在则返回null **/
+    public static ServiceException findWhenServiceException(Throwable e) throws ServiceException {
         Throwable unwrapped = e;
-        while (true) {
+        while (unwrapped != null) {
             if (unwrapped instanceof InvocationTargetException) {
                 unwrapped = ((InvocationTargetException) unwrapped).getTargetException();
             } else if (unwrapped instanceof UndeclaredThrowableException) {
@@ -160,11 +198,15 @@ public class ServiceException extends RuntimeException implements IResultMessage
         }
 
         Throwable cause = unwrapped;
+        if (cause == e) {
+            cause = e.getCause();
+        }
         while (cause != null) {
             if (cause instanceof ServiceException) {
-                throw (ServiceException) cause;
+                return (ServiceException) cause;
             }
             cause = cause.getCause();
         }
+        return null;
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
index 21004dc..9151205 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
@@ -211,7 +211,7 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
             } else {
                 String type = group == null ? "null" : group.getClass().getSimpleName();
                 String msg = "Group must be integer or string, but type of groups[" + i + "] is " + type;
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
             }
         }
     }
@@ -272,12 +272,12 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
             String g = sa.readInBracket('[', ']');
             if (!sa.isLastReadSuccess()) {
                 String msg = "Group format error:  " + rule + "  , The correct format is [1,2] regexp";
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
             }
             g = g.trim();
             if (g.length() == 0) {
                 String msg = "Group must must not be empty, --> " + rule;
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
             }
             List<Object> groups = new ArrayList<>();
             for (String group : StringTools.splits(g, ',')) {
@@ -287,7 +287,7 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
                     groups.add(group);
                 } else {
                     String msg = "Group must be digit or word characters: " + g + " --> " + rule;
-                    throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+                    throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
                 }
             }
             sa.skipWhitespace();
@@ -301,7 +301,7 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
                 return extractor;
             } catch (PatternSyntaxException e) {
                 String msg = "Regexp format error, --> " + regexp;
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg, e);
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, e).setDetails(msg);
             }
         }
 
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
index 66f1e5b..1247eae 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
@@ -213,12 +213,12 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
             String group = sa.readInBracket('[', ']');
             if (!sa.isLastReadSuccess()) {
                 String msg = "Group format error, The correct format is [1] regexp, --> " + rule;
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
             }
             group = group.trim();
             if (group.length() == 0) {
                 String msg = "Group must must not be empty, --> " + rule;
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
             }
             Integer digitGroup = null;
             String namedGroup = null;
@@ -228,7 +228,7 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
                 namedGroup = group;
             } else {
                 String msg = "Group must be digit or word characters: " + group + " --> " + rule;
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg);
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
             }
             sa.skipWhitespace();
             String regexp = sa.readToEnd();
@@ -237,7 +237,7 @@ public class RegexpStringExtractor extends ExtraData implements StringExtractor
                 pattern = Pattern.compile(regexp);
             } catch (PatternSyntaxException e) {
                 String msg = "Regexp format error, --> " + regexp;
-                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, msg, e);
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, e).setDetails(msg);
             }
             RegexpStringExtractor extractor;
             if (digitGroup != null) {
diff --git a/able/src/main/java/com/gitee/qdbp/able/result/IResultException.java b/able/src/main/java/com/gitee/qdbp/able/result/IResultException.java
new file mode 100644
index 0000000..0a6b170
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/result/IResultException.java
@@ -0,0 +1,16 @@
+package com.gitee.qdbp.able.result;
+
+/**
+ * 操作异常
+ *
+ * @author zhaohuihua
+ * @version 20220710
+ */
+public interface IResultException extends IResultMessage {
+
+    /** 简单错误提示 **/
+    String getSimpleMessage();
+
+    /** 详细描述 **/
+    String getDetails();
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
index 36688eb..92e5964 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
@@ -28,7 +28,7 @@ public class VerifyTools {
      */
     public static void requireNonNull(Object object, String name) {
         if (object == null) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be null.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be null.");
         }
     }
 
@@ -41,10 +41,10 @@ public class VerifyTools {
      */
     public static void requireNotBlank(Object object, String name) {
         if (object == null) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be null.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be null.");
         }
         if (isBlank(object)) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be empty.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be empty.");
         }
     }
 
@@ -57,10 +57,10 @@ public class VerifyTools {
      */
     public static void requireNotBlank(String string, String name) {
         if (string == null) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be null.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be null.");
         }
         if (isBlank(string)) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be empty.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be empty.");
         }
     }
 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java
index e418820..92966a5 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java
@@ -9,6 +9,7 @@ import org.apache.poi.ss.usermodel.Row;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.IResultException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
@@ -79,9 +80,12 @@ public class SheetFillCallback implements Serializable {
                     }
                 } catch (Exception e) {
                     if (!ignoreIllegalValue) {
-                        String msg = ExcelTools.newConvertErrorMessage(cellInfo, rule);
-                        Exception se = new IllegalArgumentException(msg, e);
-                        throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, se);
+                        String details = ExcelTools.newConvertErrorMessage(cellInfo, rule);
+                        if (e instanceof IResultException) {
+                            throw new ServiceException((IResultException) e).setDetails(details);
+                        } else {
+                            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR).setDetails(details);
+                        }
                     } else {
                         cellInfo.setValue(null);
                         if (log.isWarnEnabled()) {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java
index fffdae6..4e5ee80 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java
@@ -8,6 +8,7 @@ import org.apache.poi.ss.usermodel.Cell;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.IResultException;
 import com.gitee.qdbp.able.result.IResultMessage;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
@@ -92,15 +93,18 @@ public abstract class SheetParseCallback extends SheetParseResult implements Ser
                     }
                 } catch (Exception e) {
                     if (!ignoreIllegalValue) {
-                        String msg = ExcelTools.newConvertErrorMessage(cellInfo, rule);
-                        Exception se = new IllegalArgumentException(msg, e);
-                        throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, se);
+                        String details = ExcelTools.newConvertErrorMessage(cellInfo, rule);
+                        if (e instanceof IResultException) {
+                            throw new ServiceException((IResultException) e).setDetails(details);
+                        } else {
+                            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR).setDetails(details);
+                        }
                     } else {
+                        cellInfo.setValue(null);
                         if (log.isWarnEnabled()) {
                             String msg = ExcelTools.newConvertErrorMessage(cellInfo, rule);
-                            log.warn(msg + ", " + e);
+                            log.warn(msg + ", " + e.getMessage());
                         }
-                        cellInfo.setValue(null);
                     }
                 }
             }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
index c7854b4..b275702 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
@@ -336,12 +336,12 @@ public class XMetadata implements Serializable {
      * @return XMetadata
      */
     public static XMetadata of(int skipRows, String fieldNames) {
-        Properties properties = new Properties();
-        properties.put("skip.rows", String.valueOf(skipRows));
-        properties.put("field.names", fieldNames);
-        properties.put("sheet.name", "*");
-        properties.put("sheet.name.fill.to", "sheetName");
-        properties.put("row.index.fill.to", "rowIndex");
-        return MetadataTools.parseProperties(properties);
+        XMetadata instance = new XMetadata();
+        instance.setSkipRows(skipRows);
+        instance.setFieldInfos(MetadataTools.parseFieldInfoByText(fieldNames));
+        instance.setSheetNames(new NameListCondition());
+        instance.setSheetNameFillTo("sheetName");
+        instance.setRowIndexFillTo("rowIndex");
+        return instance;
     }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/model/FailedInfo.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/model/FailedInfo.java
index e5ac910..3171667 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/model/FailedInfo.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/model/FailedInfo.java
@@ -2,6 +2,7 @@ package com.gitee.qdbp.tools.excel.model;
 
 import java.io.Serializable;
 import com.gitee.qdbp.able.result.IBatchResult.Failed;
+import com.gitee.qdbp.able.result.IResultException;
 import com.gitee.qdbp.able.result.IResultMessage;
 
 /**
@@ -34,7 +35,6 @@ public class FailedInfo implements Failed, Serializable {
     private String message;
 
     public FailedInfo() {
-
     }
 
     public FailedInfo(String sheetName, Integer index, IResultMessage result) {
@@ -47,7 +47,11 @@ public class FailedInfo implements Failed, Serializable {
         this.field = field;
         this.value = value;
         this.code = result.getCode();
-        this.message = result.getMessage();
+        if (result instanceof IResultException) {
+            this.message = ((IResultException) result).getSimpleMessage();
+        } else {
+            this.message = result.getMessage();
+        }
     }
 
     /** 获取Sheet页签的名称 **/
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
index 3aea11e..aea1e95 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
@@ -71,11 +71,7 @@ public class ExcelHelper {
                 parse(sheetName, row, i + 1, columnInfos, metadata, cb);
             } catch (ServiceException e) {
                 cb.addFailed(sheetName, i + 1, e.getDetails(), null, e);
-                if (e.getCause() != null) {
-                    String error = e.getMessage();
-                    String cause = e.getCause().getMessage();
-                    log.error("Sheet[{}], excel parse error, {}, {}", sheetName, error, cause, e);
-                }
+                log.error("Sheet[{}], excel parse error, {}", sheetName, e.getMessage());
             } catch (NumberFormatException e) {
                 cb.addFailed(sheetName, i + 1, ResultCode.PARAMETER_FORMAT_ERROR);
             } catch (IllegalArgumentException e) {
@@ -139,24 +135,32 @@ public class ExcelHelper {
                 // 调用转换规则
                 cb.convert(info, data);
             } catch (ServiceException e) {
-                // 拼接[列序号]字段名(单元格的值), 如[D:年龄]100Kg
-                String title = '[' + info.getTitle() + ']';
+                String title = info.getTitle();
+                // 拼接单元格的值, 如[D3]100Kg
+                StringBuilder text = new StringBuilder();
                 if (info.getColumn() != null) {
-                    title = '[' + ExcelTools.columnIndexToName(info.getColumn()) + ':' + info.getTitle() + ']';
+                    String columnIndex = ExcelTools.columnIndexToName(info.getColumn());
+                    text.append('[').append(columnIndex).append(info.getRow()).append(']');
                 }
                 if (VerifyTools.isNotBlank(original)) {
                     // 不知道怎么取Excel单元格的原始文本, 因此日期,时间就不好提示了
                     if (original instanceof String || original instanceof Boolean || original instanceof Number) {
-                        title += StringTools.ellipsis(original.toString(), 30);
+                        text.append(StringTools.ellipsis(original.toString(), 30));
                     }
                 }
-                cb.addFailed(sheetName, rowIndex, title, original, e);
+                cb.addFailed(sheetName, rowIndex, title, text.toString(), e);
                 return;
             }
             { // 检查必填字段
                 Object value = info.getValue();
                 if (info.isRequired() && VerifyTools.isBlank(value)) {
-                    cb.addFailed(sheetName, rowIndex, info.getTitle(), value, ResultCode.PARAMETER_IS_REQUIRED);
+                    String title = info.getTitle();
+                    StringBuilder text = new StringBuilder();
+                    if (info.getColumn() != null) {
+                        String columnIndex = ExcelTools.columnIndexToName(info.getColumn());
+                        text.append('[').append(columnIndex).append(info.getRow()).append(']');
+                    }
+                    cb.addFailed(sheetName, rowIndex, title, text.toString(), ResultCode.PARAMETER_IS_REQUIRED);
                     return;
                 }
             }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
index 4c5ed12..41567f4 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
@@ -626,11 +626,11 @@ public class MetadataTools {
 
     private static XMetadata newRuleMetadata(String sheetName, String columnFields, int skipRows) {
         VerifyTools.requireNotBlank(sheetName, "sheetName");
-        Properties config = new Properties();
-        config.put("field.names", columnFields);
-        config.put("skip.rows", skipRows);
-        config.put("sheet.name", sheetName);
-        return parseProperties(config);
+        XMetadata instance = new XMetadata();
+        instance.setSkipRows(skipRows);
+        instance.setFieldInfos(parseFieldInfoByText(columnFields));
+        instance.setSheetNames(new NameListCondition(sheetName));
+        return instance;
     }
 
     private static class CellRuleCallback extends ImportCallback {
-- 
Gitee


From f90cfc11ef6a0bee2298f3b1b80f5555da30f6ea Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 10 Jul 2022 12:42:26 +0800
Subject: [PATCH 049/160] =?UTF-8?q?ServiceException=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/able/exception/ServiceException.java | 49 ++++++++-----------
 .../qdbp/able/time/FileWorkdayResolver.java   |  6 +--
 2 files changed, 23 insertions(+), 32 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
index d5bea23..d9bc7b2 100644
--- a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
+++ b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
@@ -31,19 +31,11 @@ public class ServiceException extends RuntimeException implements IResultExcepti
      * @param result 错误返回码
      */
     public ServiceException(IResultMessage result) {
-        super(result.getMessage());
+        super(getRealMessage(result));
         this.code = result.getCode();
-    }
-
-    /**
-     * 构造函数
-     *
-     * @param result 错误返回码
-     */
-    public ServiceException(IResultException result) {
-        super(result.getSimpleMessage());
-        this.code = result.getCode();
-        this.details = result.getDetails();
+        if (result instanceof IResultException) {
+            this.details = ((IResultException) result).getDetails();
+        }
     }
 
     /**
@@ -51,11 +43,11 @@ public class ServiceException extends RuntimeException implements IResultExcepti
      *
      * @param result 错误返回码
      * @param details 错误详情
-     * @deprecated 第2个参数含义容易误解为Message, 改为throw new ServiceException(...).setDetails(...);
+     * @deprecated 第2个参数含义容易误解为message, 改为throw new ServiceException(...).setDetails(...);
      */
     @Deprecated
     public ServiceException(IResultMessage result, String details) {
-        super(result.getMessage());
+        super(getRealMessage(result));
         this.code = result.getCode();
         this.details = details;
     }
@@ -67,20 +59,11 @@ public class ServiceException extends RuntimeException implements IResultExcepti
      * @param cause 引发异常的原因
      */
     public ServiceException(IResultMessage result, Throwable cause) {
-        super(result.getMessage(), cause);
-        this.code = result.getCode();
-    }
-
-    /**
-     * 构造函数
-     *
-     * @param result 错误返回码
-     * @param cause 引发异常的原因
-     */
-    public ServiceException(IResultException result, Throwable cause) {
-        super(result.getSimpleMessage(), cause);
+        super(getRealMessage(result), cause);
         this.code = result.getCode();
-        this.details = result.getDetails();
+        if (result instanceof IResultException) {
+            this.details = ((IResultException) result).getDetails();
+        }
     }
 
     /**
@@ -89,11 +72,11 @@ public class ServiceException extends RuntimeException implements IResultExcepti
      * @param result 错误返回码
      * @param details 错误详情
      * @param cause 引发异常的原因
-     * @deprecated 第2个参数含义容易误解为Message, 改为throw new ServiceException(...).setDetails(...);
+     * @deprecated 第2个参数含义容易误解为message, 改为throw new ServiceException(...).setDetails(...);
      */
     @Deprecated
     public ServiceException(IResultMessage result, String details, Throwable cause) {
-        super(result.getMessage(), cause);
+        super(getRealMessage(result), cause);
         this.code = result.getCode();
         this.details = details;
     }
@@ -176,6 +159,14 @@ public class ServiceException extends RuntimeException implements IResultExcepti
         }
     }
 
+    private static String getRealMessage(IResultMessage result) {
+        if (result instanceof IResultException) {
+            return ((IResultException) result).getSimpleMessage();
+        } else {
+            return result.getMessage();
+        }
+    }
+
     /** 从exception.cause中查找ServiceException, 如果存在则抛出该异常 **/
     public static void throwWhenServiceException(Throwable e) throws ServiceException {
         ServiceException exception = findWhenServiceException(e);
diff --git a/able/src/main/java/com/gitee/qdbp/able/time/FileWorkdayResolver.java b/able/src/main/java/com/gitee/qdbp/able/time/FileWorkdayResolver.java
index 7885d8d..1534871 100644
--- a/able/src/main/java/com/gitee/qdbp/able/time/FileWorkdayResolver.java
+++ b/able/src/main/java/com/gitee/qdbp/able/time/FileWorkdayResolver.java
@@ -30,7 +30,7 @@ public class FileWorkdayResolver extends SettingWorkdayResolver {
         try (InputStream input = url.openStream()) {
             readSettings(input);
         } catch (IOException e) {
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, PathTools.toUriPath(url), e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(PathTools.toUriPath(url));
         }
     }
 
@@ -41,7 +41,7 @@ public class FileWorkdayResolver extends SettingWorkdayResolver {
             String desc = "File location [" + file.getAbsolutePath() + "]";
             throw new ResourceNotFoundException(desc + " do not exist");
         } catch (IOException e) {
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, file.getAbsolutePath(), e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(file.getAbsolutePath());
         }
     }
 
@@ -52,7 +52,7 @@ public class FileWorkdayResolver extends SettingWorkdayResolver {
             String desc = "URL location [" + PathTools.toUriPath(url) + "]";
             throw new ResourceNotFoundException(desc + " do not exist");
         } catch (IOException e) {
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, url.toString(), e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(url.toString());
         }
     }
 
-- 
Gitee


From 29a828efe092e8764f88746593226983495439c8 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 11 Jul 2022 07:57:05 +0800
Subject: [PATCH 050/160] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AF=BC=E5=87=BA?=
 =?UTF-8?q?=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/tools/excel/XExcelExporter.java      | 133 ++++++++++++++++++
 1 file changed, 133 insertions(+)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
index 1b975fc..bb33bdc 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
@@ -90,6 +90,44 @@ public class XExcelExporter {
         }
     }
 
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param output 输出流
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, File templateFile, OutputStream output) throws ServiceException {
+        try (InputStream input = new FileInputStream(templateFile)) {
+            this.export(data, input, output, null);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(templateFile.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param output 输出流
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, URL templateFile, OutputStream output) throws ServiceException {
+        try (InputStream input = templateFile.openStream()) {
+            this.export(data, input, output, null);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(templateFile));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
     /**
      * Excel导出
      *
@@ -102,6 +140,101 @@ public class XExcelExporter {
         this.export(data, template, output, null);
     }
 
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件路径
+     * @param saveFile 保存的文件路径
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, File templateFile, File saveFile, ExportCallback callback)
+            throws ServiceException {
+        FileTools.mkdirsIfNotExists(saveFile);
+        try (InputStream input = new FileInputStream(templateFile)) {
+            try (OutputStream output = new FileOutputStream(saveFile)) {
+                export(data, input, output, callback);
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(saveFile.getPath());
+            } catch (IOException e) {
+                log.error("write excel error.", e);
+                throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR, e);
+            }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(templateFile.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param saveFile 保存的文件路径
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, URL templateFile, File saveFile, ExportCallback callback) throws ServiceException {
+        FileTools.mkdirsIfNotExists(saveFile);
+        try (InputStream input = templateFile.openStream()) {
+            try (OutputStream output = new FileOutputStream(saveFile)) {
+                export(data, input, output, callback);
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(saveFile.getPath());
+            } catch (IOException e) {
+                log.error("write excel error.", e);
+                throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR, e);
+            }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(templateFile));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param output 输出流
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, File templateFile, OutputStream output, ExportCallback callback)
+            throws ServiceException {
+        try (InputStream input = new FileInputStream(templateFile)) {
+            this.export(data, input, output, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(templateFile.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param output 输出流
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, URL templateFile, OutputStream output, ExportCallback callback)
+            throws ServiceException {
+        try (InputStream input = templateFile.openStream()) {
+            this.export(data, input, output, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(templateFile));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
     public void export(List<?> data, InputStream template, OutputStream output, ExportCallback callback)
             throws ServiceException {
 
-- 
Gitee


From 5dbba1245cb0607782c15e75aff0a2ca066587b4 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 11 Jul 2022 07:57:58 +0800
Subject: [PATCH 051/160] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=A4=BA=E4=BE=8B?=
 =?UTF-8?q?=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties  | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties
index 6b4b9b5..0e2b585 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties
@@ -31,13 +31,10 @@ sheet.name.fill.to = dept
 row.index.fill.to = index
 
 ## 转换规则
-rule.date.birthday = yyyy/MM/dd
-rule.map.positive = { true:"已转正|是|Y", false:"未转正|否|N" }
+rules.birthday = { date: "yyyy/MM/dd" }
+rules.positive = { map: { true:"已转正|是|Y", false:"未转正|否|N" } }
 # 多个规则
 rules.height = { number:"int" }, { ignoreIllegalValue:true }
-# 简写规则
-# rule.map.gender = { UNKNOWN:"未知|0", MALE:"男|1", FEMALE:"女|2" }
-# rule.map.gender = UNKNOWN:未知|0, MALE:男|1, FEMALE:女|2
 # rules.gender = { map:{ UNKNOWN:"未知|0", MALE:"男|1", FEMALE:"女|2" } }, { ignoreIllegalValue:true }
 # rules.gender = { map:"UNKNOWN:未知|0, MALE:男|1, FEMALE:女|2" }, { ignoreIllegalValue:true }
 rules.gender = { map:"UNKNOWN:未知|0, MALE:男|1, FEMALE:女|2" }
-- 
Gitee


From d4264bb01a37d48efb779d890d999b7f6b475b75 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 11 Jul 2022 07:58:15 +0800
Subject: [PATCH 052/160] 5.4.20

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 72c5346..9b125c3 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.19</version>
+	<version>5.4.20</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index 84e0f38..c6bd4d3 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.19</version>
+	<version>5.4.20</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 8ab51364c135dd9b5945b1c42296a0ce3f2467ac Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 11 Jul 2022 08:24:07 +0800
Subject: [PATCH 053/160] =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=E5=8D=87?=
 =?UTF-8?q?=E7=BA=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 929c64d..da978f2 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.4.6</version>
+        <version>5.4.20</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.4.6</version>
+        <version>5.4.20</version>
     </dependency>
 ```
\ No newline at end of file
-- 
Gitee


From 2a953d9edac5323521c35d038f8adde6e6942f4f Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 4 Aug 2022 08:16:16 +0800
Subject: [PATCH 054/160] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=B0=E7=9A=84?=
 =?UTF-8?q?=E8=A7=84=E5=88=99=E8=AF=BB=E5=8F=96=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/able/beans/StringWithAttrs.java      | 48 +++++++++++++++
 .../com/gitee/qdbp/tools/utils/RuleTools.java | 59 ++++++++++++++++++-
 2 files changed, 106 insertions(+), 1 deletion(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java b/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java
new file mode 100644
index 0000000..9dc5e77
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java
@@ -0,0 +1,48 @@
+package com.gitee.qdbp.able.beans;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 带属性的字符串
+ *
+ * @author zhaohuihua
+ * @version 20220804
+ */
+public class StringWithAttrs implements Serializable {
+
+    private static final long serialVersionUID = 3186043836380668366L;
+
+    /** 字符串内容 **/
+    private String string;
+    /** 属性列表 **/
+    private List<KeyString> attrs;
+
+    public StringWithAttrs() {
+    }
+
+    public StringWithAttrs(String string, List<KeyString> attrs) {
+        this.string = string;
+        this.attrs = attrs;
+    }
+
+    /** 字符串内容 **/
+    public String getString() {
+        return string;
+    }
+
+    /** 字符串内容 **/
+    public void setString(String string) {
+        this.string = string;
+    }
+
+    /** 属性列表 **/
+    public List<KeyString> getAttrs() {
+        return attrs;
+    }
+
+    /** 属性列表 **/
+    public void setAttrs(List<KeyString> attrs) {
+        this.attrs = attrs;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
index da812f2..ff13a07 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
@@ -3,6 +3,7 @@ package com.gitee.qdbp.tools.utils;
 import java.util.ArrayList;
 import java.util.List;
 import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.beans.StringWithAttrs;
 
 /**
  * 规则配置解析工具类
@@ -25,7 +26,7 @@ public class RuleTools {
         if (string == null) {
             return null;
         }
-        List<String> list = StringTools.splits(string, false,'\n');
+        List<String> list = StringTools.splits(string, false, '\n');
         return clearCommentLines(list);
     }
 
@@ -185,6 +186,62 @@ public class RuleTools {
         return results;
     }
 
+    /**
+     * 拆分一行文本多行属性的格式, 属性必须带缩进<br>
+     * 如下文本, 返回2项, 分别为
+     * { test1, attrs:{ attr11:"value11", attr12:"value12" } }
+     * { test2, attrs:{ attr21:"value21", attr22:"value22", attr23:"value23" } }
+     * <pre>
+     * |test1
+     * |    attr11 = value11
+     * |    attr12 = value12
+     * |test2
+     * |    attr21 = value21
+     * |    attr22 = value22
+     * |    attr23 = value23
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<StringWithAttrs> splitRuleWithAttrs(List<String> rules) {
+        List<StringWithAttrs> results = new ArrayList<>();
+        for (int i = 0, z = rules.size(); i < z; i++) {
+            String string = rules.get(0);
+            String trimmed = string.trim();
+            if (trimmed.length() == 0) {
+                continue;
+            }
+            if (trimmed.charAt(0) == '#' || trimmed.startsWith("//")) {
+                // 遇到注释行, 跳过
+                continue;
+            }
+            List<KeyString> attrs = new ArrayList<>();
+            i += collectNextIndentAttrs(rules, i + 1, attrs);
+            results.add(new StringWithAttrs(trimmed, attrs));
+        }
+        return results;
+    }
+
+    public static int collectNextIndentAttrs(List<String> rules, int start, List<KeyString> collectors) {
+        List<String> lines = new ArrayList<>();
+        int lineIndex = collectNextIndentLines(rules, start, lines);
+        for (String line : lines) {
+            String trimmed = line.trim();
+            int index = trimmed.indexOf('=');
+            String key = null;
+            String value = trimmed;
+            if (index > 0) {
+                key = StringTools.trimRight(trimmed.substring(0, index));
+            }
+            if (index >= 0) {
+                value = StringTools.trimLeft(trimmed.substring(index + 1));
+            }
+            collectors.add(new KeyString(key, value));
+        }
+        return lineIndex;
+    }
+
     public static int collectNextIndentLines(List<String> rules, int start, List<String> collectors) {
         int count = 0;
         for (int i = start, z = rules.size(); i < z; i++, count++) {
-- 
Gitee


From 7c7161fff7883d79b391dc294435c79ba20d3186 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 13 Aug 2022 13:22:25 +0800
Subject: [PATCH 055/160] =?UTF-8?q?StringTools.removeLeft=E6=94=B9?=
 =?UTF-8?q?=E4=B8=BAtrimLeft?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/pom.xml                                  |  2 +-
 .../instance/WithExtensionFileFilter.java     |  2 +-
 .../able/matches/SimpleStringReplacer.java    | 33 ++++++++
 .../qdbp/able/matches/WrapStringReplacer.java | 14 ++--
 .../com/gitee/qdbp/tools/codec/CodeTools.java |  6 ++
 .../gitee/qdbp/tools/parse/ParseUtils.java    |  2 +-
 .../com/gitee/qdbp/tools/utils/DateTools.java |  8 +-
 .../com/gitee/qdbp/tools/utils/RuleTools.java | 40 ++++++++--
 .../gitee/qdbp/tools/utils/StringTools.java   | 75 ++++++++++++++++++-
 .../qdbp/tools/utils/VersionCodeTools.java    |  4 +-
 tools/pom.xml                                 |  2 +-
 11 files changed, 163 insertions(+), 25 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 9b125c3..b3026ba 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.20</version>
+	<version>5.4.22</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/able/src/main/java/com/gitee/qdbp/able/instance/WithExtensionFileFilter.java b/able/src/main/java/com/gitee/qdbp/able/instance/WithExtensionFileFilter.java
index 1f131ba..dd5f0e7 100644
--- a/able/src/main/java/com/gitee/qdbp/able/instance/WithExtensionFileFilter.java
+++ b/able/src/main/java/com/gitee/qdbp/able/instance/WithExtensionFileFilter.java
@@ -59,7 +59,7 @@ public class WithExtensionFileFilter implements FileFilter, Serializable {
         this.include = include;
         this.extensions = new HashMap<>();
         for (String extension : extensions) {
-            this.extensions.put(StringTools.removeLeft(extension.toLowerCase(), '.'), null);
+            this.extensions.put(StringTools.trimLeft(extension.toLowerCase(), '.'), null);
         }
     }
 
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/SimpleStringReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/SimpleStringReplacer.java
index 3be66ea..538cb28 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/SimpleStringReplacer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/SimpleStringReplacer.java
@@ -20,4 +20,37 @@ public class SimpleStringReplacer implements StringReplacer {
     public String toString() {
         return "simple:" + ConvertTools.joinToString(rules, '|');
     }
+
+
+    /**
+     * 解析文本替换规则<br>
+     * 格式: AA -- BB<br>
+     *
+     * @param pattern 规则内容
+     * @return 规则对象
+     */
+    public static StringReplacer parseReplacer(String pattern) {
+        return parseReplacer(pattern, " -- ");
+    }
+
+    /**
+     * 解析文本替换规则<br>
+     * 格式: AA -- BB<br>
+     *
+     * @param pattern 规则内容
+     * @param delimiter 分隔符
+     * @return 规则对象
+     */
+    public static StringReplacer parseReplacer(String pattern, String delimiter) {
+        int splitIndex = pattern.indexOf(delimiter);
+        if (splitIndex < 0) {
+            return null;
+        }
+        String source = StringTools.trimRight(pattern.substring(0, splitIndex));
+        String target = StringTools.trimLeft(pattern.substring(splitIndex + delimiter.length()));
+        if ("{null}".equals(target)) {
+            target = "";
+        }
+        return new SimpleStringReplacer(source, target);
+    }
 }
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java
index a98b467..e388985 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java
@@ -193,7 +193,7 @@ public class WrapStringReplacer implements StringReplacer {
 
         public Parsers(boolean addDefaultParsers) {
             if (addDefaultParsers) {
-                addParser(new SimpleParser());
+                addParser(new DefaultParser());
             }
         }
 
@@ -276,9 +276,9 @@ public class WrapStringReplacer implements StringReplacer {
                 for (int i = parsers.size() - 1; i >= 0; i--) {
                     ParserByMode parser = parsers.get(i);
                     if (parser != null && parser != excludeParser) {
-                        StringReplacer Replacer = parser.parse(realRule, realMode, defaultMode);
-                        if (Replacer != null) {
-                            return Replacer;
+                        StringReplacer replacer = parser.parse(realRule, realMode, defaultMode);
+                        if (replacer != null) {
+                            return replacer;
                         }
                     }
                 }
@@ -421,17 +421,19 @@ public class WrapStringReplacer implements StringReplacer {
      * @author zhaohuihua
      * @version 20220206
      */
-    private static class SimpleParser implements ParserByMode {
+    private static class DefaultParser implements ParserByMode {
 
         @Override
         public String[] supportModes() {
-            return new String[] {"rgp", "regexp"};
+            return new String[] { "rgp", "regexp", "simple" };
         }
 
         @Override
         public StringReplacer parse(String rule, String mode, String defaultMode) {
             if ("rgp".equals(mode) || "regexp".equals(mode)) {
                 return RegexpStringReplacer.parseReplacer(rule);
+            } else if ("simple".equals(mode)) {
+                return SimpleStringReplacer.parseReplacer(rule);
             } else {
                 return null;
             }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/codec/CodeTools.java b/able/src/main/java/com/gitee/qdbp/tools/codec/CodeTools.java
index cd59c30..a5f1608 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/codec/CodeTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/codec/CodeTools.java
@@ -2,6 +2,7 @@ package com.gitee.qdbp.tools.codec;
 
 import java.util.ArrayList;
 import java.util.List;
+import com.gitee.qdbp.tools.utils.StringTools;
 
 /**
  * 编号处理工具类<br>
@@ -47,6 +48,11 @@ public class CodeTools {
         return prefix(code, start, length);
     }
 
+    /** 在上级的基础上追加序号生成子级编号 **/
+    public String pad(String parent, int index) {
+        return parent + StringTools.pad(index, length);
+    }
+
     /**
      * 拆分编号<br>
      * 如split("500100220033", 4, true) --&gt; [5001, 50010022, 500100220033]<br>
diff --git a/able/src/main/java/com/gitee/qdbp/tools/parse/ParseUtils.java b/able/src/main/java/com/gitee/qdbp/tools/parse/ParseUtils.java
index 1adcb11..8425197 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/parse/ParseUtils.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/parse/ParseUtils.java
@@ -36,7 +36,7 @@ class ParseUtils {
         if (string == null) {
             return null;
         }
-        string = StringTools.removeLeftRight(string.trim(), mark);
+        string = StringTools.trimLeftRight(string.trim(), mark);
         StringBuilder buffer = new StringBuilder();
         for (int i = 0, z = string.length() - 1; i <= z; i++) {
             char c = string.charAt(i);
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
index 1da539f..625ccf1 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
@@ -126,16 +126,16 @@ public class DateTools {
                 this.length = 0;
                 // X=+/-hh, XX=+/-hhmm, 3个X=+/-hh:mm
                 if (regexp.endsWith("XXX")) {
-                    regexp = StringTools.removeRight(regexp, 'X') + "[+-]\\d\\d:\\d\\d";
+                    regexp = StringTools.trimRight(regexp, 'X') + "[+-]\\d\\d:\\d\\d";
                 } else if (regexp.endsWith("XX")) {
-                    regexp = StringTools.removeRight(regexp, 'X') + "[+-]\\d{4}";
+                    regexp = StringTools.trimRight(regexp, 'X') + "[+-]\\d{4}";
                 } else if (regexp.endsWith("X")) {
-                    regexp = StringTools.removeRight(regexp, 'X') + "[+-]\\d{2}";
+                    regexp = StringTools.trimRight(regexp, 'X') + "[+-]\\d{2}";
                 }
             } else if (regexp.endsWith("Z")) { // GMT时区字符
                 this.length = 0;
                 // GMT或GMT+/-hh:mm或+/-hhmm
-                regexp = StringTools.removeRight(regexp, 'Z') + "(?:\\w+|\\w+[+-]\\d\\d:\\d\\d|[+-]\\d{4})";
+                regexp = StringTools.trimRight(regexp, 'Z') + "(?:\\w+|\\w+[+-]\\d\\d:\\d\\d|[+-]\\d{4})";
             }
             { // 常量字符
                 Matcher matcher = CONST_CHAR.matcher(regexp);
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
index ff13a07..a4b8451 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
@@ -158,24 +158,25 @@ public class RuleTools {
     public static List<KeyString> splitRuleKeyValues(List<String> rules) {
         List<KeyString> results = new ArrayList<>();
         for (int i = 0, z = rules.size(); i < z; i++) {
-            String string = rules.get(i).trim();
-            if (string.length() == 0) {
+            String string = rules.get(i);
+            String trimmed = string.trim();
+            if (trimmed.length() == 0) {
                 continue;
             }
-            if (string.charAt(0) == '#' || string.startsWith("//")) {
+            if (trimmed.charAt(0) == '#' || trimmed.startsWith("//")) {
                 // 遇到注释行, 跳过
                 continue;
             }
-            int index = string.indexOf('=');
+            int index = trimmed.indexOf('=');
             String key = null;
             List<String> values = new ArrayList<>();
             if (index < 0) {
-                values.add(string);
+                values.add(trimmed);
             } else {
                 if (index > 0) {
-                    key = StringTools.trimRight(string.substring(0, index));
+                    key = StringTools.trimRight(trimmed.substring(0, index));
                 }
-                String value = StringTools.trimLeft(string.substring(index + 1));
+                String value = StringTools.trimLeft(trimmed.substring(index + 1));
                 if (value.length() > 0) {
                     values.add(value);
                 }
@@ -186,6 +187,29 @@ public class RuleTools {
         return results;
     }
 
+    /**
+     * 拆分一行文本多行属性的格式, 属性必须带缩进<br>
+     * 如下文本, 返回2项, 分别为
+     * { test1, attrs:{ attr11:"value11", attr12:"value12" } }
+     * { test2, attrs:{ attr21:"value21", attr22:"value22", attr23:"value23" } }
+     * <pre>
+     * |test1
+     * |    attr11 = value11
+     * |    attr12 = value12
+     * |test2
+     * |    attr21 = value21
+     * |    attr22 = value22
+     * |    attr23 = value23
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<StringWithAttrs> splitRuleWithAttrs(String rules) {
+        List<String> strings = StringTools.splits(rules, false, '\n');
+        return splitRuleWithAttrs(strings);
+    }
+
     /**
      * 拆分一行文本多行属性的格式, 属性必须带缩进<br>
      * 如下文本, 返回2项, 分别为
@@ -207,7 +231,7 @@ public class RuleTools {
     public static List<StringWithAttrs> splitRuleWithAttrs(List<String> rules) {
         List<StringWithAttrs> results = new ArrayList<>();
         for (int i = 0, z = rules.size(); i < z; i++) {
-            String string = rules.get(0);
+            String string = rules.get(i);
             String trimmed = string.trim();
             if (trimmed.length() == 0) {
                 continue;
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index fd1ff48..e28ae79 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -923,6 +923,39 @@ public class StringTools {
         return ((start > 0) || (end < size)) ? string.substring(start, end) : string;
     }
 
+    /**
+     * 删除左右两侧的指定字符
+     *
+     * @param string 原文本
+     * @param chars 待删除的字符
+     * @return 删除后的字符串
+     */
+    public static String trimLeftRight(String string, char... chars) {
+        return removeLeftRight(string, true, true, chars);
+    }
+
+    /**
+     * 删除左侧的指定字符
+     *
+     * @param string 原文本
+     * @param chars 待删除的字符
+     * @return 删除后的字符串
+     */
+    public static String trimLeft(String string, char... chars) {
+        return removeLeftRight(string, true, false, chars);
+    }
+
+    /**
+     * 删除右侧的指定字符
+     *
+     * @param string 原文本
+     * @param chars 待删除的字符
+     * @return 删除后的字符串
+     */
+    public static String trimRight(String string, char... chars) {
+        return removeLeftRight(string, false, true, chars);
+    }
+
     /**
      * 删除左右两侧的指定字符
      *
@@ -933,7 +966,7 @@ public class StringTools {
      */
     @Deprecated
     public static String remove(String string, char... chars) {
-        return removeLeftRight(string, chars);
+        return trimLeftRight(string, chars);
     }
 
     /**
@@ -942,7 +975,9 @@ public class StringTools {
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
+     * @deprecated 与removeLeftRight(string, int, int)冲突, 改为trimLeftRight(String string, char... chars)
      */
+    @Deprecated
     public static String removeLeftRight(String string, char... chars) {
         return removeLeftRight(string, true, true, chars);
     }
@@ -953,7 +988,9 @@ public class StringTools {
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
+     * @deprecated 与removeLeft(string, int)冲突, 改为trimLeft(String string, char... chars)
      */
+    @Deprecated
     public static String removeLeft(String string, char... chars) {
         return removeLeftRight(string, true, false, chars);
     }
@@ -964,7 +1001,9 @@ public class StringTools {
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
+     * @deprecated 与removeRight(string, int)冲突, 改为trimRight(String string, char... chars)
      */
+    @Deprecated
     public static String removeRight(String string, char... chars) {
         return removeLeftRight(string, false, true, chars);
     }
@@ -1299,6 +1338,40 @@ public class StringTools {
         return string.substring(startIndex, endIndex);
     }
 
+    /**
+     * 替换相同长度的前缀
+     *
+     * @param string 源字符串
+     * @param prefix 新的前缀
+     * @return 替换后的字符串
+     */
+    public static String replacePrefix(String string, String prefix) {
+        if (string == null || prefix == null) {
+            return null;
+        }
+        if (prefix.length() == 0 || string.length() < prefix.length()) {
+            return string;
+        }
+        return prefix + removeLeftRight(string, prefix.length(), 0);
+    }
+
+    /**
+     * 替换相同长度的后缀
+     * 
+     * @param string 源字符串
+     * @param suffix 新的后缀
+     * @return 替换后的字符串
+     */
+    public static String replaceSuffix(String string, String suffix) {
+        if (string == null || suffix == null) {
+            return null;
+        }
+        if (suffix.length() == 0 || string.length() < suffix.length()) {
+            return string;
+        }
+        return removeLeftRight(string, 0, suffix.length()) + suffix;
+    }
+
     /**
      * 字符串替换(非正则)<br>
      * 例如: \t替换为空格, \r\n替换为\n<br>
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/VersionCodeTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/VersionCodeTools.java
index 225be5c..6723431 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/VersionCodeTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/VersionCodeTools.java
@@ -139,8 +139,8 @@ public class VersionCodeTools {
     }
 
     private static int compareVersionNumber(String source, String target) {
-        source = StringTools.removeLeft(source, '0');
-        target = StringTools.removeLeft(target, '0');
+        source = StringTools.trimLeft(source, '0');
+        target = StringTools.trimLeft(target, '0');
         if (source.length() > target.length()) {
             return 1;
         } else if (source.length() < target.length()) {
diff --git a/tools/pom.xml b/tools/pom.xml
index c6bd4d3..0108038 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.20</version>
+	<version>5.4.22</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From e1c81806d55a2c4ed9f8239cc74736ceb0ab1e36 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 13 Aug 2022 13:23:25 +0800
Subject: [PATCH 056/160] =?UTF-8?q?KeyValue.toMap=E6=94=B9=E4=B8=BALinkedH?=
 =?UTF-8?q?ashMap?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java b/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java
index 96e607e..3a92aa6 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java
@@ -2,7 +2,7 @@ package com.gitee.qdbp.able.beans;
 
 import java.io.Serializable;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -89,7 +89,7 @@ public class KeyValue<V> implements Comparable<KeyValue<V>>, Serializable {
             return null;
         }
 
-        Map<String, V> map = new HashMap<>();
+        Map<String, V> map = new LinkedHashMap<>();
         for (KeyValue<C> i : list) {
             map.put(i.key, i.value);
         }
-- 
Gitee


From 1031900073ff21a466bea5a8e8dfeef126462f02 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 13 Aug 2022 13:24:25 +0800
Subject: [PATCH 057/160] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=A0=91=E5=BD=A2?=
 =?UTF-8?q?=E7=BB=93=E6=9E=84=E9=80=9A=E7=94=A8=E5=A4=84=E7=90=86=E5=AE=B9?=
 =?UTF-8?q?=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/able/beans/Callable.java   |   7 +
 .../com/gitee/qdbp/able/beans/TreeNodes.java  | 471 ++++++++++++++++++
 .../gitee/qdbp/able/beans/TreeNodesTest.java  | 108 ++++
 3 files changed, 586 insertions(+)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/beans/Callable.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/Callable.java b/able/src/main/java/com/gitee/qdbp/able/beans/Callable.java
new file mode 100644
index 0000000..46bbfbf
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/Callable.java
@@ -0,0 +1,7 @@
+package com.gitee.qdbp.able.beans;
+
+/** JDK8中的Function在7中的替换接口 **/
+public interface Callable<T, R> {
+
+    R call(T t);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
new file mode 100644
index 0000000..53aec8d
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
@@ -0,0 +1,471 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * 树形结构通用处理容器
+ *
+ * @author zhaohuihua
+ * @version 20220813
+ */
+public class TreeNodes<E> {
+
+    /** 容器节点 **/
+    private final Node<E> container;
+    /** KEY与节点的映射表 **/
+    private final Map<String, Node<E>> keyNodeMaps;
+    /** 未找到上级的KEY **/
+    private final List<String> keysOfWithoutParent;
+    /** 获取KEY的方法 **/
+    private final Callable<E, String> keyGetter;
+
+    public TreeNodes(List<E> list, boolean upgrade,
+            Callable<E, String> keyGetter, Callable<E, String> parentGetter) {
+        Node<E> container = new Node<>(null);
+        String rootCode = "0";
+        List<Node<E>> all = new ArrayList<>();
+        // key - Node
+        Map<String, Node<E>> maps = new ConcurrentHashMap<>();
+        maps.put(rootCode, container); // 根节点
+
+        for (E element : list) {
+            Node<E> item = new Node<>(element);
+            all.add(item);
+            String key = keyGetter.call(element);
+            maps.put(key, item);
+        }
+
+        // 未找到上级的KEY
+        List<String> keysOfWithoutParent = new ArrayList<>();
+        for (Node<E> item : all) {
+            String parentKey = parentGetter.call(item.element);
+            if (VerifyTools.isBlank(parentKey)) {
+                maps.get(rootCode).addChildNode(item);
+            } else if (maps.containsKey(parentKey)) {
+                maps.get(parentKey).addChildNode(item);
+            } else {
+                String key = keyGetter.call(item.element);
+                keysOfWithoutParent.add(key);
+                if (upgrade) {
+                    container.addChildNode(item);
+                }
+            }
+        }
+        this.container = container;
+        this.keyNodeMaps = maps;
+        this.keysOfWithoutParent = keysOfWithoutParent;
+        this.keyGetter = keyGetter;
+    }
+
+    /** 获取容器节点 **/
+    public Node<E> getContainer() {
+        return this.container;
+    }
+
+    /** 获取所有根级元素 **/
+    public List<E> getRootElements() {
+        return this.container.getChildElements();
+    }
+
+    /** 获取所有叶子元素 **/
+    public List<E> getAllLeafElements() {
+        return container.getAllLeafElements();
+    }
+
+    /** 获取所有后代元素 **/
+    public List<E> getAllDescendantElements() {
+        return container.getAllDescendantElements();
+    }
+
+    /** 查找指定元素 **/
+    public E findElement(String key) {
+        Node<E> item = findNode(key);
+        return item == null ? null : item.element;
+    }
+
+    /** 查找指定节点 **/
+    public Node<E> findNode(String key) {
+        Node<E> node = keyNodeMaps.get(key);
+        if (node != null) {
+            // 考虑到有可能在外部修改key, 因此增加检查
+            String nodeKey = keyGetter.call(node.element);
+            if (key.equals(nodeKey)) {
+                return node;
+            }
+        }
+        Iterator<Node<E>> iterator = container.depthFirstNodeIterator();
+        while (iterator.hasNext()) {
+            Node<E> next = iterator.next();
+            String nextKey = keyGetter.call(next.element);
+            // 遍历时更新缓存中的对应关系
+            keyNodeMaps.put(nextKey, next);
+            if (key.equals(nextKey)) {
+                return next;
+            }
+        }
+        return null;
+    }
+
+    /** 获取KEY对应元素的父元素 **/
+    public E findParentElement(String key) {
+        Node<E> item = findNode(key);
+        return item == null || item.parent == null ? null : item.parent.element;
+    }
+
+    /** 获取KEY对应元素的所有祖先元素 **/
+    public List<E> findAllAncestorElements(String key) {
+        return findAllAncestorElements(key, false);
+    }
+
+    /** 获取KEY对应元素的所有祖先元素, 可以指定是否包含指定KEY元素自身 **/
+    public List<E> findAllAncestorElements(String key, boolean includeSelf) {
+        Node<E> item = findNode(key);
+        if (item == null) {
+            return new ArrayList<>();
+        }
+        List<E> parents = item.getAllAncestorElements();
+        if (includeSelf) {
+            parents.add(item.element);
+        }
+        return parents;
+    }
+
+    /** 获取KEY对应元素的直接子元素 **/
+    public List<E> findChildElements(String key) {
+        Node<E> item = findNode(key);
+        return item == null ? new ArrayList<>() : item.getChildElements();
+    }
+
+    /** 获取KEY对应元素的所有子元素 **/
+    public List<E> findAllDescendantElements(String key) {
+        return findAllDescendantElements(key, false);
+    }
+
+    /** 获取KEY对应元素的所有子元素, 可以指定是否包含指定KEY元素自身 **/
+    public List<E> findAllDescendantElements(String key, boolean includeSelf) {
+        Node<E> item = findNode(key);
+        List<E> elements = new ArrayList<>();
+        if (item == null) {
+            return elements;
+        }
+        if (includeSelf) {
+            elements.add(item.element);
+        }
+        elements.addAll(item.getAllDescendantElements());
+        return elements;
+    }
+
+    /** 未找到上级的KEY **/
+    public List<String> getKeysOfWithoutParent() {
+        return this.keysOfWithoutParent;
+    }
+
+    /** 广度优先遍历 **/
+    public Iterator<E> breadthFirstIterator() {
+        return container.breadthFirstElementIterator();
+    }
+
+    /** 深度度优先遍历 **/
+    public Iterator<E> depthFirstIterator() {
+        return container.depthFirstElementIterator();
+    }
+
+    public static class Node<T> {
+
+        /** 本级 **/
+        private final T element;
+        /** 上级 **/
+        private Node<T> parent;
+        /** 子级 **/
+        private final List<Node<T>> children;
+
+        public Node(T element) {
+            this.element = element;
+            this.children = new ArrayList<>();
+        }
+
+        public T getElement() {
+            return element;
+        }
+
+        /** 获取第一个子级元素 **/
+        public T getFirstElement() {
+            if (children.isEmpty()) {
+                return null;
+            } else {
+                return this.children.get(0).element;
+            }
+        }
+
+        /** 获取父级元素 **/
+        public T getParentElement() {
+            return parent == null ? null : parent.element;
+        }
+
+        /** 获取所有祖先元素 **/
+        public List<T> getAllAncestorElements() {
+            List<T> parents = new ArrayList<>();
+            Node<T> temp = parent;
+            while (temp != null && temp.element != null) {
+                parents.add(temp.element);
+                temp = temp.parent;
+            }
+            Collections.reverse(parents);
+            return parents;
+        }
+
+        /** 获取直接子级元素 **/
+        public List<T> getChildElements() {
+            List<T> elements = new ArrayList<>();
+            for (Node<T> item : this.children) {
+                elements.add(item.element);
+            }
+            return elements;
+        }
+
+        /** 增加子元素 **/
+        public void addChildElement(T child) {
+            addChildNode(new Node<>(child));
+        }
+
+        /** 获取后代元素 **/
+        public List<T> getAllDescendantElements() {
+            Iterator<T> iterator = depthFirstElementIterator();
+            List<T> elements = new ArrayList<>();
+            while (iterator.hasNext()) {
+                T next = iterator.next();
+                elements.add(next);
+            }
+            return elements;
+        }
+
+        /** 获取所有叶子元素 **/
+        public List<T> getAllLeafElements() {
+            Iterator<Node<T>> iterator = depthFirstNodeIterator();
+            List<T> elements = new ArrayList<>();
+            while (iterator.hasNext()) {
+                Node<T> next = iterator.next();
+                if (next.children.isEmpty()) {
+                    elements.add(next.element);
+                }
+            }
+            return elements;
+        }
+
+        /** 获取直接节点列表 **/
+        protected List<Node<T>> getChildNodes() {
+            return children;
+        }
+
+        /** 增加子级节点 **/
+        protected void addChildNode(Node<T> child) {
+            children.add(child);
+            child.parent = this;
+        }
+
+        /** 获取父级节点 **/
+        protected Node<T> getParentNode() {
+            return parent == null ? null : parent;
+        }
+
+        /** 广度优先遍历元素 **/
+        public Iterator<T> breadthFirstElementIterator() {
+            return new BreadthFirstElementIterator<>(new BreadthFirstNodeIterator<>(this));
+        }
+
+        /** 深度度优先遍历元素 **/
+        public Iterator<T> depthFirstElementIterator() {
+            return new DepthFirstElementIterator<>(new DepthFirstNodeIterator<>(this));
+        }
+
+        /** 广度优先遍历节点 **/
+        public Iterator<Node<T>> breadthFirstNodeIterator() {
+            return new BreadthFirstNodeIterator<>(this);
+        }
+
+        /** 深度度优先遍历节点 **/
+        public Iterator<Node<T>> depthFirstNodeIterator() {
+            return new DepthFirstNodeIterator<>(this);
+        }
+
+        @Override
+        public String toString() {
+            return element == null ? "{root}" : element.toString();
+        }
+    }
+
+    private static class BreadthFirstElementIterator<T> implements Iterator<T> {
+        private final BreadthFirstNodeIterator<T> iterator;
+
+        public BreadthFirstElementIterator(BreadthFirstNodeIterator<T> iterator) {
+            this.iterator = iterator;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public T next() {
+            Node<T> next = iterator.next();
+            return next == null ? null : next.element;
+        }
+
+        @Override
+        public void remove() {
+            iterator.remove();
+        }
+    }
+
+    private static class DepthFirstElementIterator<T> implements Iterator<T> {
+        private final DepthFirstNodeIterator<T> iterator;
+
+        public DepthFirstElementIterator(DepthFirstNodeIterator<T> iterator) {
+            this.iterator = iterator;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public T next() {
+            Node<T> next = iterator.next();
+            return next == null ? null : next.element;
+        }
+
+        @Override
+        public void remove() {
+            iterator.remove();
+        }
+    }
+
+    /**
+     * 广度优先遍历<br>
+     * 根节点自身不会参与遍历
+     *
+     * @author zhaohuihua
+     * @version 170507
+     */
+    private static class BreadthFirstNodeIterator<T> implements Iterator<Node<T>> {
+
+        private final Queue<Node<T>> queue;
+        private Node<T> current;
+
+        public BreadthFirstNodeIterator(Node<T> root) {
+            queue = new LinkedList<>();
+            if (!root.children.isEmpty()) {
+                for (Node<T> child : root.children) {
+                    queue.offer(child);
+                }
+            }
+        }
+
+        public boolean hasNext() {
+            return !queue.isEmpty();
+        }
+
+        public Node<T> next() {
+            Node<T> next = queue.poll();
+            if (next == null) {
+                current = null;
+                return null;
+            }
+            if (next.children != null) {
+                for (Node<T> i : next.children) {
+                    queue.offer(i);
+                }
+            }
+            current = next;
+            return next;
+        }
+
+        public void remove() {
+            if (current == null) {
+                throw new IllegalStateException("Call remove() must be after the next().");
+            }
+
+            // 先将当前节点从父节点的children中删除
+            current.parent.children.remove(current);
+            if (!current.children.isEmpty()) {
+                // 再把当前节点的子节点从队列中删除
+                // 因为当前节点的子节点在调用next()时已经加到遍历队列中了(孙子节点还没有加到队列中)
+                queue.removeAll(current.children);
+            }
+            current = null;
+        }
+    }
+
+    /**
+     * 深度优先遍历<br>
+     * 根节点自身不会参与遍历
+     *
+     * @author zhaohuihua
+     * @version 170801
+     */
+    private static class DepthFirstNodeIterator<T> implements Iterator<Node<T>> {
+
+        private final Queue<Node<T>> queue;
+        private Node<T> current;
+
+        public DepthFirstNodeIterator(Node<T> root) {
+            queue = new LinkedList<>();
+            if (!root.children.isEmpty()) {
+                for (Node<T> child : root.children) {
+                    recursiveOffer(child);
+                }
+            }
+        }
+
+        private void recursiveOffer(Node<T> self) {
+            queue.offer(self);
+            if (!self.children.isEmpty()) {
+                for (Node<T> child : self.children) {
+                    recursiveOffer(child);
+                }
+            }
+        }
+
+        public boolean hasNext() {
+            return !queue.isEmpty();
+        }
+
+        public Node<T> next() {
+            Node<T> next = queue.poll();
+            current = next;
+            return next;
+        }
+
+        public void remove() {
+            if (current == null) {
+                throw new IllegalStateException("Call remove() must be after the next().");
+            }
+
+            // 先将当前节点从父节点的children中删除
+            current.parent.children.remove(current);
+            if (!current.children.isEmpty()) {
+                // 将所有后代节点从队列中删除
+                removeAllChildren(current);
+            }
+            current = null;
+        }
+
+        // 将所有后代节点从队列中删除
+        private void removeAllChildren(Node<T> self) {
+            for (Node<T> child : self.children) {
+                queue.remove(child);
+                removeAllChildren(child);
+            }
+        }
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java b/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
new file mode 100644
index 0000000..762c61d
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
@@ -0,0 +1,108 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.codec.CodeTools;
+import com.gitee.qdbp.tools.utils.AssertTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 树形节点测试类
+ *
+ * @author zhaohuihua
+ * @version 20220813
+ */
+@Test
+public class TreeNodesTest {
+
+    private static final CodeTools codeTools = new CodeTools(1, 2);
+
+    public void test() {
+        List<String> list = generateTestData();
+
+        TreeNodes<String> container = new TreeNodes<>(list, true, new KeyGetter(), new ParentGetter());
+
+        List<String> rootElements = container.getRootElements();
+        AssertTools.assertDeepEquals(rootElements, Arrays.asList("T01", "T02"));
+
+        List<String> t01Children = container.findChildElements("T02");
+        AssertTools.assertDeepEquals(t01Children, Arrays.asList("T0201", "T0202", "T0203"));
+
+        List<String> t020304Children = container.findChildElements("T020304");
+        Assert.assertEquals(t020304Children.size(), 0);
+
+        List<String> t020304Parents = container.findAllAncestorElements("T020304");
+        AssertTools.assertDeepEquals(t020304Parents, Arrays.asList("T02", "T0203"));
+
+        List<String> t02Descendants = container.findAllDescendantElements("T02");
+        AssertTools.assertDeepEquals(t02Descendants, Arrays.asList(
+                "T0201", "T020101", "T020102", "T020103", "T020104",
+                "T0202", "T020201", "T020202", "T020203", "T020204",
+                "T0203", "T020301", "T020302", "T020303", "T020304"));
+
+        TreeNodes.Node<String> t01Node = container.findNode("T01");
+        Iterator<TreeNodes.Node<String>> t01NodeIterator = t01Node.depthFirstNodeIterator();
+        while(t01NodeIterator.hasNext()) {
+            TreeNodes.Node<String> next = t01NodeIterator.next();
+            String element = next.getElement();
+            if (element.endsWith("04")) {
+                next.getParentNode().addChildElement(StringTools.replaceSuffix(element, "05"));
+            }
+        }
+        List<String> t01Descendants = t01Node.getAllDescendantElements();
+        AssertTools.assertDeepEquals(t01Descendants, Arrays.asList(
+                "T0101", "T010101", "T010102", "T010103", "T010104", "T010105",
+                "T0102", "T010201", "T010202", "T010203", "T010204", "T010205",
+                "T0103", "T010301", "T010302", "T010303", "T010304", "T010305"));
+
+        Iterator<TreeNodes.Node<String>> t01NodeIterator2 = t01Node.depthFirstNodeIterator();
+        while(t01NodeIterator2.hasNext()) {
+            TreeNodes.Node<String> next = t01NodeIterator2.next();
+            String element = next.getElement();
+            if (element.contains("02")) {
+                t01NodeIterator2.remove();
+            }
+        }
+        List<String> t01Descendants2 = t01Node.getAllDescendantElements();
+        AssertTools.assertDeepEquals(t01Descendants2, Arrays.asList(
+                "T0101", "T010101", "T010103", "T010104", "T010105",
+                "T0103", "T010301", "T010303", "T010304", "T010305"));
+    }
+
+    private List<String> generateTestData() {
+        List<String> list = new ArrayList<>();
+        for (int i = 1; i <= 2; i++) {
+            String si = codeTools.pad("T", i);
+            list.add(si);
+            for (int j = 1; j <= 3; j++) {
+                String sj = codeTools.pad(si, j);
+                list.add(sj);
+                for (int k = 1; k <= 4; k++) {
+                    String sk = codeTools.pad(sj, k);
+                    list.add(sk);
+                }
+            }
+        }
+        return list;
+    }
+
+    private static class KeyGetter implements Callable<String, String> {
+
+        @Override
+        public String call(String s) {
+            return s;
+        }
+    }
+
+    private static class ParentGetter implements Callable<String, String> {
+
+        @Override
+        public String call(String s) {
+            return codeTools.parent(s);
+        }
+    }
+}
-- 
Gitee


From 2c90d15c6318e442aff8c9ec4fb237e2d9b3528f Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 13 Aug 2022 13:30:12 +0800
Subject: [PATCH 058/160] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=A0=91=E5=BD=A2?=
 =?UTF-8?q?=E7=BB=93=E6=9E=84=E9=80=9A=E7=94=A8=E5=A4=84=E7=90=86=E5=AE=B9?=
 =?UTF-8?q?=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/able/beans/TreeNodes.java  |  7 +-
 .../gitee/qdbp/able/beans/TreeNodesTest.java  | 86 +++++++++++++------
 2 files changed, 64 insertions(+), 29 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
index 53aec8d..d1840a4 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
@@ -27,8 +27,11 @@ public class TreeNodes<E> {
     /** 获取KEY的方法 **/
     private final Callable<E, String> keyGetter;
 
-    public TreeNodes(List<E> list, boolean upgrade,
-            Callable<E, String> keyGetter, Callable<E, String> parentGetter) {
+    public TreeNodes(List<E> list, Callable<E, String> keyGetter, Callable<E, String> parentGetter) {
+        this(list, false, keyGetter, parentGetter);
+    }
+
+    public TreeNodes(List<E> list, boolean upgrade, Callable<E, String> keyGetter, Callable<E, String> parentGetter) {
         Node<E> container = new Node<>(null);
         String rootCode = "0";
         List<Node<E>> all = new ArrayList<>();
diff --git a/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java b/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
index 762c61d..cda0331 100644
--- a/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
@@ -27,7 +27,7 @@ public class TreeNodesTest {
         TreeNodes<String> container = new TreeNodes<>(list, true, new KeyGetter(), new ParentGetter());
 
         List<String> rootElements = container.getRootElements();
-        AssertTools.assertDeepEquals(rootElements, Arrays.asList("T01", "T02"));
+        AssertTools.assertDeepEquals(rootElements, Arrays.asList("T01", "T02", "T03"));
 
         List<String> t01Children = container.findChildElements("T02");
         AssertTools.assertDeepEquals(t01Children, Arrays.asList("T0201", "T0202", "T0203"));
@@ -35,8 +35,8 @@ public class TreeNodesTest {
         List<String> t020304Children = container.findChildElements("T020304");
         Assert.assertEquals(t020304Children.size(), 0);
 
-        List<String> t020304Parents = container.findAllAncestorElements("T020304");
-        AssertTools.assertDeepEquals(t020304Parents, Arrays.asList("T02", "T0203"));
+        List<String> t020304Ancestors = container.findAllAncestorElements("T020304");
+        AssertTools.assertDeepEquals(t020304Ancestors, Arrays.asList("T02", "T0203"));
 
         List<String> t02Descendants = container.findAllDescendantElements("T02");
         AssertTools.assertDeepEquals(t02Descendants, Arrays.asList(
@@ -44,38 +44,70 @@ public class TreeNodesTest {
                 "T0202", "T020201", "T020202", "T020203", "T020204",
                 "T0203", "T020301", "T020302", "T020303", "T020304"));
 
-        TreeNodes.Node<String> t01Node = container.findNode("T01");
-        Iterator<TreeNodes.Node<String>> t01NodeIterator = t01Node.depthFirstNodeIterator();
-        while(t01NodeIterator.hasNext()) {
-            TreeNodes.Node<String> next = t01NodeIterator.next();
-            String element = next.getElement();
-            if (element.endsWith("04")) {
-                next.getParentNode().addChildElement(StringTools.replaceSuffix(element, "05"));
+        { // 深度优先遍历
+            TreeNodes.Node<String> t01Node = container.findNode("T01");
+            Iterator<TreeNodes.Node<String>> t01NodeIterator = t01Node.depthFirstNodeIterator();
+            while (t01NodeIterator.hasNext()) {
+                TreeNodes.Node<String> next = t01NodeIterator.next();
+                String element = next.getElement();
+                if (element.endsWith("04")) {
+                    next.getParentNode().addChildElement(StringTools.replaceSuffix(element, "05"));
+                }
+            }
+            List<String> t01Descendants = t01Node.getAllDescendantElements();
+            AssertTools.assertDeepEquals(t01Descendants, Arrays.asList(
+                    "T0101", "T010101", "T010102", "T010103", "T010104", "T010105",
+                    "T0102", "T010201", "T010202", "T010203", "T010204", "T010205",
+                    "T0103", "T010301", "T010302", "T010303", "T010304", "T010305"));
+
+            Iterator<TreeNodes.Node<String>> t01NodeIterator2 = t01Node.depthFirstNodeIterator();
+            while (t01NodeIterator2.hasNext()) {
+                TreeNodes.Node<String> next = t01NodeIterator2.next();
+                String element = next.getElement();
+                if (element.contains("02")) {
+                    t01NodeIterator2.remove();
+                }
             }
+            List<String> t01Descendants2 = t01Node.getAllDescendantElements();
+            AssertTools.assertDeepEquals(t01Descendants2, Arrays.asList(
+                    "T0101", "T010101", "T010103", "T010104", "T010105",
+                    "T0103", "T010301", "T010303", "T010304", "T010305"));
         }
-        List<String> t01Descendants = t01Node.getAllDescendantElements();
-        AssertTools.assertDeepEquals(t01Descendants, Arrays.asList(
-                "T0101", "T010101", "T010102", "T010103", "T010104", "T010105",
-                "T0102", "T010201", "T010202", "T010203", "T010204", "T010205",
-                "T0103", "T010301", "T010302", "T010303", "T010304", "T010305"));
-
-        Iterator<TreeNodes.Node<String>> t01NodeIterator2 = t01Node.depthFirstNodeIterator();
-        while(t01NodeIterator2.hasNext()) {
-            TreeNodes.Node<String> next = t01NodeIterator2.next();
-            String element = next.getElement();
-            if (element.contains("02")) {
-                t01NodeIterator2.remove();
+
+        { // 广度优先遍历
+            TreeNodes.Node<String> t03Node = container.findNode("T03");
+            Iterator<TreeNodes.Node<String>> t03NodeIterator = t03Node.depthFirstNodeIterator();
+            while (t03NodeIterator.hasNext()) {
+                TreeNodes.Node<String> next = t03NodeIterator.next();
+                String element = next.getElement();
+                if (element.endsWith("04")) {
+                    next.getParentNode().addChildElement(StringTools.replaceSuffix(element, "05"));
+                }
+            }
+            List<String> t03Descendants = t03Node.getAllDescendantElements();
+            AssertTools.assertDeepEquals(t03Descendants, Arrays.asList(
+                    "T0301", "T030101", "T030102", "T030103", "T030104", "T030105",
+                    "T0302", "T030201", "T030202", "T030203", "T030204", "T030205",
+                    "T0303", "T030301", "T030302", "T030303", "T030304", "T030305"));
+
+            Iterator<TreeNodes.Node<String>> t03NodeIterator2 = t03Node.depthFirstNodeIterator();
+            while (t03NodeIterator2.hasNext()) {
+                TreeNodes.Node<String> next = t03NodeIterator2.next();
+                String element = next.getElement();
+                if (element.contains("02")) {
+                    t03NodeIterator2.remove();
+                }
             }
+            List<String> t03Descendants2 = t03Node.getAllDescendantElements();
+            AssertTools.assertDeepEquals(t03Descendants2, Arrays.asList(
+                    "T0301", "T030101", "T030103", "T030104", "T030105",
+                    "T0303", "T030301", "T030303", "T030304", "T030305"));
         }
-        List<String> t01Descendants2 = t01Node.getAllDescendantElements();
-        AssertTools.assertDeepEquals(t01Descendants2, Arrays.asList(
-                "T0101", "T010101", "T010103", "T010104", "T010105",
-                "T0103", "T010301", "T010303", "T010304", "T010305"));
     }
 
     private List<String> generateTestData() {
         List<String> list = new ArrayList<>();
-        for (int i = 1; i <= 2; i++) {
+        for (int i = 1; i <= 3; i++) {
             String si = codeTools.pad("T", i);
             list.add(si);
             for (int j = 1; j <= 3; j++) {
-- 
Gitee


From 2f1502893d8c4119662cd612240c7c536e4afc4b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 14 Aug 2022 12:36:15 +0800
Subject: [PATCH 059/160] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=A0=91=E5=BD=A2?=
 =?UTF-8?q?=E7=BB=93=E6=9E=84=E9=80=9A=E7=94=A8=E5=A4=84=E7=90=86=E5=AE=B9?=
 =?UTF-8?q?=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
index d1840a4..dd54d50 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
@@ -144,7 +144,7 @@ public class TreeNodes<E> {
     /** 获取KEY对应元素的直接子元素 **/
     public List<E> findChildElements(String key) {
         Node<E> item = findNode(key);
-        return item == null ? new ArrayList<>() : item.getChildElements();
+        return item == null ? new ArrayList<E>() : item.getChildElements();
     }
 
     /** 获取KEY对应元素的所有子元素 **/
-- 
Gitee


From b3714c98daf7a4bdd616ea5ac8b6bebca9b932f0 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 14 Aug 2022 12:37:25 +0800
Subject: [PATCH 060/160] =?UTF-8?q?AcceptAttrs=E5=A2=9E=E5=8A=A0=E7=B1=BB?=
 =?UTF-8?q?=E5=9E=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/able/beans/AcceptAttrs.java    | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/AcceptAttrs.java b/able/src/main/java/com/gitee/qdbp/able/beans/AcceptAttrs.java
index b1f90f3..ba48ddd 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/AcceptAttrs.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/AcceptAttrs.java
@@ -27,11 +27,28 @@ public class AcceptAttrs<T> implements Serializable {
     protected T defaultValue;
     /** 是否必填 **/
     protected boolean required;
+    /** 允许的数据类型 **/
+    protected final Class<T> acceptType;
     /** 允许的值 **/
     protected Collection<T> acceptValues;
 
+    /**
+     * 默认构造函数
+     * @deprecated 改为 AcceptAttrs(Class) 以便确定数据类型
+     */
+    @Deprecated
     public AcceptAttrs() {
-        acceptValues = new HashSet<>();
+        this(null);
+    }
+
+    public AcceptAttrs(Class<T> acceptType) {
+        this.acceptType = acceptType;
+        this.acceptValues = new HashSet<>();
+    }
+
+    /** 允许的数据类型 **/
+    public Class<T> getAcceptType() {
+        return acceptType;
     }
 
     /** 默认值 **/
-- 
Gitee


From 8db899f25fbf0d19641e1f170f3dce41f4d38e16 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 28 Sep 2022 22:19:56 +0800
Subject: [PATCH 061/160] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=A9=BA=E6=8C=87?=
 =?UTF-8?q?=E9=92=88=E5=88=A4=E6=96=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/StringTools.java   | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index e28ae79..6f6fd01 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -1228,6 +1228,9 @@ public class StringTools {
      * @return 删除后的字符串
      */
     public static String removeSubStrings(String string, String... sub) {
+        if (string == null || sub == null || sub.length == 0) {
+            return string;
+        }
         for (String s : sub) {
             string = removeSubString(string, s);
         }
@@ -1243,6 +1246,9 @@ public class StringTools {
      * @return 删除后的字符串
      */
     public static String removeSubString(String string, String sub) {
+        if (string == null || sub == null || sub.length() == 0) {
+            return string;
+        }
         StringBuilder buffer = new StringBuilder();
         int index = 0;
         int startIndex;
@@ -1267,6 +1273,9 @@ public class StringTools {
      * @return 删除后的字符串
      */
     public static String removeChars(String string, char... chars) {
+        if (string == null || chars == null || chars.length == 0) {
+            return string;
+        }
         StringBuilder buffer = new StringBuilder();
         char[] sources = string.toCharArray();
         for (char source : sources) {
@@ -1295,6 +1304,9 @@ public class StringTools {
      * @return 删除后的字符串
      */
     public static String removeInPairedSymbol(String string, String leftSymbol, String rightSymbol) {
+        if (string == null || leftSymbol == null || rightSymbol == null) {
+            return null;
+        }
         StringBuilder buffer = new StringBuilder();
         int index = 0;
         int leftIndex;
@@ -1326,6 +1338,9 @@ public class StringTools {
      * @since 5.0.0
      */
     public static String getSubstringInPairedSymbol(String string, String leftSymbol, String rightSymbol) {
+        if (string == null || leftSymbol == null || rightSymbol == null) {
+            return null;
+        }
         int startIndex = string.indexOf(leftSymbol);
         if (startIndex < 0) {
             return null;
@@ -1347,7 +1362,7 @@ public class StringTools {
      */
     public static String replacePrefix(String string, String prefix) {
         if (string == null || prefix == null) {
-            return null;
+            return string;
         }
         if (prefix.length() == 0 || string.length() < prefix.length()) {
             return string;
@@ -1364,7 +1379,7 @@ public class StringTools {
      */
     public static String replaceSuffix(String string, String suffix) {
         if (string == null || suffix == null) {
-            return null;
+            return string;
         }
         if (suffix.length() == 0 || string.length() < suffix.length()) {
             return string;
-- 
Gitee


From 0ca943e3c4a5d81f0a1bca69b59ecd0271a43ca7 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 28 Sep 2022 22:20:26 +0800
Subject: [PATCH 062/160] =?UTF-8?q?ConvertTools.toBoolean=E9=80=BB?=
 =?UTF-8?q?=E8=BE=91=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/ConvertTools.java    | 17 +++++------------
 1 file changed, 5 insertions(+), 12 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index bbbd79a..ddea211 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -809,11 +809,12 @@ public class ConvertTools {
         if (VerifyTools.isBlank(value)) {
             throw new IllegalArgumentException("null");
         }
-        Boolean number = toBoolean(value, null);
-        if (number == null) {
+        value = value.trim();
+        Boolean b = StringTools.isPositive(value, null);
+        if (b == null) {
             throw new IllegalArgumentException(value);
         } else {
-            return number;
+            return b;
         }
     }
 
@@ -828,16 +829,8 @@ public class ConvertTools {
         if (VerifyTools.isBlank(value)) {
             return defaults;
         }
-
         value = value.trim();
-
-        if (Boolean.TRUE.equals(StringTools.isPositive(value, null))) {
-            return true;
-        } else if (Boolean.FALSE.equals(StringTools.isNegative(value, null))) {
-            return false;
-        } else {
-            return defaults;
-        }
+        return StringTools.isPositive(value, defaults);
     }
 
     /**
-- 
Gitee


From 5fa1994df87c5835709d67f8bbeff1a560d16721 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 28 Sep 2022 22:22:46 +0800
Subject: [PATCH 063/160] =?UTF-8?q?HttpTools=E5=A2=9E=E5=8A=A0header/cooki?=
 =?UTF-8?q?e=E5=A4=84=E7=90=86=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/tools/http/BaseHttpHandler.java      | 63 +++++++++++++++++--
 .../com/gitee/qdbp/tools/http/HttpTools.java  | 25 ++++++--
 .../gitee/qdbp/tools/http/IHttpHandler.java   |  8 ++-
 .../qdbp/tools/http/SimpleHttpHandler.java    | 43 +++++++++++++
 .../qdbp/tools/http/HttpExecutorTest.java     | 13 +---
 .../qdbp/tools/http/XxxAuthHttpExecutor.java  | 18 ++----
 6 files changed, 135 insertions(+), 35 deletions(-)
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/http/SimpleHttpHandler.java

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java b/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java
index 7b3ffb5..28989c0 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java
@@ -2,20 +2,43 @@ package com.gitee.qdbp.tools.http;
 
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.http.Header;
+import org.apache.http.HeaderIterator;
+import org.apache.http.client.CookieStore;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.HeaderGroup;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.http.Header;
-import org.apache.http.HeaderIterator;
-import org.apache.http.message.BasicHeader;
-import org.apache.http.message.HeaderGroup;
 
 public class BaseHttpHandler implements IHttpHandler {
 
+    private CookieStore cookieStore;
     private HeaderGroup headers;
 
+    /** 获取Cookie存储器 **/
+    public CookieStore getCookieStore() {
+        return cookieStore;
+    }
+
+    /** 设置Cookie存储器 **/
+    protected void setCookieStore(CookieStore cookieStore) {
+        this.cookieStore = cookieStore;
+    }
+
+    /** 设置是否保持Cookie **/
+    protected void setKeepCookie(boolean keep) {
+        if (keep) {
+            this.cookieStore = new BasicCookieStore();
+        } else {
+            this.cookieStore = null;
+        }
+    }
+
     /** 追加header参数 **/
     protected void addHeader(String name, String value) {
         if (VerifyTools.isAnyBlank(name, value)) {
@@ -27,6 +50,13 @@ public class BaseHttpHandler implements IHttpHandler {
         this.headers.addHeader(new BasicHeader(name, value));
     }
 
+    /** 追加header参数 **/
+    protected void addHeaders(Map<String, ?> headers) {
+        for (Map.Entry<String, ?> entry : headers.entrySet()) {
+            addHeader(entry.getKey(), ConvertTools.toString(entry.getValue()));
+        }
+    }
+
     /** 删除header参数 **/
     protected void removeHeader(String name) {
         if (VerifyTools.isBlank(name)) {
@@ -43,6 +73,26 @@ public class BaseHttpHandler implements IHttpHandler {
         }
     }
 
+    /** 删除header参数 **/
+    protected void removeHeaders(String... names) {
+        if (VerifyTools.isBlank(names)) {
+            return;
+        }
+        if (this.headers == null) {
+            return;
+        }
+        Map<String, ?> maps = new HashMap<>();
+        for (String name : names) {
+            maps.put(name.toLowerCase(), null);
+        }
+        for (HeaderIterator i = this.headers.iterator(); i.hasNext();) {
+            Header header = i.nextHeader();
+            if (maps.containsKey(header.getName().toLowerCase())) {
+                i.remove();
+            }
+        }
+    }
+
     /** 遍历header参数 **/
     protected HeaderIterator headerIterator() {
         if (this.headers == null) {
@@ -52,8 +102,9 @@ public class BaseHttpHandler implements IHttpHandler {
         }
     }
 
-    /** 获取全部header参数 **/
-    public Header[] getAllHeaders() {
+    /** 获取基础header参数 **/
+    @Override
+    public Header[] getBaseHeaders() {
         return this.headers == null ? null : this.headers.getAllHeaders();
     }
 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java b/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
index a12fdb9..f2afc32 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
@@ -7,6 +7,7 @@ import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import com.alibaba.fastjson.JSON;
@@ -40,6 +41,14 @@ import org.slf4j.LoggerFactory;
     HttpTools.form.post("http://127.0.0.1/api/xxx", map);
     // 以json方式提交参数: {x:11,y:22,z:33}
     HttpTools.json.post("http://127.0.0.1/api/xxx", map);
+
+    // 设置Header示例
+    SimpleHttpHandler httpHandler = new SimpleHttpHandler();
+    httpHandler.addHeader("token", "xxx-token");
+    httpHandler.setKeepCookie(true);
+    HttpTools http = new HttpJsonImpl(httpHandler);
+    System.out.println(http.post("http://www.xxx.com/api/xxx"));
+
     // 自定义超时时间示例
     RequestConfig config = RequestConfig.custom() // 自定义配置
             .setConnectionRequestTimeout(1000) // 从连接池获取连接的超时时间
@@ -47,7 +56,6 @@ import org.slf4j.LoggerFactory;
             .setSocketTimeout(10 * 1000) // 客户端从服务器读取数据的超时时间
             .build();
     HttpTools http = new HttpFormImpl(config);
-    System.out.println(http.post("http://www.xxx.com/api/xxx"));
  * </pre>
  * 
  * @author zhaohuihua
@@ -313,6 +321,9 @@ public class HttpTools {
 
         // 创建HttpClientBuilder
         HttpClientBuilder builder = HttpClientBuilder.create();
+        if (httpHandler.getCookieStore() != null) {
+            builder.setDefaultCookieStore(httpHandler.getCookieStore());
+        }
         // HttpClient
         try (CloseableHttpClient client = builder.build()) {
             HttpGet get = new HttpGet(uri);
@@ -357,6 +368,9 @@ public class HttpTools {
     public <P> String post(String url, Map<String, P> params) throws HttpException {
         // 创建HttpClientBuilder
         HttpClientBuilder builder = HttpClientBuilder.create();
+        if (httpHandler.getCookieStore() != null) {
+            builder.setDefaultCookieStore(httpHandler.getCookieStore());
+        }
         // HttpClient
         try (CloseableHttpClient client = builder.build()) {
             HttpPost post = new HttpPost(url);
@@ -387,6 +401,9 @@ public class HttpTools {
     public <P> String upload(String url, Map<String, P> params) throws HttpException {
         // 创建HttpClientBuilder
         HttpClientBuilder builder = HttpClientBuilder.create();
+        if (httpHandler.getCookieStore() != null) {
+            builder.setDefaultCookieStore(httpHandler.getCookieStore());
+        }
         // HttpClient
         try (CloseableHttpClient client = builder.build()) {
             HttpPost post = new HttpPost(url);
@@ -514,9 +531,9 @@ public class HttpTools {
 
     /** 发送请求前设置header参数等操作 **/
     protected void onBeforeExecute(HttpMessage hm) {
-        Header[] allHeaders = httpHandler.getAllHeaders();
-        if (VerifyTools.isNotBlank(allHeaders)) {
-            hm.setHeaders(allHeaders);
+        Header[] baseHeaders = httpHandler.getBaseHeaders();
+        if (VerifyTools.isNotBlank(baseHeaders)) {
+            hm.setHeaders(baseHeaders);
         }
     }
 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/IHttpHandler.java b/tools/src/main/java/com/gitee/qdbp/tools/http/IHttpHandler.java
index b086b78..3119ebf 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/IHttpHandler.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/IHttpHandler.java
@@ -3,6 +3,7 @@ package com.gitee.qdbp.tools.http;
 import java.util.Map;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import org.apache.http.Header;
+import org.apache.http.client.CookieStore;
 
 /**
  * 参数处理和结果解析
@@ -12,8 +13,11 @@ import org.apache.http.Header;
  */
 public interface IHttpHandler {
 
-    /** 获取全部header参数 **/
-    Header[] getAllHeaders();
+    /** 获取Cookie存储器 **/
+    CookieStore getCookieStore();
+
+    /** 获取基础header参数 **/
+    Header[] getBaseHeaders();
 
     /**
      * 填充基础参数, 如填充配置信息在的公共参数/计算摘要等操作
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/SimpleHttpHandler.java b/tools/src/main/java/com/gitee/qdbp/tools/http/SimpleHttpHandler.java
new file mode 100644
index 0000000..9786e3c
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/SimpleHttpHandler.java
@@ -0,0 +1,43 @@
+package com.gitee.qdbp.tools.http;
+
+import org.apache.http.HeaderIterator;
+import org.apache.http.client.CookieStore;
+
+public class SimpleHttpHandler extends BaseHttpHandler {
+
+    /** 设置Cookie存储器 **/
+    @Override
+    public void setCookieStore(CookieStore cookieStore) {
+        super.setCookieStore(cookieStore);
+    }
+
+    /** 设置是否保持Cookie **/
+    public void setKeepCookie(boolean keep) {
+        super.setKeepCookie(keep);
+    }
+
+    /** 追加header参数 **/
+    @Override
+    public void addHeader(String name, String value) {
+        super.addHeader(name, value);
+    }
+
+    /** 删除header参数 **/
+    @Override
+    public void removeHeader(String name) {
+        super.removeHeader(name);
+    }
+
+    /** 删除header参数 **/
+    @Override
+    public void removeHeaders(String... names) {
+        super.removeHeaders(names);
+    }
+
+    /** 遍历header参数 **/
+    @Override
+    public HeaderIterator headerIterator() {
+        return super.headerIterator();
+    }
+
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java
index 4a7e8c1..e061770 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java
@@ -1,8 +1,6 @@
 package com.gitee.qdbp.tools.http;
 
 import java.net.URL;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.HashMap;
@@ -12,7 +10,6 @@ import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.codec.DigestTools;
-import com.gitee.qdbp.tools.codec.HexTools;
 import com.gitee.qdbp.tools.files.PathTools;
 import com.gitee.qdbp.tools.http.HostUrlConfig.KeyedHttpUrl;
 import com.gitee.qdbp.tools.http.HttpTools.HttpJsonImpl;
@@ -56,7 +53,7 @@ public class HttpExecutorTest extends HttpExecutor {
             String versionSecretKey = config.getString("cim.interface.version.secret.key");
 
             String createTime = new SimpleDateFormat(createTimePattern).format(new Date());
-            long ts = Long.parseLong(RandomTools.generateNumber(5));
+            long ts = RandomTools.generateNumber(10001, 99999);
             String info = account + ts + imeiuuid + appcode + versionSecretKey;
 
             map.put("appCode", appcode);
@@ -66,17 +63,11 @@ public class HttpExecutorTest extends HttpExecutor {
             map.put("sourceType", config.getString("cim.interface.source.type"));
             map.put("createTime", createTime);
             map.put("ts", ts);
+            map.put("digest", DigestTools.md5(info));
             if (data != null) {
                 map.put("jsonData", data);
             }
 
-            try {
-                byte[] digests = MessageDigest.getInstance("MD5").digest(info.getBytes());
-                map.put("digest", HexTools.toString(digests));
-            } catch (NoSuchAlgorithmException e) {
-                throw new RuntimeException(e);
-            }
-
             return map;
         }
 
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java b/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java
index d8e4c20..7bfac54 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java
@@ -1,8 +1,6 @@
 package com.gitee.qdbp.tools.http;
 
 import java.net.URL;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -11,7 +9,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import com.gitee.qdbp.able.result.ResultCode;
-import com.gitee.qdbp.tools.codec.HexTools;
+import com.gitee.qdbp.tools.codec.DigestTools;
 import com.gitee.qdbp.tools.files.PathTools;
 import com.gitee.qdbp.tools.http.HttpTools.HttpJsonImpl;
 import com.gitee.qdbp.tools.utils.RandomTools;
@@ -60,7 +58,7 @@ public class XxxAuthHttpExecutor extends HttpExecutor {
 
     // 如果针对每个接口存在不一样的请求参数或响应报文配置, 可将hurl强转为KeyedHttpUrl
     // String key = ((KeyedHttpUrl) hurl).getKey();
-    // config.getString(key + ".any.subffix"); // 针对每个接口作不同的配置
+    // config.getString(key + ".any.suffix"); // 针对每个接口作不同的配置
 
     private static class XxxAuthHandler extends ConfigHttpHandler {
 
@@ -73,7 +71,7 @@ public class XxxAuthHttpExecutor extends HttpExecutor {
             // 组装基础参数
             String account = config.getString("xxx.auth.account"); // 本平台在xxx平台申请的账号
             String password = config.getString("xxx.auth.password"); // 对应的密码
-            long ts = Long.parseLong(RandomTools.generateNumber(5)); // xxx平台要求用一个5位随机数加密
+            long ts = RandomTools.generateNumber(10001, 99999); // xxx平台要求用一个5位随机数加密
             long time = System.currentTimeMillis();
 
             map.put("account", account);
@@ -83,13 +81,9 @@ public class XxxAuthHttpExecutor extends HttpExecutor {
                 map.put("body", data);
             }
 
-            try { // 计算摘要
-                String info = account + password + ts + time;
-                byte[] digests = MessageDigest.getInstance("MD5").digest(info.getBytes());
-                map.put("digest", HexTools.toString(digests));
-            } catch (NoSuchAlgorithmException e) { // 这个异常不会出现, 除非你用的JDK不支持MD5加密
-                throw new IllegalStateException("NoSuchAlgorithm: MD5", e);
-            }
+            // 计算摘要
+            String info = account + password + ts + time;
+            map.put("digest", DigestTools.md5(info));
 
             return map;
         }
-- 
Gitee


From 0932e66dc35c17652c22bf085f41d0513b62a4bf Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 28 Sep 2022 22:24:11 +0800
Subject: [PATCH 064/160] 5.4.23

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index b3026ba..7f2e40f 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.22</version>
+	<version>5.4.23</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index 0108038..6a872ef 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.22</version>
+	<version>5.4.23</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 312c659fd8c7207f902dcb95cbada7bdbff0f947 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 17 Dec 2022 20:45:47 +0800
Subject: [PATCH 065/160] =?UTF-8?q?BigDecimal=E8=BD=AC=E6=8D=A2=E4=B8=BA?=
 =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/tools/utils/ConvertTools.java     | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index ddea211..e36a5f2 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -798,6 +798,14 @@ public class ConvertTools {
         }
     }
 
+    /** BigDecimal转换为字符串 (不使用科学计数法) **/
+    public static String toPlainString(BigDecimal number) {
+        if (number == null) {
+            return null;
+        }
+        return number.toPlainString();
+    }
+
     /**
      * 转换为Boolean
      * 
-- 
Gitee


From 5c46b14b74fd39f7c5c7a8a84ab84a03f0b830a8 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 25 Dec 2022 23:38:13 +0800
Subject: [PATCH 066/160] =?UTF-8?q?MapTools=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/able/beans/DepthMap.java   | 132 +++++---
 .../gitee/qdbp/tools/utils/ConvertTools.java  | 316 +++++++++++-------
 .../com/gitee/qdbp/tools/utils/MapTools.java  | 251 +++++++++-----
 3 files changed, 454 insertions(+), 245 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/DepthMap.java b/able/src/main/java/com/gitee/qdbp/able/beans/DepthMap.java
index b0f70d0..42f9509 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/DepthMap.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/DepthMap.java
@@ -1,15 +1,12 @@
 package com.gitee.qdbp.able.beans;
 
 import java.io.Serializable;
-import java.util.Arrays;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
-import java.util.regex.Pattern;
-import com.gitee.qdbp.tools.utils.ReflectTools;
+import com.gitee.qdbp.tools.utils.MapTools;
 
 /**
  * 深度路径Map, 用于Ognl取值<br>
@@ -23,7 +20,7 @@ import com.gitee.qdbp.tools.utils.ReflectTools;
  *
  * dpm.put("code.folder.service", "service");
  * dpm.put("code.folder.page", "views");
- * dpm.put("code.folder", "java"); // 该设置无效, 会被忽略掉
+ * // dpm.put("code.folder", "java"); // 该设置会将上面两行覆盖掉
  *
  * Map<String, Object> map = dpm.map();
  *
@@ -39,13 +36,11 @@ import com.gitee.qdbp.tools.utils.ReflectTools;
  * @author zhaohuihua
  * @version 151221
  */
-public class DepthMap implements Serializable {
+public class DepthMap implements Copyable, Serializable {
 
     /** serialVersionUID **/
     private static final long serialVersionUID = 1L;
 
-    private static final Pattern SEPARATOR = Pattern.compile("\\.");
-
     private Map<String, Object> map = new HashMap<>();
 
     public DepthMap() {
@@ -57,46 +52,107 @@ public class DepthMap implements Serializable {
 
     @SuppressWarnings("unchecked")
     public DepthMap put(String key, Object value) {
-        String[] keys = SEPARATOR.split(key);
-
-        List<String> list = Arrays.asList(keys);
-        Iterator<String> iterator = list.iterator();
-
-        Map<String, Object> parent = map;
-        while (parent != null && iterator.hasNext()) {
-            String name = iterator.next();
-
-            Object older = parent.get(name);
-            if (iterator.hasNext()) {
-                // 有下一级
-                if (!(older instanceof Map)) {
-                    // !(older instanceof Map):
-                    // 如果之前有a.b.c, 新加一个a.b.c.d, 覆盖掉a.b.c
-                    older = new HashMap<>();
-                    parent.put(name, older);
-                }
-                parent = (Map<String, Object>) older;
-            } else {
-                // 没有下一级
-                if (older instanceof Map) {
-                    // 如果之前有a.b.c.d, 新加一个a.b.c, 忽略a.b.c
-                    continue;
-                }
-                parent.put(name, value);
-                parent = null; // 没有下一级了, 不需要再处理parent
-            }
-        }
+        MapTools.putValue(map, key, value);
         return this;
     }
 
+    @SuppressWarnings("unchecked")
     public <T> T get(String key) {
-        return ReflectTools.getDepthValue(this.map, key);
+        return (T) MapTools.getValue(map, key);
+    }
+
+    /**
+     * 判断Map中是否存在指定的KEY
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 是否存在
+     */
+    public boolean containsKey(String key) {
+        return MapTools.containsKey(map, key);
+    }
+
+    /**
+     * 获取字符串类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getString("xxx")将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 字符串
+     */
+    public String getString(String key) {
+        return MapTools.getString(map, key);
+    }
+
+    /**
+     * 获取Map值
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return Map值
+     */
+    public Object getValue(String key) {
+        return MapTools.getValue(map, key);
+    }
+
+    /**
+     * 获取指定类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param clazz 指定类型
+     * @return Map值
+     */
+    public <T> T getValue(String key, Class<T> clazz) {
+        return MapTools.getValue(map, key, clazz);
+    }
+
+    /**
+     * 获取指定类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param defaults 值不存在或值类型不匹配时返回的默认值
+     * @param clazz 指定类型
+     * @return Map值
+     */
+    public <T> T getValue(String key, T defaults, Class<T> clazz) {
+        return MapTools.getValue(map, key, defaults, clazz);
+    }
+
+    /**
+     * 获取指定类型的Map值列表
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param clazz 指定类型
+     * @return Map值列表, 值不存在时将返回空List而不是null
+     */
+    public <T> List<T> getList(String key, Class<T> clazz) {
+        return MapTools.getList(map, key, clazz);
+    }
+
+    /**
+     * 获取子Map
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0]
+     * @return 子Map, 值不存在时将返回空Map而不是null
+     */
+    public Map<String, Object> getSubMap(String key) {
+        return MapTools.getSubMap(map, key);
+    }
+
+    /**
+     * 获取子Map数组
+     *
+     * @param key KEY, 支持多级, 如user.addresses
+     * @return 子Map数组, 值不存在时将返回空List而不是null
+     */
+    public List<Map<String, Object>> getSubMaps(String key) {
+        return MapTools.getSubMaps(map, key);
     }
 
     public Map<String, Object> map() {
         return map;
     }
 
+    @Override
     public DepthMap copy() {
         DepthMap n = new DepthMap();
         n.map.putAll(this.map);
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index e36a5f2..fd6a073 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -40,35 +40,15 @@ public class ConvertTools {
 
     /**
      * 将对象解析为列表
-     * 
-     * @param object 待解析的对象
-     * @return 解析后的列表, 如果object=null则返回null
-     * @since 4.1
-     */
-    public static List<Object> parseList(Object object) {
-        if (object == null) {
-            return null;
-        } else {
-            return parseListWithoutNull(object);
-        }
-    }
-
-    /**
-     * 将对象解析为列表
-     * 
+     *
      * @param object 待解析的对象
-     * @return 解析后的列表, 如果object=null则返回空列表
+     * @return 解析后的列表, 如果object=null将返回空列表
      * @since 4.1.0
      */
-    public static List<Object> parseListIfNullToEmpty(Object object) {
+    public static List<Object> parseList(Object object) {
         if (object == null) {
             return new ArrayList<>();
-        } else {
-            return parseListWithoutNull(object);
         }
-    }
-
-    private static List<Object> parseListWithoutNull(Object object) {
         if (object instanceof Collection) {
             return new ArrayList<>((Collection<?>) object);
         } else if (object.getClass() == int[].class) {
@@ -121,9 +101,6 @@ public class ConvertTools {
             return list;
         } else if (object.getClass().isArray()) {
             return new ArrayList<>(Arrays.asList((Object[]) object));
-        } else if (object instanceof Map) {
-            Map<?, ?> map = (Map<?, ?>) object;
-            return new ArrayList<>(map.values());
         } else if (object instanceof Iterable) {
             List<Object> values = new ArrayList<>();
             Iterable<?> iterable = (Iterable<?>) object;
@@ -150,21 +127,30 @@ public class ConvertTools {
         }
     }
 
+    /**
+     * 将对象解析为列表
+     *
+     * @param object 待解析的对象
+     * @return 解析后的列表, 如果object=null则返回空列表
+     * @since 4.1.0
+     * @deprecated 改为 {@linkplain #parseList(Object)}
+     */
+    @Deprecated
+    public static List<Object> parseListIfNullToEmpty(Object object) {
+        return parseList(object);
+    }
+
     /**
      * List转数组
-     * 
+     *
      * @param list 待转换的List
      * @param clazz 目标类型
      * @param <T> 目标类型
      * @param <C> List内容的类型
-     * @return 转换后的数组, 如果list=null则返回null
+     * @return 转换后的数组, 如果list=null将返回T[0]
      */
     public static <T, C extends T> T[] toArray(Collection<C> list, Class<T> clazz) {
-        if (list == null) {
-            return null;
-        }
-
-        int size = list.size();
+        int size = list == null ? 0 : list.size();
         @SuppressWarnings("unchecked")
         T[] copy = clazz == Object[].class ? (T[]) new Object[size] : (T[]) Array.newInstance(clazz, size);
         return size == 0 ? copy : list.toArray(copy);
@@ -172,35 +158,34 @@ public class ConvertTools {
 
     /**
      * List转数组
-     * 
+     *
      * @param list 待转换的List
      * @param clazz 目标类型
      * @param <T> 目标类型
      * @param <C> List内容的类型
      * @return 转换后的数组, 如果list=null则返回T[0]
+     * @deprecated 改为 {@linkplain #toArray(Collection, Class)}
      */
+    @Deprecated
     public static <T, C extends T> T[] toArrayIfNullToEmpty(Collection<C> list, Class<T> clazz) {
-        int size = list == null ? 0 : list.size();
-        @SuppressWarnings("unchecked")
-        T[] copy = clazz == Object[].class ? (T[]) new Object[size] : (T[]) Array.newInstance(clazz, size);
-        return size == 0 ? copy : list.toArray(copy);
+        return toArray(list, clazz);
     }
 
     /**
      * 数组转List
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
-     * @return 转换后的List, 如果array=null则返回null
+     * @return 转换后的List, 如果array=null将返回空List
      */
     @SafeVarargs
     public static <T, C extends T> List<T> toList(C... array) {
         if (array == null) {
-            return null;
+            return new ArrayList<>();
         } else {
             // JDK1.7必须强转, JDK1.8不需要
-            @SuppressWarnings("unchecked")
+            @SuppressWarnings("all")
             List<T> temp = (List<T>) new ArrayList<>(Arrays.asList(array));
             return temp;
         }
@@ -208,37 +193,32 @@ public class ConvertTools {
 
     /**
      * 数组转List
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
-     * @return 转换后的List, 如果array=null则返回EmptyList
+     * @return 转换后的List, 如果array=null则返回空List
+     * @deprecated 改为 {@linkplain #toList(Object...)}
      */
+    @Deprecated
     @SafeVarargs
     public static <T, C extends T> List<T> toListIfNullToEmpty(C... array) {
-        if (array == null) {
-            return new ArrayList<>();
-        } else {
-            // JDK1.7必须强转, JDK1.8不需要
-            @SuppressWarnings("unchecked")
-            List<T> temp = (List<T>) new ArrayList<>(Arrays.asList(array));
-            return temp;
-        }
+        return toList(array);
     }
 
     /**
      * 数组转Set
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
-     * @return 转换后的Set, 如果array=null则返回null
+     * @return 转换后的Set, 如果array=null将返回空Set
      */
     @SafeVarargs
     @SuppressWarnings("unchecked")
     public static <T, C extends T> Set<T> toSet(C... array) {
         if (array == null) {
-            return null;
+            return new HashSet<>();
         } else {
             Set<C> list = new HashSet<>(Arrays.asList(array));
             // JDK1.7必须强转, JDK1.8不需要
@@ -248,37 +228,34 @@ public class ConvertTools {
 
     /**
      * 数组转Set
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
-     * @return 转换后的Set, 如果array=null则返回EmptySet
+     * @return 转换后的Set, 如果array=null则返回空Set
+     * @deprecated 改为 {@linkplain #toSet(Object...)}
      */
+    @Deprecated
     @SafeVarargs
-    @SuppressWarnings("unchecked")
     public static <T, C extends T> Set<T> toSetIfNullToEmpty(C... array) {
-        if (array == null) {
-            return new HashSet<>();
-        } else {
-            Set<C> list = new HashSet<>(Arrays.asList(array));
-            // JDK1.7必须强转, JDK1.8不需要
-            return (Set<T>) list;
-        }
+        return toSet(array);
     }
 
     /**
      * 将key转换为map<br>
      * Map&lt;String, ?&gt; maps = ConvertTools.toKeyMaps("a,b,c");
-     * 
+     *
      * @param keys KEY
      * @return Map
      */
     public static Map<String, ?> toKeyMaps(String... keys) {
         Map<String, ?> map = new HashMap<>();
-        for (String item : keys) {
-            String[] array = StringTools.split(item, ',');
-            for (String key : array) {
-                map.put(key, null);
+        if (keys != null) {
+            for (String item : keys) {
+                String[] array = StringTools.split(item, ',');
+                for (String key : array) {
+                    map.put(key, null);
+                }
             }
         }
         return map;
@@ -287,16 +264,18 @@ public class ConvertTools {
     /**
      * 将key的字符转换为map<br>
      * Map&lt;Character, ?&gt; maps = ConvertTools.toKeyMaps("abc");
-     * 
+     *
      * @param strings KEY
      * @return Map
      */
     public static Map<Character, ?> toCharMaps(String... strings) {
         Map<Character, ?> map = new HashMap<>();
-        for (String string : strings) {
-            char[] chars = string.toCharArray();
-            for (int i = 0, z = chars.length; i < z; i++) {
-                map.put(chars[i], null);
+        if (strings != null) {
+            for (String string : strings) {
+                char[] chars = string.toCharArray();
+                for (int i = 0, z = chars.length; i < z; i++) {
+                    map.put(chars[i], null);
+                }
             }
         }
         return map;
@@ -306,12 +285,12 @@ public class ConvertTools {
      * 数组转Map: key=数组项, value=null<br>
      * List&lt;String&gt; list = Arrays.asList("a", "b", ...);<br>
      * Map&lt;String, ?&gt; maps = ConvertTools.toMap(list);
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
      * @param <V> 值类型
-     * @return 转换后的Map, 如果array=null则返回null
+     * @return 转换后的Map, 如果array=null将返回空Map
      */
     public static <T, C extends T, V> Map<T, V> toMap(Collection<C> array) {
         return toMap(array, null);
@@ -321,18 +300,18 @@ public class ConvertTools {
      * 数组转Map: key=数组项, 所有key都会指向同一个value<br>
      * List&lt;String&gt; list = Arrays.asList("a", "b", ...);<br>
      * Map&lt;String, Boolean&gt; maps = ConvertTools.toMap(list, true);
-     * 
+     *
      * @param array 数组
      * @param value 值
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
      * @param <V> 值类型
-     * @return 转换后的Map, 如果array=null则返回null
+     * @return 转换后的Map, 如果array=null将返回空Map
      */
     @SuppressWarnings("unchecked")
     public static <T, C extends T, V> Map<T, V> toMap(Collection<C> array, V value) {
         if (array == null) {
-            return null;
+            return new HashMap<>();
         } else {
             Map<C, V> map = new HashMap<>();
             for (C field : array) {
@@ -347,41 +326,35 @@ public class ConvertTools {
      * 数组转Map: key=数组项, value=null<br>
      * List&lt;String&gt; list = Arrays.asList("a", "b", ...);<br>
      * Map&lt;String, ?&gt; maps = ConvertTools.toMapIfNullToEmpty(list);
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
      * @param <V> 值类型
-     * @return 转换后的Map, 如果array=null则返回EmptyHashMap
+     * @return 转换后的Map, 如果array=null则返回空Map
+     * @deprecated 改为 {@linkplain #toMap(Collection)}
      */
+    @Deprecated
     public static <T, C extends T, V> Map<T, V> toMapIfNullToEmpty(Collection<C> array) {
-        return toMapIfNullToEmpty(array, null);
+        return toMap(array, null);
     }
 
     /**
      * 数组转Map: key=数组项, 所有key都会指向同一个value<br>
      * List&lt;String&gt; list = Arrays.asList("a", "b", ...);<br>
      * Map&lt;String, Boolean&gt; maps = ConvertTools.toMapIfNullToEmpty(list, true);
-     * 
+     *
      * @param array 数组
      * @param value 值
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
      * @param <V> 值类型
-     * @return 转换后的Map, 如果array=null则返回EmptyHashMap
+     * @return 转换后的Map, 如果array=null则返回空Map
+     * @deprecated 改为 {@linkplain #toMap(Collection, Object)}
      */
-    @SuppressWarnings("unchecked")
+    @Deprecated
     public static <T, C extends T, V> Map<T, V> toMapIfNullToEmpty(Collection<C> array, V value) {
-        if (array == null) {
-            return new HashMap<>();
-        } else {
-            Map<C, V> map = new HashMap<>();
-            for (C field : array) {
-                map.put(field, value);
-            }
-            // JDK1.7必须强转, JDK1.8不需要
-            return (Map<T, V>) map;
-        }
+        return toMap(array, value);
     }
 
     /**
@@ -560,6 +533,7 @@ public class ConvertTools {
 
     /**
      * 清除科学计数法
+     *
      * @param number 目标数字
      * @return 最终结果
      */
@@ -798,17 +772,17 @@ public class ConvertTools {
         }
     }
 
-    /** BigDecimal转换为字符串 (不使用科学计数法) **/
+    /** BigDecimal转换为字符串 (清除小数点后末尾的0,不使用科学计数法) **/
     public static String toPlainString(BigDecimal number) {
         if (number == null) {
             return null;
         }
-        return number.toPlainString();
+        return clearTrailingZeros(number).toPlainString();
     }
 
     /**
      * 转换为Boolean
-     * 
+     *
      * @param value 源字符串
      * @return 解析结果
      * @throws IllegalArgumentException 格式错误
@@ -828,7 +802,7 @@ public class ConvertTools {
 
     /**
      * 转换为Boolean
-     * 
+     *
      * @param value 源字符串
      * @param defaults 默认值, 在表达式为空/表达式格式错误/表达式结果不是数字时返回默认值
      * @return 解析结果
@@ -952,6 +926,7 @@ public class ConvertTools {
     private static final long pebibyte = tebibyte * kibibyte;
     private static final long exbibyte = pebibyte * kibibyte;
     private static final Map<String, Long> BYTE_UNITS = new HashMap<>();
+
     static { // 单位对应的倍数
         BYTE_UNITS.put("B", 1L);
         BYTE_UNITS.put("KB", kibibyte);
@@ -1231,13 +1206,102 @@ public class ConvertTools {
         return buffer.toString();
     }
 
+    /**
+     * 解析基本数据类型的字段值
+     *
+     * @param fieldName 字段名
+     * @param fieldValue 字段值
+     * @return 新值
+     */
+    public static Object tryParsePrimitiveValue(String fieldName, String fieldValue) {
+        fieldValue = StringTools.trim(fieldValue);
+        if (VerifyTools.isBlank(fieldValue)) {
+            return null;
+        }
+        if ("true".equals(fieldValue)) {
+            return true;
+        } else if ("false".equals(fieldValue)) {
+            return false;
+        }
+
+        if (StringTools.isNumber(fieldValue)) {
+            if ("0".equals(fieldValue) || fieldValue.startsWith("0.") || !fieldValue.startsWith("0")) {
+                try {
+                    return ConvertTools.toNumber(fieldValue);
+                } catch (NumberFormatException ignore) {
+                }
+            }
+        } else {
+            try {
+                if (fieldName.endsWith("OfMinInDay") || fieldName.endsWith("OfMinDay")) {
+                    Date date = DateTools.parse(fieldValue);
+                    return DateTools.toStartTime(date);
+                } else if (fieldName.endsWith("OfMaxInDay") || fieldName.endsWith("OfMaxDay")) {
+                    Date date = DateTools.parse(fieldValue);
+                    return DateTools.toEndTime(date);
+                } else if (fieldName.endsWith("Date") || fieldName.endsWith("_DATE")) {
+                    return DateTools.parse(fieldValue);
+                } else if (fieldName.endsWith("Time") || fieldName.endsWith("_TIME")) {
+                    return DateTools.parse(fieldValue);
+                } else if (isLikeDateFormat(fieldValue)) {
+                    return DateTools.parse(fieldValue);
+                }
+            } catch (IllegalArgumentException ignore) {
+            }
+        }
+
+        return fieldValue;
+    }
+
+    /** 支持哪些日期格式 **/
+    private static final List<String> DATE_FORMATS = Arrays.asList(
+            "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss.SSS"
+    );
+
+    private static boolean isLikeDateFormat(String source) {
+        for (String format : DATE_FORMATS) {
+            if (isDateFormatMatches(source, format)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // 检查字符串是不是日期时间格式
+    private static boolean isDateFormatMatches(String source, String format) {
+        if (VerifyTools.isAnyBlank(source, format)) {
+            return false;
+        }
+        int sLength = source.length();
+        int fLength = format.length();
+        if (sLength != fLength) {
+            return false;
+        }
+        for (int i = 0; i < fLength; i++) {
+            char s = source.charAt(i);
+            char c = format.charAt(i);
+            if (c == 'y' || c == 'M' || c == 'd' || c == 'H' || c == 'h' || c == 'm' || c == 's' || c == 'S') {
+                // 遇到yyyy MM dd HH mm ss SSS之类的字符, 则原字符必须是数字
+                if (s < '0' || s > '9') {
+                    return false;
+                }
+            } else {
+                // 遇到其他字符, 则原字符必须与该字符相等
+                if (s != c) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
     /**
      * 压缩列表, 保留指定数目
      *
-     * @author zhaohuihua
      * @param list 列表
      * @param max 保留数目
      * @return 压缩后的列表
+     * @author zhaohuihua
      */
     public static <T> List<T> compressList(List<T> list, int max) {
         int total = list.size();
@@ -1262,7 +1326,7 @@ public class ConvertTools {
             // 那要取多少次才能减少掉5个点呢, 取最小公倍数
             // 就是210 = 5 * 6 * 7
             int end = diff * interval * (interval + 1);
-            for (int i = 0; i < total;) {
+            for (int i = 0; i < total; ) {
                 compressed.add(list.get(i));
                 i += interval;
                 // 达到end之前多隔一个点取一个
@@ -1319,7 +1383,7 @@ public class ConvertTools {
 
     /**
      * 清除Map中的空值
-     * 
+     *
      * @param map Map对象
      * @deprecated move to {@link MapTools#clearBlankValue(Map)}
      */
@@ -1330,7 +1394,7 @@ public class ConvertTools {
 
     /**
      * 解析请求参数
-     * 
+     *
      * @param params 请求参数
      * @return Map
      * @deprecated move to {@link MapTools#parseRequestParams(String)}
@@ -1342,7 +1406,7 @@ public class ConvertTools {
 
     /**
      * 解析请求参数
-     * 
+     *
      * @param params 请求参数
      * @param charset 字符编码: 如果不为空, 将对value执行URLDecoder.decode(value, charset)
      * @return Map
@@ -1356,7 +1420,7 @@ public class ConvertTools {
     /**
      * 获取字符串类型的Map值<br>
      * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getString("xxx")将返回null
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @return 字符串
@@ -1370,7 +1434,7 @@ public class ConvertTools {
     /**
      * 获取指定类型的Map值<br>
      * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getValue("xxx", Boolean.class)将返回null
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
@@ -1385,7 +1449,7 @@ public class ConvertTools {
     /**
      * 获取指定类型的Map值<br>
      * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getValue("xxx", Boolean.class)将返回null
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param defaults 值不存在或值类型不匹配时返回的默认值
@@ -1402,7 +1466,7 @@ public class ConvertTools {
      * 获取指定类型的Map值列表<br>
      * 注意: 该方法不进行类型转换, 如果key指向的对象不是Iterable, 将返回null或空列表<br>
      * 注意: 如果Iterable中存在类型不匹配的元素, 也会返回null或空列表
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
@@ -1418,7 +1482,7 @@ public class ConvertTools {
      * 获取指定类型的Map值列表<br>
      * 注意: 该方法不进行类型转换, 如果key指向的对象不是Iterable, 将返回null或空列表<br>
      * 注意: 如果Iterable中存在类型不匹配的元素, 也会返回null或空列表
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param nullToEmpty 值不存在或值类型不匹配时是否返回空列表
@@ -1434,15 +1498,15 @@ public class ConvertTools {
     /**
      * 过滤数据字段<br>
      * <pre>
-        Map&lt;String, Object&gt; list = [ { main:{}, detail:{}, subject:{}, finance:[], target:[] } ];
-        String mainFields = "main:id,projectName,publishStatus";
-        String detailFields = "detail:id,totalAmount,holdingTime";
-        String subjectFields = "subject:id,customerName,industryCategory";
-        String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
-        String targetFields = "target:id,industryCategory,latestMkt,latestPer";
-        ConvertTools.filterFields(list, mainFields, detailFields, subjectFields, financeFields, targetFields);
+     * Map&lt;String, Object&gt; list = [ { main:{}, detail:{}, subject:{}, finance:[], target:[] } ];
+     * String mainFields = "main:id,projectName,publishStatus";
+     * String detailFields = "detail:id,totalAmount,holdingTime";
+     * String subjectFields = "subject:id,customerName,industryCategory";
+     * String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
+     * String targetFields = "target:id,industryCategory,latestMkt,latestPer";
+     * ConvertTools.filterFields(list, mainFields, detailFields, subjectFields, financeFields, targetFields);
      * </pre>
-     * 
+     *
      * @param list 过滤前的数据
      * @param fields 需要保留的字段列表
      * @return 过滤后的数据
@@ -1456,15 +1520,15 @@ public class ConvertTools {
     /**
      * 过滤数据字段<br>
      * <pre>
-        Map&lt;String, Object&gt; data = { main:{}, detail:{}, subject:{}, finance:[], target:[] };
-        String mainFields = "main:id,projectName,publishStatus";
-        String detailFields = "detail:id,totalAmount,holdingTime";
-        String subjectFields = "subject:id,customerName,industryCategory";
-        String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
-        String targetFields = "target:id,industryCategory,latestMkt,latestPer";
-        ConvertTools.filterFields(data, mainFields, detailFields, subjectFields, financeFields, targetFields);
+     * Map&lt;String, Object&gt; data = { main:{}, detail:{}, subject:{}, finance:[], target:[] };
+     * String mainFields = "main:id,projectName,publishStatus";
+     * String detailFields = "detail:id,totalAmount,holdingTime";
+     * String subjectFields = "subject:id,customerName,industryCategory";
+     * String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
+     * String targetFields = "target:id,industryCategory,latestMkt,latestPer";
+     * ConvertTools.filterFields(data, mainFields, detailFields, subjectFields, financeFields, targetFields);
      * </pre>
-     * 
+     *
      * @param data 过滤前的数据
      * @param fields 需要保留的字段列表
      * @return 过滤后的数据
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index 78dd397..be3c931 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -29,7 +29,7 @@ public class MapTools {
 
     /**
      * 清除Map中的空值
-     * 
+     *
      * @param map Map对象
      */
     @SuppressWarnings("unchecked")
@@ -79,7 +79,7 @@ public class MapTools {
 
     /**
      * 解析请求参数: name=zhh&phone=139xxxx8888&code=&amp;amp;
-     * 
+     *
      * @param params 请求参数
      * @param charset 字符编码: 如果不为空, 将对value执行URLDecoder.decode(value, charset)
      * @return Map
@@ -229,7 +229,7 @@ public class MapTools {
 
     /**
      * 判断Map中是否存在指定的KEY
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @return 是否存在
@@ -237,7 +237,7 @@ public class MapTools {
     public static boolean containsKey(Map<?, ?> map, String key) {
         if (key == null || map.containsKey(key)) {
             return map.containsKey(key);
-        } else if (key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0) {
+        } else if (isComplexKey(key)) {
             return ReflectTools.containsDepthFields(map, key);
         } else {
             return false;
@@ -247,7 +247,7 @@ public class MapTools {
     /**
      * 获取字符串类型的Map值<br>
      * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getString("xxx")将返回null
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @return 字符串
@@ -259,7 +259,7 @@ public class MapTools {
 
     /**
      * 获取Map值
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @return Map值
@@ -267,11 +267,11 @@ public class MapTools {
     public static Object getValue(Map<?, ?> map, String key) {
         if (key == null || map.containsKey(key)) {
             if (map instanceof ExpressionMap) {
-                return ((ExpressionMap)map).getOriginal(key);
+                return ((ExpressionMap) map).getOriginal(key);
             } else {
                 return map.get(key);
             }
-        } else if (key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0) {
+        } else if (isComplexKey(key)) {
             return ReflectTools.getDepthValue(map, key);
         } else {
             return null;
@@ -281,7 +281,7 @@ public class MapTools {
     /**
      * 获取指定类型的Map值<br>
      * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
@@ -296,7 +296,7 @@ public class MapTools {
     /**
      * 获取指定类型的Map值<br>
      * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param defaults 值不存在或值类型不匹配时返回的默认值
@@ -310,129 +310,213 @@ public class MapTools {
     }
 
     /**
-     * 获取指定类型的Map值列表<br>
-     * 注意: 该方法不进行类型转换, 如果key指向的对象不是Iterable, 将返回null<br>
-     * 注意: 如果Iterable中存在类型不匹配的元素, 也会返回null
-     * 
+     * 获取指定类型的Map值列表, 值不存在时将返回空List
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
-     * @return Map值列表, 值不存在或值类型不匹配时返回null
+     * @return Map值列表, 值不存在或值类型不匹配时将返回空List
      */
+    @SuppressWarnings("unchecked")
     public static <T> List<T> getList(Map<?, ?> map, String key, Class<T> clazz) {
-        return getList(map, key, false, clazz);
+        Object value = getValue(map, key);
+        if (value == null) {
+            return new ArrayList<T>();
+        }
+        List<Object> list = ConvertTools.parseList(value);
+        List<T> result = new ArrayList<>();
+        for (Object item : list) {
+            if (item != null && clazz.isAssignableFrom(item.getClass())) {
+                result.add((T) item);
+            } else {
+                result.add(null);
+            }
+        }
+        return result;
     }
 
     /**
-     * 获取指定类型的Map值列表<br>
-     * 注意: 该方法不进行类型转换, 如果key指向的对象不是Iterable且不是clazz对象, 将返回null或空列表<br>
-     * 注意: 如果Iterable中存在类型不匹配的元素, 也会返回null或空列表
-     * 
+     * 获取指定类型的Map值列表, 值不存在时将返回空List
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param nullToEmpty 值不存在或值类型不匹配时是否返回空列表
      * @param clazz 指定类型
      * @return Map值列表
+     * @deprecated 改为 {@linkplain #getList(Map, String, Class)}
      */
-    @SuppressWarnings("unchecked")
+    @Deprecated
     public static <T> List<T> getList(Map<?, ?> map, String key, boolean nullToEmpty, Class<T> clazz) {
-        Object value = getValue(map, key);
-        if (value == null) {
-            return nullToEmpty ? new ArrayList<T>() : null;
-        } else if (!(value instanceof Iterable)) {
-            if (clazz.isAssignableFrom(value.getClass())) {
-                List<T> list = new ArrayList<>();
-                list.add((T) value);
-                return list;
-            } else {
-                return nullToEmpty ? new ArrayList<T>() : null;
-            }
-        }
-        Iterable<?> collection = (Iterable<?>) value;
-        List<T> list = new ArrayList<>();
-        for (Object item : collection) {
-            if (item == null) {
-                list.add(null);
-            } else if (clazz.isAssignableFrom(item.getClass())) {
-                list.add((T) item);
-            } else {
-                return nullToEmpty ? new ArrayList<T>() : null; // 存在类型不匹配的元素, 返回空对象
-            }
-        }
-        return list;
+        return getList(map, key, clazz);
     }
 
     /**
      * 获取子Map
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0]
      * @return 子Map
      */
     public static Map<String, Object> getSubMap(Map<?, ?> map, String key) {
-        return getSubMap(map, key, false);
+        Map<?, ?> value = getValue(map, key, Map.class);
+        Map<String, Object> result = new HashMap<>();
+        if (value != null) {
+            for (Map.Entry<?, ?> entry : value.entrySet()) {
+                String fieldKey = entry.getKey() == null ? null : entry.getKey().toString();
+                result.put(fieldKey, entry.getValue());
+            }
+        }
+        return result;
     }
 
     /**
      * 获取子Map
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0]
      * @param nullToEmpty 值不存在或值类型不匹配时是否返回空Map
      * @return 子Map
+     * @deprecated 改为 {@linkplain #getSubMap(Map, String)}
      */
-    @SuppressWarnings("unchecked")
+    @Deprecated
     public static Map<String, Object> getSubMap(Map<?, ?> map, String key, boolean nullToEmpty) {
-        Map<String, Object> result = getValue(map, key, Map.class);
-        return result == null && nullToEmpty ? new HashMap<String, Object>() : result;
+        return getSubMap(map, key);
     }
 
     /**
      * 获取子Map数组
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses
      * @return 子Map数组
      */
     public static List<Map<String, Object>> getSubMaps(Map<?, ?> map, String key) {
-        return getSubMaps(map, key, false);
+        @SuppressWarnings("rawtypes")
+        List<Map> maps = getList(map, key, Map.class);
+        List<Map<String, Object>> results = new ArrayList<>();
+        for (Map<?, ?> item : maps) {
+            Map<String, Object> result = new HashMap<>();
+            results.add(result);
+            for (Map.Entry<?, ?> entry : item.entrySet()) {
+                String fieldKey = entry.getKey() == null ? null : entry.getKey().toString();
+                result.put(fieldKey, entry.getValue());
+            }
+        }
+        return results;
     }
 
     /**
      * 获取子Map数组
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses
      * @param nullToEmpty 值不存在或值类型不匹配时是否返回空列表
      * @return 子Map数组
+     * @deprecated 改为 {@linkplain #getSubMaps(Map, String)}
      */
-    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Deprecated
     public static List<Map<String, Object>> getSubMaps(Map<?, ?> map, String key, boolean nullToEmpty) {
-        List<Map> maps = getList(map, key, nullToEmpty, Map.class);
-        if (maps == null) {
-            return null;
+        return getSubMaps(map, key);
+    }
+
+    /**
+     * 深度设置Map值<br>
+     * a.b.c, a.b.c.d 不能共存, 保留路径最深的a.b.c.d
+     *
+     * <pre>
+     * Map&lt;String, Object&gt; map = new HashMap&lt;&gt;();
+     * MapTools.putValue(map, "author", "xxx"); // 该设置无效, 会被后面的覆盖掉
+     * MapTools.putValue(map, "author.code", 100);
+     * MapTools.putValue(map, "author.name", "zhaohuihua");
+     *
+     * MapTools.putValue(map, "code.folder.service", "service");
+     * MapTools.putValue(map, "code.folder.page", "views");
+     * MapTools.putValue(map, "code.folder", "java"); // 该设置会将上面两行覆盖掉
+     * </pre>
+     *
+     * @param map Map容器
+     * @param key KEY, 支持多级, 如user.addresses
+     * @param value 值
+     */
+    @SuppressWarnings("unchecked")
+    public static Object putValue(Map<String, Object> map, String key, Object value) {
+        if (key == null || key.indexOf('.') <= 0) {
+            return map.put(key, value);
         }
-        // Cannot cast from List<Map> to List<Map<String,Object>>
-        // return (List<Map<String, Object>>) maps;
-        List<Map<String, Object>> result = new ArrayList<>();
-        for (Map item : maps) {
-            result.add(item);
+
+        List<String> keys = StringTools.splits(key, '.');
+        Iterator<String> iterator = keys.iterator();
+
+        Map<String, Object> parent = map;
+        while (parent != null && iterator.hasNext()) {
+            String name = iterator.next();
+
+            Object older = parent.get(name);
+            if (iterator.hasNext()) {
+                // 有下一级
+                if (older instanceof Map) {
+                    parent = (Map<String, Object>) older;
+                } else {
+                    // 如果之前有a.b.c, 新加一个a.b.c.d, 则用map覆盖掉a.b.c
+                    Map<String, Object> newer = new HashMap<>();
+                    parent.put(name, newer);
+                    parent = newer;
+                }
+            } else {
+                // 没有下一级
+                return parent.put(name, value);
+            }
         }
-        return result;
+        return null;
+    }
+
+    /**
+     * 删除map中的key
+     *
+     * @param map 原Map
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 被删除的对象
+     */
+    public static Object removeKey(Map<String, Object> map, String key) {
+        if (key == null || key.indexOf('.') <= 0 || map.containsKey(key)) {
+            return map.remove(key);
+        }
+
+        List<String> keys = StringTools.splits(key, '.');
+        Iterator<String> iterator = keys.iterator();
+
+        Map<?, ?> parent = map;
+        while (iterator.hasNext()) {
+            String name = iterator.next();
+
+            Object older = parent.get(name);
+            if (iterator.hasNext()) {
+                // 有下一级
+                if (older instanceof Map) {
+                    parent = (Map<?, ?>) older;
+                } else {
+                    break;
+                }
+            } else {
+                // 没有下一级
+                return parent.remove(name);
+            }
+        }
+        return null;
     }
 
     /**
      * 过滤数据字段<br>
      * <pre>
-        Map&lt;String, Object&gt; list = [ { main:{}, detail:{}, subject:{}, finance:[], target:[] } ];
-        String mainFields = "main:id,projectName,publishStatus";
-        String detailFields = "detail:id,totalAmount,holdingTime";
-        String subjectFields = "subject:id,customerName,industryCategory";
-        String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
-        String targetFields = "target:id,industryCategory,latestMkt,latestPer";
-        MapTools.filterFields(list, mainFields, detailFields, subjectFields, financeFields, targetFields);
+     * Map&lt;String, Object&gt; list = [ { main:{}, detail:{}, subject:{}, finance:[], target:[] } ];
+     * String mainFields = "main:id,projectName,publishStatus";
+     * String detailFields = "detail:id,totalAmount,holdingTime";
+     * String subjectFields = "subject:id,customerName,industryCategory";
+     * String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
+     * String targetFields = "target:id,industryCategory,latestMkt,latestPer";
+     * MapTools.filterFields(list, mainFields, detailFields, subjectFields, financeFields, targetFields);
      * </pre>
-     * 
+     *
      * @param list 过滤前的数据
      * @param fields 需要保留的字段列表
      * @return 过滤后的数据
@@ -448,15 +532,15 @@ public class MapTools {
     /**
      * 过滤数据字段<br>
      * <pre>
-        Map&lt;String, Object&gt; data = { main:{}, detail:{}, subject:{}, finance:[], target:[] };
-        String mainFields = "main:id,projectName,publishStatus";
-        String detailFields = "detail:id,totalAmount,holdingTime";
-        String subjectFields = "subject:id,customerName,industryCategory";
-        String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
-        String targetFields = "target:id,industryCategory,latestMkt,latestPer";
-        MapTools.filterFields(data, mainFields, detailFields, subjectFields, financeFields, targetFields);
+     * Map&lt;String, Object&gt; data = { main:{}, detail:{}, subject:{}, finance:[], target:[] };
+     * String mainFields = "main:id,projectName,publishStatus";
+     * String detailFields = "detail:id,totalAmount,holdingTime";
+     * String subjectFields = "subject:id,customerName,industryCategory";
+     * String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
+     * String targetFields = "target:id,industryCategory,latestMkt,latestPer";
+     * MapTools.filterFields(data, mainFields, detailFields, subjectFields, financeFields, targetFields);
      * </pre>
-     * 
+     *
      * @param data 过滤前的数据
      * @param fields 需要保留的字段列表
      * @return 过滤后的数据
@@ -566,7 +650,8 @@ public class MapTools {
     }
 
     /** 树节点排序, 从顶级节点开始向下排列 **/
-    public static List<Map<String, Object>> sortTreeNodes(List<Map<String, Object>> list, String codeField, String parentField) {
+    public static List<Map<String, Object>> sortTreeNodes(List<Map<String, Object>> list, String codeField,
+            String parentField) {
         // key=codeValue
         Map<Object, Map<String, Object>> idMaps = new HashMap<>();
         // key=codeValue, value=children
@@ -613,4 +698,8 @@ public class MapTools {
         }
         return sorted;
     }
+
+    protected static boolean isComplexKey(String key) {
+        return key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0;
+    }
 }
-- 
Gitee


From 88b9acf51b9c9d9ddf41000b00514a45c1c4195a Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 25 Dec 2022 23:39:30 +0800
Subject: [PATCH 067/160] MapPlus

---
 .../com/gitee/qdbp/tools/beans/MapPlus.java   | 193 ++++++++++++++++++
 .../com/gitee/qdbp/tools/utils/JsonTools.java | 117 ++++++++++-
 .../gitee/qdbp/tools/utils/DepthMapTest.java  |   6 +-
 3 files changed, 307 insertions(+), 9 deletions(-)
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java b/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
new file mode 100644
index 0000000..66f62f0
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
@@ -0,0 +1,193 @@
+package com.gitee.qdbp.tools.beans;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
+import com.gitee.qdbp.tools.utils.ReflectTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 增强的map
+ *
+ * @author zhaohuihua
+ * @version 20221207
+ */
+public class MapPlus extends HashMap<String, Object> {
+    private static final long serialVersionUID = 4479911245968765771L;
+
+    public MapPlus() {
+    }
+
+    public MapPlus(int initialCapacity) {
+        super(initialCapacity);
+    }
+
+    public MapPlus(int initialCapacity, float loadFactor) {
+        super(initialCapacity, loadFactor);
+    }
+
+    public MapPlus(Map<? extends String, ?> m) {
+        super();
+        putAll(m);
+    }
+
+    @Override
+    public void putAll(Map<? extends String, ?> m) {
+        if (m != null) {
+            for (Entry<? extends String, ?> entry : m.entrySet()) {
+                put(entry.getKey(), entry.getValue());
+            }
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Object put(String key, Object value) {
+        if (key == null) {
+            return super.put(null, value);
+        } else if (key.indexOf('.') <= 0) {
+            return super.put(key, value);
+        }
+
+        List<String> keys = StringTools.splits(key, '.');
+        Iterator<String> iterator = keys.iterator();
+
+        Map<String, Object> parent = this;
+        while (iterator.hasNext()) {
+            String name = iterator.next();
+
+            Object older = parent.get(name);
+            if (iterator.hasNext()) {
+                // 有下一级
+                if (older instanceof Map) {
+                    parent = (Map<String, Object>) older;
+                } else {
+                    // 如果之前有a.b.c, 新加一个a.b.c.d, 则用map覆盖掉a.b.c
+                    MapPlus newer = new MapPlus();
+                    parent.put(name, newer);
+                    parent = newer;
+                }
+            } else {
+                // 没有下一级
+                return parent.put(name, value);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Object remove(Object key) {
+        if (key == null) {
+            return super.remove(null);
+        } else if (key instanceof String && ((String) key).indexOf('.') > 0) {
+            return MapTools.removeKey(this, (String) key);
+        } else {
+            return super.remove(key);
+        }
+    }
+
+    @Override
+    public Object get(Object key) {
+        if (key == null || super.containsKey(key)) {
+            return super.get(key);
+        }
+        String sk = key.toString();
+        if (isComplexKey(sk)) {
+            return ReflectTools.getDepthValue(this, sk);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 判断Map中是否存在指定的KEY
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 是否存在
+     */
+    @Override
+    public boolean containsKey(Object key) {
+        if (key == null || super.containsKey(key)) {
+            return super.containsKey(key);
+        }
+        String sk = key.toString();
+        if (isComplexKey(sk)) {
+            return ReflectTools.containsDepthFields(this, sk);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 获取字符串类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, MapTools.getString("xxx")将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 字符串
+     */
+    public String getString(String key) {
+        return JsonTools.getMapString(this, key);
+    }
+
+    /**
+     * 获取指定类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), MapTools.getValue("xxx", Double.class)将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param clazz 指定类型
+     * @return Map值
+     */
+    public <T> T getValue(String key, Class<T> clazz) {
+        return JsonTools.getMapValue(this, key, clazz);
+    }
+
+    /**
+     * 获取指定类型的Map值
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param defaults 值不存在或值类型不匹配时返回的默认值
+     * @param clazz 指定类型
+     * @return Map值
+     */
+    public <T> T getValue(String key, T defaults, Class<T> clazz) {
+        return JsonTools.getMapValue(this, key, defaults, clazz);
+    }
+
+    /**
+     * 获取指定类型的Map值列表
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param clazz 指定类型
+     * @return Map值列表, 值不存在时将返回空List而不是null
+     */
+    public <T> List<T> getList(String key, Class<T> clazz) {
+        return JsonTools.getMapValues(this, key, clazz);
+    }
+
+    /**
+     * 获取子Map
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0]
+     * @return 子Map, 值不存在时将返回空Map而不是null
+     */
+    public Map<String, Object> getSubMap(String key) {
+        return JsonTools.getMapSubMap(this, key);
+    }
+
+    /**
+     * 获取子Map数组
+     *
+     * @param key KEY, 支持多级, 如user.addresses
+     * @return 子Map数组, 值不存在时将返回空List而不是null
+     */
+    public List<Map<String, Object>> getSubMaps(String key) {
+        return JsonTools.getMapSubMaps(this, key);
+    }
+
+    protected boolean isComplexKey(String key) {
+        return key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0;
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
index 064810e..b5e26a7 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
@@ -45,36 +45,137 @@ public class JsonTools {
     private JsonTools() {
     }
 
+    /** 对象转换为JavaBean **/
+    public static <T> T convert(Object object, Class<T> clazz) {
+        return TypeUtils.castToJavaBean(object, clazz);
+    }
+
+    /**
+     * 获取Map中的字符串值, 并转换为字符串
+     *
+     * @param map Map
+     * @param key KEY
+     * @return 字符串
+     */
     public static String getMapString(Map<String, Object> map, String key) {
         Object value = MapTools.getValue(map, key);
         return TypeUtils.castToJavaBean(value, String.class);
     }
 
+    /**
+     * 获取Map中的值, 并转换为指定类型
+     *
+     * @param map Map
+     * @param key KEY
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 指定类型的值
+     */
     public static <T> T getMapValue(Map<String, Object> map, String key, Class<T> clazz) {
         Object value = MapTools.getValue(map, key);
         return TypeUtils.castToJavaBean(value, clazz);
     }
 
+    /**
+     * 获取Map中的值, 并转换为指定类型
+     *
+     * @param map Map
+     * @param key KEY
+     * @param defaults 默认值
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 指定类型的值
+     */
     public static <T> T getMapValue(Map<String, Object> map, String key, T defaults, Class<T> clazz) {
         Object value = MapTools.getValue(map, key);
         return TypeUtils.castToJavaBean(value == null ? defaults : value, clazz);
     }
 
+    /**
+     * 获取Map中的列表值
+     *
+     * @param map Map
+     * @param key KEY
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 列表
+     */
     public static <T> List<T> getMapValues(Map<String, Object> map, String key, Class<T> clazz) {
-        return getMapValues(map, key, false, clazz);
+        Object value = MapTools.getValue(map, key);
+        List<T> result = new ArrayList<>();
+        if (value == null) {
+            return result;
+        }
+        List<Object> values = ConvertTools.parseList(value);
+        for (Object item : values) {
+            result.add(TypeUtils.castToJavaBean(item, clazz));
+        }
+        return result;
     }
 
+    /**
+     * 获取Map中的列表值
+     *
+     * @param map 原Map
+     * @param key KEY
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 列表
+     * @deprecated 改为 {@linkplain #getMapValue(Map, String, Class)}
+     */
+    @Deprecated
     public static <T> List<T> getMapValues(Map<String, Object> map, String key, boolean nullToEmpty, Class<T> clazz) {
+        return getMapValues(map, key, clazz);
+    }
+
+    /**
+     * 获取Map中的子Map
+     *
+     * @param map 原Map
+     * @param key KEY
+     * @return Map
+     */
+    public static Map<String, Object> getMapSubMap(Map<String, Object> map, String key) {
         Object value = MapTools.getValue(map, key);
-        if (!(value instanceof Collection)) {
-            return nullToEmpty ? new ArrayList<T>() : null;
+        Map<String, Object> result = new HashMap<>();
+        if (value == null) {
+            return result;
         }
-        Collection<?> collection = (Collection<?>) value;
-        List<T> list = new ArrayList<>();
-        for (Object item : collection) {
-            list.add(TypeUtils.castToJavaBean(item, clazz));
+        Map<?, ?> bean = TypeUtils.castToJavaBean(value, Map.class);
+        for (Map.Entry<?, ?> entry : bean.entrySet()) {
+            String fieldKey = entry.getKey() == null ? null : entry.getKey().toString();
+            result.put(fieldKey, entry.getValue());
         }
-        return list;
+        return result;
+    }
+
+    /**
+     * 获取Map中的子Map列表
+     *
+     * @param map 原Map
+     * @param key KEY
+     * @return Map列表
+     */
+    public static List<Map<String, Object>> getMapSubMaps(Map<String, Object> map, String key) {
+        Object value = MapTools.getValue(map, key);
+        List<Map<String, Object>> results = new ArrayList<>();
+        if (value == null) {
+            return results;
+        }
+        List<Object> values = ConvertTools.parseList(value);
+        for (Object item : values) {
+            if (item == null) {
+                results.add(null);
+            }
+            Map<?, ?> bean = TypeUtils.castToJavaBean(item, Map.class);
+            Map<String, Object> result = new HashMap<>();
+            for (Map.Entry<?, ?> entry : bean.entrySet()) {
+                String fieldKey = entry.getKey() == null ? null : entry.getKey().toString();
+                result.put(fieldKey, entry.getValue());
+            }
+            results.add(result);
+        }
+        return results;
     }
 
     /**
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/DepthMapTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/DepthMapTest.java
index d28a21f..6af3f8e 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/DepthMapTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/DepthMapTest.java
@@ -14,7 +14,6 @@ public class DepthMapTest {
 
         dpm.put("code.folder.service", "service");
         dpm.put("code.folder.page", "views");
-        dpm.put("code.folder", "java"); // 该设置无效, 会被忽略掉
 
         System.out.println("author = " + JsonTools.toLogString(dpm.get("author"))); // Map({code=100, name=zhaohuihua})
         System.out.println("author.code = " + dpm.get("author.code")); // 100
@@ -24,5 +23,10 @@ public class DepthMapTest {
         System.out.println("code.folder.service = " + dpm.get("code.folder.service")); // service
         System.out.println("code.folder.page = " + dpm.get("code.folder.page")); // views
 
+        dpm.put("code.folder", "java"); // 该设置会覆盖掉code.folder.service和code.folder.page
+        System.out.println("code.folder = " + dpm.get("code.folder")); // views
+        System.out.println("code.folder.service = " + dpm.get("code.folder.service")); // service
+        System.out.println("code.folder.page = " + dpm.get("code.folder.page")); // views
+
     }
 }
-- 
Gitee


From a4fb46ad0ee167b72c542a356807dbd9f37a233a Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 25 Dec 2022 23:40:07 +0800
Subject: [PATCH 068/160] splitRuleMaps

---
 .../com/gitee/qdbp/tools/utils/RuleTools.java | 62 +++++++++++++++++--
 1 file changed, 58 insertions(+), 4 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
index a4b8451..093e6cb 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
@@ -1,7 +1,10 @@
 package com.gitee.qdbp.tools.utils;
 
 import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import com.gitee.qdbp.able.beans.KeyString;
 import com.gitee.qdbp.able.beans.StringWithAttrs;
 
@@ -120,6 +123,57 @@ public class RuleTools {
         return false;
     }
 
+    public static Map<String, Object> splitRuleMaps(String rules) {
+        List<KeyString> values = splitRuleKeyValues(rules);
+        Map<String, Object> map = new HashMap<>();
+        for (KeyString item : values) {
+            String key = item.getKey();
+            if (key == null) {
+                continue;
+            }
+            String value = item.getValue();
+            if (value == null) {
+                map.put(key, null);
+                continue;
+            }
+            if (key.endsWith("OfMinDay")) {
+                try {
+                    Date date = DateTools.parse(value);
+                    map.put(key, DateTools.toStartTime(date));
+                } catch (IllegalArgumentException e) {
+                    map.put(key, value);
+                }
+            } else if (key.endsWith("OfMaxDay")) {
+                try {
+                    Date date = DateTools.parse(value);
+                    map.put(key, DateTools.toEndTime(date));
+                } catch (IllegalArgumentException e) {
+                    map.put(key, value);
+                }
+            } else if (key.endsWith("Date") || key.endsWith("Time")) {
+                try {
+                    map.put(key, DateTools.parse(value));
+                } catch (IllegalArgumentException e) {
+                    map.put(key, value);
+                }
+            } else if (StringTools.isNumber(value)) {
+                if (value.startsWith("0") && !value.startsWith("0.")) {
+                    // 0开头的数字, 不能解析为Number对象
+                    map.put(key, value);
+                } else {
+                    try {
+                        map.put(key, ConvertTools.toNumber(value));
+                    } catch (NumberFormatException e) {
+                        map.put(key, value);
+                    }
+                }
+            } else {
+                map.put(key, value);
+            }
+        }
+        return map;
+    }
+
     /**
      * 按指定的分隔符将字符串拆分为键值对, 且按缩进合并行 (缩进的行与其上一行合并)<br>
      * 如下文本, 返回三项, 分别为 [ name, class, params ], 其中 params = Value1\nValue2\nValue3
@@ -189,8 +243,8 @@ public class RuleTools {
 
     /**
      * 拆分一行文本多行属性的格式, 属性必须带缩进<br>
-     * 如下文本, 返回2项, 分别为
-     * { test1, attrs:{ attr11:"value11", attr12:"value12" } }
+     * 如下文本, 返回2项, 分别为<br>
+     * { test1, attrs:{ attr11:"value11", attr12:"value12" } }<br>
      * { test2, attrs:{ attr21:"value21", attr22:"value22", attr23:"value23" } }
      * <pre>
      * |test1
@@ -212,8 +266,8 @@ public class RuleTools {
 
     /**
      * 拆分一行文本多行属性的格式, 属性必须带缩进<br>
-     * 如下文本, 返回2项, 分别为
-     * { test1, attrs:{ attr11:"value11", attr12:"value12" } }
+     * 如下文本, 返回2项, 分别为<br>
+     * { test1, attrs:{ attr11:"value11", attr12:"value12" } }<br>
      * { test2, attrs:{ attr21:"value21", attr22:"value22", attr23:"value23" } }
      * <pre>
      * |test1
-- 
Gitee


From 5f515a0d7985bc2985a3c753e0fbc89fade04321 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 25 Dec 2022 23:42:31 +0800
Subject: [PATCH 069/160] =?UTF-8?q?getLocalIpv4Addresses=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/utils/NetworkTools.java   | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/NetworkTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/NetworkTools.java
index 6d843d3..8919445 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/NetworkTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/NetworkTools.java
@@ -11,7 +11,7 @@ import java.util.List;
 
 /**
  * 网络工具类
- * 
+ *
  * @author zhaohuihua
  * @version 190908
  */
@@ -43,7 +43,7 @@ public class NetworkTools {
 
     /**
      * 获取本机IPv4地址
-     * 
+     *
      * @return 本机IP地址
      * @deprecated use {@link #getLocalIpv4Addresses()}
      */
@@ -68,6 +68,13 @@ public class NetworkTools {
             if (network.isVirtual()) {
                 continue; // 忽略虚拟机的IP地址 (JDK8并没有作用)
             }
+            try {
+                if (network.isLoopback() || !network.isUp()) {
+                    continue;
+                }
+            } catch (SocketException ignore) {
+                continue;
+            }
             String name = network.getDisplayName().toLowerCase();
             if (name.contains("virtual") || name.contains("veth") || name.contains("virbr")
                     || name.contains("docker")) {
@@ -83,6 +90,9 @@ public class NetworkTools {
                 if (address.isLoopbackAddress() || address.getHostAddress().startsWith("127")) {
                     continue; // 忽略127.0.0.1地址, 127.xxx.xxx.xxx属于loopback, 只有本机可见
                 }
+                // if (!address.isReachable(50)) {
+                //     continue;
+                // }
                 if (address.isSiteLocalAddress()) { // 优先返回该地址
                     // 192.168.xxx.xxx属于private私有地址(SiteLocalAddress), 只能在本地局域网可见
                     // 公网地址一般都不会出现在网卡配置中, 都是通过映射访问的
-- 
Gitee


From 9eed37c9394a2916bb98845d309feaa1d7173ad7 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 25 Dec 2022 23:42:57 +0800
Subject: [PATCH 070/160] scanClassResources

---
 .../com/gitee/qdbp/tools/files/PathTools.java   | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
index ed6b34d..5ddbc65 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
@@ -366,6 +366,23 @@ public abstract class PathTools {
         }
     }
 
+    /**
+     * 扫描class资源
+     *
+     * @param packages 包名前缀 (包名不需要输全, com.gitee也可以)
+     * @param className 类名 (如StringTools)
+     * @return 路径列表
+     */
+    public static List<String> scanClassResources(String packages, String className) {
+        String folder = StringTools.replace(packages, ".", "/");
+        List<URL> urls = PathTools.scanResources(folder, className);
+        List<String> result = new ArrayList<>();
+        for (URL url : urls) {
+            result.add(getClasspathRelativePath(url.toString()));
+        }
+        return result;
+    }
+
     /**
      * 按名称扫描资源, 不支持通配符
      * 
-- 
Gitee


From 0f258e3830bd186b2d3c6e791e9c5e00dc90e7f6 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 25 Dec 2022 23:43:37 +0800
Subject: [PATCH 071/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java | 1 -
 1 file changed, 1 deletion(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java b/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
index f2afc32..e6b9f93 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
@@ -7,7 +7,6 @@ import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import com.alibaba.fastjson.JSON;
-- 
Gitee


From 7ed983df7b4f517f1530e7178144422cdfe9a5d2 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 25 Dec 2022 23:43:58 +0800
Subject: [PATCH 072/160] 5.4.24

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 7f2e40f..0c2556b 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.23</version>
+	<version>5.4.24</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index 6a872ef..817320a 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.23</version>
+	<version>5.4.24</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 69b7716fe85e9aedbdab98a044a123aab391ce1f Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 4 Jan 2023 22:09:38 +0800
Subject: [PATCH 073/160] findDepthValues

---
 .../gitee/qdbp/tools/utils/ReflectTools.java  | 238 ++++++++++++--
 .../qdbp/tools/utils/ReflectToolsTest.java    | 302 ++++++++++++++++++
 2 files changed, 516 insertions(+), 24 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
index 4da9775..5f99c15 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
@@ -15,6 +15,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import com.gitee.qdbp.able.beans.KeyValue;
 import com.gitee.qdbp.able.model.reusable.ExpressionMap;
 import com.gitee.qdbp.tools.parse.StringParser;
 
@@ -33,20 +34,20 @@ public abstract class ReflectTools {
     /**
      * 从对象中获取字段值, 支持多级字段名<br>
      * 如 target = { domain:{ text:"baidu", url:"http://baidu.com" } }<br>
-     * -- findFieldValue(target, "domain.text"); 返回 "baidu"<br>
+     * -- getDepthValue(target, "domain.text"); 返回 "baidu"<br>
      * 如 target = { domain:{ text:"baidu", url:"http://baidu.com" } }<br>
-     * -- findFieldValue(target, "domain.name || domain.text"); 返回 "baidu"<br>
+     * -- getDepthValue(target, "domain.name || domain.text"); 返回 "baidu"<br>
      * 如 list = [{ domain:{ text:"baidu", url:"http://baidu.com" } },<br>
      * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"bing", url:"http://cn.bing.com" } }]<br>
-     * -- findFieldValue(list, "[1].domain.text"); 返回 "bing"<br>
+     * -- getDepthValue(list, "[1].domain.text"); 返回 "bing"<br>
      * 如 data = { data: [<br>
      * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"baidu", url:"http://baidu.com" } },<br>
      * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"bing", url:"http://cn.bing.com" } }<br>
      * ] }<br>
-     * -- findFieldValue(list, "data[1].domain.text"); 返回 "bing"<br>
+     * -- getDepthValue(list, "data[1].domain.text"); 返回 "bing"<br>
      *
      * @param target 目标对象, 支持数组/List/Map/Bean
-     * @param fieldNames 字段名, 支持带点的多级字段名和数组下标
+     * @param fieldNames 字段名, 支持带点的多级字段名/数组下标/负数下标
      * @return 字段值
      */
     @SuppressWarnings("unchecked")
@@ -67,27 +68,27 @@ public abstract class ReflectTools {
 
     private static Object doGetDepthValue(Object target, String fields) {
         // 将表达式以.或[]拆分为数组
-        String[] list = StringParser.splitFields(fields);
+        List<String> fieldNames = StringParser.splitsFields(fields);
+        // 清除空值和this
+        clearFieldNames(fieldNames);
         // 逐层取值
         Object value = target;
-        for (int j = 0, z = list == null ? 0 : list.length; value != null && j < z; j++) {
-            String field = list[j];
-            if (VerifyTools.isNotBlank(field) && !field.equals("this")) {
-                if (value instanceof List) {
-                    value = getListFieldValue((List<?>) value, field);
-                } else if (value instanceof Map) {
-                    value = getMapFieldValue((Map<?, ?>) value, field);
-                } else if (value.getClass().isArray()) {
-                    value = getArrayFieldValue(value, field);
-                } else if (value instanceof Iterable) {
-                    value = getIterableFieldValue((Iterable<?>) value, field);
-                } else if (value instanceof Iterator) {
-                    value = getIteratorFieldValue((Iterator<?>) value, field);
-                } else if (value instanceof Enumeration) {
-                    value = getEnumerationFieldValue((Enumeration<?>) value, field);
-                } else {
-                    value = getFieldValue(value, field, false);
-                }
+        for (int i = 0, z = fieldNames.size(); value != null && i < z; i++) {
+            String field = fieldNames.get(i);
+            if (value instanceof List) {
+                value = getListFieldValue((List<?>) value, field);
+            } else if (value instanceof Map) {
+                value = getMapFieldValue((Map<?, ?>) value, field);
+            } else if (value.getClass().isArray()) {
+                value = getArrayFieldValue(value, field);
+            } else if (value instanceof Iterable) {
+                value = getIterableFieldValue((Iterable<?>) value, field);
+            } else if (value instanceof Iterator) {
+                value = getIteratorFieldValue((Iterator<?>) value, field);
+            } else if (value instanceof Enumeration) {
+                value = getEnumerationFieldValue((Enumeration<?>) value, field);
+            } else {
+                value = getFieldValue(value, field, false);
             }
         }
         return value;
@@ -223,6 +224,195 @@ public abstract class ReflectTools {
         }
     }
 
+    /**
+     * 从对象中获取字段值, 支持多级字段名<br>
+     * 如 target = { domain:{ text:"baidu", url:"http://baidu.com" } }<br>
+     * -- findDepthValues(target, "domain.text"); 返回 [{ key:"domain.text", value:"baidu" }]<br>
+     * 如 target = { domain:{ text:"baidu", url:"http://baidu.com" } }<br>
+     * -- findDepthValues(target, "domain.name || domain.text"); 返回 [{ key:"domain.text", value:"baidu" }]<br>
+     * 如 list = [{ domain:{ text:"baidu", url:"http://baidu.com" } },<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"bing", url:"http://cn.bing.com" } }]<br>
+     * -- findDepthValues(list, "domain.text");<br>
+     * -- 返回 [{ key:"[0].domain.text", value:"baidu" },{ key:"[1].domain.text", value:"bing" }]<br>
+     * 如 data = { data: [<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"baidu", url:"http://baidu.com" } },<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"bing", url:"http://cn.bing.com" } }<br>
+     * ] }<br>
+     * -- findDepthValues(list, "data.domain.text");<br>
+     * -- 返回 [{ key:"data[0].domain.text", value:"baidu" },{ key:"data[1].domain.text", value:"bing" }]<br>
+     * 如 data = { data: [<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{} },<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:null }<br>
+     * ] }<br>
+     * -- findDepthValues(list, "data.domain.text");<br>
+     * -- 返回 [{ key:"data[0].domain.text", value:null }] // 上级为空的, 不返回 (第2个domain为空)<br>
+     * 如 data = { data: [] }<br>
+     * -- findDepthValues(list, "data.domain.text"); 返回 []<br>
+     *
+     * @param target 目标对象, 支持数组/List/Map/Bean
+     * @param fieldNames 字段名, 支持带点的多级字段名/数组下标/负数下标
+     * @return 字段值
+     */
+    public static List<KeyValue<Object>> findDepthValues(Object target, String fieldNames) {
+        VerifyTools.requireNotBlank(fieldNames, "field");
+        if (target == null) {
+            return new ArrayList<>();
+        }
+        String[] orFields = StringTools.split(fieldNames, "||");
+        for (int i = 0, z = orFields.length - 1; i <= z; i++) {
+            String fields = orFields[i];
+            List<KeyValue<Object>> result = doFindDepthValues(target, fields);
+            if (i == z) {
+                // 最后一个OR字段, 直接返回
+                return result;
+            } else {
+                // 不是最后一个OR字段, 判断是否存在非空的值
+                boolean existValue = false;
+                for (KeyValue<Object> item : result) {
+                    if (VerifyTools.isNotBlank(item.getValue())) {
+                        existValue = true;
+                    }
+                }
+                if (existValue) {
+                    return result;
+                }
+            }
+        }
+        return new ArrayList<>();
+    }
+
+    private static List<KeyValue<Object>> doFindDepthValues(Object target, String fields) {
+        // 将表达式以.或[]拆分为数组
+        List<String> fieldNames = StringParser.splitsFields(fields);
+        // 清除空值和this
+        clearFieldNames(fieldNames);
+        // 逐层取值
+        List<KeyValue<Object>> items = parseListValues(null, target, needExpandList(fieldNames, 0));
+        for (int i = 0, z = fieldNames.size() - 1; i <= z; i++) {
+            String field = fieldNames.get(i);
+            // 是否需要展开 (有下一层且下一层是数字时,不需要展开)
+            boolean expand = needExpandList(fieldNames, i + 1);
+            List<KeyValue<Object>> temp = new ArrayList<>();
+            for (KeyValue<Object> item : items) {
+                // 上级字段名
+                String parentFields = item.getKey();
+                // 本级字段值
+                Object currValue = item.getValue();
+                // 下级字段值
+                Object nextValue;
+                // 下级字段名
+                boolean currList;
+                if (currValue == null) {
+                    nextValue = null;
+                    currList = false;
+                } else if (currValue instanceof List) {
+                    nextValue = getListFieldValue((List<?>) currValue, field);
+                    currList = true;
+                } else if (currValue.getClass().isArray()) {
+                    nextValue = getArrayFieldValue(currValue, field);
+                    currList = true;
+                } else if (currValue instanceof Iterable) {
+                    nextValue = getIterableFieldValue((Iterable<?>) currValue, field);
+                    currList = true;
+                } else if (currValue instanceof Iterator) {
+                    nextValue = getIteratorFieldValue((Iterator<?>) currValue, field);
+                    currList = true;
+                } else if (currValue instanceof Enumeration) {
+                    nextValue = getEnumerationFieldValue((Enumeration<?>) currValue, field);
+                    currList = true;
+                } else if (currValue instanceof Map) {
+                    nextValue = getMapFieldValue((Map<?, ?>) currValue, field);
+                    currList = false;
+                } else {
+                    nextValue = getFieldValue(currValue, field, false);
+                    currList = false;
+                }
+                String nextField;
+                if (currList) {
+                    nextField = StringTools.concat(parentFields, "[" + field + "]");
+                } else {
+                    nextField = StringTools.concat('.', parentFields, field);
+                }
+                // 最末级即使值为空也要加入结果集, 不是最末级则只要值为空就不再继续
+                boolean isLastField = i >= z;
+                if (isLastField || nextValue != null) {
+                    temp.addAll(parseListValues(nextField, nextValue, expand));
+                }
+            }
+            items = temp;
+        }
+        return items;
+    }
+
+    @SuppressWarnings("all")
+    private static void clearFieldNames(List<String> fieldNames) {
+        // 清除空值和this
+        Iterator<String> iterator = fieldNames.iterator();
+        while (iterator.hasNext()) {
+            String next = iterator.next();
+            if (VerifyTools.isBlank(next) || next.equals("this")) {
+                iterator.remove();
+            }
+        }
+    }
+
+    // 是否需要展开 (有下一层且下一层是数字时,不需要展开)
+    private static boolean needExpandList(List<String> fields, int index) {
+        if (index >= fields.size()) {
+            return true;
+        } else {
+            String value = fields.get(index);
+            return !StringTools.isNumber(value) || value.indexOf('.') >= 0;
+        }
+    }
+
+    private static List<KeyValue<Object>> parseListValues(String field, Object value, boolean expand) {
+        List<KeyValue<Object>> results = new ArrayList<>();
+        if (!expand) {
+            results.add(new KeyValue<>(field, value));
+        } else {
+            if (value == null) {
+                results.add(new KeyValue<>(field, null));
+            } else if (value instanceof List) {
+                List<?> values = (List<?>) value;
+                for (int i = 0; i < values.size(); i++) {
+                    String key = StringTools.concat(field, "[" + i + "]");
+                    results.add(new KeyValue<>(key, values.get(i)));
+                }
+            } else if (value.getClass().isArray()) {
+                Object[] values = (Object[]) value;
+                for (int i = 0; i < values.length; i++) {
+                    String key = StringTools.concat(field, "[" + i + "]");
+                    results.add(new KeyValue<>(key, values[i]));
+                }
+            } else if (value instanceof Iterable) {
+                Iterable<?> iterable = (Iterable<?>) value;
+                int i = 0;
+                for (Object item : iterable) {
+                    String key = StringTools.concat(field, "[" + (i++) + "]");
+                    results.add(new KeyValue<>(key, item));
+                }
+            } else if (value instanceof Iterator) {
+                Iterator<?> iterator = (Iterator<?>) value;
+                int i = 0;
+                while (iterator.hasNext()) {
+                    String key = StringTools.concat(field, "[" + (i++) + "]");
+                    results.add(new KeyValue<>(key, iterator.next()));
+                }
+            } else if (value instanceof Enumeration) {
+                Enumeration<?> enumeration = (Enumeration<?>) value;
+                int i = 0;
+                while (enumeration.hasMoreElements()) {
+                    String key = StringTools.concat(field, "[" + (i++) + "]");
+                    results.add(new KeyValue<>(key, enumeration.nextElement()));
+                }
+            } else {
+                results.add(new KeyValue<>(field, value));
+            }
+        }
+        return results;
+    }
+
     /**
      * 深度判断对象中是否存在指定的字段, 支持多级字段名
      *
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
index 8a36477..d98f7e3 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
@@ -1,7 +1,9 @@
 package com.gitee.qdbp.tools.utils;
 
+import java.util.List;
 import java.util.Map;
 import com.alibaba.fastjson.JSON;
+import com.gitee.qdbp.able.beans.KeyValue;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -51,6 +53,18 @@ public class ReflectToolsTest {
             System.out.println(key + " = " + text);
             Assert.assertNull(text, key);
         }
+        {
+            String key = "[-1].domain.text";
+            String text = ReflectTools.getDepthValue(object, key);
+            System.out.println(key + " = " + text);
+            Assert.assertEquals(text, "bing", key);
+        }
+        {
+            String key = "[-2].domain.text";
+            String text = ReflectTools.getDepthValue(object, key);
+            System.out.println(key + " = " + text);
+            Assert.assertEquals(text, "baidu", key);
+        }
     }
 
     @Test
@@ -242,4 +256,292 @@ public class ReflectToolsTest {
             Assert.assertTrue(contains, key);
         }
     }
+
+    @Test
+    public void testFindDepthValue11() {
+        String json = "{domain:{text:'baidu',url:'https://baidu.com'}}";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        {
+            String key = "domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "baidu", key);
+        }
+        {
+            String key = "domain.name";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "domain.name", key);
+            Assert.assertNull(values.get(0).getValue(), key);
+        }
+        {
+            String key = "domain.name || domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "baidu", key);
+        }
+    }
+
+    @Test
+    public void testFindDepthValue21() {
+        String json =
+                "[{domain:{text:'baidu',url:'https://baidu.com'}},{domain:{text:'bing',url:'https://cn.bing.com'}}]";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        {
+            String key = "[1].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "[1].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "bing", key);
+        }
+        {
+            String key = "[2].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 0, key + ".size");
+        }
+        {
+            String key = "[-1].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "[-1].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "bing", key);
+        }
+        {
+            String key = "[-2].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "[-2].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "baidu", key);
+        }
+    }
+
+    @Test
+    public void testFindDepthValue22() {
+        String json =
+                "[{domain:{text:'baidu',url:'https://baidu.com'}},{domain:{text:'bing',url:'https://cn.bing.com'}}]";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        String key = "domain.text";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        System.out.println(key + " = " + values);
+        Assert.assertEquals(values.size(), 2, key + ".size");
+        Assert.assertEquals(values.get(0).getKey(), "[0].domain.text", key);
+        Assert.assertEquals(values.get(0).getValue(), "baidu", key);
+        Assert.assertEquals(values.get(1).getKey(), "[1].domain.text", key);
+        Assert.assertEquals(values.get(1).getValue(), "bing", key);
+    }
+
+    @Test
+    public void testFindDepthValue31() {
+        String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
+                + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        {
+            String key = "data[1].domain";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            Object value = values.get(0).getValue();
+            System.out.println(key + " = " + JsonTools.toLogString(value));
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain", key);
+            Assert.assertNotNull(value);
+            Assert.assertTrue(Map.class.isAssignableFrom(value.getClass()));
+            Map<?, ?> domain = (Map<?, ?>) value;
+            Assert.assertEquals(domain.get("text"), "bing", key + ".get('text')");
+            Assert.assertEquals(domain.get("url"), "https://cn.bing.com", key + ".get('url')");
+        }
+        {
+            String key = "data[2].domain";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + JsonTools.toLogString(values));
+            Assert.assertEquals(values.size(), 0, key);
+        }
+    }
+
+    @Test
+    public void testFindDepthValue32() {
+        String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
+                + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        String key = "data.domain";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        Assert.assertEquals(values.size(), 2, key + ".size");
+
+        String key1 = values.get(0).getKey();
+        Object value1 = values.get(0).getValue();
+        System.out.println(key1 + " = " + JsonTools.toLogString(value1));
+        Assert.assertEquals(key1, "data[0].domain", key);
+        Assert.assertNotNull(value1);
+        Assert.assertTrue(Map.class.isAssignableFrom(value1.getClass()));
+        Map<?, ?> domain1 = (Map<?, ?>) value1;
+        Assert.assertEquals(domain1.get("text"), "baidu", key + ".get('text')");
+        Assert.assertEquals(domain1.get("url"), "https://baidu.com", key + ".get('url')");
+
+        String key2 = values.get(1).getKey();
+        Object value2 = values.get(1).getValue();
+        System.out.println(key2 + " = " + JsonTools.toLogString(value2));
+        Assert.assertEquals(key2, "data[1].domain", key);
+        Assert.assertNotNull(value2);
+        Assert.assertTrue(Map.class.isAssignableFrom(value2.getClass()));
+        Map<?, ?> domain2 = (Map<?, ?>) value2;
+        Assert.assertEquals(domain2.get("text"), "bing", key + ".get('text')");
+        Assert.assertEquals(domain2.get("url"), "https://cn.bing.com", key + ".get('url')");
+    }
+
+    @Test
+    public void testFindDepthValue41() {
+        String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
+                + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        {
+            String key = "data[1].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "bing", key);
+        }
+        {
+            String key = "data[1].domain.name";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain.name", key);
+            Assert.assertNull(values.get(0).getValue(), key);
+        }
+        {
+            String key = "data[1].domain.name || data[1].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "bing", key);
+        }
+        {
+            String key = ".data[1].domain.name";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain.name", key);
+            Assert.assertNull(values.get(0).getValue(), key);
+        }
+    }
+
+    @Test
+    public void testFindDepthValue51() {
+        String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com',address:[{city:'Guangzhou'},{city:'Nanjing'}]}},"
+                + "{domain:{text:'bing',url:'https://cn.bing.com',address:[{city:'Beijing'},{city:'Shanghai'},{city:'Shenzhen'}]}}]}";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        String key = "data.domain.address.city";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        System.out.println(key + " = " + values);
+        Assert.assertEquals(values.size(), 5, key + ".size");
+        Assert.assertEquals(values.get(0).getKey(), "data[0].domain.address[0].city", key);
+        Assert.assertEquals(values.get(0).getValue(), "Guangzhou", key);
+        Assert.assertEquals(values.get(1).getKey(), "data[0].domain.address[1].city", key);
+        Assert.assertEquals(values.get(1).getValue(), "Nanjing", key);
+        Assert.assertEquals(values.get(2).getKey(), "data[1].domain.address[0].city", key);
+        Assert.assertEquals(values.get(2).getValue(), "Beijing", key);
+        Assert.assertEquals(values.get(3).getKey(), "data[1].domain.address[1].city", key);
+        Assert.assertEquals(values.get(3).getValue(), "Shanghai", key);
+        Assert.assertEquals(values.get(4).getKey(), "data[1].domain.address[2].city", key);
+        Assert.assertEquals(values.get(4).getValue(), "Shenzhen", key);
+    }
+
+    @Test
+    public void testFindDepthValue61() {
+        String json = "{ data: [ { domain:{} }, { domain:null } ] }";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        String key = "data.domain.text";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        System.out.println(key + " = " + values);
+        Assert.assertEquals(values.size(), 1, key + ".size");
+        Assert.assertEquals(values.get(0).getKey(), "data[0].domain.text", key);
+        Assert.assertNull(values.get(0).getValue(), key);
+    }
+
+    @Test
+    public void testFindDepthValue62() {
+        String json = "{ data: [] }";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        String key = "data.domain.text";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        System.out.println(key + " = " + values);
+        Assert.assertEquals(values.size(), 0, key + ".size");
+    }
+
+    @Test
+    public void testFindLastListIndex() {
+        String json = "{data:[1,2,3,4,5,6]}";
+        System.out.println(json);
+        Object object = JSON.parse(json);
+        {
+            String key = "data[0]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 1, key);
+        }
+        {
+            String key = "data[5]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 6, key);
+        }
+        {
+            String key = "data[-1]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 6, key);
+        }
+        {
+            String key = "data[-2]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 5, key);
+        }
+        {
+            String key = "data[-6]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 1, key);
+        }
+        {
+            String key = "data[+7]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 0, key + ".size");
+        }
+        {
+            String key = "data[-7]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 0, key + ".size");
+        }
+    }
+
 }
-- 
Gitee


From 8cb40577c106024014c865e0836171683b2988ca Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 4 Jan 2023 22:10:05 +0800
Subject: [PATCH 074/160] MapPlus.of

---
 .../com/gitee/qdbp/tools/beans/MapPlus.java   | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java b/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
index 66f62f0..9106d11 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
@@ -190,4 +190,23 @@ public class MapPlus extends HashMap<String, Object> {
     protected boolean isComplexKey(String key) {
         return key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0;
     }
+
+    /**
+     * 将任意Map转换为MapPlus
+     *
+     * @param map 源MapPlus
+     * @return MapPlus
+     */
+    public static MapPlus of(Map<?, ?> map) {
+        MapPlus result = new MapPlus();
+        for (Entry<?, ?> entry : map.entrySet()) {
+            Object key = entry.getKey();
+            if (key == null) {
+                result.put(null, entry.getValue());
+            } else {
+                result.put(key.toString(), entry.getValue());
+            }
+        }
+        return result;
+    }
 }
-- 
Gitee


From 39d85cebebadbb126853424f4e22f2e441697697 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 4 Jan 2023 22:10:23 +0800
Subject: [PATCH 075/160] qdbp-5.4.25

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 0c2556b..8c71381 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.24</version>
+	<version>5.4.25</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index 817320a..80a0fa9 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.24</version>
+	<version>5.4.25</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 352799dcc5d6f3033216de7d3321bf418f986a68 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 7 Jan 2023 10:49:28 +0800
Subject: [PATCH 076/160] =?UTF-8?q?Zip=E8=A7=A3=E5=8E=8B=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?Charset=E5=8F=82=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/tools/files/ZipTools.java   | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/files/ZipTools.java b/able/src/main/java/com/gitee/qdbp/tools/files/ZipTools.java
index d3a217e..6205e3f 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/files/ZipTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/files/ZipTools.java
@@ -37,13 +37,24 @@ public class ZipTools {
      * @param saveFolder 保存文件的路径
      */
     public static void decompression(String srcPath, String saveFolder) throws IOException {
+        decompression(srcPath, saveFolder, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * ZIP文件解压
+     *
+     * @param srcPath 源文件
+     * @param saveFolder 保存文件的路径
+     * @param charset 编码格式
+     */
+    public static void decompression(String srcPath, String saveFolder, Charset charset) throws IOException {
         File srcFile = new File(srcPath);
         if (!srcFile.exists()) { // 判断源文件是否存在
             throw new ResourceNotFoundException(srcPath);
         }
         String fileName = PathTools.removeExtension(new File(srcPath).getName()) + '/';
         // 开始解压
-        try (ZipFile zipFile = new ZipFile(srcFile)) {
+        try (ZipFile zipFile = new ZipFile(srcFile, charset)) {
             Enumeration<?> entries = zipFile.entries();
             while (entries.hasMoreElements()) {
                 ZipEntry entry = (ZipEntry) entries.nextElement();
-- 
Gitee


From 8f5db1c5c40f6925a7a9dcaf30dc0f2e9d4e9062 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 7 Jan 2023 10:49:49 +0800
Subject: [PATCH 077/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/able/model/reusable/StackWrapper.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/StackWrapper.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/StackWrapper.java
index b2339bb..5d1e12d 100644
--- a/able/src/main/java/com/gitee/qdbp/able/model/reusable/StackWrapper.java
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/StackWrapper.java
@@ -21,7 +21,7 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  */
 public class StackWrapper {
 
-    private final Map<String, Object> map;
+    protected final Map<String, Object> map;
 
     public StackWrapper(Map<String, Object> map) {
         this.map = map;
-- 
Gitee


From aa898daa752af9054ec9903d15851ee9dc06862f Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 7 Jan 2023 10:51:50 +0800
Subject: [PATCH 078/160] 5.4.26

---
 able/pom.xml  | 2 +-
 tools/pom.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 8c71381..7ee0eaa 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.25</version>
+	<version>5.4.26</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index 80a0fa9..cf47963 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.25</version>
+	<version>5.4.26</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From d1495e9f30f4c3eb50fdb48a7104bc5d896deb6a Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 24 Jan 2023 16:47:56 +0800
Subject: [PATCH 079/160] =?UTF-8?q?separator=E5=8D=95=E8=AF=8D=E5=86=99?=
 =?UTF-8?q?=E9=94=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/ConvertTools.java  | 52 +++++++++----------
 1 file changed, 26 insertions(+), 26 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index fd6a073..3c9fe54 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -1019,23 +1019,23 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param list 数组
-     * @param seprator 分隔符, 可为空
+     * @param separator 分隔符, 可为空
      * @return 合并后的字符串
      */
-    public static String joinToString(Collection<?> list, String seprator) {
-        return joinToString(list, seprator, false);
+    public static String joinToString(Collection<?> list, String separator) {
+        return joinToString(list, separator, false);
     }
 
     /**
      * 将数组合并为字符串
      *
      * @param list 数组
-     * @param seprator 分隔符
+     * @param separator 分隔符
      * @return 合并后的字符串
      * @since 4.1.0
      */
-    public static String joinToString(Collection<?> list, char seprator) {
-        return joinToString(list, seprator, false);
+    public static String joinToString(Collection<?> list, char separator) {
+        return joinToString(list, separator, false);
     }
 
     /**
@@ -1052,23 +1052,23 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param array 数组
-     * @param seprator 分隔符, 可为空
+     * @param separator 分隔符, 可为空
      * @return 合并后的字符串
      */
-    public static String joinToString(Object[] array, String seprator) {
-        return joinToString(array, seprator, false);
+    public static String joinToString(Object[] array, String separator) {
+        return joinToString(array, separator, false);
     }
 
     /**
      * 将数组合并为字符串
      *
      * @param array 数组
-     * @param seprator 分隔符
+     * @param separator 分隔符
      * @return 合并后的字符串
      * @since 4.1.0
      */
-    public static String joinToString(Object[] array, char seprator) {
-        return joinToString(array, seprator, false);
+    public static String joinToString(Object[] array, char separator) {
+        return joinToString(array, separator, false);
     }
 
     /**
@@ -1086,19 +1086,19 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param list 数组
-     * @param seprator 分隔符, 可为空
+     * @param separator 分隔符, 可为空
      * @param wrap 是否用括号包起来
      * @return 合并后的字符串
      */
-    public static String joinToString(Collection<?> list, String seprator, boolean wrap) {
+    public static String joinToString(Collection<?> list, String separator, boolean wrap) {
         if (list == null) {
             return null;
         }
         StringBuilder buffer = new StringBuilder();
         if (VerifyTools.isNotBlank(list)) {
             for (Object tmp : list) {
-                if (seprator != null && buffer.length() > 0) {
-                    buffer.append(seprator);
+                if (separator != null && buffer.length() > 0) {
+                    buffer.append(separator);
                 }
                 buffer.append(tmp);
             }
@@ -1114,11 +1114,11 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param list 数组
-     * @param seprator 分隔符
+     * @param separator 分隔符
      * @param wrap 是否用括号包起来
      * @return 合并后的字符串
      */
-    public static String joinToString(Collection<?> list, char seprator, boolean wrap) {
+    public static String joinToString(Collection<?> list, char separator, boolean wrap) {
         if (list == null) {
             return null;
         }
@@ -1126,7 +1126,7 @@ public class ConvertTools {
         if (VerifyTools.isNotBlank(list)) {
             for (Object tmp : list) {
                 if (buffer.length() > 0) {
-                    buffer.append(seprator);
+                    buffer.append(separator);
                 }
                 buffer.append(tmp);
             }
@@ -1153,19 +1153,19 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param array 数组
-     * @param seprator 分隔符, 可为空
+     * @param separator 分隔符, 可为空
      * @param wrap 是否用括号包起来
      * @return 合并后的字符串
      */
-    public static String joinToString(Object[] array, String seprator, boolean wrap) {
+    public static String joinToString(Object[] array, String separator, boolean wrap) {
         if (array == null) {
             return null;
         }
         StringBuilder buffer = new StringBuilder();
         if (VerifyTools.isNotBlank(array)) {
             for (Object tmp : array) {
-                if (seprator != null && buffer.length() > 0) {
-                    buffer.append(seprator);
+                if (separator != null && buffer.length() > 0) {
+                    buffer.append(separator);
                 }
                 buffer.append(tmp);
             }
@@ -1181,12 +1181,12 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param array 数组
-     * @param seprator 分隔符
+     * @param separator 分隔符
      * @param wrap 是否用括号包起来
      * @return 合并后的字符串
      * @since 4.1.0
      */
-    public static String joinToString(Object[] array, char seprator, boolean wrap) {
+    public static String joinToString(Object[] array, char separator, boolean wrap) {
         if (array == null) {
             return null;
         }
@@ -1194,7 +1194,7 @@ public class ConvertTools {
         if (VerifyTools.isNotBlank(array)) {
             for (Object tmp : array) {
                 if (buffer.length() > 0) {
-                    buffer.append(seprator);
+                    buffer.append(separator);
                 }
                 buffer.append(tmp);
             }
-- 
Gitee


From ca477e363e954bef3bc75f3d2dc65e79cfa53a79 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 30 Jan 2023 21:10:14 +0800
Subject: [PATCH 080/160] JsonService

---
 .../com/gitee/qdbp/able/beans/Callable.java   |   7 -
 .../com/gitee/qdbp/able/beans/KeyStrings.java |  22 +
 .../qdbp/able/beans/StringWithAttrs.java      |   9 +-
 .../com/gitee/qdbp/able/beans/TreeNodes.java  |  17 +-
 .../qdbp/able/exception/ServiceException.java |  19 +
 .../qdbp/able/function/BaseConsumer.java      |  21 +
 .../qdbp/able/function/BaseFunction.java      |  15 +
 .../qdbp/able/function/BaseSupplier.java      |  21 +
 .../able/matches/RegexpGroupExtractor.java    |   4 +-
 .../java/com/gitee/qdbp/json/JsonFeature.java | 391 +++++++++
 .../java/com/gitee/qdbp/json/JsonService.java | 113 +++
 .../com/gitee/qdbp/tools/files/PathTools.java |  72 +-
 .../gitee/qdbp/tools/utils/IndentTools.java   |  67 +-
 .../gitee/qdbp/tools/utils/JsonFactory.java   | 143 ++++
 .../com/gitee/qdbp/tools/utils/JsonMaps.java  | 114 +++
 .../com/gitee/qdbp/tools/utils/JsonTools.java | 184 +++++
 .../com/gitee/qdbp/tools/utils/MapTools.java  | 159 +++-
 .../com/gitee/qdbp/tools/utils/RuleTools.java |  75 +-
 .../gitee/qdbp/tools/utils/StringTools.java   |  22 +
 .../settings/global/json-service.yml          |  10 +
 json/pom.xml                                  |  58 ++
 .../com/gitee/qdbp/json/JsonInitializer.java  |  52 ++
 .../gitee/qdbp/json/JsonServiceForBase.java   | 167 ++++
 .../qdbp/json/JsonServiceForFastjson.java     | 744 +++++++-----------
 .../gitee/qdbp/json/JsonServiceForGson.java   | 178 +++++
 .../qdbp/json/JsonServiceForJackson.java      | 166 ++++
 pom.xml                                       |   2 +
 test/pom.xml                                  |  28 +
 test/qdbp-json-test-base/pom.xml              |  58 ++
 .../com/gitee/qdbp/json/test/TestCase1.java   | 107 +++
 .../gitee/qdbp/json/test/entity/Address.java  |  20 +
 .../gitee/qdbp/json/test/entity/Child.java    |  20 +
 .../gitee/qdbp/json/test/entity/Father.java   |  27 +
 .../gitee/qdbp/json/test/entity/Gender.java   |  21 +
 .../gitee/qdbp/json/test/entity/Person.java   |  78 ++
 test/qdbp-json-test-fastjson/pom.xml          |  51 ++
 .../qdbp/json/test/TestCase1ForFastJson.java  |  43 +
 .../src/test/resources/logback.xml            |  51 ++
 .../src/test/resources/testng.xml             |   9 +
 test/qdbp-json-test-gson/pom.xml              |  51 ++
 .../src/test/java/test/TestCase1ForGson.java  |  40 +
 .../src/test/resources/logback.xml            |  51 ++
 .../src/test/resources/testng.xml             |   9 +
 test/qdbp-json-test-jackson/pom.xml           |  52 ++
 .../qdbp/json/test/TestCase1ForJackson.java   |  44 ++
 .../src/test/resources/logback.xml            |  51 ++
 .../src/test/resources/testng.xml             |   9 +
 .../com/gitee/qdbp/tools/beans/MapPlus.java   |  12 +-
 .../gitee/qdbp/able/beans/TreeNodesTest.java  |   9 +-
 .../gitee/qdbp/tools/utils/PathToolsTest.java |  43 +
 .../gitee/qdbp/tools/utils/XmlToolsTest.java  |  13 +-
 51 files changed, 3166 insertions(+), 583 deletions(-)
 delete mode 100644 able/src/main/java/com/gitee/qdbp/able/beans/Callable.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/beans/KeyStrings.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/function/BaseConsumer.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/json/JsonFeature.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/json/JsonService.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/tools/utils/JsonFactory.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/tools/utils/JsonMaps.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
 create mode 100644 able/src/main/resources/settings/global/json-service.yml
 create mode 100644 json/pom.xml
 create mode 100644 json/src/main/java/com/gitee/qdbp/json/JsonInitializer.java
 create mode 100644 json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
 rename tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java => json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java (33%)
 create mode 100644 json/src/main/java/com/gitee/qdbp/json/JsonServiceForGson.java
 create mode 100644 json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java
 create mode 100644 test/pom.xml
 create mode 100644 test/qdbp-json-test-base/pom.xml
 create mode 100644 test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java
 create mode 100644 test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Address.java
 create mode 100644 test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Child.java
 create mode 100644 test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Father.java
 create mode 100644 test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Gender.java
 create mode 100644 test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Person.java
 create mode 100644 test/qdbp-json-test-fastjson/pom.xml
 create mode 100644 test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java
 create mode 100644 test/qdbp-json-test-fastjson/src/test/resources/logback.xml
 create mode 100644 test/qdbp-json-test-fastjson/src/test/resources/testng.xml
 create mode 100644 test/qdbp-json-test-gson/pom.xml
 create mode 100644 test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java
 create mode 100644 test/qdbp-json-test-gson/src/test/resources/logback.xml
 create mode 100644 test/qdbp-json-test-gson/src/test/resources/testng.xml
 create mode 100644 test/qdbp-json-test-jackson/pom.xml
 create mode 100644 test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java
 create mode 100644 test/qdbp-json-test-jackson/src/test/resources/logback.xml
 create mode 100644 test/qdbp-json-test-jackson/src/test/resources/testng.xml
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/utils/PathToolsTest.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/Callable.java b/able/src/main/java/com/gitee/qdbp/able/beans/Callable.java
deleted file mode 100644
index 46bbfbf..0000000
--- a/able/src/main/java/com/gitee/qdbp/able/beans/Callable.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.gitee.qdbp.able.beans;
-
-/** JDK8中的Function在7中的替换接口 **/
-public interface Callable<T, R> {
-
-    R call(T t);
-}
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/KeyStrings.java b/able/src/main/java/com/gitee/qdbp/able/beans/KeyStrings.java
new file mode 100644
index 0000000..7fe0865
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/KeyStrings.java
@@ -0,0 +1,22 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * KeyString类的数组
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class KeyStrings extends ArrayList<KeyString> {
+    private static final long serialVersionUID = -2726116922920104529L;
+
+    public Map<String, String> asMap() {
+        return KeyString.toMap(this);
+    }
+
+    public Map<String, Object> asJson() {
+        return KeyString.toMap(this);
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java b/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java
index 9dc5e77..ff36d15 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java
@@ -1,7 +1,6 @@
 package com.gitee.qdbp.able.beans;
 
 import java.io.Serializable;
-import java.util.List;
 
 /**
  * 带属性的字符串
@@ -16,12 +15,12 @@ public class StringWithAttrs implements Serializable {
     /** 字符串内容 **/
     private String string;
     /** 属性列表 **/
-    private List<KeyString> attrs;
+    private KeyStrings attrs;
 
     public StringWithAttrs() {
     }
 
-    public StringWithAttrs(String string, List<KeyString> attrs) {
+    public StringWithAttrs(String string, KeyStrings attrs) {
         this.string = string;
         this.attrs = attrs;
     }
@@ -37,12 +36,12 @@ public class StringWithAttrs implements Serializable {
     }
 
     /** 属性列表 **/
-    public List<KeyString> getAttrs() {
+    public KeyStrings getAttrs() {
         return attrs;
     }
 
     /** 属性列表 **/
-    public void setAttrs(List<KeyString> attrs) {
+    public void setAttrs(KeyStrings attrs) {
         this.attrs = attrs;
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
index dd54d50..7252722 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
@@ -8,6 +8,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentHashMap;
+import com.gitee.qdbp.able.function.BaseFunction;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -25,13 +26,13 @@ public class TreeNodes<E> {
     /** 未找到上级的KEY **/
     private final List<String> keysOfWithoutParent;
     /** 获取KEY的方法 **/
-    private final Callable<E, String> keyGetter;
+    private final BaseFunction<E, String> keyGetter;
 
-    public TreeNodes(List<E> list, Callable<E, String> keyGetter, Callable<E, String> parentGetter) {
+    public TreeNodes(List<E> list, BaseFunction<E, String> keyGetter, BaseFunction<E, String> parentGetter) {
         this(list, false, keyGetter, parentGetter);
     }
 
-    public TreeNodes(List<E> list, boolean upgrade, Callable<E, String> keyGetter, Callable<E, String> parentGetter) {
+    public TreeNodes(List<E> list, boolean upgrade, BaseFunction<E, String> keyGetter, BaseFunction<E, String> parentGetter) {
         Node<E> container = new Node<>(null);
         String rootCode = "0";
         List<Node<E>> all = new ArrayList<>();
@@ -42,20 +43,20 @@ public class TreeNodes<E> {
         for (E element : list) {
             Node<E> item = new Node<>(element);
             all.add(item);
-            String key = keyGetter.call(element);
+            String key = keyGetter.apply(element);
             maps.put(key, item);
         }
 
         // 未找到上级的KEY
         List<String> keysOfWithoutParent = new ArrayList<>();
         for (Node<E> item : all) {
-            String parentKey = parentGetter.call(item.element);
+            String parentKey = parentGetter.apply(item.element);
             if (VerifyTools.isBlank(parentKey)) {
                 maps.get(rootCode).addChildNode(item);
             } else if (maps.containsKey(parentKey)) {
                 maps.get(parentKey).addChildNode(item);
             } else {
-                String key = keyGetter.call(item.element);
+                String key = keyGetter.apply(item.element);
                 keysOfWithoutParent.add(key);
                 if (upgrade) {
                     container.addChildNode(item);
@@ -99,7 +100,7 @@ public class TreeNodes<E> {
         Node<E> node = keyNodeMaps.get(key);
         if (node != null) {
             // 考虑到有可能在外部修改key, 因此增加检查
-            String nodeKey = keyGetter.call(node.element);
+            String nodeKey = keyGetter.apply(node.element);
             if (key.equals(nodeKey)) {
                 return node;
             }
@@ -107,7 +108,7 @@ public class TreeNodes<E> {
         Iterator<Node<E>> iterator = container.depthFirstNodeIterator();
         while (iterator.hasNext()) {
             Node<E> next = iterator.next();
-            String nextKey = keyGetter.call(next.element);
+            String nextKey = keyGetter.apply(next.element);
             // 遍历时更新缓存中的对应关系
             keyNodeMaps.put(nextKey, next);
             if (key.equals(nextKey)) {
diff --git a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
index d9bc7b2..3668684 100644
--- a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
+++ b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
@@ -4,6 +4,7 @@ import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.UndeclaredThrowableException;
 import com.gitee.qdbp.able.result.IResultException;
 import com.gitee.qdbp.able.result.IResultMessage;
+import com.gitee.qdbp.tools.utils.StringTools;
 
 /**
  * 业务异常类
@@ -120,6 +121,24 @@ public class ServiceException extends RuntimeException implements IResultExcepti
         return this;
     }
 
+    /**
+     * 设置错误详情
+     *
+     * @param pattern 错误消息
+     * @param args 替换参数
+     * @return 对象自身
+     */
+    public ServiceException setDetails(String pattern, Object... args) {
+        if (pattern == null || pattern.length() == 0 || args == null || args.length == 0) {
+            this.details = pattern;
+            return this;
+        } else {
+            String fmt = StringTools.replace(pattern, "{}", "%s");
+            this.details = String.format(fmt, args);
+            return this;
+        }
+    }
+
     /**
      * 获取简化的错误消息 (不会包含错误详情)
      *
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BaseConsumer.java b/able/src/main/java/com/gitee/qdbp/able/function/BaseConsumer.java
new file mode 100644
index 0000000..f7bd069
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BaseConsumer.java
@@ -0,0 +1,21 @@
+package com.gitee.qdbp.able.function;
+
+/**
+ * JDK8中的Consumer在7中的替换接口<br>
+ * Represents an operation that accepts a single input argument and returns no
+ * result. Unlike most other functional interfaces, {@code Consumer} is expected
+ * to operate via side-effects.
+ *
+ * <p>This is a functional interface, whose functional method is {@link #accept(Object)}.
+ *
+ * @param <T> the type of the input to the operation
+ */
+public interface BaseConsumer<T> {
+
+    /**
+     * Performs this operation on the given argument.
+     *
+     * @param t the input argument
+     */
+    void accept(T t);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java b/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java
new file mode 100644
index 0000000..4b9dac7
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java
@@ -0,0 +1,15 @@
+package com.gitee.qdbp.able.function;
+
+/**
+ * JDK8中的Function在7中的替换接口<br>
+ * Represents a function that accepts one argument and produces a result.
+ *
+ * <p>This is a functional interface, whose functional method is {@link #apply(Object)}.
+ *
+ * @param <T> the type of the input to the function
+ * @param <R> the type of the result of the function
+ */
+public interface BaseFunction<T, R> {
+
+    R apply(T t);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java b/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java
new file mode 100644
index 0000000..0d1c5b7
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java
@@ -0,0 +1,21 @@
+package com.gitee.qdbp.able.function;
+
+/**
+ * Represents a supplier of results.
+ *
+ * <p>There is no requirement that a new or distinct result be returned each
+ * time the supplier is invoked.
+ *
+ * <p>This is a functional interface, whose functional method is {@link #get()}.
+ *
+ * @param <T> the type of results supplied by this supplier
+ */
+public interface BaseSupplier<T> {
+
+    /**
+     * Gets a result.
+     *
+     * @return a result
+     */
+    T get();
+}
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
index 9151205..3c7e97b 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
@@ -8,7 +8,6 @@ import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
-import com.gitee.qdbp.able.beans.KeyString;
 import com.gitee.qdbp.able.beans.SubString;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.model.reusable.ExtraData;
@@ -340,8 +339,7 @@ public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
                 i += RuleTools.collectNextIndentLines(cleared, i + 1, extraLines);
                 Map<String, Object> extras = null;
                 if (!extraLines.isEmpty()) {
-                    List<KeyString> keyStrings = RuleTools.splitRuleKeyValues(extraLines);
-                    extras = KeyString.toMap(keyStrings);
+                    extras = RuleTools.splitRuleKeyValues(extraLines).asJson();
                 }
                 // 解析正则表达式提取规则
                 List<RegexpGroupExtractor> list = doParseExtractors(trimmed, extras);
diff --git a/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java b/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java
new file mode 100644
index 0000000..2a257de
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java
@@ -0,0 +1,391 @@
+package com.gitee.qdbp.json;
+
+import java.io.Serializable;
+import com.gitee.qdbp.able.beans.Copyable;
+import com.gitee.qdbp.able.beans.ModifyStatus;
+
+/**
+ * Json特征配置
+ *
+ * @author zhaohuihua
+ * @version 20211212
+ */
+public class JsonFeature {
+
+    private JsonFeature() {
+    }
+
+    public static Serialization serialization() {
+        return new Serialization();
+    }
+
+    public static Deserialization deserialization() {
+        return new Deserialization();
+    }
+
+    /**
+     * Json序列化的特征配置
+     *
+     * @author zhaohuihua
+     * @version 20211212
+     */
+    public static class Serialization implements Copyable, Serializable {
+        private static final long serialVersionUID = 176597457566019762L;
+        private final ModifyStatus modifyStatus = new ModifyStatus();
+        /** 是否处理循环引用 (设置为true就不会报错, 在fastjson下将会输出$ref, 在jackson下将会输出null) **/
+        private boolean handleCircularReference = true;
+        /** 字段名是否使用引号括起来 **/
+        private boolean quoteFieldNames = true;
+        /** 是否使用单引号 **/
+        private boolean useSingleQuotes = false;
+        /** 是否输出值为null的字段 **/
+        private boolean writeFieldNullValue = true;
+        /** 是否输出Map中的null值 **/
+        private boolean writeMapNullValue = true;
+        /** 输出枚举时是否使用toString **/
+        private boolean writeEnumUsingToString = false;
+        /** 序列化List类型数据时是否将null输出为空数组 **/
+        private boolean writeNullListAsEmpty = false;
+        /** 序列化String类型数据时是否将null输出为空字符串 **/
+        private boolean writeNullStringAsEmpty = false;
+        /** 序列化Number类型数据时是否将null输出为0 **/
+        private boolean writeNullNumberAsZero = false;
+        /** 序列化Boolean类型数据时是否将null输出为false **/
+        private boolean writeNullBooleanAsFalse = false;
+        /** 序列化BigDecimal类型数据时是否使用toPlainString (即不使用科学计数法显示) **/
+        private boolean writeBigDecimalAsPlain = true;
+        /** 序列化Date类型数据时是否输出为long时间戳 **/
+        private boolean writeDateAsTimestamp = false;
+        /** 是否跳过transient修饰的字段 **/
+        private boolean skipTransientField = true;
+        /** 是否输出格式化后的Json字符串 **/
+        private boolean prettyFormat = false;
+        /** 日期格式 (writeDateAsTimestamp=false时生效) **/
+        private String dateFormatPattern = "yyyy-MM-dd HH:mm:ss.SSS";
+        /** 数字格式 (防止double使用科学计数法, 默认情况下100000000.0将输出1.0E8) **/
+        private String doubleFormatPattern = "0.####################";
+
+        /** 是否处理循环引用 (设置为true就不会报错, 在fastjson下将会输出$ref, 在jackson下将会输出null) **/
+        public boolean isHandleCircularReference() {
+            return handleCircularReference;
+        }
+
+        /** 是否处理循环引用 (设置为true就不会报错, 在fastjson下将会输出$ref, 在jackson下将会输出null) **/
+        public Serialization setHandleCircularReference(boolean handleCircularReference) {
+            this.modifyStatus.checkModifiable();
+            this.handleCircularReference = handleCircularReference;
+            return this;
+        }
+
+        /** 字段名是否使用引号括起来 **/
+        public boolean isQuoteFieldNames() {
+            return quoteFieldNames;
+        }
+
+        /** 字段名是否使用引号括起来 **/
+        public Serialization setQuoteFieldNames(boolean quoteFieldNames) {
+            this.modifyStatus.checkModifiable();
+            this.quoteFieldNames = quoteFieldNames;
+            return this;
+        }
+
+        /** 是否使用单引号 **/
+        public boolean isUseSingleQuotes() {
+            return useSingleQuotes;
+        }
+
+        /** 是否使用单引号 **/
+        public Serialization setUseSingleQuotes(boolean useSingleQuotes) {
+            this.modifyStatus.checkModifiable();
+            this.useSingleQuotes = useSingleQuotes;
+            return this;
+        }
+
+        /** 是否输出值为null的字段 **/
+        public boolean isWriteFieldNullValue() {
+            return writeFieldNullValue;
+        }
+
+        /** 是否输出值为null的字段 **/
+        public Serialization setWriteFieldNullValue(boolean writeFieldNullValue) {
+            this.modifyStatus.checkModifiable();
+            this.writeFieldNullValue = writeFieldNullValue;
+            return this;
+        }
+
+        /** 是否输出Map中的null值 **/
+        public boolean isWriteMapNullValue() {
+            return writeMapNullValue;
+        }
+
+        /** 是否输出Map中的null值 **/
+        public Serialization setWriteMapNullValue(boolean writeMapNullValue) {
+            this.modifyStatus.checkModifiable();
+            this.writeMapNullValue = writeMapNullValue;
+            return this;
+        }
+
+        /** 输出枚举时是否使用toString **/
+        public boolean isWriteEnumUsingToString() {
+            return writeEnumUsingToString;
+        }
+
+        /** 输出枚举时是否使用toString **/
+        public Serialization setWriteEnumUsingToString(boolean writeEnumUsingToString) {
+            this.modifyStatus.checkModifiable();
+            this.writeEnumUsingToString = writeEnumUsingToString;
+            return this;
+        }
+
+        /** 序列化List类型数据时是否将null输出为空数组 **/
+        public boolean isWriteNullListAsEmpty() {
+            return writeNullListAsEmpty;
+        }
+
+        /** 序列化List类型数据时是否将null输出为空数组 **/
+        public Serialization setWriteNullListAsEmpty(boolean writeNullListAsEmpty) {
+            this.modifyStatus.checkModifiable();
+            this.writeNullListAsEmpty = writeNullListAsEmpty;
+            return this;
+        }
+
+        /** 序列化String类型数据时是否将null输出为空字符串 **/
+        public boolean isWriteNullStringAsEmpty() {
+            return writeNullStringAsEmpty;
+        }
+
+        /** 序列化String类型数据时是否将null输出为空字符串 **/
+        public Serialization setWriteNullStringAsEmpty(boolean writeNullStringAsEmpty) {
+            this.modifyStatus.checkModifiable();
+            this.writeNullStringAsEmpty = writeNullStringAsEmpty;
+            return this;
+        }
+
+        /** 序列化Number类型数据时是否将null输出为0 **/
+        public boolean isWriteNullNumberAsZero() {
+            return writeNullNumberAsZero;
+        }
+
+        /** 序列化Number类型数据时是否将null输出为0 **/
+        public Serialization setWriteNullNumberAsZero(boolean writeNullNumberAsZero) {
+            this.modifyStatus.checkModifiable();
+            this.writeNullNumberAsZero = writeNullNumberAsZero;
+            return this;
+        }
+
+        /** 序列化Boolean类型数据时是否将null输出为false **/
+        public boolean isWriteNullBooleanAsFalse() {
+            return writeNullBooleanAsFalse;
+        }
+
+        /** 序列化Boolean类型数据时是否将null输出为false **/
+        public Serialization setWriteNullBooleanAsFalse(boolean writeNullBooleanAsFalse) {
+            this.modifyStatus.checkModifiable();
+            this.writeNullBooleanAsFalse = writeNullBooleanAsFalse;
+            return this;
+        }
+
+        /** 序列化BigDecimal类型数据时是否使用toPlainString (即不使用科学计数法显示) **/
+        public boolean isWriteBigDecimalAsPlain() {
+            return writeBigDecimalAsPlain;
+        }
+
+        /** 序列化BigDecimal类型数据时是否使用toPlainString (即不使用科学计数法显示) **/
+        public Serialization setWriteBigDecimalAsPlain(boolean writeBigDecimalAsPlain) {
+            this.modifyStatus.checkModifiable();
+            this.writeBigDecimalAsPlain = writeBigDecimalAsPlain;
+            return this;
+        }
+
+        /** 序列化Date类型数据时是否输出为long时间戳 **/
+        public boolean isWriteDateAsTimestamp() {
+            return writeDateAsTimestamp;
+        }
+
+        /** 序列化Date类型数据时是否输出为long时间戳 **/
+        public Serialization setWriteDateAsTimestamp(boolean writeDateAsTimestamp) {
+            this.modifyStatus.checkModifiable();
+            this.writeDateAsTimestamp = writeDateAsTimestamp;
+            return this;
+        }
+
+        /** 是否跳过transient修饰的字段 **/
+        public boolean isSkipTransientField() {
+            return skipTransientField;
+        }
+
+        /** 是否跳过transient修饰的字段 **/
+        public Serialization setSkipTransientField(boolean skipTransientField) {
+            this.modifyStatus.checkModifiable();
+            this.skipTransientField = skipTransientField;
+            return this;
+        }
+
+        /** 是否输出格式化后的Json字符串 **/
+        public boolean isPrettyFormat() {
+            return prettyFormat;
+        }
+
+        /** 是否输出格式化后的Json字符串 **/
+        public Serialization setPrettyFormat(boolean prettyFormat) {
+            this.modifyStatus.checkModifiable();
+            this.prettyFormat = prettyFormat;
+            return this;
+        }
+
+        /** 日期格式 (writeDateAsTimestamp=false时生效) **/
+        public String getDateFormatPattern() {
+            return dateFormatPattern;
+        }
+
+        /** 日期格式 (writeDateAsTimestamp=false时生效) **/
+        public Serialization setDateFormatPattern(String dateFormatPattern) {
+            this.modifyStatus.checkModifiable();
+            this.dateFormatPattern = dateFormatPattern;
+            return this;
+        }
+
+        /** 数字格式 (防止double使用科学计数法) **/
+        public String getDoubleFormatPattern() {
+            return doubleFormatPattern;
+        }
+
+        /** 数字格式 (防止double使用科学计数法) **/
+        public Serialization setDoubleFormatPattern(String doubleFormatPattern) {
+            this.modifyStatus.checkModifiable();
+            this.doubleFormatPattern = doubleFormatPattern;
+            return this;
+        }
+
+        /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
+        public Serialization lockToUnmodifiable() {
+            this.modifyStatus.setModifiable(false);
+            return this;
+        }
+
+        @Override
+        public Serialization copy() {
+            Serialization feature = new Serialization();
+            // 是否处理循环引用 (设置为true就不会报错, 在fastjson下将会输出$ref, 在jackson下将会输出null)
+            feature.setHandleCircularReference(this.isHandleCircularReference());
+            // 字段名是否使用引号括起来
+            feature.setQuoteFieldNames(this.isQuoteFieldNames());
+            // 是否使用单引号
+            feature.setUseSingleQuotes(this.isUseSingleQuotes());
+            // 是否输出值为null的字段
+            feature.setWriteFieldNullValue(this.isWriteFieldNullValue());
+            // 是否输出Map中的null值
+            feature.setWriteMapNullValue(this.isWriteMapNullValue());
+            // 输出枚举时是否使用toString
+            feature.setWriteEnumUsingToString(this.isWriteEnumUsingToString());
+            // 序列化List类型数据时是否将null输出为空数组
+            feature.setWriteNullListAsEmpty(this.isWriteNullListAsEmpty());
+            // 序列化String类型数据时是否将null输出为空字符串
+            feature.setWriteNullStringAsEmpty(this.isWriteNullStringAsEmpty());
+            // 序列化Number类型数据时是否将null输出为0
+            feature.setWriteNullNumberAsZero(this.isWriteNullNumberAsZero());
+            // 序列化Boolean类型数据时是否将null输出为false
+            feature.setWriteNullBooleanAsFalse(this.isWriteNullBooleanAsFalse());
+            // 序列化BigDecimal类型数据时是否使用toPlainString (即不使用科学计数法显示)
+            feature.setWriteBigDecimalAsPlain(this.isWriteBigDecimalAsPlain());
+            // 序列化Date类型数据时是否输出为long时间戳
+            feature.setWriteDateAsTimestamp(this.isWriteDateAsTimestamp());
+            // 是否跳过transient修饰的字段
+            feature.setSkipTransientField(this.isSkipTransientField());
+            // 是否输出格式化后的Json字符串
+            feature.setPrettyFormat(this.isPrettyFormat());
+            // 日期格式 (writeDateAsTimestamp=false时生效)
+            feature.setDateFormatPattern(this.getDateFormatPattern());
+            // 数字格式 (防止double使用科学计数法)
+            feature.setDoubleFormatPattern(this.getDoubleFormatPattern());
+            return feature;
+        }
+    }
+
+    /**
+     * Json反序列化的特征配置
+     *
+     * @author zhaohuihua
+     * @version 20211212
+     */
+    public static class Deserialization implements Copyable, Serializable {
+        private static final long serialVersionUID = -8068715156133134255L;
+        private final ModifyStatus modifyStatus = new ModifyStatus();
+        /** 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象 **/
+        private boolean useBigDecimalForFloats = true;
+        /** 遇到未知属性时是否抛出异常 **/
+        private boolean failOnUnknownFields = false;
+        /** 遇到未知的枚举值时是否抛出异常 **/
+        private boolean failOnUnknownEnumValues = false;
+        /** 是否将null字段串初始化为空字符串 **/
+        private boolean readNullStringAsEmpty = false;
+
+        /** 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象 **/
+        public boolean isUseBigDecimalForFloats() {
+            return useBigDecimalForFloats;
+        }
+
+        /** 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象 **/
+        public Deserialization setUseBigDecimalForFloats(boolean useBigDecimalForFloats) {
+            this.modifyStatus.checkModifiable();
+            this.useBigDecimalForFloats = useBigDecimalForFloats;
+            return this;
+        }
+
+        /** 遇到未知属性时是否抛出异常 **/
+        public boolean isFailOnUnknownFields() {
+            return failOnUnknownFields;
+        }
+
+        /** 遇到未知属性时是否抛出异常 **/
+        public Deserialization setFailOnUnknownFields(boolean failOnUnknownFields) {
+            this.modifyStatus.checkModifiable();
+            this.failOnUnknownFields = failOnUnknownFields;
+            return this;
+        }
+
+        /** 遇到未知属性时是否抛出异常 **/
+        public boolean isFailOnUnknownEnumValues() {
+            return failOnUnknownEnumValues;
+        }
+
+        /** 遇到未知属性时是否抛出异常 **/
+        public Deserialization setFailOnUnknownEnumValues(boolean failOnUnknownEnumValues) {
+            this.modifyStatus.checkModifiable();
+            this.failOnUnknownEnumValues = failOnUnknownEnumValues;
+            return this;
+        }
+
+        /** 是否将null字段串初始化为空字符串 **/
+        public boolean isReadNullStringAsEmpty() {
+            return readNullStringAsEmpty;
+        }
+
+        /** 是否将null字段串初始化为空字符串 **/
+        public Deserialization setReadNullStringAsEmpty(boolean readNullStringAsEmpty) {
+            this.modifyStatus.checkModifiable();
+            this.readNullStringAsEmpty = readNullStringAsEmpty;
+            return this;
+        }
+
+        /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
+        public Deserialization lockToUnmodifiable() {
+            modifyStatus.setModifiable(false);
+            return this;
+        }
+
+        @Override
+        public Deserialization copy() {
+            Deserialization feature = new Deserialization();
+            // 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象
+            feature.setUseBigDecimalForFloats(this.isUseBigDecimalForFloats());
+            // 遇到未知属性时是否抛出异常
+            feature.setFailOnUnknownFields(this.isFailOnUnknownFields());
+            // 遇到未知的枚举值时是否抛出异常
+            feature.setFailOnUnknownEnumValues(this.isFailOnUnknownEnumValues());
+            // 是否将null字段串初始化为空字符串
+            feature.setReadNullStringAsEmpty(this.isReadNullStringAsEmpty());
+            return feature;
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/json/JsonService.java b/able/src/main/java/com/gitee/qdbp/json/JsonService.java
new file mode 100644
index 0000000..0f99ae0
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/json/JsonService.java
@@ -0,0 +1,113 @@
+package com.gitee.qdbp.json;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Json转换服务
+ *
+ * @author zhaohuihua
+ * @version 20211207
+ */
+public interface JsonService {
+
+    /** 数据类型转换 **/
+    <T> T convert(Object object, Class<T> clazz);
+
+    /** 对象转换为Json字符串 **/
+    String toJsonString(Object object);
+
+    /** 对象转换为Json字符串 **/
+    String toJsonString(Object object, JsonFeature.Serialization feature);
+
+    /** 解析Json字符串对象 **/
+    Map<String, Object> parseAsMap(String jsonString);
+
+    /** 解析Json字符串对象 **/
+    Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature);
+
+    /** 解析Json字符串数组 **/
+    List<Map<String, Object>> parseAsMaps(String jsonString);
+
+    /** 解析Json字符串数组 **/
+    List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature);
+
+    /** 解析Json字符串对象 **/
+    <T> T parseAsObject(String jsonString, Class<T> clazz);
+
+    /** 解析Json字符串对象 **/
+    <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature);
+
+    /** 解析Json字符串数组 **/
+    <T> List<T> parseAsObjects(String jsonString, Class<T> clazz);
+
+    /** 解析Json字符串数组 **/
+    <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature);
+
+    /**
+     * 将Map内容设置到Java对象中<br>
+     * 遍历map中的key, 逐一设置到目标对象中
+     *
+     * @param map Map
+     * @param bean 目标Java对象
+     */
+    void mapFillToBean(Map<String, ?> map, Object bean);
+
+    /**
+     * 将Map转换为Java对象
+     *
+     * @param <T> 目标类型
+     * @param map Map
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    <T> T mapToBean(Map<String, ?> map, Class<T> clazz);
+
+    /**
+     * 将Java对象转换为Map
+     *
+     * @param bean JavaBean对象
+     * @return Map
+     */
+    Map<String, Object> beanToMap(Object bean);
+
+    /**
+     * 将Java对象转换为Map
+     *
+     * @param bean Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map
+     */
+    Map<String, Object> beanToMap(Object bean, boolean deep, boolean clearBlankValue);
+
+    /**
+     * 将Map数组转换为Java对象列表
+     *
+     * @param <T> 目标类型
+     * @param maps Map数组
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    <T> List<T> mapToBeans(Collection<Map<String, ?>> maps, Class<T> clazz);
+
+    /**
+     * 将Java对象转换为Map列表<br>
+     * copy from fastjson JSON.toJSON(), 保留enum和date
+     *
+     * @param beans JavaBean对象列表
+     * @return Map列表
+     */
+    List<Map<String, Object>> beanToMaps(Collection<?> beans);
+
+    /**
+     * 将Java对象转换为Map列表
+     *
+     * @param beans Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map列表
+     */
+    List<Map<String, Object>> beanToMaps(Collection<?> beans, boolean deep, boolean clearBlankValue);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
index 5ddbc65..d38be4f 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
@@ -1340,33 +1340,83 @@ public abstract class PathTools {
     }
 
     /**
-     * 获取classpath相对路径
-     * 
+     * 获取classpath相对路径<br>
+     * jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties<br>
+     * 都返回 settings/sqls/xxx.properties<br>
+     *
      * @param url URL路径
      * @return 相对路径
      */
     public static String getClasspathRelativePath(URL url) {
-        return getClasspathRelativePath(url.toString());
+        return doGetClasspathRelativePath(url.toString(), false);
     }
 
     /**
-     * 获取classpath相对路径
-     * 
+     * 获取classpath相对路径<br>
+     * jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties<br>
+     * 都返回 settings/sqls/xxx.properties<br>
+     *
      * @param urlPath URL路径
      * @return 相对路径
      */
     public static String getClasspathRelativePath(String urlPath) {
-        // jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties
-        // file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties
+        return doGetClasspathRelativePath(urlPath, false);
+    }
+
+    /**
+     * 获取classpath相对路径<br>
+     * jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * 返回 xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties<br>
+     * 返回 classes/settings/sqls/xxx.properties
+     *
+     * @param url URL路径
+     * @return 相对路径
+     */
+    public static String getJarRelativePath(URL url) {
+        return doGetClasspathRelativePath(url.toString(), true);
+    }
+
+    /**
+     * 获取classpath相对路径<br>
+     * jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * 返回 xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties<br>
+     * 返回 classes/settings/sqls/xxx.properties
+     *
+     * @param urlPath URL路径
+     * @return 相对路径
+     */
+    public static String getJarRelativePath(String urlPath) {
+        return doGetClasspathRelativePath(urlPath, true);
+    }
+
+    private static String doGetClasspathRelativePath(String urlPath, boolean includeJarName) {
         String lowerCase = urlPath.toLowerCase();
         String jarFlag = ".jar!/";
         int jarIndex = lowerCase.indexOf(jarFlag);
         if (jarIndex > 0) {
-            return urlPath.substring(jarIndex + jarFlag.length());
+            if (!includeJarName) {
+                return urlPath.substring(jarIndex + jarFlag.length());
+            } else {
+                int slashIndex = lowerCase.lastIndexOf('/', jarIndex);
+                if (slashIndex >= 0) {
+                    return urlPath.substring(slashIndex + 1);
+                } else {
+                    return urlPath;
+                }
+            }
         }
-        int classesIndex = lowerCase.indexOf("/classes/");
-        if (classesIndex > 0) {
-            return urlPath.substring(classesIndex + "/classes/".length());
+        String classFlag = "/classes/";
+        int classesIndex = lowerCase.lastIndexOf(classFlag);
+        if (classesIndex >= 0) {
+            if (!includeJarName) {
+                return urlPath.substring(classesIndex + classFlag.length());
+            } else {
+                return urlPath.substring(classesIndex + 1);
+            }
         }
         return urlPath;
     }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java
index 72d5402..dcb94c5 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java
@@ -18,13 +18,45 @@ public class IndentTools {
     public static final Operator tabs = new Operator(true);
     public static final Operator space = new Operator(false);
 
-    /** 获取缩进字符 **/
+    /**
+     * 获取缩进字符
+     * @param size 缩进数量
+     * @return 缩进字符
+     * @deprecated 单词写错了
+     */
+    @Deprecated
     public static char[] getIndenTabs(int size) {
-        return getIndenTabs(size, false);
+        return getIndentTabs(size, false);
     }
 
-    /** 获取缩进字符 **/
+    /**
+     * 获取缩进字符
+     * @param size 缩进数量
+     * @param useTab 是否使用TAB字符
+     * @return 缩进字符
+     * @deprecated 单词写错了
+     */
+    @Deprecated
     public static char[] getIndenTabs(int size, boolean useTab) {
+        return getIndentTabs(size, useTab);
+    }
+
+    /**
+     * 获取缩进字符
+     * @param size 缩进数量
+     * @return 缩进字符
+     */
+    public static char[] getIndentTabs(int size) {
+        return getIndentTabs(size, false);
+    }
+
+    /**
+     * 获取缩进字符
+     * @param size 缩进数量
+     * @param useTab 是否使用TAB字符
+     * @return 缩进字符
+     */
+    public static char[] getIndentTabs(int size, boolean useTab) {
         if (size <= 0) {
             return new char[0];
         }
@@ -162,6 +194,23 @@ public class IndentTools {
 
     /** 计算空白字符等于多少缩进量(余1个空格不算缩进,余2个空格以上算1个缩进) **/
     public static int calcSpacesToTabSize(CharSequence string, int start, int end) {
+        int spaceSize = countLeadingSpaceSize(string, start, end);
+        // 余1个空格不算缩进,余2个空格算1个缩进
+        return (spaceSize + 2) / 4;
+    }
+
+    /** 计算左侧有多少个空格 **/
+    public static int countLeadingSpaceSize(CharSequence string) {
+        return countLeadingSpaceSize(string, 0, string.length());
+    }
+
+    /** 计算左侧有多少个空格 **/
+    public static int countLeadingSpaceSize(CharSequence string, int start) {
+        return countLeadingSpaceSize(string, start, string.length());
+    }
+
+    /** 计算左侧有多少个空格 **/
+    public static int countLeadingSpaceSize(CharSequence string, int start, int end) {
         int size = 0;
         int pending = 0;
         for (int i = start; i < end; i++) {
@@ -179,11 +228,7 @@ public class IndentTools {
                 break;
             }
         }
-        if (pending > 0) {
-            // 余1个空格不算缩进,余2个空格算1个缩进
-            size += (pending + 2) / 4;
-        }
-        return size;
+        return size * 4 + pending;
     }
 
     /**
@@ -411,7 +456,7 @@ public class IndentTools {
         }
 
         public char[] getIndenTabs(int size) {
-            return IndentTools.getIndenTabs(size, useTab);
+            return IndentTools.getIndentTabs(size, useTab);
         }
 
         /**
@@ -602,7 +647,7 @@ public class IndentTools {
          * 在最后一个换行符之后插入信息<pre>
          * 例如: \n=换行符, \t=TAB符, \s=空格
          * \n\tABC\n\t\t --> \n\tABC\n[\tMESSAGE\n]\t\tDEF // 换行模式(插入的信息与上一行缩进对齐)
-         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF      // 非换行模式
+         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF     // 非换行模式
          * \n\tABC       --> \n\tABC[\sMESSAGE\s]DEF       // 非换行模式</pre>
          * 
          * @param string 原字符串
@@ -618,7 +663,7 @@ public class IndentTools {
          * 在最后一个换行符之后插入信息<pre>
          * 例如: \n=换行符, \t=TAB符, \s=空格
          * \n\tABC\n\t\t --> \n\tABC\n[\tMESSAGE\n]\t\tDEF // 换行模式(插入的信息与上一行缩进对齐)
-         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF      // 非换行模式
+         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF     // 非换行模式
          * \n\tABC       --> \n\tABC[\sMESSAGE\s]DEF       // 非换行模式</pre>
          * 
          * @param string 原字符串
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonFactory.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonFactory.java
new file mode 100644
index 0000000..3d06e73
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonFactory.java
@@ -0,0 +1,143 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.able.beans.StringWithAttrs;
+import com.gitee.qdbp.able.enums.FileErrorCode;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.json.JsonService;
+import com.gitee.qdbp.tools.files.PathTools;
+
+/**
+ * JsonService实现类查找
+ *
+ * @author zhaohuihua
+ * @version 20230126
+ */
+class JsonFactory {
+
+    private static JsonService DEFAULT_JSON_SERVICE;
+    private static ServiceException FIND_EXCEPTION;
+
+    private static final String BASE_SERVICE_CLASS = "com.gitee.qdbp.json.JsonServiceForBase";
+
+    public static boolean enabled() {
+        if (DEFAULT_JSON_SERVICE == null && FIND_EXCEPTION == null) {
+            doFindDefaultJsonService();
+        }
+        return DEFAULT_JSON_SERVICE != null;
+    }
+
+    public static void setDefaultJsonService(JsonService jsonService) {
+        VerifyTools.requireNonNull(jsonService, "jsonService");
+        DEFAULT_JSON_SERVICE = jsonService;
+        FIND_EXCEPTION = null;
+    }
+
+    public static JsonService findDefaultJsonService() {
+        if (DEFAULT_JSON_SERVICE == null && FIND_EXCEPTION == null) {
+            doFindDefaultJsonService();
+        }
+        if (FIND_EXCEPTION != null) {
+            throw FIND_EXCEPTION;
+        } else {
+            return DEFAULT_JSON_SERVICE;
+        }
+    }
+
+    private static void doFindDefaultJsonService() {
+        try {
+            Class.forName(BASE_SERVICE_CLASS);
+        } catch (ClassNotFoundException e) {
+            FIND_EXCEPTION = new ServiceException(ResultCode.SERVER_INNER_ERROR).setDetails("Missing qdbp-json.jar");
+            return;
+        }
+
+        try {
+            DEFAULT_JSON_SERVICE = findConfigJsonService();
+        } catch (ServiceException e) {
+            FIND_EXCEPTION = e;
+        } catch (Exception e) {
+            FIND_EXCEPTION = new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        }
+    }
+
+    private static synchronized JsonService findConfigJsonService() {
+        String configPath = "settings/global/json-service.yml";
+        List<URL> urls = PathTools.scanResources(configPath);
+
+        URL url = urls.get(0);
+        String string;
+        String path = PathTools.getJarRelativePath(url);
+        try {
+            string = PathTools.downloadString(url);
+        } catch (IOException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(path);
+        }
+        Map<String, String> roots = RuleTools.splitRuleKeyValues(string).asMap();
+        String rootValue = roots.get("json-service");
+        if (VerifyTools.isBlank(rootValue)) {
+            throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR)
+                    .setDetails("json-service config missing --> " + path);
+        }
+        List<StringWithAttrs> contents = RuleTools.splitRuleWithAttrs(rootValue);
+        if (contents.isEmpty()) {
+            throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR)
+                    .setDetails("json-service config error --> " + path);
+        }
+        List<String> names = new ArrayList<>();
+        for (StringWithAttrs content : contents) {
+            String name = StringTools.trimRight(content.getString(), ':', ' ');
+            if (VerifyTools.isBlank(content.getAttrs())) {
+                String msg = "json-service.{} config error --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, name, path);
+            }
+            Map<String, String> attrs = content.getAttrs().asMap();
+            String serviceType = attrs.get("serviceClass");
+            String requiredType = attrs.get("requiredClass");
+            if (VerifyTools.isBlank(serviceType)) {
+                String msg = "json-service.{} missing 'serviceClass' --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, name, path);
+            }
+            if (VerifyTools.isBlank(requiredType)) {
+                String msg = "json-service.{} missing 'requiredClass' --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, name, path);
+            }
+            try {
+                Class.forName(requiredType);
+            } catch (ClassNotFoundException ignore) {
+                // requiredClass不在jvm当中
+                names.add(name);
+                continue;
+            }
+            Class<?> serviceClass;
+            try {
+                serviceClass = Class.forName(serviceType);
+            } catch (ClassNotFoundException e) {
+                // serviceClass不在jvm当中
+                String msg = "json-service.{}.serviceClass not found: {} --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR, e).setDetails(msg, name, serviceType, path);
+            }
+            if (!JsonService.class.isAssignableFrom(serviceClass)) {
+                // serviceClass不是JsonService的子类
+                String msg = "json-service.{}.serviceClass is not implements JsonService: {} --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, name, serviceType, path);
+            }
+            try {
+                return (JsonService) serviceClass.newInstance();
+            } catch (Exception e) {
+                // 初始化服务类失败
+                String msg = "json-service.{}.serviceClass new instance error: {} --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR, e).setDetails(msg, name, serviceType, path);
+            }
+        }
+        // 所有requiredClass都不在jvm当中
+        String msg = "JsonService instance not found for [{}] --> {}";
+        String traces = ConvertTools.joinToString(names);
+        throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, traces, path);
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonMaps.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonMaps.java
new file mode 100644
index 0000000..cd1b7b3
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonMaps.java
@@ -0,0 +1,114 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Map工具类
+ *
+ * @author zhaohuihua
+ * @version 20230126
+ */
+class JsonMaps extends JsonFactory {
+
+    /**
+     * 获取Map中的字符串值, 并转换为字符串
+     *
+     * @param map Map
+     * @param key KEY
+     * @return 字符串
+     * @deprecated 改为 {@linkplain MapTools#getString(Map, String)}
+     */
+    @Deprecated
+    public static String getMapString(Map<String, Object> map, String key) {
+        return MapTools.getString(map, key);
+    }
+
+    /**
+     * 获取Map中的值, 并转换为指定类型
+     *
+     * @param map Map
+     * @param key KEY
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 指定类型的值
+     * @deprecated 改为 {@linkplain MapTools#getValue(Map, String, Class)}
+     */
+    @Deprecated
+    public static <T> T getMapValue(Map<String, Object> map, String key, Class<T> clazz) {
+        return MapTools.getValue(map, key, clazz);
+    }
+
+    /**
+     * 获取Map中的值, 并转换为指定类型
+     *
+     * @param map Map
+     * @param key KEY
+     * @param defaults 默认值
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 指定类型的值
+     * @deprecated 改为 {@linkplain MapTools#getValue(Map, String, Object, Class)}
+     */
+    @Deprecated
+    public static <T> T getMapValue(Map<String, Object> map, String key, T defaults, Class<T> clazz) {
+        return MapTools.getValue(map, key, defaults, clazz);
+    }
+
+    /**
+     * 获取Map中的列表值
+     *
+     * @param map Map
+     * @param key KEY
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 列表
+     * @deprecated 改为 {@linkplain MapTools#getList(Map, String, Class)}
+     */
+    @Deprecated
+    public static <T> List<T> getMapValues(Map<String, Object> map, String key, Class<T> clazz) {
+        return MapTools.getList(map, key, clazz);
+    }
+
+    /**
+     * 获取Map中的列表值
+     *
+     * @param map 原Map
+     * @param key KEY
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 列表
+     * @deprecated 改为 {@linkplain MapTools#getList(Map, String, Class)}
+     */
+    @Deprecated
+    public static <T> List<T> getMapValues(Map<String, Object> map, String key, boolean nullToEmpty, Class<T> clazz) {
+        return MapTools.getList(map, key, clazz);
+    }
+
+    /**
+     * 获取Map中的子Map
+     *
+     * @param map 原Map
+     * @param key KEY
+     * @return Map
+     * @deprecated 改为 {@linkplain MapTools#getSubMap(Map, String)}
+     */
+    @Deprecated
+    public static Map<String, Object> getMapSubMap(Map<String, Object> map, String key) {
+        return MapTools.getSubMap(map, key);
+    }
+
+    /**
+     * 获取Map中的子Map列表
+     *
+     * @param map 原Map
+     * @param key KEY
+     * @return Map列表
+     * @deprecated 改为 {@linkplain MapTools#getSubMaps(Map, String)}
+     */
+    @Deprecated
+    public static List<Map<String, Object>> getMapSubMaps(Map<String, Object> map, String key) {
+        return MapTools.getSubMaps(map, key);
+    }
+
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
new file mode 100644
index 0000000..a4fe835
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
@@ -0,0 +1,184 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.json.JsonFeature;
+
+/**
+ * Json工具类<br>
+ * 必须引入qdbp-json.jar, 且必须有fastjson/jackson/gson三个json基础jar之一
+ *
+ * @author zhaohuihua
+ * @version 180621
+ */
+public class JsonTools extends JsonMaps {
+
+    /** 静态工具类私有构造方法 **/
+    private JsonTools() {
+    }
+
+    public static <T> T convert(Object object, Class<T> clazz) {
+        return findDefaultJsonService().convert(object, clazz);
+    }
+
+    /** 对象转换为字符串 **/
+    public static String toJsonString(Object object) {
+        return findDefaultJsonService().toJsonString(object);
+    }
+
+    /** 对象转换为字符串 **/
+    public static String toJsonString(Object object, JsonFeature.Serialization feature) {
+        return findDefaultJsonService().toJsonString(object, feature);
+    }
+
+    private static final JsonFeature.Serialization UNQUOTE_FIELD_NAME_FEATURE =
+            JsonFeature.serialization().setQuoteFieldNames(false).lockToUnmodifiable();
+
+    /** 转换为日志字符串, json的key不会有引号, 可以稍微简化一下结构 **/
+    public static String toLogString(Object object) {
+        return findDefaultJsonService().toJsonString(object, UNQUOTE_FIELD_NAME_FEATURE);
+    }
+
+    /** 解析Json字符串对象 **/
+    public static Map<String, Object> parseAsMap(String jsonString) {
+        return findDefaultJsonService().parseAsMap(jsonString);
+    }
+
+    /** 解析Json字符串对象 **/
+    public static Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature) {
+        return findDefaultJsonService().parseAsMap(jsonString, feature);
+    }
+
+    /** 解析Json字符串数组 **/
+    public static List<Map<String, Object>> parseAsMaps(String jsonString) {
+        return findDefaultJsonService().parseAsMaps(jsonString);
+    }
+
+    /** 解析Json字符串数组 **/
+    public static List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature) {
+        return findDefaultJsonService().parseAsMaps(jsonString, feature);
+    }
+
+    /** 解析Json字符串对象 **/
+    public static <T> T parseAsObject(String jsonString, Class<T> clazz) {
+        return findDefaultJsonService().parseAsObject(jsonString, clazz);
+    }
+
+    /** 解析Json字符串对象 **/
+    public static <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        return findDefaultJsonService().parseAsObject(jsonString, clazz, feature);
+    }
+
+    /** 解析Json字符串数组 **/
+    public static <T> List<T> parseAsObjects(String jsonString, Class<T> clazz) {
+        return findDefaultJsonService().parseAsObjects(jsonString, clazz);
+    }
+
+    /** 解析Json字符串数组 **/
+    public static <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        return findDefaultJsonService().parseAsObjects(jsonString, clazz, feature);
+    }
+
+    /**
+     * 将Map内容设置到Java对象中
+     *
+     * @param map Map
+     * @param bean 目标Java对象
+     */
+    public static void mapFillToBean(Map<String, ?> map, Object bean) {
+        findDefaultJsonService().mapFillToBean(map, bean);
+    }
+
+    /**
+     * 将Map转换为Java对象
+     *
+     * @param <T> 目标类型
+     * @param map Map
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    public static <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
+        return findDefaultJsonService().mapToBean(map, clazz);
+    }
+
+    /**
+     * 将Java对象转换为Map
+     *
+     * @param bean JavaBean对象
+     * @return Map
+     */
+    public static Map<String, Object> beanToMap(Object bean) {
+        return findDefaultJsonService().beanToMap(bean);
+    }
+
+    /**
+     * 将Java对象转换为Map
+     *
+     * @param bean Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map
+     */
+    public static Map<String, Object> beanToMap(Object bean, boolean deep, boolean clearBlankValue) {
+        return findDefaultJsonService().beanToMap(bean, deep, clearBlankValue);
+    }
+
+    /**
+     * 将Map数组转换为Java对象列表
+     *
+     * @param <T> 目标类型
+     * @param maps Map数组
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    public static <T> List<T> mapToBeans(Collection<Map<String, ?>> maps, Class<T> clazz) {
+        return findDefaultJsonService().mapToBeans(maps, clazz);
+    }
+
+    /**
+     * 将Java对象转换为Map列表<br>
+     * copy from fastjson JSON.toJSON(), 保留enum和date
+     *
+     * @param beans JavaBean对象列表
+     * @return Map列表
+     */
+    public static List<Map<String, Object>> beanToMaps(Collection<?> beans) {
+        return findDefaultJsonService().beanToMaps(beans);
+    }
+
+    /**
+     * 将Java对象转换为Map列表
+     *
+     * @param beans Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map列表
+     */
+    public static List<Map<String, Object>> beanToMaps(Collection<?> beans, boolean deep, boolean clearBlankValue) {
+        return findDefaultJsonService().beanToMaps(beans, deep, clearBlankValue);
+    }
+
+    /**
+     * 将对象转换为以换行符分隔的日志文本<br>
+     * 如 newlineLogs(params, operator) 返回 \n\t{paramsJson}\n\t{operatorJson}<br>
+     *
+     * @param objects 对象
+     * @return 日志文本
+     */
+    public static String newlineLogs(Object... objects) {
+        StringBuilder buffer = new StringBuilder();
+        for (Object object : objects) {
+            buffer.append("\n\t");
+            if (object == null) {
+                buffer.append("null");
+            } else if (object instanceof String) {
+                buffer.append(object);
+            } else {
+                buffer.append(object.getClass().getSimpleName()).append(": ");
+                buffer.append(toLogString(object));
+            }
+        }
+        return buffer.toString();
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index be3c931..2e93afe 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -7,11 +7,14 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import com.gitee.qdbp.able.beans.DepthMap;
+import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.model.reusable.ExpressionMap;
+import com.gitee.qdbp.able.result.ResultCode;
 
 /**
  * Map工具类<br>
@@ -27,6 +30,94 @@ public class MapTools {
     private MapTools() {
     }
 
+    /**
+     * 将Map转换为JsonMap (key转换为String)
+     *
+     * @param map Map对象
+     * @return JsonMap
+     */
+    public static Map<String, Object> toJsonMap(Map<?, ?> map) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+            String key = entry.getKey() == null ? null : entry.getKey().toString();
+            result.put(key, entry.getValue());
+        }
+        return result;
+    }
+
+    /**
+     * 将对象列表转换为JsonMap列表
+     *
+     * @param list 列表
+     * @return JsonMap列表
+     */
+    public static List<Map<String, Object>> toJsonMaps(Collection<?> list) {
+        List<Map<String, Object>> results = new ArrayList<>();
+        int i = -1;
+        for (Object item : list) {
+            i++;
+            if (item == null) {
+                results.add(null);
+            } else if (item instanceof Map) {
+                results.add(toJsonMap((Map<?, ?>) item));
+            } else {
+                if (ReflectTools.isPrimitive(item.getClass(), false)) {
+                    String msg = "[" + i + "] " + item.getClass().getSimpleName() + " can't convert to map.";
+                    throw new ServiceException(ResultCode.SERVER_INNER_ERROR).setDetails(msg);
+                }
+                try {
+                    results.add(JsonTools.beanToMap(item));
+                } catch (ServiceException e) {
+                    if (e.getDetails() == null) {
+                        e.setDetails("[" + i + "] " + item.getClass().getSimpleName() + " can't convert to map.");
+                    } else {
+                        e.setDetails("[" + i + "] " + e.getDetails());
+                    }
+                    throw e;
+                } catch (Exception e) {
+                    throw new ServiceException(ResultCode.SERVER_INNER_ERROR)
+                            .setDetails("[" + i + "] " + e.getMessage());
+                }
+            }
+        }
+        return results;
+    }
+
+    /**
+     * 将Map的KEY转换为驼峰名
+     *
+     * @param data 数据列表
+     * @return 转换后的数据
+     */
+    public static Map<String, Object> toCamelKeyMap(Map<String, Object> data) {
+        if (data == null) {
+            return null;
+        }
+        Map<String, Object> map = new LinkedHashMap<>();
+        for (Map.Entry<String, Object> entry : data.entrySet()) {
+            String key = entry.getKey();
+            map.put(NamingTools.toCamelString(key), entry.getValue());
+        }
+        return map;
+    }
+
+    /**
+     * 将Map的KEY转换为驼峰名
+     *
+     * @param data 数据列表
+     * @return 转换后的数据
+     */
+    public static List<Map<String, Object>> toCamelKeyMaps(List<Map<String, Object>> data) {
+        if (data == null) {
+            return null;
+        }
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (Map<String, Object> item : data) {
+            result.add(toCamelKeyMap(item));
+        }
+        return result;
+    }
+
     /**
      * 清除Map中的空值
      *
@@ -245,8 +336,7 @@ public class MapTools {
     }
 
     /**
-     * 获取字符串类型的Map值<br>
-     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getString("xxx")将返回null
+     * 获取字符串类型的Map值
      *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
@@ -254,7 +344,7 @@ public class MapTools {
      */
     public static String getString(Map<?, ?> map, String key) {
         Object value = getValue(map, key);
-        return value instanceof String ? (String) value : null;
+        return value == null ? null : value.toString();
     }
 
     /**
@@ -279,23 +369,50 @@ public class MapTools {
     }
 
     /**
-     * 获取指定类型的Map值<br>
-     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
+     * 获取指定类型的Map值
      *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
      * @return Map值
      */
-    @SuppressWarnings("unchecked")
     public static <T> T getValue(Map<?, ?> map, String key, Class<T> clazz) {
         Object value = getValue(map, key);
-        return value != null && clazz.isAssignableFrom(value.getClass()) ? (T) value : null;
+        return convertValue(value, null, clazz);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T convertValue(Object value, T defaults, Class<T> clazz) {
+        if (value == null) {
+            if (clazz == int.class) {
+                return (T) Integer.valueOf(0);
+            } else if (clazz == long.class) {
+                return (T) Long.valueOf(0);
+            } else if (clazz == short.class) {
+                return (T) Short.valueOf((short) 0);
+            } else if (clazz == byte.class) {
+                return (T) Byte.valueOf((byte) 0);
+            } else if (clazz == float.class) {
+                return (T) Float.valueOf(0);
+            } else if (clazz == double.class) {
+                return (T) Double.valueOf(0);
+            } else if (clazz == boolean.class) {
+                return (T) Boolean.FALSE;
+            } else {
+                return null;
+            }
+        }
+        if (clazz.isAssignableFrom(value.getClass())) {
+            return (T) value;
+        } else if (JsonTools.enabled()) {
+            return JsonTools.convert(value, clazz);
+        } else {
+            return defaults;
+        }
     }
 
     /**
-     * 获取指定类型的Map值<br>
-     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
+     * 获取指定类型的Map值
      *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
@@ -306,7 +423,7 @@ public class MapTools {
     @SuppressWarnings("unchecked")
     public static <T> T getValue(Map<?, ?> map, String key, T defaults, Class<T> clazz) {
         Object value = getValue(map, key);
-        return value != null && clazz.isAssignableFrom(value.getClass()) ? (T) value : defaults;
+        return convertValue(value, defaults, clazz);
     }
 
     /**
@@ -317,7 +434,6 @@ public class MapTools {
      * @param clazz 指定类型
      * @return Map值列表, 值不存在或值类型不匹配时将返回空List
      */
-    @SuppressWarnings("unchecked")
     public static <T> List<T> getList(Map<?, ?> map, String key, Class<T> clazz) {
         Object value = getValue(map, key);
         if (value == null) {
@@ -326,11 +442,7 @@ public class MapTools {
         List<Object> list = ConvertTools.parseList(value);
         List<T> result = new ArrayList<>();
         for (Object item : list) {
-            if (item != null && clazz.isAssignableFrom(item.getClass())) {
-                result.add((T) item);
-            } else {
-                result.add(null);
-            }
+            result.add(convertValue(item, null, clazz));
         }
         return result;
     }
@@ -391,18 +503,11 @@ public class MapTools {
      * @return 子Map数组
      */
     public static List<Map<String, Object>> getSubMaps(Map<?, ?> map, String key) {
-        @SuppressWarnings("rawtypes")
-        List<Map> maps = getList(map, key, Map.class);
-        List<Map<String, Object>> results = new ArrayList<>();
-        for (Map<?, ?> item : maps) {
-            Map<String, Object> result = new HashMap<>();
-            results.add(result);
-            for (Map.Entry<?, ?> entry : item.entrySet()) {
-                String fieldKey = entry.getKey() == null ? null : entry.getKey().toString();
-                result.put(fieldKey, entry.getValue());
-            }
+        Object value = getValue(map, key);
+        if (value == null) {
+            return new ArrayList<>();
         }
-        return results;
+        return toJsonMaps(ConvertTools.parseList(value));
     }
 
     /**
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
index 093e6cb..a9b7347 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
@@ -1,11 +1,12 @@
 package com.gitee.qdbp.tools.utils;
 
 import java.util.ArrayList;
-import java.util.Date;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.beans.KeyStrings;
 import com.gitee.qdbp.able.beans.StringWithAttrs;
 
 /**
@@ -124,7 +125,7 @@ public class RuleTools {
     }
 
     public static Map<String, Object> splitRuleMaps(String rules) {
-        List<KeyString> values = splitRuleKeyValues(rules);
+        KeyStrings values = splitRuleKeyValues(rules);
         Map<String, Object> map = new HashMap<>();
         for (KeyString item : values) {
             String key = item.getKey();
@@ -134,41 +135,8 @@ public class RuleTools {
             String value = item.getValue();
             if (value == null) {
                 map.put(key, null);
-                continue;
-            }
-            if (key.endsWith("OfMinDay")) {
-                try {
-                    Date date = DateTools.parse(value);
-                    map.put(key, DateTools.toStartTime(date));
-                } catch (IllegalArgumentException e) {
-                    map.put(key, value);
-                }
-            } else if (key.endsWith("OfMaxDay")) {
-                try {
-                    Date date = DateTools.parse(value);
-                    map.put(key, DateTools.toEndTime(date));
-                } catch (IllegalArgumentException e) {
-                    map.put(key, value);
-                }
-            } else if (key.endsWith("Date") || key.endsWith("Time")) {
-                try {
-                    map.put(key, DateTools.parse(value));
-                } catch (IllegalArgumentException e) {
-                    map.put(key, value);
-                }
-            } else if (StringTools.isNumber(value)) {
-                if (value.startsWith("0") && !value.startsWith("0.")) {
-                    // 0开头的数字, 不能解析为Number对象
-                    map.put(key, value);
-                } else {
-                    try {
-                        map.put(key, ConvertTools.toNumber(value));
-                    } catch (NumberFormatException e) {
-                        map.put(key, value);
-                    }
-                }
             } else {
-                map.put(key, value);
+                map.put(key, ConvertTools.tryParsePrimitiveValue(key, value));
             }
         }
         return map;
@@ -189,7 +157,7 @@ public class RuleTools {
      * @param rules 规则字符串
      * @return 拆分后的字符串数组
      */
-    public static List<KeyString> splitRuleKeyValues(String rules) {
+    public static KeyStrings splitRuleKeyValues(String rules) {
         List<String> strings = StringTools.splits(rules, false, '\n');
         return splitRuleKeyValues(strings);
     }
@@ -209,8 +177,11 @@ public class RuleTools {
      * @param rules 规则字符串
      * @return 拆分后的字符串数组
      */
-    public static List<KeyString> splitRuleKeyValues(List<String> rules) {
-        List<KeyString> results = new ArrayList<>();
+    public static KeyStrings splitRuleKeyValues(List<String> rules) {
+        KeyStrings results = new KeyStrings();
+        if (rules == null) {
+            return results;
+        }
         for (int i = 0, z = rules.size(); i < z; i++) {
             String string = rules.get(i);
             String trimmed = string.trim();
@@ -221,7 +192,7 @@ public class RuleTools {
                 // 遇到注释行, 跳过
                 continue;
             }
-            int index = trimmed.indexOf('=');
+            int index = StringTools.charIndex(trimmed, '=', ':');
             String key = null;
             List<String> values = new ArrayList<>();
             if (index < 0) {
@@ -294,7 +265,7 @@ public class RuleTools {
                 // 遇到注释行, 跳过
                 continue;
             }
-            List<KeyString> attrs = new ArrayList<>();
+            KeyStrings attrs = new KeyStrings();
             i += collectNextIndentAttrs(rules, i + 1, attrs);
             results.add(new StringWithAttrs(trimmed, attrs));
         }
@@ -306,7 +277,7 @@ public class RuleTools {
         int lineIndex = collectNextIndentLines(rules, start, lines);
         for (String line : lines) {
             String trimmed = line.trim();
-            int index = trimmed.indexOf('=');
+            int index = StringTools.charIndex(trimmed, '=', ':');
             String key = null;
             String value = trimmed;
             if (index > 0) {
@@ -322,6 +293,7 @@ public class RuleTools {
 
     public static int collectNextIndentLines(List<String> rules, int start, List<String> collectors) {
         int count = 0;
+        int firstLeadingSpaceSize = -1;
         for (int i = start, z = rules.size(); i < z; i++, count++) {
             String next = rules.get(i);
             String trimmed = next.trim();
@@ -340,13 +312,24 @@ public class RuleTools {
                 break;
             }
             // 计算缩进量
-            int indent = IndentTools.countLeadingIndentSize(next);
-            if (indent > 0) {
-                collectors.add(IndentTools.tabs.tabAll(next, -1));
-            } else {
+            int leadingSpaceSize = IndentTools.countLeadingSpaceSize(next);
+            if (leadingSpaceSize < 2) {
                 // 遇到未缩进的行, 说明已经结束了
                 break;
             }
+            if (firstLeadingSpaceSize < 0) {
+                // 遇到第1个有效行, 记录缩进数量
+                firstLeadingSpaceSize = leadingSpaceSize;
+                collectors.add(StringTools.trimLeft(next));
+            } else if (leadingSpaceSize <= firstLeadingSpaceSize) {
+                // 比第1行缩进还少, 只能清除左侧缩进, 与第1行对齐
+                collectors.add(StringTools.trimLeft(next));
+            } else {
+                // 比第1行缩进更多的, 保留多出部分的缩进空格
+                char[] indentSpaces = new char[leadingSpaceSize - firstLeadingSpaceSize];
+                Arrays.fill(indentSpaces, ' ');
+                collectors.add(new String(indentSpaces) + StringTools.trimLeft(next));
+            }
         }
         return count;
     }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index 6f6fd01..6ae9a26 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -397,6 +397,28 @@ public class StringTools {
         return format(string, map);
     }
 
+    /**
+     * 求目标字符在原字符串中最先出现的位置
+     *
+     * @param string 原字符串
+     * @param chars 目标字符
+     * @return 位置 (均未找到时返回-1)
+     */
+    public static int charIndex(String string, char... chars) {
+        if (string == null || string.length() == 0 || chars == null || chars.length == 0) {
+            return -1;
+        }
+        char[] sChars = string.toCharArray();
+        for (int i = 0; i < sChars.length; i++) {
+            for (int j = 0; j < chars.length; j++) {
+                if (sChars[i] == chars[j]) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
     /**
      * 拆分字符串, 以竖杠|为分隔符<br>
      * 每一个子字符串都已经trim()过了<br>
diff --git a/able/src/main/resources/settings/global/json-service.yml b/able/src/main/resources/settings/global/json-service.yml
new file mode 100644
index 0000000..62e63e7
--- /dev/null
+++ b/able/src/main/resources/settings/global/json-service.yml
@@ -0,0 +1,10 @@
+json-service:
+  fastjson:
+    serviceClass: com.gitee.qdbp.json.JsonServiceForFastjson
+    requiredClass: com.alibaba.fastjson.JSON
+  jackson:
+    serviceClass: com.gitee.qdbp.json.JsonServiceForJackson
+    requiredClass: com.fasterxml.jackson.databind.ObjectMapper
+  gson:
+    serviceClass: com.gitee.qdbp.json.JsonServiceForGson
+    requiredClass: com.google.gson.Gson
diff --git a/json/pom.xml b/json/pom.xml
new file mode 100644
index 0000000..f6c1eaf
--- /dev/null
+++ b/json/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-parent</artifactId>
+		<version>5.0.1</version>
+	</parent>
+
+	<artifactId>qdbp-json</artifactId>
+	<packaging>jar</packaging>
+	<version>5.5.0</version>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json library</description>
+
+    <organization>
+        <name>zhaohuihua</name>
+        <url>https://gitee.com/qdbp/qdbp-json/</url>
+    </organization>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-able</artifactId>
+			<version>5.5.0</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+			<version>1.2.83</version>
+			<optional>true</optional>
+		</dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+			<version>2.14.2</version>
+			<optional>true</optional>
+		</dependency>
+		<dependency>
+			<groupId>com.google.code.gson</groupId>
+			<artifactId>gson</artifactId>
+			<version>2.10.1</version>
+			<optional>true</optional>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>1.7.25</version>
+			<optional>true</optional>
+		</dependency>
+
+	</dependencies>
+
+</project>
diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonInitializer.java b/json/src/main/java/com/gitee/qdbp/json/JsonInitializer.java
new file mode 100644
index 0000000..3fb3d5e
--- /dev/null
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonInitializer.java
@@ -0,0 +1,52 @@
+package com.gitee.qdbp.json;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * JSON全局特性初始化<br>
+ * 正确的操作顺序是先调JsonInitializer, 再使用JsonService
+ *
+ * @author zhaohuihua
+ * @version 20230126
+ */
+public class JsonInitializer {
+
+    private static final Logger log = LoggerFactory.getLogger(JsonServiceForFastjson.class);
+
+    private static JsonFeature.Serialization GLOBAL_SERIALIZATION_FEATURE;
+
+    public static void initGlobalSerializationFeature(JsonFeature.Serialization feature) {
+        if (GLOBAL_SERIALIZATION_FEATURE != null && log.isDebugEnabled()) {
+            // 正确的操作顺序是先调JsonInitializer.initGlobalSerializationFeature(), 再使用JsonService
+            log.debug("Json global serialization feature already initialized, " +
+                    "executing again will override the global setting.");
+        }
+        GLOBAL_SERIALIZATION_FEATURE = feature.copy().lockToUnmodifiable();
+    }
+
+    protected static JsonFeature.Serialization getGlobalSerializationFeature() {
+        if (GLOBAL_SERIALIZATION_FEATURE == null) {
+            GLOBAL_SERIALIZATION_FEATURE = new JsonFeature.Serialization().lockToUnmodifiable();
+        }
+        return GLOBAL_SERIALIZATION_FEATURE;
+    }
+
+    private static JsonFeature.Deserialization GLOBAL_DESERIALIZATION_FEATURE;
+
+    public static void initGlobalDeserializationFeature(JsonFeature.Deserialization feature) {
+        if (GLOBAL_DESERIALIZATION_FEATURE != null && log.isDebugEnabled()) {
+            // 正确的操作顺序是先调JsonInitializer.initGlobalDeserializationFeature(), 再使用JsonService
+            log.debug("Json global deserialization feature already initialized, " +
+                    "executing again will override the global setting.");
+        }
+        GLOBAL_DESERIALIZATION_FEATURE = feature.copy().lockToUnmodifiable();
+    }
+
+    protected static JsonFeature.Deserialization getGlobalDeserializationFeature() {
+        if (GLOBAL_DESERIALIZATION_FEATURE == null) {
+            GLOBAL_DESERIALIZATION_FEATURE = new JsonFeature.Deserialization().lockToUnmodifiable();
+        }
+        return GLOBAL_DESERIALIZATION_FEATURE;
+    }
+}
diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
new file mode 100644
index 0000000..218a1e6
--- /dev/null
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
@@ -0,0 +1,167 @@
+package com.gitee.qdbp.json;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.utils.MapTools;
+import com.gitee.qdbp.tools.utils.ReflectTools;
+
+/**
+ * JsonService的基础实现类
+ *
+ * @author zhaohuihua
+ * @version 20211207
+ */
+public abstract class JsonServiceForBase implements JsonService {
+
+    protected final JsonFeature.Serialization serializationFeature;
+
+    protected final JsonFeature.Deserialization deserializationFeature;
+
+    public JsonServiceForBase() {
+        this.serializationFeature = JsonInitializer.getGlobalSerializationFeature();
+        this.deserializationFeature = JsonInitializer.getGlobalDeserializationFeature();
+    }
+
+    public JsonServiceForBase(JsonFeature.Serialization serializationFeature,
+            JsonFeature.Deserialization deserializationFeature) {
+        this.serializationFeature = serializationFeature.copy().lockToUnmodifiable();
+        this.deserializationFeature = deserializationFeature.copy().lockToUnmodifiable();
+    }
+
+    @Override
+    public <T> T parseAsObject(String jsonString, Class<T> clazz) {
+        return parseAsObject(jsonString, clazz, deserializationFeature);
+    }
+
+    @Override
+    public String toJsonString(Object object) {
+        return toJsonString(object, serializationFeature);
+    }
+    @Override
+    public Map<String, Object> parseAsMap(String jsonString) {
+        return parseAsMap(jsonString, deserializationFeature);
+    }
+    @Override
+    public <T> List<T> parseAsObjects(String jsonString, Class<T> clazz) {
+        return parseAsObjects(jsonString, clazz, deserializationFeature);
+    }
+    @Override
+    public List<Map<String, Object>> parseAsMaps(String jsonString) {
+        return parseAsMaps(jsonString, deserializationFeature);
+    }
+
+    /**
+     * 将Java对象转换为Map<br>
+     * copy from fastjson JSON.toJSON(), 保留enum和date
+     *
+     * @param bean JavaBean对象
+     * @return Map
+     */
+    @Override
+    public Map<String, Object> beanToMap(Object bean) {
+        if (bean == null) {
+            return null;
+        }
+        return beanToMap(bean, true, true);
+    }
+
+    /**
+     * 将Map数组转换为Java对象列表
+     *
+     * @param <T> 目标类型
+     * @param maps Map数组
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    @Override
+    public <T> List<T> mapToBeans(Collection<Map<String, ?>> maps, Class<T> clazz) {
+        if (maps == null) {
+            return null;
+        }
+        List<T> result = new ArrayList<>();
+        for (Map<String, ?> map : maps) {
+            result.add(mapToBean(map, clazz));
+        }
+        return result;
+    }
+
+    /**
+     * 将Java对象转换为Map列表<br>
+     * copy from fastjson JSON.toJSON(), 保留enum和date
+     *
+     * @param beans JavaBean对象列表
+     * @return Map列表
+     */
+    @Override
+    public List<Map<String, Object>> beanToMaps(Collection<?> beans) {
+        return beanToMaps(beans, true, true);
+    }
+
+    /**
+     * 将Java对象转换为Map列表
+     *
+     * @param beans Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map列表
+     */
+    @Override
+    public List<Map<String, Object>> beanToMaps(Collection<?> beans, boolean deep, boolean clearBlankValue) {
+        if (beans == null) {
+            return null;
+        }
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (Object bean : beans) {
+            result.add(beanToMap(bean, deep, clearBlankValue));
+        }
+        return result;
+    }
+
+    //\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
+    // 以下是低效率的默认实现方法, 如果对应的json框架有更高效的方法, 应重写
+    ///////////////////////////////////////////////////////////////
+
+    @Override
+    public void mapFillToBean(Map<String, ?> map, Object bean) {
+        if (map == null || map.isEmpty()) {
+            return;
+        }
+        Class<?> type = bean.getClass();
+        Object newer = mapToBean(map, type);
+        Field[] fields = ReflectTools.getAllFields(type);
+        for (Field field : fields) {
+            if (map.containsKey(field.getName())) {
+                try {
+                    Object value = ReflectTools.getFieldValue(newer, field);
+                    ReflectTools.setFieldValue(bean, field, value);
+                } catch (Exception ignore) {
+                }
+            }
+        }
+    }
+
+    @Override
+    public <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
+        if (map == null) {
+            return null;
+        }
+        String string = toJsonString(map, serializationFeature);
+        return parseAsObject(string, clazz, deserializationFeature);
+    }
+
+    @Override
+    public Map<String, Object> beanToMap(Object bean, boolean deep, boolean clearBlankValue) {
+        if (bean == null) {
+            return null;
+        }
+        String string = toJsonString(bean, serializationFeature);
+        Map<String, Object> map = parseAsMap(string, deserializationFeature);
+        if (clearBlankValue) {
+            MapTools.clearBlankValue(map);
+        }
+        return map;
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java
similarity index 33%
rename from tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
rename to json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java
index b5e26a7..fbaad55 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java
@@ -1,12 +1,9 @@
-package com.gitee.qdbp.tools.utils;
+package com.gitee.qdbp.json;
 
 import java.lang.reflect.Array;
-import java.lang.reflect.Field;
-import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -17,10 +14,13 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONException;
 import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.annotation.JSONType;
+import com.alibaba.fastjson.parser.DefaultJSONParser;
+import com.alibaba.fastjson.parser.Feature;
+import com.alibaba.fastjson.parser.JSONLexer;
+import com.alibaba.fastjson.parser.JSONToken;
 import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.parser.deserializer.FieldDeserializer;
 import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
-import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer;
 import com.alibaba.fastjson.serializer.DoubleSerializer;
 import com.alibaba.fastjson.serializer.JSONSerializable;
 import com.alibaba.fastjson.serializer.JSONSerializer;
@@ -29,346 +29,170 @@ import com.alibaba.fastjson.serializer.ObjectSerializer;
 import com.alibaba.fastjson.serializer.SerializeConfig;
 import com.alibaba.fastjson.serializer.SerializeWriter;
 import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.alibaba.fastjson.util.FieldInfo;
+import com.alibaba.fastjson.serializer.SimpleDateFormatSerializer;
 import com.alibaba.fastjson.util.TypeUtils;
-import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.MapTools;
+import com.gitee.qdbp.tools.utils.ReflectTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
- * Json工具类
+ * JsonService的Fastjson实现类<br>
+ * 最低支持版本为fastjson-1.2.16
  *
  * @author zhaohuihua
- * @version 180621
+ * @version 20211207
  */
-public class JsonTools {
+public class JsonServiceForFastjson extends JsonServiceForBase {
 
-    /** 静态工具类私有构造方法 **/
-    private JsonTools() {
-    }
+    /** 全局实例 **/
+    private static JsonServiceForFastjson DEFAULTS;
 
-    /** 对象转换为JavaBean **/
-    public static <T> T convert(Object object, Class<T> clazz) {
-        return TypeUtils.castToJavaBean(object, clazz);
+    /** 获取全局默认实例 **/
+    public static JsonServiceForFastjson defaults() {
+        if (DEFAULTS == null) {
+            DEFAULTS = InnerInstance.INSTANCE;
+        }
+        return DEFAULTS;
     }
 
-    /**
-     * 获取Map中的字符串值, 并转换为字符串
-     *
-     * @param map Map
-     * @param key KEY
-     * @return 字符串
-     */
-    public static String getMapString(Map<String, Object> map, String key) {
-        Object value = MapTools.getValue(map, key);
-        return TypeUtils.castToJavaBean(value, String.class);
-    }
+    private static class InnerInstance {
 
-    /**
-     * 获取Map中的值, 并转换为指定类型
-     *
-     * @param map Map
-     * @param key KEY
-     * @param clazz 目标类型
-     * @param <T> 目标类型
-     * @return 指定类型的值
-     */
-    public static <T> T getMapValue(Map<String, Object> map, String key, Class<T> clazz) {
-        Object value = MapTools.getValue(map, key);
-        return TypeUtils.castToJavaBean(value, clazz);
+        public static final JsonServiceForFastjson INSTANCE = new JsonServiceForFastjson();
     }
 
-    /**
-     * 获取Map中的值, 并转换为指定类型
-     *
-     * @param map Map
-     * @param key KEY
-     * @param defaults 默认值
-     * @param clazz 目标类型
-     * @param <T> 目标类型
-     * @return 指定类型的值
-     */
-    public static <T> T getMapValue(Map<String, Object> map, String key, T defaults, Class<T> clazz) {
-        Object value = MapTools.getValue(map, key);
-        return TypeUtils.castToJavaBean(value == null ? defaults : value, clazz);
+    public JsonServiceForFastjson() {
+        super();
     }
 
-    /**
-     * 获取Map中的列表值
-     *
-     * @param map Map
-     * @param key KEY
-     * @param clazz 目标类型
-     * @param <T> 目标类型
-     * @return 列表
-     */
-    public static <T> List<T> getMapValues(Map<String, Object> map, String key, Class<T> clazz) {
-        Object value = MapTools.getValue(map, key);
-        List<T> result = new ArrayList<>();
-        if (value == null) {
-            return result;
-        }
-        List<Object> values = ConvertTools.parseList(value);
-        for (Object item : values) {
-            result.add(TypeUtils.castToJavaBean(item, clazz));
-        }
-        return result;
+    public JsonServiceForFastjson(JsonFeature.Serialization serializationFeature,
+            JsonFeature.Deserialization deserializationFeature) {
+        super(serializationFeature, deserializationFeature);
     }
 
-    /**
-     * 获取Map中的列表值
-     *
-     * @param map 原Map
-     * @param key KEY
-     * @param clazz 目标类型
-     * @param <T> 目标类型
-     * @return 列表
-     * @deprecated 改为 {@linkplain #getMapValue(Map, String, Class)}
-     */
-    @Deprecated
-    public static <T> List<T> getMapValues(Map<String, Object> map, String key, boolean nullToEmpty, Class<T> clazz) {
-        return getMapValues(map, key, clazz);
-    }
-
-    /**
-     * 获取Map中的子Map
-     *
-     * @param map 原Map
-     * @param key KEY
-     * @return Map
-     */
-    public static Map<String, Object> getMapSubMap(Map<String, Object> map, String key) {
-        Object value = MapTools.getValue(map, key);
-        Map<String, Object> result = new HashMap<>();
-        if (value == null) {
-            return result;
-        }
-        Map<?, ?> bean = TypeUtils.castToJavaBean(value, Map.class);
-        for (Map.Entry<?, ?> entry : bean.entrySet()) {
-            String fieldKey = entry.getKey() == null ? null : entry.getKey().toString();
-            result.put(fieldKey, entry.getValue());
-        }
-        return result;
+    @Override
+    public <T> T convert(Object object, Class<T> clazz) {
+        return TypeUtils.castToJavaBean(object, clazz);
     }
 
-    /**
-     * 获取Map中的子Map列表
-     *
-     * @param map 原Map
-     * @param key KEY
-     * @return Map列表
-     */
-    public static List<Map<String, Object>> getMapSubMaps(Map<String, Object> map, String key) {
-        Object value = MapTools.getValue(map, key);
-        List<Map<String, Object>> results = new ArrayList<>();
-        if (value == null) {
-            return results;
-        }
-        List<Object> values = ConvertTools.parseList(value);
-        for (Object item : values) {
-            if (item == null) {
-                results.add(null);
-            }
-            Map<?, ?> bean = TypeUtils.castToJavaBean(item, Map.class);
-            Map<String, Object> result = new HashMap<>();
-            for (Map.Entry<?, ?> entry : bean.entrySet()) {
-                String fieldKey = entry.getKey() == null ? null : entry.getKey().toString();
-                result.put(fieldKey, entry.getValue());
-            }
-            results.add(result);
+    @Override
+    public String toJsonString(Object object, JsonFeature.Serialization feature) {
+        if (object == null) {
+            return null;
+        }
+        try (SerializeWriter out = new SerializeWriter()) {
+            JSONSerializer serializer = generateJsonSerializer(out, feature);
+            serializer.write(object);
+            return out.toString();
         }
-        return results;
     }
 
-    /**
-     * 将对象转换为以换行符分隔的日志文本<br>
-     * 如 newlineLogs(params, operator) 返回 \n\t{paramsJson}\n\t{operatorJson}<br>
-     *
-     * @param objects 对象
-     * @return 日志文本
-     */
-    public static String newlineLogs(Object... objects) {
-        StringBuilder buffer = new StringBuilder();
-        for (Object object : objects) {
-            buffer.append("\n\t");
-            if (object == null) {
-                buffer.append("null");
-            } else if (object instanceof String) {
-                buffer.append(object);
-            } else {
-                buffer.append(object.getClass().getSimpleName()).append(": ");
-                buffer.append(toLogString(object));
-            }
+    @Override
+    public <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return null;
         }
-        return buffer.toString();
+        Feature[] features = generateParserFeatures(feature);
+        return JSON.parseObject(jsonString, clazz, features);
     }
 
-    private static final SerializeConfig JSON_CONFIG = new SerializeConfig();
-
-    static {
-        JSON_CONFIG.put(Double.class, new DoubleSerializer("#.##################"));
+    @Override
+    public Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new LinkedHashMap<>();
+        }
+        if (VerifyTools.isJsonObjectString(jsonString)) {
+            Feature[] features = generateParserFeatures(feature);
+            return JSON.parseObject(jsonString, features);
+        } else {
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR)
+                    .setDetails("parameter must be json object string.");
+        }
     }
 
-    /** 转换为日志字符串, json的key不会有引号, 可以稍微简化一下结构 **/
-    public static String toLogString(Object object) {
-        return toLogString(object, new SerializerFeature[0]);
+    @Override
+    public <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        return doParseJsonArray(jsonString, clazz, feature);
     }
 
-    /** 转换为日志字符串, json的key不会有引号, 可以稍微简化一下结构 **/
-    public static String toLogString(Object object, SerializerFeature... features) {
-        if (object == null) {
-            return "null";
-        }
-        try (SerializeWriter out = new SerializeWriter()) {
-            JSONSerializer serializer = new JSONSerializer(out, JSON_CONFIG);
-            serializer.config(SerializerFeature.QuoteFieldNames, false);
-            serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
-            serializer.config(SerializerFeature.DisableCircularReferenceDetect, true);
-            if (features != null) {
-                for (SerializerFeature reature : features) {
-                    serializer.config(reature, true);
-                }
+    protected <T> List<T> doParseJsonArray(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        if (!VerifyTools.isJsonArrayString(jsonString)) {
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR)
+                    .setDetails("parameter must be json array string.");
+        }
+
+        Feature[] features = generateParserFeatures(feature);
+        int featureValues = 0;
+        for (Feature f : features) {
+            featureValues = Feature.config(featureValues, f, true);
+        }
+
+        // 只有 JSON.parseObject(String, Class, Feature[])
+        // 没有 JSON.parseArray(String, Class, Feature[])
+        // 参考 JSON.parseArray(String, Class, ParserConfig)
+        ParserConfig parserConfig = ParserConfig.getGlobalInstance();
+        try (DefaultJSONParser parser = new DefaultJSONParser(jsonString, parserConfig, featureValues)) {
+            JSONLexer lexer = parser.lexer;
+            int token = lexer.token();
+            if (token == JSONToken.NULL) {
+                lexer.nextToken();
+                return new ArrayList<>();
+            } else if (token == JSONToken.EOF && lexer.isBlankInput()) {
+                return new ArrayList<>();
+            } else {
+                List<T> list = new ArrayList<>();
+                parser.parseArray(clazz, list);
+                parser.handleResovleTask(list);
+                return list;
             }
-            serializer.write(object);
-            return out.toString();
         }
     }
 
-    /** 对象转换为字符串 **/
-    public static String toJsonString(Object object) {
-        return toJsonString(object, new SerializerFeature[0]);
-    }
-
-    /** 对象转换为字符串 **/
-    public static String toJsonString(Object object, SerializerFeature... features) {
-        if (object == null) {
-            return "null";
-        }
-        try (SerializeWriter out = new SerializeWriter()) {
-            JSONSerializer serializer = new JSONSerializer(out, JSON_CONFIG);
-            serializer.config(SerializerFeature.QuoteFieldNames, true);
-            serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
-            serializer.config(SerializerFeature.DisableCircularReferenceDetect, true);
-            if (features != null) {
-                for (SerializerFeature reature : features) {
-                    serializer.config(reature, true);
-                }
+    @Override
+    public List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature) {
+        List<Object> objects = parseAsObjects(jsonString, Object.class, feature);
+        if (objects.isEmpty()) {
+            return new ArrayList<>();
+        }
+        List<Map<String, Object>> maps = new ArrayList<>();
+        for (int i = 0; i < objects.size(); i++) {
+            Object item = objects.get(i);
+            if (item instanceof Map) {
+                maps.add(MapTools.toJsonMap((Map<?, ?>) item));
+            } else {
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR)
+                        .setDetails("parameter[" + i + "] can not convert to Map.");
             }
-            serializer.write(object);
-            return out.toString();
         }
+        return maps;
     }
 
     /**
-     * 将Map内容设置到Java对象中<br>
-     * copy from JavaBeanDeserializer.createInstance(Map<String, Object>, ParserConfig);
+     * 将Map内容设置到Java对象中
      *
      * @param map Map
      * @param bean 目标Java对象
      */
-    public static void mapFillToBean(Map<String, ?> map, Object bean) {
-        Class<?> clazz = bean.getClass();
-        ParserConfig config = ParserConfig.getGlobalInstance();
-        ObjectDeserializer deserializer = config.getDeserializer(clazz);
-        if (!(deserializer instanceof JavaBeanDeserializer)) {
-            throw new JSONException("can not get javaBeanDeserializer. " + clazz.getName());
+    @Override
+    public void mapFillToBean(Map<String, ?> map, Object bean) {
+        if (map == null || map.isEmpty()) {
+            return;
         }
-        JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer) deserializer;
+        ParserConfig config = ParserConfig.getGlobalInstance();
         try {
-            mapFillToBean(map, bean, config, javaBeanDeser);
+            MapFillToBeanHandler handler = new MapFillToBeanHandler(bean, config);
+            handler.createInstance(MapTools.toJsonMap(map), config);
+        } catch (JSONException e) {
+            throw e;
         } catch (Exception e) {
             throw new JSONException(e.getMessage(), e);
         }
     }
 
-    private static void mapFillToBean(Map<String, ?> map, Object bean, ParserConfig config,
-            JavaBeanDeserializer javaBeanDeser) throws IllegalArgumentException, IllegalAccessException {
-        for (Map.Entry<String, ?> entry : map.entrySet()) {
-            String key = entry.getKey();
-            Object value = entry.getValue();
-
-            FieldDeserializer fieldDeser = javaBeanDeser.smartMatch(key);
-            if (fieldDeser == null) {
-                continue;
-            }
-
-            final FieldInfo fieldInfo = fieldDeser.fieldInfo;
-            Field field = fieldDeser.fieldInfo.field;
-            Type paramType = fieldInfo.fieldType;
-
-            if (field != null) {
-                Class<?> fieldType = field.getType();
-                if (fieldType == boolean.class) {
-                    if (value == Boolean.FALSE) {
-                        field.setBoolean(bean, false);
-                        continue;
-                    }
-
-                    if (value == Boolean.TRUE) {
-                        field.setBoolean(bean, true);
-                        continue;
-                    }
-                } else if (fieldType == int.class) {
-                    if (value instanceof Number) {
-                        field.setInt(bean, ((Number) value).intValue());
-                        continue;
-                    }
-                } else if (fieldType == long.class) {
-                    if (value instanceof Number) {
-                        field.setLong(bean, ((Number) value).longValue());
-                        continue;
-                    }
-                } else if (fieldType == float.class) {
-                    if (value instanceof Number) {
-                        field.setFloat(bean, ((Number) value).floatValue());
-                        continue;
-                    } else if (value instanceof String) {
-                        String strVal = (String) value;
-                        float floatValue;
-                        if (strVal.length() <= 10) {
-                            floatValue = TypeUtils.parseFloat(strVal);
-                        } else {
-                            floatValue = Float.parseFloat(strVal);
-                        }
-
-                        field.setFloat(bean, floatValue);
-                        continue;
-                    }
-                } else if (fieldType == double.class) {
-                    if (value instanceof Number) {
-                        field.setDouble(bean, ((Number) value).doubleValue());
-                        continue;
-                    } else if (value instanceof String) {
-                        String strVal = (String) value;
-                        double doubleValue;
-                        if (strVal.length() <= 10) {
-                            doubleValue = TypeUtils.parseDouble(strVal);
-                        } else {
-                            doubleValue = Double.parseDouble(strVal);
-                        }
-
-                        field.setDouble(bean, doubleValue);
-                        continue;
-                    }
-                } else if (value != null && paramType == value.getClass()) {
-                    field.set(bean, value);
-                    continue;
-                }
-            }
-
-            String format = fieldInfo.format;
-            if (format != null && paramType == Date.class) {
-                value = TypeUtils.castToDate(value, format);
-            } else {
-                if (paramType instanceof ParameterizedType) {
-                    value = TypeUtils.cast(value, (ParameterizedType) paramType, config);
-                } else {
-                    value = TypeUtils.cast(value, paramType, config);
-                }
-            }
-
-            fieldDeser.setValue(bean, value);
-        }
-    }
-
     /**
      * 将Map转换为Java对象
      *
@@ -377,12 +201,12 @@ public class JsonTools {
      * @param clazz 目标Java类
      * @return Java对象
      */
-    public static <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
+    @Override
+    public <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
         if (map == null) {
             return null;
         }
-        @SuppressWarnings("unchecked")
-        Map<String, Object> json = (Map<String, Object>) map;
+        Map<String, Object> json = MapTools.toJsonMap(map);
         return TypeUtils.castToJavaBean(json, clazz, ParserConfig.getGlobalInstance());
     }
 
@@ -390,85 +214,24 @@ public class JsonTools {
      * 将Java对象转换为Map<br>
      * copy from fastjson JSON.toJSON(), 保留enum和date
      *
-     * @param bean JavaBean对象
-     * @return Map
-     */
-    public static Map<String, Object> beanToMap(Object bean) {
-        if (bean == null) {
-            return null;
-        }
-        return beanToMap(bean, true, true);
-    }
-
-    /**
-     * 将Java对象转换为Map
-     *
-     * @param object Java对象
+     * @param bean Java对象
      * @param deep 是否递归转换子对象
      * @param clearBlankValue 是否清除空值
      * @return Map
      */
-    public static Map<String, Object> beanToMap(Object object, boolean deep, boolean clearBlankValue) {
-        if (object == null) {
+    @Override
+    public Map<String, Object> beanToMap(Object bean, boolean deep, boolean clearBlankValue) {
+        if (bean == null) {
             return null;
         }
-        Map<String, Object> map = getBeanFieldValuesMap(object, deep, SerializeConfig.getGlobalInstance());
+        Map<String, Object> map = getBeanFieldValuesMap(bean, deep, SerializeConfig.getGlobalInstance());
         if (clearBlankValue) {
             MapTools.clearBlankValue(map);
         }
         return map;
     }
 
-    /**
-     * 将Map数组转换为Java对象列表
-     *
-     * @param <T> 目标类型
-     * @param maps Map数组
-     * @param clazz 目标Java类
-     * @return Java对象
-     */
-    public static <T> List<T> mapToBeans(Collection<Map<String, ?>> maps, Class<T> clazz) {
-        if (maps == null) {
-            return null;
-        }
-        List<T> result = new ArrayList<>();
-        for (Map<String, ?> map : maps) {
-            result.add(mapToBean(map, clazz));
-        }
-        return result;
-    }
-
-    /**
-     * 将Java对象转换为Map列表<br>
-     * copy from fastjson JSON.toJSON(), 保留enum和date
-     *
-     * @param beans JavaBean对象列表
-     * @return Map列表
-     */
-    public static List<Map<String, Object>> beanToMaps(Collection<?> beans) {
-        return beanToMaps(beans, true, true);
-    }
-
-    /**
-     * 将Java对象转换为Map列表
-     *
-     * @param beans Java对象
-     * @param deep 是否递归转换子对象
-     * @param clearBlankValue 是否清除空值
-     * @return Map列表
-     */
-    public static List<Map<String, Object>> beanToMaps(Collection<?> beans, boolean deep, boolean clearBlankValue) {
-        if (beans == null) {
-            return null;
-        }
-        List<Map<String, Object>> result = new ArrayList<>();
-        for (Object bean : beans) {
-            result.add(beanToMap(bean, true, true));
-        }
-        return result;
-    }
-
-    protected static JSONObject getBeanFieldValuesMap(Object bean, boolean deep, SerializeConfig config) {
+    protected JSONObject getBeanFieldValuesMap(Object bean, boolean deep, SerializeConfig config) {
         if (bean == null) {
             return null;
         }
@@ -552,31 +315,42 @@ public class JsonTools {
         throw new IllegalArgumentException(bean.getClass().getSimpleName() + " can't convert to map.");
     }
 
-    private static Object beanToJson(Object bean, SerializeConfig config) {
-        if (bean == null) {
+    /**
+     * 将JavaBean对象转换为JSON对象<br>
+     * 复制代码是为了保留日期/枚举不要被序列化
+     *
+     * @param javaObject JavaBean对象
+     * @param config 序列化配置
+     * @return JSON对象
+     * @see JSON#toJSON(Object, SerializeConfig)
+     */
+    private Object beanToJson(Object javaObject, SerializeConfig config) {
+        if (javaObject == null) {
             return null;
         }
 
-        if (bean instanceof JSON) {
-            return bean;
+        if (javaObject instanceof JSON) {
+            return javaObject;
         }
 
-        if (bean instanceof Map) {
-            Map<?, ?> map = (Map<?, ?>) bean;
+        if (javaObject instanceof Map) {
+            Map<?, ?> map = (Map<?, ?>) javaObject;
 
+            int size = map.size();
             Map<String, Object> innerMap;
             if (map instanceof LinkedHashMap) {
-                innerMap = new LinkedHashMap<>(map.size());
+                innerMap = new LinkedHashMap<>(size);
             } else if (map instanceof TreeMap) {
                 innerMap = new TreeMap<>();
             } else {
-                innerMap = new HashMap<>(map.size());
+                innerMap = new HashMap<>(size);
             }
 
             JSONObject json = new JSONObject(innerMap);
 
             for (Map.Entry<?, ?> entry : map.entrySet()) {
-                String jsonKey = TypeUtils.castToString(entry.getKey());
+                Object key = entry.getKey();
+                String jsonKey = TypeUtils.castToString(key);
                 Object jsonValue = beanToJson(entry.getValue(), config);
                 json.put(jsonKey, jsonValue);
             }
@@ -584,8 +358,8 @@ public class JsonTools {
             return json;
         }
 
-        if (bean instanceof Collection) {
-            Collection<?> collection = (Collection<?>) bean;
+        if (javaObject instanceof Collection) {
+            Collection<?> collection = (Collection<?>) javaObject;
 
             JSONArray array = new JSONArray(collection.size());
 
@@ -597,34 +371,31 @@ public class JsonTools {
             return array;
         }
 
-        if (bean instanceof JSONSerializable) {
-            String json = JSON.toJSONString(bean);
+        if (javaObject instanceof JSONSerializable) {
+            String json = JSON.toJSONString(javaObject);
             return JSON.parse(json);
         }
 
-        Class<?> clazz = bean.getClass();
+        Class<?> clazz = javaObject.getClass();
 
         if (clazz.isEnum()) {
             // return ((Enum<?>) bean).name();
-            return bean;
+            return javaObject;
         }
         if (clazz == String.class) {
-            return bean;
+            return javaObject;
         }
         if (CharSequence.class.isAssignableFrom(clazz)) {
-            return bean.toString();
-        }
-        if (ReflectTools.isPrimitive(clazz, false)) {
-            return bean; // 保留原始类型及原始类型的包装类型/字符串/日期/枚举/Number的所有子类
+            return javaObject.toString();
         }
 
         if (clazz.isArray()) {
-            int len = Array.getLength(bean);
+            int len = Array.getLength(javaObject);
 
             JSONArray array = new JSONArray(len);
 
             for (int i = 0; i < len; ++i) {
-                Object item = Array.get(bean, i);
+                Object item = Array.get(javaObject, i);
                 Object jsonValue = beanToJson(item, config);
                 array.add(jsonValue);
             }
@@ -632,13 +403,33 @@ public class JsonTools {
             return array;
         }
 
+        if (ReflectTools.isPrimitive(clazz, false)) {
+            // 保留原始类型及原始类型的包装类型/字符串/日期/枚举/Number的所有子类
+            return javaObject;
+        }
+
         ObjectSerializer serializer = config.getObjectWriter(clazz);
         if (serializer instanceof JavaBeanSerializer) {
             JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer) serializer;
 
-            JSONObject json = new JSONObject();
+            boolean ordered = false;
+            if (serializerHasGetJsonType) {
+                JSONType jsonType = javaBeanSerializer.getJSONType();
+                if (jsonType != null) {
+                    for (SerializerFeature serializerFeature : jsonType.serialzeFeatures()) {
+                        if (serializerFeature == SerializerFeature.SortField
+                                || serializerFeature == SerializerFeature.MapSortField) {
+                            ordered = true;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            JSONObject json = new JSONObject(ordered);
             try {
-                Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(bean);
+                // 自fastjson-1.2.11开始, 有getFieldValuesMap()方法
+                Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
                 for (Map.Entry<String, Object> entry : values.entrySet()) {
                     json.put(entry.getKey(), beanToJson(entry.getValue(), config));
                 }
@@ -648,74 +439,129 @@ public class JsonTools {
             return json;
         }
 
-        String text = JSON.toJSONString(bean);
+        String text = JSON.toJSONString(javaObject);
         return JSON.parse(text);
     }
 
-    /**
-     * 将字符串转换为KeyString对象<br>
-     * toKeyString("{'key':1,'value':'冷水'}")<br>
-     * 或: toKeyString("{'1':'冷水'}")<br>
-     *
-     * @param text 字符串
-     * @return KeyString对象
-     */
-    public static KeyString toKeyString(String text) {
-        if (VerifyTools.isBlank(text)) {
-            return null;
-        }
-        JSONObject json = JSON.parseObject(text);
-        return toKeyString(json);
-    }
-
-    /**
-     * 将字符串转换为KeyString对象数组<br>
-     * toKeyStrings("[{'key':1,'value':'冷水'},{'key':2,'value':'热水'}]")<br>
-     * 或: toKeyStrings("{'1':'冷水','2':'热水','3':'直饮水'}")<br>
-     * --&gt; List&lt;KeyString&gt;
-     *
-     * @param text 字符串
-     * @return List&lt;KeyString&gt; 对象数组
-     */
-    public static List<KeyString> toKeyStrings(String text) {
-        if (VerifyTools.isBlank(text)) {
-            return null;
-        }
-        List<KeyString> list = new ArrayList<>();
 
-        Object object = JSON.parse(text);
-        if (object instanceof JSONArray) {
-            JSONArray array = (JSONArray) object;
-            for (Object i : array) {
-                list.add(toKeyString((JSONObject) i));
-            }
-        } else if (object instanceof JSONObject) {
-            JSONObject json = (JSONObject) object;
-            for (Map.Entry<String, Object> entry : json.entrySet()) {
-                String key = entry.getKey();
-                Object value = entry.getValue();
-                String string = TypeUtils.castToJavaBean(value, String.class);
-                list.add(new KeyString(key, string));
+    protected static JSONSerializer generateJsonSerializer(SerializeWriter writer, JsonFeature.Serialization feature) {
+        SerializeConfig config = new SerializeConfig();
+        if (!feature.isWriteDateAsTimestamp() && feature.getDateFormatPattern() != null) {
+            config.put(Date.class, new SimpleDateFormatSerializer(feature.getDateFormatPattern()));
+        }
+        if (feature.getDoubleFormatPattern() != null) {
+            config.put(Double.class, new DoubleSerializer(feature.getDoubleFormatPattern()));
+        }
+        JSONSerializer serializer = new JSONSerializer(writer, config);
+        // fastjson无法按对象的原始字段顺序输出, 设置为true是按字母顺序, 设置为false则是乱序
+        // serializer.config(SerializerFeature.SortField, false);
+        // 是否处理循环引用 (设置为true就不会报错)
+        serializer.config(SerializerFeature.DisableCircularReferenceDetect, !feature.isHandleCircularReference());
+        // 字段名是否使用引号括起来
+        serializer.config(SerializerFeature.QuoteFieldNames, feature.isQuoteFieldNames());
+        // 是否使用单引号
+        serializer.config(SerializerFeature.UseSingleQuotes, feature.isUseSingleQuotes());
+        // 是否输出值为null的字段
+        // serializer.config(SerializerFeature.WriteFieldNullValue, feature.isWriteFieldNullValue());
+        // 是否输出Map中的null值
+        serializer.config(SerializerFeature.WriteMapNullValue, feature.isWriteMapNullValue());
+        // 输出枚举时是否使用toString
+        serializer.config(SerializerFeature.WriteEnumUsingToString, feature.isWriteEnumUsingToString());
+        // 序列化List类型数据时是否将null输出为空数组
+        serializer.config(SerializerFeature.WriteNullListAsEmpty, feature.isWriteNullListAsEmpty());
+        // 序列化String类型数据时是否将null输出为空字符串
+        serializer.config(SerializerFeature.WriteNullStringAsEmpty, feature.isWriteNullStringAsEmpty());
+        // 序列化Number类型数据时是否将null输出为0
+        serializer.config(SerializerFeature.WriteNullNumberAsZero, feature.isWriteNullNumberAsZero());
+        // 序列化Boolean类型数据时是否将null输出为false
+        serializer.config(SerializerFeature.WriteNullBooleanAsFalse, feature.isWriteNullBooleanAsFalse());
+        // 序列化BigDecimal类型数据时是否使用toPlainString()
+        // Since: 1.2.16
+        serializer.config(SerializerFeature.WriteBigDecimalAsPlain, feature.isWriteBigDecimalAsPlain());
+        // 是否跳过transient修饰的字段
+        serializer.config(SerializerFeature.SkipTransientField, feature.isSkipTransientField());
+        // 是否输出格式化后的Json字符串
+        serializer.config(SerializerFeature.PrettyFormat, feature.isPrettyFormat());
+        // 序列化Date类型数据时是否输出为long时间戳
+        if (feature.isWriteDateAsTimestamp()) {
+            serializer.config(SerializerFeature.WriteDateUseDateFormat, false);
+        } else {
+            serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
+            if (feature.getDateFormatPattern() != null) {
+                serializer.setDateFormat(feature.getDateFormatPattern());
             }
         }
+        return serializer;
+    }
+
+    protected static Feature[] generateParserFeatures(JsonFeature.Deserialization feature) {
+        List<Feature> config = new ArrayList<>();
+        // Since: 1.2.42
+        if (hasNonStringKeyAsString) {
+            config.add(Feature.NonStringKeyAsString);
+        }
+        // 是否自动关闭流
+        config.add(Feature.AutoCloseSource);
+        // 对字段名调用String.intern()
+        config.add(Feature.InternFieldNames);
+        // 是否允许Json字符串中带注释
+        config.add(Feature.AllowComment);
+        // 是否允许json字段名不被引号括起来
+        config.add(Feature.AllowUnQuotedFieldNames);
+        // 是否允许json字段名和字段值使用单引号括起来
+        config.add(Feature.AllowSingleQuotes);
+        // 是否允许多余的逗号
+        config.add(Feature.AllowArbitraryCommas);
+        // 排序的字段顺序快速匹配
+        config.add(Feature.SortFeidFastMatch);
+        // 忽略不匹配
+        config.add(Feature.IgnoreNotMatch);
+        // 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象
+        if (feature.isUseBigDecimalForFloats()) {
+            config.add(Feature.UseBigDecimal);
+        }
+        // 遇到未知属性时是否抛出异常 ( FastJson不支持, 默认不会抛出异常 )
+        // if (feature.isFailOnUnknownFields()) {
+        // }
+        // 遇到未知的枚举值时是否抛出异常
+        if (feature.isFailOnUnknownEnumValues() && hasErrorOnEnumNotMatch) {
+            // Since: 1.2.55
+            config.add(Feature.ErrorOnEnumNotMatch);
+        }
+        // 是否将null字段串初始化为空字符串
+        if (feature.isReadNullStringAsEmpty()) {
+            config.add(Feature.InitStringFieldAsEmpty);
+        }
+        return config.toArray(new Feature[0]);
+    }
 
-        Collections.sort(list);
-        return list;
+    // Since: 1.2.42
+    private static final boolean hasErrorOnEnumNotMatch;
+    // Since: 1.2.55
+    private static final boolean hasNonStringKeyAsString;
+    // Since: 1.2.76
+    private static final boolean serializerHasGetJsonType;
 
+    static {
+        hasErrorOnEnumNotMatch = ReflectTools.findField(Feature.class, "ErrorOnEnumNotMatch") != null;
+        hasNonStringKeyAsString = ReflectTools.findField(Feature.class, "NonStringKeyAsString") != null;
+        serializerHasGetJsonType = ReflectTools.findMethod(JavaBeanSerializer.class, "getJSONType") != null;
     }
 
-    private static KeyString toKeyString(JSONObject json) {
-        if (json.containsKey("key")) {
-            return JSON.toJavaObject(json, KeyString.class);
-        } else {
-            for (Map.Entry<String, Object> entry : json.entrySet()) {
-                String key = entry.getKey();
-                Object value = entry.getValue();
-                String string = TypeUtils.castToJavaBean(value, String.class);
-                return new KeyString(key, string);
-            }
-            throw new IllegalArgumentException("json is empty.");
+    private static class MapFillToBeanHandler extends JavaBeanDeserializer {
+        private final Object bean;
+
+        public MapFillToBeanHandler(Object bean, ParserConfig config) {
+            super(config, bean.getClass());
+            this.bean = bean;
         }
-    }
 
+        @Override
+        public Object createInstance(DefaultJSONParser parser, Type type) {
+            // 调用此方法时: JavaBeanDeserializer.createInstance(Map<String, Object> map, ParserConfig config)
+            // 会调用本方法: createInstance(DefaultJSONParser parser, Type type)
+            // 这里直接返回构造方法中传入的bean, 达到将map赋值给已知对象的目的
+            return bean;
+        }
+    }
 }
diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForGson.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForGson.java
new file mode 100644
index 0000000..0ef7b30
--- /dev/null
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForGson.java
@@ -0,0 +1,178 @@
+package com.gitee.qdbp.json;
+
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.ToNumberPolicy;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * JsonService的Gson实现类
+ *
+ * @author zhaohuihua
+ * @version 20211207
+ */
+public class JsonServiceForGson extends JsonServiceForBase {
+
+    @Override
+    public <T> T convert(Object object, Class<T> clazz) {
+        String string = toJsonString(object, serializationFeature);
+        return parseAsObject(string, clazz, deserializationFeature);
+    }
+
+    @Override
+    public String toJsonString(Object object, JsonFeature.Serialization feature) {
+        if (object == null) {
+            return null;
+        }
+        Gson gson = generateGson(feature, deserializationFeature);
+        return gson.toJson(object);
+    }
+
+    @Override
+    public Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new LinkedHashMap<>();
+        }
+        Gson gson = generateGson(serializationFeature, feature);
+        Type type = new TypeToken<Map<String, Object>>() {}.getType();
+        return gson.fromJson(jsonString, type);
+    }
+
+    @Override
+    public List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        Gson gson = generateGson(serializationFeature, feature);
+        Type type = new TypeToken<List<Map<String, Object>>>() {}.getType();
+        return gson.fromJson(jsonString, type);
+    }
+
+    @Override
+    public <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return null;
+        }
+        Gson gson = generateGson(serializationFeature, feature);
+        return gson.fromJson(jsonString, clazz);
+    }
+
+    @Override
+    public <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        Gson gson = generateGson(serializationFeature, feature);
+        Type type = TypeToken.getParameterized(List.class, clazz).getType();
+        return gson.fromJson(jsonString, type);
+    }
+
+    protected Gson generateGson(JsonFeature.Serialization sf, JsonFeature.Deserialization df) {
+        GsonBuilder builder = new GsonBuilder();
+        // 禁用HTML转义
+        builder.disableHtmlEscaping();
+        // 宽容模式
+        builder.setLenient();
+        // 默认的转数字策略是ToNumberPolicy.DOUBLE, 导致所有的整数全转换为double了
+        builder.setObjectToNumberStrategy(ToNumberPolicy.LAZILY_PARSED_NUMBER);
+
+        // 是否处理循环引用 (设置为true时不应该报错)
+        // mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
+        // mapper.configure(SerializationFeature.WRITE_SELF_REFERENCES_AS_NULL, sf.isHandleCircularReference());
+        // 字段名是否使用引号括起来
+        // mapper.configure(JsonWriteFeature.QUOTE_FIELD_NAMES, feature.isQuoteFieldNames());
+        // 是否使用单引号 (jackson不支持)
+        // feature.isUseSingleQuotes()
+        // 是否输出值为null的字段
+        if (sf.isWriteMapNullValue()) {
+            builder.serializeNulls();
+        }
+        // 是否输出Map中的null值
+        // sf.isWriteMapNullValue()
+        // 输出枚举时是否使用toString
+        // mapper.configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, !sf.isWriteEnumUsingToString());
+        // mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, sf.isWriteEnumUsingToString());
+
+        // gson无法实现null转换为其他值, 因为TreeTypeAdapter的nullSafe=true
+        // 只有注解才通指定@JsonAdapter(nullSafe=false)
+        // 序列化List类型数据时是否将null输出为空数组
+        if (sf.isWriteNullListAsEmpty()) {
+        }
+        // 序列化String类型数据时是否将null输出为空字符串
+        if (sf.isWriteNullStringAsEmpty()) {
+            // builder.registerTypeAdapter(String.class, new StringNullValueSerializer());
+        }
+        // 序列化Number类型数据时是否将null输出为0
+        if (sf.isWriteNullNumberAsZero()) {
+            // builder.registerTypeAdapter(Number.class, new NumberNullValueSerializer());
+        }
+        // 序列化Boolean类型数据时是否将null输出为false
+        if (sf.isWriteNullBooleanAsFalse()) {
+            // builder.registerTypeAdapter(Boolean.class, new BooleanNullValueSerializer());
+        }
+
+        // 序列化BigDecimal类型数据时是否使用toPlainString()
+        if (sf.isWriteBigDecimalAsPlain()) {
+            builder.registerTypeAdapter(BigDecimal.class, new BigDecimalToPlainStringSerializer());
+        }
+        // 是否跳过transient修饰的字段
+        if (sf.isSkipTransientField()) {
+            builder.excludeFieldsWithModifiers(Modifier.TRANSIENT);
+        }
+        // 是否输出格式化后的Json字符串
+        if (sf.isPrettyFormat()) {
+            builder.setPrettyPrinting();
+        }
+        // 序列化Date类型数据时是否输出为long时间戳
+        if (sf.isWriteDateAsTimestamp()) {
+            builder.registerTypeAdapter(Date.class, new DateToTimestampSerializer());
+        }
+        if (!sf.isWriteDateAsTimestamp() && sf.getDateFormatPattern() != null) {
+            builder.setDateFormat(sf.getDateFormatPattern());
+        }
+
+        // // 是否允许Json字符串中带注释
+        // mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
+        // // 是否允许json字段名不被引号括起来
+        // mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
+        // // 是否允许json字段名和字段值使用单引号括起来
+        // mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
+        // // 遇到未知属性时是否抛出异常
+        // mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, df.isFailOnUnknownFields());
+        // // 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象
+        // mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, df.isUseBigDecimalForFloats());
+        // // 遇到未知属性时是否抛出异常
+        // mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, !df.isFailOnUnknownEnumValues());
+        // // 是否将null字段串初始化为空字符串
+        // // feature.isReadNullStringAsEmpty()
+
+        return builder.create();
+    }
+
+    private static class BigDecimalToPlainStringSerializer implements JsonSerializer<BigDecimal> {
+        @Override
+        public JsonElement serialize(BigDecimal src, Type typeOfSrc, JsonSerializationContext context) {
+            return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.toPlainString());
+        }
+    }
+
+    private static class DateToTimestampSerializer implements JsonSerializer<Date> {
+        @Override
+        public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
+            return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.getTime());
+        }
+    }
+}
diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java
new file mode 100644
index 0000000..43dfda3
--- /dev/null
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java
@@ -0,0 +1,166 @@
+package com.gitee.qdbp.json;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+
+/**
+ * JsonService的Jackson实现类
+ *
+ * @author zhaohuihua
+ * @version 20211207
+ */
+public class JsonServiceForJackson extends JsonServiceForBase {
+
+    @Override
+    public <T> T convert(Object object, Class<T> clazz) {
+        String string = toJsonString(object, serializationFeature);
+        return parseAsObject(string, clazz, deserializationFeature);
+    }
+
+    @Override
+    public String toJsonString(Object object, JsonFeature.Serialization feature) {
+        if (object == null) {
+            return null;
+        }
+        ObjectMapper mapper = generateObjectMapper(feature, deserializationFeature);
+        try {
+            return mapper.writeValueAsString(object);
+        } catch (JacksonException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e)
+                    .setDetails("{} to json string error", object.getClass().getSimpleName());
+        }
+    }
+
+    @Override
+    public Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new LinkedHashMap<>();
+        }
+        ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
+        try {
+            return mapper.readValue(jsonString, new TypeReference<Map<String, Object>>() {});
+        } catch (JacksonException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json object string error");
+        }
+    }
+
+    @Override
+    public List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
+        try {
+            return mapper.readValue(jsonString, new TypeReference<List<Map<String, Object>>>() {});
+        } catch (JacksonException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json array string error");
+        }
+    }
+
+    @Override
+    public <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return null;
+        }
+        ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
+        try {
+            return mapper.readValue(jsonString, clazz);
+        } catch (JacksonException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json object string error");
+        }
+    }
+
+    @Override
+    public <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
+        JavaType targetType = mapper.getTypeFactory().constructCollectionType(List.class, clazz);
+        try {
+            return mapper.readValue(jsonString, targetType);
+        } catch (JacksonException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json array string error");
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    protected ObjectMapper generateObjectMapper(JsonFeature.Serialization sf, JsonFeature.Deserialization df) {
+        ObjectMapper mapper = new ObjectMapper();
+        // 是否处理循环引用 (设置为true时不应该报错)
+        mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
+        mapper.configure(SerializationFeature.WRITE_SELF_REFERENCES_AS_NULL, sf.isHandleCircularReference());
+        // 字段名是否使用引号括起来
+        // mapper.configure(JsonWriteFeature.QUOTE_FIELD_NAMES, feature.isQuoteFieldNames());
+        // 是否使用单引号 (jackson不支持)
+        // feature.isUseSingleQuotes()
+        // 是否输出值为null的字段
+        // sf.isWriteFieldNullValue()
+        // 是否输出Map中的null值
+        mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, sf.isWriteMapNullValue());
+        // 输出枚举时是否使用toString
+        mapper.configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, !sf.isWriteEnumUsingToString());
+        mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, sf.isWriteEnumUsingToString());
+        // 序列化List类型数据时是否将null输出为空数组
+        if (sf.isWriteNullListAsEmpty()) {
+
+        }
+        // 序列化String类型数据时是否将null输出为空字符串
+        if (sf.isWriteNullStringAsEmpty()) {
+
+        }
+        // 序列化Number类型数据时是否将null输出为0
+        if (sf.isWriteNullNumberAsZero()) {
+
+        }
+        // 序列化Boolean类型数据时是否将null输出为false
+        if (sf.isWriteNullBooleanAsFalse()) {
+
+        }
+        // 序列化BigDecimal类型数据时是否使用toPlainString()
+        // Since: 1.2.16
+        mapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, sf.isWriteBigDecimalAsPlain());
+        // 是否跳过transient修饰的字段
+        if (sf.isSkipTransientField()) {
+
+        }
+        // 是否输出格式化后的Json字符串
+        mapper.configure(SerializationFeature.INDENT_OUTPUT, sf.isPrettyFormat());
+        // 序列化Date类型数据时是否输出为long时间戳
+        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, sf.isWriteDateAsTimestamp());
+        if (!sf.isWriteDateAsTimestamp() && sf.getDateFormatPattern() != null) {
+            mapper.setDateFormat(new SimpleDateFormat(sf.getDateFormatPattern()));
+        }
+
+
+        // 是否允许Json字符串中带注释
+        mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
+        // 是否允许json字段名不被引号括起来
+        mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
+        // 是否允许json字段名和字段值使用单引号括起来
+        mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
+        // 遇到未知属性时是否抛出异常
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, df.isFailOnUnknownFields());
+        // 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象
+        mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, df.isUseBigDecimalForFloats());
+        // 遇到未知属性时是否抛出异常
+        mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, !df.isFailOnUnknownEnumValues());
+        // 是否将null字段串初始化为空字符串
+        // feature.isReadNullStringAsEmpty()
+
+        return mapper;
+    }
+}
diff --git a/pom.xml b/pom.xml
index 33e812d..55de679 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,7 +29,9 @@
 
 	<modules>
 		<module>able</module>
+		<module>json</module>
 		<module>tools</module>
+		<module>test</module>
 	</modules>
 
 	<properties>
diff --git a/test/pom.xml b/test/pom.xml
new file mode 100644
index 0000000..5135b23
--- /dev/null
+++ b/test/pom.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-parent</artifactId>
+		<version>5.0.1</version>
+	</parent>
+
+	<artifactId>qdbp-json-test</artifactId>
+	<packaging>pom</packaging>
+	<version>5.5.0</version>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<modules>
+		<module>qdbp-json-test-base</module>
+		<module>qdbp-json-test-fastjson</module>
+		<module>qdbp-json-test-gson</module>
+		<module>qdbp-json-test-jackson</module>
+	</modules>
+</project>
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
new file mode 100644
index 0000000..4761829
--- /dev/null
+++ b/test/qdbp-json-test-base/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.0</version>
+	</parent>
+
+	<artifactId>qdbp-json-test-base</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>jcl-over-slf4j</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>log4j-over-slf4j</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-core</artifactId>
+			<version>1.2.3</version>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-classic</artifactId>
+			<version>1.2.3</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.testng</groupId>
+			<artifactId>testng</artifactId>
+			<version>6.14.3</version>
+		</dependency>
+
+	</dependencies>
+</project>
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java
new file mode 100644
index 0000000..ed8e6a5
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java
@@ -0,0 +1,107 @@
+package com.gitee.qdbp.json.test;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.testng.Assert;
+import com.gitee.qdbp.json.JsonFeature;
+import com.gitee.qdbp.json.JsonService;
+import com.gitee.qdbp.json.test.entity.Address;
+import com.gitee.qdbp.json.test.entity.Child;
+import com.gitee.qdbp.json.test.entity.Father;
+import com.gitee.qdbp.json.test.entity.Gender;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
+
+/**
+ * TestCase1
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class TestCase1 {
+
+    public void test1() {
+        JsonService jsonService = JsonTools.findDefaultJsonService();
+        Assert.assertNotNull(jsonService);
+    }
+
+    public void test2() {
+        Father father = createTestData();
+
+        {
+            String jsonString = JsonTools.toJsonString(father, JsonFeature.serialization()
+                    .setQuoteFieldNames(false).setUseSingleQuotes(true).setHandleCircularReference(false));
+            System.out.println(jsonString);
+        }
+        {
+            String jsonString = JsonTools.toJsonString(father, JsonFeature.serialization()
+                    .setWriteNullNumberAsZero(true)
+                    .setWriteNullBooleanAsFalse(true)
+                    .setWriteNullStringAsEmpty(true)
+                    .setWriteNullListAsEmpty(true)
+                    .setWriteBigDecimalAsPlain(false)
+                    .setQuoteFieldNames(false)
+                    .setUseSingleQuotes(true)
+                    .setHandleCircularReference(false));
+            System.out.println(jsonString);
+        }
+    }
+
+    private Father createTestData() {
+        Address address = new Address();
+        address.setDetails("address1");
+
+        Father father = new Father();
+        father.setName("father1");
+        father.setGender(Gender.FEMALE);
+        father.setAge(40);
+        father.setHeight(170D);
+        father.setWeight(new BigDecimal("6.217e+18"));
+        father.addAddress(address);
+
+        Child child1 = new Child();
+        child1.setName("child1");
+        // child1.setFather(father);
+        child1.addAddress(address);
+        Child child2 = new Child();
+        child2.setName("child2");
+        // child2.setFather(father);
+        child2.addAddress(address);
+        father.addChild(child1);
+        father.addChild(child2);
+        return father;
+    }
+
+    public void test3() {
+        String jsonString = "{addresses:[{details:'address1'}],age:40,children:[{addresses:[{details:'address1'}],name:'child1'},{addresses:[{details:'address1'}],name:'child2'}],gender:'FEMALE',height:170,name:'father1',weight:75}";
+        Father father = JsonTools.parseAsObject(jsonString, Father.class);
+        Assert.assertNotNull(father);
+        Assert.assertEquals(father.getChildren().size(), 2);
+    }
+
+    public void test4() {
+        String jsonString = "[{addresses:[{details:'address1'}],name:'child1'},{addresses:[{details:'address1'}],name:'child2'}]";
+        List<Child> children = JsonTools.parseAsObjects(jsonString, Child.class);
+        Assert.assertNotNull(children);
+        Assert.assertEquals(children.size(), 2);
+        Assert.assertEquals(children.get(0).getClass(), Child.class);
+    }
+
+    @SuppressWarnings("deprecation")
+    public void test5() {
+        Father father = createTestData();
+        Map<String, Object> map = JsonTools.beanToMap(father);
+        Object children = map.get("children");
+        Assert.assertTrue(Collection.class.isAssignableFrom(children.getClass()));
+        List<Child> children1 = MapTools.getList(map, "children", Child.class);
+        Assert.assertEquals(children1.size(), 2);
+        Assert.assertEquals(JsonTools.getMapString(map, "name"), "father1");
+
+        Father newer = JsonTools.mapToBean(map, Father.class);
+        Assert.assertEquals(newer.getName(), "father1");
+        Assert.assertEquals(newer.getChildren().size(), 2);
+        Assert.assertEquals(newer.getChildren().get(0).getClass(), Child.class);
+    }
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Address.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Address.java
new file mode 100644
index 0000000..d9cd96d
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Address.java
@@ -0,0 +1,20 @@
+package com.gitee.qdbp.json.test.entity;
+
+/**
+ * Address
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class Address {
+
+    private String details;
+
+    public String getDetails() {
+        return details;
+    }
+
+    public void setDetails(String details) {
+        this.details = details;
+    }
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Child.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Child.java
new file mode 100644
index 0000000..587321b
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Child.java
@@ -0,0 +1,20 @@
+package com.gitee.qdbp.json.test.entity;
+
+/**
+ * Child
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class Child extends Person {
+
+    private Father father;
+
+    public Father getFather() {
+        return father;
+    }
+
+    public void setFather(Father father) {
+        this.father = father;
+    }
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Father.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Father.java
new file mode 100644
index 0000000..8efa53c
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Father.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.json.test.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Father
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class Father extends Person {
+
+    private List<Child> children = new ArrayList<>();
+
+    public List<Child> getChildren() {
+        return children;
+    }
+
+    public void setChildren(List<Child> children) {
+        this.children = children;
+    }
+
+    public void addChild(Child child) {
+        this.children.add(child);
+    }
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Gender.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Gender.java
new file mode 100644
index 0000000..d778f8b
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Gender.java
@@ -0,0 +1,21 @@
+package com.gitee.qdbp.json.test.entity;
+
+/**
+ * Gender枚举类<br>
+ * 性别:0.未知|1.男|2.女
+ *
+ * @author zhh
+ * @version 150917
+ */
+public enum Gender {
+    
+    /** 0.未知 **/
+    UNKNOWN,
+    
+    /** 1.男 **/
+    MALE,
+    
+    /** 2.女 **/
+    FEMALE
+    
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Person.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Person.java
new file mode 100644
index 0000000..b0ab1df
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Person.java
@@ -0,0 +1,78 @@
+package com.gitee.qdbp.json.test.entity;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Person
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class Person {
+
+    private String name;
+
+    private Gender gender;
+
+    private Integer age;
+
+    private Double height;
+
+    private BigDecimal weight;
+
+    private List<Address> addresses = new ArrayList<>();
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Gender getGender() {
+        return gender;
+    }
+
+    public void setGender(Gender gender) {
+        this.gender = gender;
+    }
+
+    public Integer getAge() {
+        return age;
+    }
+
+    public void setAge(Integer age) {
+        this.age = age;
+    }
+
+    public Double getHeight() {
+        return height;
+    }
+
+    public void setHeight(Double height) {
+        this.height = height;
+    }
+
+    public BigDecimal getWeight() {
+        return weight;
+    }
+
+    public void setWeight(BigDecimal weight) {
+        this.weight = weight;
+    }
+
+    public List<Address> getAddresses() {
+        return addresses;
+    }
+
+    public void setAddresses(List<Address> addresses) {
+        this.addresses = addresses;
+    }
+
+    public void addAddress(Address address) {
+        this.addresses.add(address);
+    }
+}
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
new file mode 100644
index 0000000..aa6b837
--- /dev/null
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.0</version>
+	</parent>
+
+	<artifactId>qdbp-json-test-fastjson</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json-test-base</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+			<version>1.2.16</version>
+		</dependency>
+
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.12.4</version>
+				<configuration>
+					<suiteXmlFiles>
+						<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
+					</suiteXmlFiles>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java b/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java
new file mode 100644
index 0000000..7948016
--- /dev/null
+++ b/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java
@@ -0,0 +1,43 @@
+package com.gitee.qdbp.json.test;
+
+import org.testng.annotations.Test;
+import com.alibaba.fastjson.JSON;
+
+/**
+ * TestCase1ForFastJson
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class TestCase1ForFastJson extends TestCase1 {
+    @Test
+    public void test0() {
+        System.out.println("fastjson version: " + JSON.VERSION);
+    }
+
+    @Test
+    public void test1() {
+        super.test1();
+    }
+
+    @Test
+    public void test2() {
+        super.test2();
+    }
+
+    @Test
+    public void test3() {
+        super.test3();
+    }
+
+    @Test
+    public void test4() {
+        super.test4();
+    }
+
+    @Test
+    public void test5() {
+        super.test5();
+    }
+}
diff --git a/test/qdbp-json-test-fastjson/src/test/resources/logback.xml b/test/qdbp-json-test-fastjson/src/test/resources/logback.xml
new file mode 100644
index 0000000..4a622e2
--- /dev/null
+++ b/test/qdbp-json-test-fastjson/src/test/resources/logback.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<configuration>
+	<property name="pattern.stdout" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\)%n" />
+	<property name="pattern.normal" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\) | %d{yyyy-MM-dd}%n" />
+
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>${pattern.stdout}</pattern>
+		</encoder>
+	</appender>
+	<appender name="ManagerFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>../logs/qdbp-json-test-fastjson.log</file>
+		<!-- 按天滚动 保存30天-->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>../logs/qdbp-json-test-fastjson-%d{yyyyMMdd}-%i.log</FileNamePattern>
+			<MaxHistory>30</MaxHistory>
+			<!-- 超过指定大小滚动当天日志 -->
+			<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<MaxFileSize>50MB</MaxFileSize>
+			</TimeBasedFileNamingAndTriggeringPolicy>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${pattern.normal}</pattern>
+		</encoder>
+	</appender>
+	<appender name ="ManagerAppender" class= "ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+		<queueSize>1024</queueSize>
+		<!-- 添加附加的appender,最多只能添加一个 -->
+	 	<appender-ref ref ="ManagerFile"/>
+	</appender>
+
+	<logger name="com.gitee.qdbp" level="DEBUG" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.sql.parse" level="ALL" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.plugins.impl.BaseSqlFileScanner" level="ALL" additivity="true" />
+	<!-- <logger name="org.springframework.orm.jpa" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="true" /> -->
+	<!-- <logger name="com.alibaba.druid.pool" level="DEBUG" additivity="true" /> -->
+
+	<!-- 只要使用了BeanPostProcessor的子类就很难避免此日志: is not eligible for getting processed by all BeanPostProcessors -->
+	<logger name="org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker" level="WARN" additivity="true" />
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ManagerAppender" />
+	</root>
+
+</configuration>
\ No newline at end of file
diff --git a/test/qdbp-json-test-fastjson/src/test/resources/testng.xml b/test/qdbp-json-test-fastjson/src/test/resources/testng.xml
new file mode 100644
index 0000000..9cba606
--- /dev/null
+++ b/test/qdbp-json-test-fastjson/src/test/resources/testng.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
+<suite name="JsonSuite" parallel="classes">
+	<test name="JsonTestForFastjson" verbose="2" preserve-order="true">
+		<packages>
+			<package name="com.gitee.qdbp.json.test" />
+		</packages>
+	</test>
+</suite>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
new file mode 100644
index 0000000..e60ef8c
--- /dev/null
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.0</version>
+	</parent>
+
+	<artifactId>qdbp-json-test-gson</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json-test-base</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.google.code.gson</groupId>
+			<artifactId>gson</artifactId>
+			<version>2.10.1</version>
+		</dependency>
+
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.12.4</version>
+				<configuration>
+					<suiteXmlFiles>
+						<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
+					</suiteXmlFiles>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java b/test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java
new file mode 100644
index 0000000..e0593db
--- /dev/null
+++ b/test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java
@@ -0,0 +1,40 @@
+package test;
+
+import org.testng.annotations.Test;
+import com.gitee.qdbp.json.test.TestCase1;
+import com.google.gson.Gson;
+
+/**
+ * TestCase1ForGson
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class TestCase1ForGson extends TestCase1 {
+
+    @Test
+    public void test1() {
+        super.test1();
+    }
+
+    @Test
+    public void test2() {
+        super.test2();
+    }
+
+    @Test
+    public void test3() {
+        super.test3();
+    }
+
+    @Test
+    public void test4() {
+        super.test4();
+    }
+
+    @Test
+    public void test5() {
+        super.test5();
+    }
+}
diff --git a/test/qdbp-json-test-gson/src/test/resources/logback.xml b/test/qdbp-json-test-gson/src/test/resources/logback.xml
new file mode 100644
index 0000000..16509ee
--- /dev/null
+++ b/test/qdbp-json-test-gson/src/test/resources/logback.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<configuration>
+	<property name="pattern.stdout" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\)%n" />
+	<property name="pattern.normal" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\) | %d{yyyy-MM-dd}%n" />
+
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>${pattern.stdout}</pattern>
+		</encoder>
+	</appender>
+	<appender name="ManagerFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>../logs/qdbp-json-test-gson.log</file>
+		<!-- 按天滚动 保存30天-->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>../logs/qdbp-json-test-gson-%d{yyyyMMdd}-%i.log</FileNamePattern>
+			<MaxHistory>30</MaxHistory>
+			<!-- 超过指定大小滚动当天日志 -->
+			<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<MaxFileSize>50MB</MaxFileSize>
+			</TimeBasedFileNamingAndTriggeringPolicy>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${pattern.normal}</pattern>
+		</encoder>
+	</appender>
+	<appender name ="ManagerAppender" class= "ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+		<queueSize>1024</queueSize>
+		<!-- 添加附加的appender,最多只能添加一个 -->
+	 	<appender-ref ref ="ManagerFile"/>
+	</appender>
+
+	<logger name="com.gitee.qdbp" level="DEBUG" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.sql.parse" level="ALL" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.plugins.impl.BaseSqlFileScanner" level="ALL" additivity="true" />
+	<!-- <logger name="org.springframework.orm.jpa" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="true" /> -->
+	<!-- <logger name="com.alibaba.druid.pool" level="DEBUG" additivity="true" /> -->
+
+	<!-- 只要使用了BeanPostProcessor的子类就很难避免此日志: is not eligible for getting processed by all BeanPostProcessors -->
+	<logger name="org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker" level="WARN" additivity="true" />
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ManagerAppender" />
+	</root>
+
+</configuration>
\ No newline at end of file
diff --git a/test/qdbp-json-test-gson/src/test/resources/testng.xml b/test/qdbp-json-test-gson/src/test/resources/testng.xml
new file mode 100644
index 0000000..d43b056
--- /dev/null
+++ b/test/qdbp-json-test-gson/src/test/resources/testng.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
+<suite name="JsonSuite" parallel="classes">
+	<test name="JsonTestForGson" verbose="2" preserve-order="true">
+		<packages>
+			<package name="com.gitee.qdbp.json.test" />
+		</packages>
+	</test>
+</suite>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
new file mode 100644
index 0000000..4f989c5
--- /dev/null
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.0</version>
+	</parent>
+
+	<artifactId>qdbp-json-test-jackson</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json-test-base</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+			<version>2.14.2</version>
+			<optional>true</optional>
+		</dependency>
+
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.12.4</version>
+				<configuration>
+					<suiteXmlFiles>
+						<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
+					</suiteXmlFiles>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java b/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java
new file mode 100644
index 0000000..4405c80
--- /dev/null
+++ b/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java
@@ -0,0 +1,44 @@
+package com.gitee.qdbp.json.test;
+
+import org.testng.annotations.Test;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * TestCase1ForJackson
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class TestCase1ForJackson extends TestCase1 {
+    @Test
+    public void test0() {
+        Package pkg = ObjectMapper.class.getPackage();
+        System.out.println("jackson version: " + pkg.getImplementationVersion());
+    }
+
+    @Test
+    public void test1() {
+        super.test1();
+    }
+
+    @Test
+    public void test2() {
+        super.test2();
+    }
+
+    @Test
+    public void test3() {
+        super.test3();
+    }
+
+    @Test
+    public void test4() {
+        super.test4();
+    }
+
+    @Test
+    public void test5() {
+        super.test5();
+    }
+}
diff --git a/test/qdbp-json-test-jackson/src/test/resources/logback.xml b/test/qdbp-json-test-jackson/src/test/resources/logback.xml
new file mode 100644
index 0000000..acff406
--- /dev/null
+++ b/test/qdbp-json-test-jackson/src/test/resources/logback.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<configuration>
+	<property name="pattern.stdout" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\)%n" />
+	<property name="pattern.normal" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\) | %d{yyyy-MM-dd}%n" />
+
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>${pattern.stdout}</pattern>
+		</encoder>
+	</appender>
+	<appender name="ManagerFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>../logs/qdbp-json-test-jackson.log</file>
+		<!-- 按天滚动 保存30天-->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>../logs/qdbp-json-test-jackson-%d{yyyyMMdd}-%i.log</FileNamePattern>
+			<MaxHistory>30</MaxHistory>
+			<!-- 超过指定大小滚动当天日志 -->
+			<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<MaxFileSize>50MB</MaxFileSize>
+			</TimeBasedFileNamingAndTriggeringPolicy>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${pattern.normal}</pattern>
+		</encoder>
+	</appender>
+	<appender name ="ManagerAppender" class= "ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+		<queueSize>1024</queueSize>
+		<!-- 添加附加的appender,最多只能添加一个 -->
+	 	<appender-ref ref ="ManagerFile"/>
+	</appender>
+
+	<logger name="com.gitee.qdbp" level="DEBUG" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.sql.parse" level="ALL" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.plugins.impl.BaseSqlFileScanner" level="ALL" additivity="true" />
+	<!-- <logger name="org.springframework.orm.jpa" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="true" /> -->
+	<!-- <logger name="com.alibaba.druid.pool" level="DEBUG" additivity="true" /> -->
+
+	<!-- 只要使用了BeanPostProcessor的子类就很难避免此日志: is not eligible for getting processed by all BeanPostProcessors -->
+	<logger name="org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker" level="WARN" additivity="true" />
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ManagerAppender" />
+	</root>
+
+</configuration>
\ No newline at end of file
diff --git a/test/qdbp-json-test-jackson/src/test/resources/testng.xml b/test/qdbp-json-test-jackson/src/test/resources/testng.xml
new file mode 100644
index 0000000..d37209d
--- /dev/null
+++ b/test/qdbp-json-test-jackson/src/test/resources/testng.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
+<suite name="JsonSuite" parallel="classes">
+	<test name="JsonTestForJackson" verbose="2" preserve-order="true">
+		<packages>
+			<package name="com.gitee.qdbp.json.test" />
+		</packages>
+	</test>
+</suite>
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java b/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
index 9106d11..5e3ae2d 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
@@ -129,7 +129,7 @@ public class MapPlus extends HashMap<String, Object> {
      * @return 字符串
      */
     public String getString(String key) {
-        return JsonTools.getMapString(this, key);
+        return MapTools.getString(this, key);
     }
 
     /**
@@ -141,7 +141,7 @@ public class MapPlus extends HashMap<String, Object> {
      * @return Map值
      */
     public <T> T getValue(String key, Class<T> clazz) {
-        return JsonTools.getMapValue(this, key, clazz);
+        return MapTools.getValue(this, key, clazz);
     }
 
     /**
@@ -153,7 +153,7 @@ public class MapPlus extends HashMap<String, Object> {
      * @return Map值
      */
     public <T> T getValue(String key, T defaults, Class<T> clazz) {
-        return JsonTools.getMapValue(this, key, defaults, clazz);
+        return MapTools.getValue(this, key, defaults, clazz);
     }
 
     /**
@@ -164,7 +164,7 @@ public class MapPlus extends HashMap<String, Object> {
      * @return Map值列表, 值不存在时将返回空List而不是null
      */
     public <T> List<T> getList(String key, Class<T> clazz) {
-        return JsonTools.getMapValues(this, key, clazz);
+        return MapTools.getList(this, key, clazz);
     }
 
     /**
@@ -174,7 +174,7 @@ public class MapPlus extends HashMap<String, Object> {
      * @return 子Map, 值不存在时将返回空Map而不是null
      */
     public Map<String, Object> getSubMap(String key) {
-        return JsonTools.getMapSubMap(this, key);
+        return MapTools.getSubMap(this, key);
     }
 
     /**
@@ -184,7 +184,7 @@ public class MapPlus extends HashMap<String, Object> {
      * @return 子Map数组, 值不存在时将返回空List而不是null
      */
     public List<Map<String, Object>> getSubMaps(String key) {
-        return JsonTools.getMapSubMaps(this, key);
+        return MapTools.getSubMaps(this, key);
     }
 
     protected boolean isComplexKey(String key) {
diff --git a/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java b/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
index cda0331..30ed780 100644
--- a/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
@@ -6,6 +6,7 @@ import java.util.Iterator;
 import java.util.List;
 import org.testng.Assert;
 import org.testng.annotations.Test;
+import com.gitee.qdbp.able.function.BaseFunction;
 import com.gitee.qdbp.tools.codec.CodeTools;
 import com.gitee.qdbp.tools.utils.AssertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
@@ -122,18 +123,18 @@ public class TreeNodesTest {
         return list;
     }
 
-    private static class KeyGetter implements Callable<String, String> {
+    private static class KeyGetter implements BaseFunction<String, String> {
 
         @Override
-        public String call(String s) {
+        public String apply(String s) {
             return s;
         }
     }
 
-    private static class ParentGetter implements Callable<String, String> {
+    private static class ParentGetter implements BaseFunction<String, String> {
 
         @Override
-        public String call(String s) {
+        public String apply(String s) {
             return codeTools.parent(s);
         }
     }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/PathToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/PathToolsTest.java
new file mode 100644
index 0000000..1760ab5
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/PathToolsTest.java
@@ -0,0 +1,43 @@
+package com.gitee.qdbp.tools.utils;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.files.PathTools;
+
+/**
+ * PathTools测试类
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class PathToolsTest {
+
+    @Test
+    public void getClasspathRelativePath1() {
+        String s1 = "jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties";
+        String r1 = PathTools.getClasspathRelativePath(s1);
+        Assert.assertEquals(r1, "settings/sqls/xxx.properties");
+    }
+
+    @Test
+    public void getClasspathRelativePath2() {
+        String s1 = "file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties";
+        String r1 = PathTools.getClasspathRelativePath(s1);
+        Assert.assertEquals(r1, "settings/sqls/xxx.properties");
+    }
+
+    @Test
+    public void getJarRelativePath1() {
+        String s1 = "jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties";
+        String r1 = PathTools.getJarRelativePath(s1);
+        Assert.assertEquals(r1, "xxx-1.0.0.jar!/settings/sqls/xxx.properties");
+    }
+
+    @Test
+    public void getJarRelativePath2() {
+        String s1 = "file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties";
+        String r1 = PathTools.getJarRelativePath(s1);
+        Assert.assertEquals(r1, "classes/settings/sqls/xxx.properties");
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/XmlToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/XmlToolsTest.java
index ed3d8b6..87c981e 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/XmlToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/XmlToolsTest.java
@@ -3,11 +3,11 @@ package com.gitee.qdbp.tools.utils;
 import java.io.IOException;
 import java.net.URL;
 import java.util.Map;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.gitee.qdbp.tools.files.PathTools;
-import com.gitee.qdbp.tools.utils.XmlTools.XmlToJsonOptions;
 import org.testng.Assert;
 import org.testng.annotations.Test;
+import com.gitee.qdbp.json.JsonFeature;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.XmlTools.XmlToJsonOptions;
 
 @Test
 public class XmlToolsTest {
@@ -131,9 +131,10 @@ public class XmlToolsTest {
 
     private void testXmlToJson(String xml, String json, XmlToJsonOptions options) {
         Object result = XmlTools.xmlToJson(xml, options);
-        String actual = JsonTools.toLogString(result,
-            // 使用单引号, 输出Map中的null值
-            SerializerFeature.UseSingleQuotes, SerializerFeature.WriteMapNullValue);
+        // 使用单引号, 输出Map中的null值
+        JsonFeature.Serialization feature = new JsonFeature.Serialization()
+                .setQuoteFieldNames(false).setUseSingleQuotes(true).setWriteMapNullValue(true);
+        String actual = JsonTools.toJsonString(result, feature);
         System.out.println("--------------------------------");
         System.out.println(xml);
         System.out.println(json);
-- 
Gitee


From f215954ff3c6687941aef7804cc1720d02a2aa99 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 30 Jan 2023 21:10:53 +0800
Subject: [PATCH 081/160] YamlTools

---
 .../com/gitee/qdbp/tools/utils/YamlTools.java | 68 +++++++++++++++++++
 1 file changed, 68 insertions(+)
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/utils/YamlTools.java

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/YamlTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/YamlTools.java
new file mode 100644
index 0000000..e86d559
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/YamlTools.java
@@ -0,0 +1,68 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * Yaml工具类
+ *
+ * @author zhaohuihua
+ * @version 20230118
+ */
+public class YamlTools {
+
+    /** 静态工具类私有构造方法 **/
+    private YamlTools() {
+    }
+
+    /** 将Java对象转换为Yaml字符串 **/
+    public static String toYamlString(Object object) {
+        Yaml yaml = createYaml();
+        return yaml.dump(object);
+    }
+
+    /** 将Yaml字符串解析为Map对象 **/
+    public static Map<String, Object> parseAsMap(String string) {
+        Yaml yaml = createYaml();
+        Object result = yaml.load(string);
+        return JsonTools.beanToMap(result, false, false);
+    }
+
+    /** 将Yaml字符串解析为Map数组 **/
+    public static List<Map<String, Object>> parseAsMaps(String string) {
+        Yaml yaml = createYaml();
+        Object object = yaml.load(string);
+        List<Object> list = ConvertTools.parseList(object);
+        return JsonTools.beanToMaps(list, false, false);
+    }
+
+    /** 将Yaml字符串解析为Java对象 **/
+    public static <T> T parseAsObject(String string, Class<T> clazz) {
+        Yaml yaml = createYaml();
+        return yaml.loadAs(string, clazz);
+    }
+
+    /** 将Yaml字符串解析为Java对象列表 **/
+    public static <T> List<T> parseAsObjects(String string, Class<T> clazz) {
+        Yaml yaml = createYaml();
+        Object object = yaml.load(string);
+        List<Object> list = ConvertTools.parseList(object);
+        List<T> results = new ArrayList<>();
+        for (Object item : list) {
+            results.add(JsonTools.convert(item, clazz));
+        }
+        return results;
+    }
+
+    private static Yaml createYaml() {
+        LoaderOptions options = new LoaderOptions();
+        options.setAllowDuplicateKeys(false);
+        options.setAllowRecursiveKeys(true);
+        // spring boot 是这么设置的
+        options.setMaxAliasesForCollections(Integer.MAX_VALUE);
+        return new Yaml(options);
+    }
+}
-- 
Gitee


From 4ea25e5e42b1ee5a36d71b7f84449a21a4577cbe Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 30 Jan 2023 21:11:23 +0800
Subject: [PATCH 082/160] 5.5.0

---
 able/pom.xml  |  2 +-
 tools/pom.xml | 18 ++++++++++++++++--
 2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 7ee0eaa..b95cf4f 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.26</version>
+	<version>5.5.0</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/tools/pom.xml b/tools/pom.xml
index cf47963..b263c84 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.26</version>
+	<version>5.5.0</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
@@ -28,10 +28,24 @@
 			<optional>true</optional>
 		</dependency>
 
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json</artifactId>
+			<version>${project.version}</version>
+			<optional>true</optional>
+		</dependency>
+
 		<dependency>
 			<groupId>com.alibaba</groupId>
 			<artifactId>fastjson</artifactId>
-			<version>1.2.76</version>
+			<version>1.2.83</version>
+			<optional>true</optional>
+		</dependency>
+
+		<dependency>
+			<groupId>org.yaml</groupId>
+			<artifactId>snakeyaml</artifactId>
+			<version>1.33</version>
 			<optional>true</optional>
 		</dependency>
 
-- 
Gitee


From 475414de34ecaa2e1f06fed299ee0b14a271fa2c Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 30 Jan 2023 22:43:23 +0800
Subject: [PATCH 083/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/tools/utils/StringTools.java   | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index 6ae9a26..0cf1135 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -1959,7 +1959,7 @@ public class StringTools {
      *
      * @param name 待转换的名称
      * @return 转换后的名称
-     * @deprecated 改为{@link NamingTools#toLowerCaseIfAllUpperCase(String)}
+     * @deprecated 改为 {@link NamingTools#toLowerCaseIfAllUpperCase(String)}
      */
     @Deprecated
     public static String toLowerCaseIfAllUpperCase(String name) {
@@ -1972,7 +1972,7 @@ public class StringTools {
      *
      * @param name 待转换的名称
      * @return 驼峰命名法名称
-     * @deprecated 改为{@link NamingTools#toCamelString(String)}
+     * @deprecated 改为 {@link NamingTools#toCamelString(String)}
      */
     @Deprecated
     public static String toCamelNaming(String name) {
@@ -1987,7 +1987,7 @@ public class StringTools {
      * @param name 待转换的名称
      * @param startsWithUpperCase 是否以大写字母开头
      * @return 驼峰命名法名称
-     * @deprecated 改为{@link NamingTools#toCamelString(String, boolean)}
+     * @deprecated 改为 {@link NamingTools#toCamelString(String, boolean)}
      */
     @Deprecated
     public static String toCamelNaming(String name, boolean startsWithUpperCase) {
@@ -2001,7 +2001,7 @@ public class StringTools {
      *
      * @param name 待转换的名称
      * @return 下划线命名法名称
-     * @deprecated 改为{@link NamingTools#toUnderlineNaming(String)}
+     * @deprecated 改为 {@link NamingTools#toUnderlineString(String)}
      */
     @Deprecated
     public static String toUnderlineNaming(String name) {
@@ -2029,7 +2029,7 @@ public class StringTools {
      * @param string the String to capitalize, may be null
      * @return the capitalized String, {@code null} if null String input
      * @since 2.0
-     * @deprecated 改为{@link NamingTools#capitalize(String)}
+     * @deprecated 改为 {@link NamingTools#capitalize(String)}
      */
     @Deprecated
     public static String capitalize(final String string) {
-- 
Gitee


From e59bccf0764c16b9474b4f9b72197e1caaf65004 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 31 Jan 2023 22:19:44 +0800
Subject: [PATCH 084/160] =?UTF-8?q?MapTools=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/utils/MapTools.java  | 123 +++++++++++++-----
 1 file changed, 92 insertions(+), 31 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index 2e93afe..1410fa7 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -1,6 +1,8 @@
 package com.gitee.qdbp.tools.utils;
 
 import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -11,6 +13,8 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import com.gitee.qdbp.able.beans.DepthMap;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.model.reusable.ExpressionMap;
@@ -52,6 +56,16 @@ public class MapTools {
      * @return JsonMap列表
      */
     public static List<Map<String, Object>> toJsonMaps(Collection<?> list) {
+        return toJsonMaps(list, true);
+    }
+
+    /**
+     * 将对象列表转换为JsonMap列表
+     *
+     * @param list 列表
+     * @return JsonMap列表
+     */
+    private static List<Map<String, Object>> toJsonMaps(Collection<?> list, boolean throwOnError) {
         List<Map<String, Object>> results = new ArrayList<>();
         int i = -1;
         for (Object item : list) {
@@ -61,28 +75,47 @@ public class MapTools {
             } else if (item instanceof Map) {
                 results.add(toJsonMap((Map<?, ?>) item));
             } else {
-                if (ReflectTools.isPrimitive(item.getClass(), false)) {
-                    String msg = "[" + i + "] " + item.getClass().getSimpleName() + " can't convert to map.";
-                    throw new ServiceException(ResultCode.SERVER_INNER_ERROR).setDetails(msg);
-                }
-                try {
-                    results.add(JsonTools.beanToMap(item));
-                } catch (ServiceException e) {
-                    if (e.getDetails() == null) {
-                        e.setDetails("[" + i + "] " + item.getClass().getSimpleName() + " can't convert to map.");
-                    } else {
-                        e.setDetails("[" + i + "] " + e.getDetails());
-                    }
-                    throw e;
-                } catch (Exception e) {
-                    throw new ServiceException(ResultCode.SERVER_INNER_ERROR)
-                            .setDetails("[" + i + "] " + e.getMessage());
-                }
+                results.add(parseListItem(item, i, throwOnError));
             }
         }
         return results;
     }
 
+    private static Map<String, Object> parseListItem(Object item, int i, boolean throwOnError) {
+        if (ReflectTools.isPrimitive(item.getClass(), false)) {
+            if (!throwOnError) {
+                return null;
+            } else {
+                String msg = "[" + i + "] " + item.getClass().getSimpleName() + " can't convert to map.";
+                throw new ServiceException(ResultCode.SERVER_INNER_ERROR).setDetails(msg);
+            }
+        }
+        if (!throwOnError && !JsonTools.enabled()) {
+            return null;
+        }
+        try {
+            return JsonTools.beanToMap(item);
+        } catch (ServiceException e) {
+            if (!throwOnError) {
+                return null;
+            } else {
+                if (e.getDetails() == null) {
+                    e.setDetails("[" + i + "] " + item.getClass().getSimpleName() + " can't convert to map.");
+                } else {
+                    e.setDetails("[" + i + "] " + e.getDetails());
+                }
+                throw e;
+            }
+        } catch (Exception e) {
+            if (!throwOnError) {
+                return null;
+            } else {
+                throw new ServiceException(ResultCode.SERVER_INNER_ERROR)
+                        .setDetails("[" + i + "] " + e.getMessage());
+            }
+        }
+    }
+
     /**
      * 将Map的KEY转换为驼峰名
      *
@@ -374,7 +407,7 @@ public class MapTools {
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
-     * @return Map值
+     * @return Map值, 类型不匹配时返回null
      */
     public static <T> T getValue(Map<?, ?> map, String key, Class<T> clazz) {
         Object value = getValue(map, key);
@@ -384,28 +417,57 @@ public class MapTools {
     @SuppressWarnings("unchecked")
     private static <T> T convertValue(Object value, T defaults, Class<T> clazz) {
         if (value == null) {
-            if (clazz == int.class) {
+            if (clazz == boolean.class) {
+                return (T) Boolean.FALSE;
+            } else if (clazz == int.class) {
                 return (T) Integer.valueOf(0);
             } else if (clazz == long.class) {
                 return (T) Long.valueOf(0);
+            } else if (clazz == double.class) {
+                return (T) Double.valueOf(0);
             } else if (clazz == short.class) {
                 return (T) Short.valueOf((short) 0);
             } else if (clazz == byte.class) {
                 return (T) Byte.valueOf((byte) 0);
             } else if (clazz == float.class) {
                 return (T) Float.valueOf(0);
-            } else if (clazz == double.class) {
-                return (T) Double.valueOf(0);
-            } else if (clazz == boolean.class) {
-                return (T) Boolean.FALSE;
             } else {
-                return null;
+                return defaults;
             }
         }
         if (clazz.isAssignableFrom(value.getClass())) {
             return (T) value;
-        } else if (JsonTools.enabled()) {
-            return JsonTools.convert(value, clazz);
+        }
+        if (value instanceof Number) {
+            Number number = (Number) value;
+            if (clazz == int.class || clazz == Integer.class) {
+                return (T) Integer.valueOf(number.intValue());
+            } else if (clazz == long.class || clazz == Long.class) {
+                return (T) Long.valueOf(number.longValue());
+            } else if (clazz == double.class || clazz == Double.class) {
+                return (T) Double.valueOf(number.doubleValue());
+            } else if (clazz == short.class || clazz == Short.class) {
+                return (T) Short.valueOf(number.shortValue());
+            } else if (clazz == byte.class || clazz == Byte.class) {
+                return (T) Byte.valueOf(number.byteValue());
+            } else if (clazz == float.class || clazz == Float.class) {
+                return (T) Float.valueOf(number.floatValue());
+            } else if (clazz == BigDecimal.class) {
+                return (T) new BigDecimal(String.valueOf(number));
+            } else if (clazz == BigInteger.class) {
+                return (T) new BigInteger(String.valueOf(number));
+            } else if (clazz == AtomicInteger.class) {
+                return (T) new AtomicInteger(number.intValue());
+            } else if (clazz == AtomicLong.class) {
+                return (T) new AtomicLong(number.longValue());
+            }
+        }
+        if (JsonTools.enabled()) {
+            try {
+                return JsonTools.convert(value, clazz);
+            } catch (Exception e) {
+                return defaults;
+            }
         } else {
             return defaults;
         }
@@ -420,7 +482,6 @@ public class MapTools {
      * @param clazz 指定类型
      * @return Map值
      */
-    @SuppressWarnings("unchecked")
     public static <T> T getValue(Map<?, ?> map, String key, T defaults, Class<T> clazz) {
         Object value = getValue(map, key);
         return convertValue(value, defaults, clazz);
@@ -432,12 +493,12 @@ public class MapTools {
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
-     * @return Map值列表, 值不存在或值类型不匹配时将返回空List
+     * @return Map值列表, 值不存在将返回空List, 值类型不匹配时List项将返回null
      */
     public static <T> List<T> getList(Map<?, ?> map, String key, Class<T> clazz) {
         Object value = getValue(map, key);
         if (value == null) {
-            return new ArrayList<T>();
+            return new ArrayList<>();
         }
         List<Object> list = ConvertTools.parseList(value);
         List<T> result = new ArrayList<>();
@@ -500,14 +561,14 @@ public class MapTools {
      *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses
-     * @return 子Map数组
+     * @return 子Map数组, 值不存在将返回空List, 值不能转换为Map时List项将返回null
      */
     public static List<Map<String, Object>> getSubMaps(Map<?, ?> map, String key) {
         Object value = getValue(map, key);
         if (value == null) {
             return new ArrayList<>();
         }
-        return toJsonMaps(ConvertTools.parseList(value));
+        return toJsonMaps(ConvertTools.parseList(value), false);
     }
 
     /**
-- 
Gitee


From b116fdbf8f3f9f0fb450197ffc0b1106883418fc Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 1 Feb 2023 00:54:03 +0800
Subject: [PATCH 085/160] =?UTF-8?q?convert=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/utils/JsonTools.java | 88 +++++++++++++++++++
 .../com/gitee/qdbp/tools/utils/MapTools.java  | 60 ++-----------
 2 files changed, 93 insertions(+), 55 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
index a4fe835..f2b8fb5 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
@@ -1,8 +1,14 @@
 package com.gitee.qdbp.tools.utils;
 
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import com.gitee.qdbp.json.JsonFeature;
 
 /**
@@ -18,10 +24,92 @@ public class JsonTools extends JsonMaps {
     private JsonTools() {
     }
 
+    @SuppressWarnings("unchecked")
     public static <T> T convert(Object object, Class<T> clazz) {
+        if (object == null) {
+            return convertNullToPrimitive(clazz);
+        }
+        if (clazz.isAssignableFrom(object.getClass())) {
+            return (T) object;
+        }
+        if (object instanceof Number) {
+            return convertNumberValue((Number) object, clazz);
+        }
+        if (object instanceof String) {
+            return convertStringValue((String) object, clazz);
+        }
         return findDefaultJsonService().convert(object, clazz);
     }
 
+    @SuppressWarnings("unchecked")
+    private static <T> T convertNullToPrimitive(Class<T> clazz) {
+        if (clazz == boolean.class) {
+            return (T) Boolean.FALSE;
+        } else if (clazz == int.class) {
+            return (T) Integer.valueOf(0);
+        } else if (clazz == long.class) {
+            return (T) Long.valueOf(0);
+        } else if (clazz == double.class) {
+            return (T) Double.valueOf(0);
+        } else if (clazz == short.class) {
+            return (T) Short.valueOf((short) 0);
+        } else if (clazz == byte.class) {
+            return (T) Byte.valueOf((byte) 0);
+        } else if (clazz == float.class) {
+            return (T) Float.valueOf(0);
+        } else {
+            return null;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T convertNumberValue(Number number, Class<T> clazz) {
+        if (clazz == int.class || clazz == Integer.class) {
+            return (T) Integer.valueOf(number.intValue());
+        } else if (clazz == long.class || clazz == Long.class) {
+            return (T) Long.valueOf(number.longValue());
+        } else if (clazz == double.class || clazz == Double.class) {
+            return (T) Double.valueOf(number.doubleValue());
+        } else if (clazz == short.class || clazz == Short.class) {
+            return (T) Short.valueOf(number.shortValue());
+        } else if (clazz == byte.class || clazz == Byte.class) {
+            return (T) Byte.valueOf(number.byteValue());
+        } else if (clazz == float.class || clazz == Float.class) {
+            return (T) Float.valueOf(number.floatValue());
+        } else if (clazz == BigDecimal.class) {
+            return (T) new BigDecimal(String.valueOf(number));
+        } else if (clazz == BigInteger.class) {
+            return (T) new BigInteger(String.valueOf(number));
+        } else if (clazz == AtomicInteger.class) {
+            return (T) new AtomicInteger(number.intValue());
+        } else if (clazz == AtomicLong.class) {
+            return (T) new AtomicLong(number.longValue());
+        } else {
+            return findDefaultJsonService().convert(number, clazz);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T convertStringValue(String string, Class<T> clazz) {
+        if (clazz == Date.class) {
+            return (T) DateTools.parse(string);
+        }
+        if (VerifyTools.isJsonObjectString(string)) {
+            if (clazz == Map.class) {
+                return (T) parseAsMap(string);
+            }
+            if (clazz == HashMap.class) {
+                return (T) new HashMap<>(parseAsMap(string));
+            }
+            if (!ReflectTools.isPrimitive(clazz, false)
+                    && !clazz.isArray() && !Collection.class.isAssignableFrom(clazz)) {
+                Map<String, Object> map = parseAsMap(string);
+                return mapToBean(map, clazz);
+            }
+        }
+        return findDefaultJsonService().convert(string, clazz);
+    }
+
     /** 对象转换为字符串 **/
     public static String toJsonString(Object object) {
         return findDefaultJsonService().toJsonString(object);
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index 1410fa7..5bb8289 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -1,8 +1,6 @@
 package com.gitee.qdbp.tools.utils;
 
 import java.io.UnsupportedEncodingException;
-import java.math.BigDecimal;
-import java.math.BigInteger;
 import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -13,8 +11,6 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
 import com.gitee.qdbp.able.beans.DepthMap;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.model.reusable.ExpressionMap;
@@ -414,62 +410,16 @@ public class MapTools {
         return convertValue(value, null, clazz);
     }
 
-    @SuppressWarnings("unchecked")
     private static <T> T convertValue(Object value, T defaults, Class<T> clazz) {
-        if (value == null) {
-            if (clazz == boolean.class) {
-                return (T) Boolean.FALSE;
-            } else if (clazz == int.class) {
-                return (T) Integer.valueOf(0);
-            } else if (clazz == long.class) {
-                return (T) Long.valueOf(0);
-            } else if (clazz == double.class) {
-                return (T) Double.valueOf(0);
-            } else if (clazz == short.class) {
-                return (T) Short.valueOf((short) 0);
-            } else if (clazz == byte.class) {
-                return (T) Byte.valueOf((byte) 0);
-            } else if (clazz == float.class) {
-                return (T) Float.valueOf(0);
-            } else {
-                return defaults;
-            }
-        }
-        if (clazz.isAssignableFrom(value.getClass())) {
-            return (T) value;
-        }
-        if (value instanceof Number) {
-            Number number = (Number) value;
-            if (clazz == int.class || clazz == Integer.class) {
-                return (T) Integer.valueOf(number.intValue());
-            } else if (clazz == long.class || clazz == Long.class) {
-                return (T) Long.valueOf(number.longValue());
-            } else if (clazz == double.class || clazz == Double.class) {
-                return (T) Double.valueOf(number.doubleValue());
-            } else if (clazz == short.class || clazz == Short.class) {
-                return (T) Short.valueOf(number.shortValue());
-            } else if (clazz == byte.class || clazz == Byte.class) {
-                return (T) Byte.valueOf(number.byteValue());
-            } else if (clazz == float.class || clazz == Float.class) {
-                return (T) Float.valueOf(number.floatValue());
-            } else if (clazz == BigDecimal.class) {
-                return (T) new BigDecimal(String.valueOf(number));
-            } else if (clazz == BigInteger.class) {
-                return (T) new BigInteger(String.valueOf(number));
-            } else if (clazz == AtomicInteger.class) {
-                return (T) new AtomicInteger(number.intValue());
-            } else if (clazz == AtomicLong.class) {
-                return (T) new AtomicLong(number.longValue());
-            }
-        }
-        if (JsonTools.enabled()) {
+        if (value == null && defaults != null) {
+            return defaults;
+        } else {
             try {
-                return JsonTools.convert(value, clazz);
+                T result = JsonTools.convert(value, clazz);
+                return result != null ? result : defaults;
             } catch (Exception e) {
                 return defaults;
             }
-        } else {
-            return defaults;
         }
     }
 
-- 
Gitee


From 93c80b0f529276e5e10b71728f638d55906c9326 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 1 Feb 2023 00:54:35 +0800
Subject: [PATCH 086/160] =?UTF-8?q?convert=E6=B5=8B=E8=AF=95=E7=94=A8?=
 =?UTF-8?q?=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/main/java/com/gitee/qdbp/json/test/TestCase1.java   | 5 +++++
 .../java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java | 5 +++++
 .../src/test/java/test/TestCase1ForGson.java                | 6 +++++-
 .../java/com/gitee/qdbp/json/test/TestCase1ForJackson.java  | 5 +++++
 4 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java
index ed8e6a5..ee0d28f 100644
--- a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java
@@ -104,4 +104,9 @@ public class TestCase1 {
         Assert.assertEquals(newer.getChildren().size(), 2);
         Assert.assertEquals(newer.getChildren().get(0).getClass(), Child.class);
     }
+
+    public void test6() {
+        int n1 = JsonTools.convert(null, int.class);
+        Assert.assertEquals(n1, 0);
+    }
 }
diff --git a/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java b/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java
index 7948016..73db2a1 100644
--- a/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java
+++ b/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java
@@ -40,4 +40,9 @@ public class TestCase1ForFastJson extends TestCase1 {
     public void test5() {
         super.test5();
     }
+
+    @Test
+    public void test6() {
+        super.test6();
+    }
 }
diff --git a/test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java b/test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java
index e0593db..90b991b 100644
--- a/test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java
+++ b/test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java
@@ -2,7 +2,6 @@ package test;
 
 import org.testng.annotations.Test;
 import com.gitee.qdbp.json.test.TestCase1;
-import com.google.gson.Gson;
 
 /**
  * TestCase1ForGson
@@ -37,4 +36,9 @@ public class TestCase1ForGson extends TestCase1 {
     public void test5() {
         super.test5();
     }
+
+    @Test
+    public void test6() {
+        super.test6();
+    }
 }
diff --git a/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java b/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java
index 4405c80..b4803b6 100644
--- a/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java
+++ b/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java
@@ -41,4 +41,9 @@ public class TestCase1ForJackson extends TestCase1 {
     public void test5() {
         super.test5();
     }
+
+    @Test
+    public void test6() {
+        super.test6();
+    }
 }
-- 
Gitee


From 490414f1264636d45850024a315f8635e085155b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 1 Feb 2023 00:54:56 +0800
Subject: [PATCH 087/160] =?UTF-8?q?=E5=8E=BB=E9=99=A4fastjson=E7=9A=84?=
 =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E4=BE=9D=E8=B5=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/tools/cache/BaseCacheService.java    | 41 +++++++++----------
 .../gitee/qdbp/tools/cache/InMemoryCache.java |  9 ++--
 .../gitee/qdbp/tools/excel/ImportAsBean.java  |  3 +-
 .../qdbp/tools/excel/json/BeanContainer.java  |  3 +-
 .../qdbp/tools/excel/json/BeanGroup.java      |  9 ++--
 .../qdbp/tools/excel/json/ExcelToJson.java    |  9 +---
 .../gitee/qdbp/tools/excel/rule/DateRule.java |  4 +-
 .../gitee/qdbp/tools/excel/rule/MapRule.java  |  4 +-
 .../qdbp/tools/excel/rule/NumberRule.java     | 14 +++----
 .../gitee/qdbp/tools/excel/rule/RateRule.java |  6 +--
 .../qdbp/tools/excel/rule/RuleFactory.java    | 11 +++--
 .../qdbp/tools/excel/utils/ExcelTools.java    | 27 ++++++------
 .../qdbp/tools/excel/utils/MetadataTools.java | 35 +++++-----------
 .../qdbp/tools/http/BaseHttpHandler.java      | 12 +++---
 .../com/gitee/qdbp/tools/http/HttpTools.java  | 25 ++++++-----
 .../qdbp/tools/http/ParamSetterForForm.java   |  6 +--
 .../qdbp/tools/http/ParamSetterForJson.java   |  6 +--
 .../gitee/qdbp/tools/utils/QueryTools.java    | 36 ++++++++--------
 .../gitee/qdbp/tools/compare/SortTest.java    | 15 ++++---
 .../qdbp/tools/excel/ExcelInOutTest.java      |  3 +-
 .../qdbp/tools/http/HttpExecutorTest.java     | 19 ++++-----
 .../gitee/qdbp/tools/http/HttpToolsTest.java  |  6 +--
 .../qdbp/tools/http/XxxAuthHttpExecutor.java  | 36 ++++++++--------
 .../gitee/qdbp/tools/utils/KeyValueTest.java  |  3 +-
 .../gitee/qdbp/tools/utils/OgnlToolsTest.java |  5 +--
 .../qdbp/tools/utils/ReflectToolsTest.java    | 35 ++++++++--------
 26 files changed, 175 insertions(+), 207 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/cache/BaseCacheService.java b/tools/src/main/java/com/gitee/qdbp/tools/cache/BaseCacheService.java
index b12cee5..e888d4e 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/cache/BaseCacheService.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/cache/BaseCacheService.java
@@ -4,13 +4,12 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.beans.Duration;
 import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 基础缓存类
@@ -58,22 +57,22 @@ public abstract class BaseCacheService extends AbstractCacheService {
     }
 
     protected <T> Map<String, String> serializeFields(T value) {
-        Object object = JSON.toJSON(value);
-        if (object instanceof JSONObject) {
-            JSONObject json = (JSONObject) object;
-            Map<String, String> map = new HashMap<>();
-            for (Map.Entry<String, Object> entry : json.entrySet()) {
-                if (VerifyTools.isNotBlank(entry.getValue())) {
-                    map.put(entry.getKey(), serializeValue(entry.getValue()));
-                }
+        Map<String, Object> json;
+        try {
+            json = JsonTools.beanToMap(value);
+        } catch (Exception e){
+            throw new IllegalArgumentException("value must be a plain object", e);
+        }
+        Map<String, String> map = new HashMap<>();
+        for (Map.Entry<String, Object> entry : json.entrySet()) {
+            if (VerifyTools.isNotBlank(entry.getValue())) {
+                map.put(entry.getKey(), serializeValue(entry.getValue()));
             }
-            return map;
-        } else {
-            throw new IllegalArgumentException("value must be a plain object");
         }
+        return map;
     }
 
-    protected <T> T deserializeFeilds(Map<String, String> map, Class<T> clazz) {
+    protected <T> T deserializeFields(Map<String, String> map, Class<T> clazz) {
         StringBuilder buffer = new StringBuilder();
         for (Entry<String, String> entry : map.entrySet()) {
             if (VerifyTools.isAnyBlank(entry.getKey(), entry.getValue())) continue;
@@ -85,13 +84,13 @@ public abstract class BaseCacheService extends AbstractCacheService {
             } else if (value.startsWith("[") && value.endsWith("]")) { // 数组
                 buffer.append(value);
             } else { // 普通字符串
-                buffer.append(JSON.toJSONString(value)); // 替换字符串中的引号反斜杠
+                buffer.append(JsonTools.toJsonString(value)); // 替换字符串中的引号反斜杠
             }
         }
         String string = "{" + buffer + "}";
 
         try {
-            return JSON.parseObject(string, clazz);
+            return JsonTools.parseAsObject(string, clazz);
         } catch (Exception e) {
             log.error("JsonParseError:{}, class={}, text={}", e, clazz.getSimpleName(), string);
             throw e;
@@ -106,7 +105,7 @@ public abstract class BaseCacheService extends AbstractCacheService {
         } else if (value instanceof Enum) {
             return ((Enum<?>) value).name();
         } else {
-            return JSON.toJSONString(value);
+            return JsonTools.toJsonString(value);
         }
     }
 
@@ -118,7 +117,7 @@ public abstract class BaseCacheService extends AbstractCacheService {
             return (T) string;
         } else {
             try {
-                return JSON.parseObject(string, clazz);
+                return JsonTools.parseAsObject(string, clazz);
             } catch (Exception e) {
                 log.error("JsonParseError:{}, class={}, text={}", e, clazz.getSimpleName(), string);
                 throw e;
@@ -131,7 +130,7 @@ public abstract class BaseCacheService extends AbstractCacheService {
             return null;
         } else {
             try {
-                return JSON.parseArray(string, clazz);
+                return JsonTools.parseAsObjects(string, clazz);
             } catch (Exception e) {
                 log.error("JsonParseError:{}, class={}, text={}", e, clazz.getSimpleName(), string);
                 throw e;
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/cache/InMemoryCache.java b/tools/src/main/java/com/gitee/qdbp/tools/cache/InMemoryCache.java
index 993bf3b..a8e1279 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/cache/InMemoryCache.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/cache/InMemoryCache.java
@@ -7,9 +7,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.beans.VolatileData;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -252,7 +251,7 @@ public class InMemoryCache extends BaseCacheService {
         if (hash.isEmpty()) {
             return null;
         } else {
-            return deserializeFeilds(hash, clazz);
+            return deserializeFields(hash, clazz);
         }
     }
 
@@ -305,7 +304,7 @@ public class InMemoryCache extends BaseCacheService {
     @SuppressWarnings("unchecked")
     protected <T> List<T> castToList(String key, Object value, Class<T> clazz) {
         if (value instanceof String) {
-            return JSONArray.parseArray((String) value, clazz);
+            return JsonTools.parseAsObjects((String) value, clazz);
         } else if (value instanceof Collection) {
             if (value instanceof List) { // value就是List对象
                 List<?> list = (List<?>) value;
@@ -325,7 +324,7 @@ public class InMemoryCache extends BaseCacheService {
             List<T> list = new ArrayList<>();
             Collection<?> values = (Collection<?>) value;
             for (Object i : values) { // 逐一转换为目标类
-                list.add(TypeUtils.castToJavaBean(i, clazz));
+                list.add(JsonTools.convert(i, clazz));
             }
             return list;
         } else {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java
index 4e5737d..49ceba7 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java
@@ -3,7 +3,6 @@ package com.gitee.qdbp.tools.excel;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSONException;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.utils.JsonTools;
@@ -44,7 +43,7 @@ public class ImportAsBean<T> extends ImportCallback {
         try {
             T bean = JsonTools.mapToBean(map, beanClass);
             contents.add(bean);
-        } catch (JSONException e) {
+        } catch (Exception e) {
             throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
         }
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanContainer.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanContainer.java
index 121ef82..c74353a 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanContainer.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanContainer.java
@@ -4,7 +4,6 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.tools.excel.exception.ResultSetMismatchException;
 import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.StringTools;
@@ -130,7 +129,7 @@ public class BeanContainer implements Serializable {
 
         List<T> temp = new ArrayList<>();
         for (Object object : list) {
-            temp.add(TypeUtils.castToJavaBean(object, type));
+            temp.add(JsonTools.convert(object, type));
         }
         return temp;
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanGroup.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanGroup.java
index 1c87cdf..0a63c29 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanGroup.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanGroup.java
@@ -7,7 +7,6 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.tools.excel.exception.ResultSetMismatchException;
 import com.gitee.qdbp.tools.excel.model.ColumnInfo;
 import com.gitee.qdbp.tools.utils.ConvertTools;
@@ -450,8 +449,8 @@ public class BeanGroup implements Serializable {
 
     private static boolean compareNumber(Object expectValue, Object actualValue) {
         try {
-            Double expectNumber = TypeUtils.castToDouble(expectValue);
-            Double actualNumber = TypeUtils.castToDouble(actualValue);
+            Double expectNumber = JsonTools.convert(expectValue, Double.class);
+            Double actualNumber = JsonTools.convert(actualValue, Double.class);
             return expectNumber.doubleValue() == actualNumber.doubleValue();
         } catch (Exception e) {
             return compareString(expectValue, actualValue);
@@ -460,8 +459,8 @@ public class BeanGroup implements Serializable {
 
     private static boolean compareDate(Object expectValue, Object actualValue) {
         try {
-            Date expectDate = TypeUtils.castToDate(expectValue);
-            Date actualDate = TypeUtils.castToDate(actualValue);
+            Date expectDate = JsonTools.convert(expectValue, Date.class);
+            Date actualDate = JsonTools.convert(actualValue, Date.class);
             return expectDate.getTime() == actualDate.getTime();
         } catch (Exception e) {
             return compareString(expectValue, actualValue);
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelToJson.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelToJson.java
index 346bee3..c45034a 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelToJson.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelToJson.java
@@ -11,7 +11,6 @@ import java.util.List;
 import java.util.Map;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import com.alibaba.fastjson.JSONException;
 import com.alibaba.fastjson.serializer.JSONSerializer;
 import com.alibaba.fastjson.serializer.SerializeConfig;
 import com.alibaba.fastjson.serializer.SerializeWriter;
@@ -19,7 +18,6 @@ import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.alibaba.fastjson.serializer.ToStringSerializer;
 import com.gitee.qdbp.able.enums.FileErrorCode;
 import com.gitee.qdbp.able.exception.ServiceException;
-import com.gitee.qdbp.tools.excel.ExcelErrorCode;
 import com.gitee.qdbp.tools.excel.ImportCallback;
 import com.gitee.qdbp.tools.excel.XExcelParser;
 import com.gitee.qdbp.tools.excel.XMetadata;
@@ -229,11 +227,8 @@ public class ExcelToJson {
 
         @Override
         public void callback(Map<String, Object> map, RowInfo row) throws ServiceException {
-            try {
-                rows.add(map);
-            } catch (JSONException e) {
-                throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
-            }
+            // throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
+            rows.add(map);
         }
 
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/DateRule.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/DateRule.java
index a517aae..6209d84 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/DateRule.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/DateRule.java
@@ -5,10 +5,10 @@ import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -54,7 +54,7 @@ public class DateRule implements CellRule, Serializable {
         } else if (cellInfo.getValue() instanceof Date) {
             cellInfo.setValue(new SimpleDateFormat(pattern).format((Date) cellInfo.getValue()));
         } else {
-            Date date = TypeUtils.castToDate(cellInfo.getValue());
+            Date date = JsonTools.convert(cellInfo.getValue(), Date.class);
             cellInfo.setValue(new SimpleDateFormat(pattern).format(date));
         }
         return null;
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/MapRule.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/MapRule.java
index be63485..1d4d241 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/MapRule.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/MapRule.java
@@ -4,11 +4,11 @@ import java.io.Serializable;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
-import com.alibaba.fastjson.JSON;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
 import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
@@ -33,7 +33,7 @@ public class MapRule implements CellRule, Serializable {
      * @param rule 映射规则 如 { "PROVINCE":"1|省", "CITY":"2|市", "DISTRICT":"3|区|县|区/县" }
      */
     public MapRule(String rule) {
-        this(JSON.parseObject(rule));
+        this(JsonTools.parseAsMap(rule));
     }
 
     /**
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/NumberRule.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/NumberRule.java
index 7666cc7..522d6dd 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/NumberRule.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/NumberRule.java
@@ -2,9 +2,9 @@ package com.gitee.qdbp.tools.excel.rule;
 
 import java.io.Serializable;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -48,17 +48,17 @@ public class NumberRule implements CellRule, Serializable {
             type = "double";
         }
         if (type.equalsIgnoreCase("int") || type.equalsIgnoreCase("integer")) {
-            return TypeUtils.castToInt(value);
+            return JsonTools.convert(value, Integer.class);
         } else if (type.equalsIgnoreCase("float")) {
-            return TypeUtils.castToFloat(value);
+            return JsonTools.convert(value, Float.class);
         } else if (type.equalsIgnoreCase("long")) {
-            return TypeUtils.castToLong(value);
+            return JsonTools.convert(value, Long.class);
         } else if (type.equalsIgnoreCase("short")) {
-            return TypeUtils.castToShort(value);
+            return JsonTools.convert(value, Short.class);
         } else if (type.equalsIgnoreCase("byte")) {
-            return TypeUtils.castToByte(value);
+            return JsonTools.convert(value, Byte.class);
         } else {
-            return TypeUtils.castToDouble(value);
+            return JsonTools.convert(value, Double.class);
         }
     }
 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RateRule.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RateRule.java
index fe26df8..5f34265 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RateRule.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RateRule.java
@@ -2,10 +2,10 @@ package com.gitee.qdbp.tools.excel.rule;
 
 import java.io.Serializable;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 public class RateRule implements CellRule, Serializable {
@@ -30,7 +30,7 @@ public class RateRule implements CellRule, Serializable {
             return null;
         }
         if (value instanceof String) {
-            Double number = TypeUtils.castToDouble(value);
+            Double number = JsonTools.convert(value, Double.class);
             cellInfo.setValue(number == null ? null : number * rate);
         } else if (value instanceof Number) {
             Number number = (Number) value;
@@ -50,7 +50,7 @@ public class RateRule implements CellRule, Serializable {
         if (value instanceof Number) {
             cellInfo.setValue(((Number) value).doubleValue() / rate);
         } else {
-            Double number = TypeUtils.castToDouble(value);
+            Double number = JsonTools.convert(value, Double.class);
             cellInfo.setValue(number == null ? null : number / rate);
         }
         return null;
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RuleFactory.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RuleFactory.java
index 83b1fef..3624c0b 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RuleFactory.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RuleFactory.java
@@ -3,12 +3,11 @@ package com.gitee.qdbp.tools.excel.rule;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Pattern;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.DateTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
  * 规则注册工厂
@@ -60,7 +59,7 @@ public class RuleFactory {
                     return null;
                 }
                 try {
-                    Boolean value = TypeUtils.castToBoolean(options);
+                    Boolean value = JsonTools.convert(options, Boolean.class);
                     return value ? new IgnoreIllegalValue() : null;
                 } catch (Exception e) {
                     throw new IllegalArgumentException(illegalArgumentType("ignoreIllegalValue", options));
@@ -147,8 +146,8 @@ public class RuleFactory {
                     throw new IllegalArgumentException(illegalArgumentType("map", options));
                 }
                 String string = (String) options;
-                if (string.startsWith("{")) {
-                    JSONObject json = (JSONObject) JSON.parse(string);
+                if (VerifyTools.isJsonObjectString(string)) {
+                    Map<String, Object> json = JsonTools.parseAsMap(string);
                     return new MapRule(json);
                 } else {
                     // 0:未知, 1:男, 2:女
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java
index 09e9199..786997f 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java
@@ -7,13 +7,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import com.alibaba.fastjson.util.TypeUtils;
-import com.gitee.qdbp.tools.excel.model.CellInfo;
-import com.gitee.qdbp.tools.excel.model.CopyConcat;
-import com.gitee.qdbp.tools.excel.rule.CellRule;
-import com.gitee.qdbp.tools.utils.ConvertTools;
-import com.gitee.qdbp.tools.utils.JsonTools;
-import com.gitee.qdbp.tools.utils.VerifyTools;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.CellStyle;
 import org.apache.poi.ss.usermodel.CellType;
@@ -28,6 +21,12 @@ import org.apache.poi.ss.util.CellRangeAddress;
 import org.apache.poi.ss.util.PaneInformation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import com.gitee.qdbp.tools.excel.model.CellInfo;
+import com.gitee.qdbp.tools.excel.model.CopyConcat;
+import com.gitee.qdbp.tools.excel.rule.CellRule;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
  * Excel工具类
@@ -479,16 +478,16 @@ public abstract class ExcelTools {
             } else if (value instanceof Calendar) {
                 cell.setCellValue((Calendar) value);
             } else if (value instanceof Boolean) {
-                cell.setCellValue(TypeUtils.castToBoolean(value));
+                cell.setCellValue(JsonTools.convert(value, Boolean.class));
             } else if (value instanceof Number) {
                 // Cell.setCellValue()对数字的处理只有double类型
                 cell.setCellValue(((Number) value).doubleValue());
             } else {
-                cell.setCellValue(TypeUtils.castToString(value));
+                cell.setCellValue(value.toString());
             }
             break;
         case BOOLEAN:
-            cell.setCellValue(TypeUtils.castToBoolean(value));
+            cell.setCellValue(JsonTools.convert(value, Boolean.class));
             break;
         case NUMERIC:
             switch (cell.getCellStyle().getDataFormat()) {
@@ -499,19 +498,19 @@ public abstract class ExcelTools {
             case 21: // HH:mm:ss
             case 22: // yyyy-MM-dd HH:mm:ss
             case 32: // h时mm分
-                cell.setCellValue(TypeUtils.castToDate(value));
+                cell.setCellValue(JsonTools.convert(value, Date.class));
                 break;
             default:
                 if (DateUtil.isCellDateFormatted(cell)) { // 日期时间
-                    cell.setCellValue(TypeUtils.castToDate(value));
+                    cell.setCellValue(JsonTools.convert(value, Date.class));
                 } else { // 数字
-                    cell.setCellValue(TypeUtils.castToDouble(value));
+                    cell.setCellValue(JsonTools.convert(value, Double.class));
                 }
                 break;
             }
             break;
         default: // STRING and other
-            cell.setCellValue(TypeUtils.castToString(value));
+            cell.setCellValue(value.toString());
             break;
         }
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
index 41567f4..c187bd3 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
@@ -15,9 +15,6 @@ import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.ImportCallback;
 import com.gitee.qdbp.tools.excel.XExcelParser;
@@ -302,20 +299,16 @@ public class MetadataTools {
             jsonString = "[" + jsonString + "]";
         }
         // 转换为JSON数组
-        JSONArray array;
+        List<Map<String, Object>> array;
         try {
-            array = JSON.parseArray(jsonString);
+            array = JsonTools.parseAsMaps(jsonString);
         } catch (Exception e) {
             log.warn("CellRuleError, json string format error: " + jsonString, e);
             return null;
         }
         // 逐一解析
         List<CellRule> rules = new ArrayList<>();
-        for (Object i : array) {
-            if (!(i instanceof JSONObject)) {
-                continue;
-            }
-            JSONObject json = (JSONObject) i;
+        for (Map<String, Object> json : array) {
             for (Entry<String, Object> entry : json.entrySet()) {
                 String key = entry.getKey();
                 if (VerifyTools.isBlank(key)) {
@@ -349,20 +342,16 @@ public class MetadataTools {
             jsonString = "[" + jsonString + "]";
         }
         // 转换为JSON数组
-        JSONArray array;
+        List<Map<String, Object>> array;
         try {
-            array = JSON.parseArray(jsonString);
+            array = JsonTools.parseAsMaps(jsonString);
         } catch (Exception e) {
-            log.warn("ContainsTextConditionError, json string format error: " + jsonString, e);
+            log.warn("ContainsTextConditionError, json array string format error: " + jsonString, e);
             return null;
         }
         // 逐一解析
         List<List<Item>> conditions = new ArrayList<>();
-        for (Object i : array) {
-            if (!(i instanceof JSONObject)) {
-                continue;
-            }
-            JSONObject json = (JSONObject) i;
+        for (Map<String, Object> json : array) {
             if (json.isEmpty()) {
                 continue;
             }
@@ -397,20 +386,16 @@ public class MetadataTools {
             jsonString = "[" + jsonString + "]";
         }
         // 转换为JSON数组
-        JSONArray array;
+        List<Map<String, Object>> array;
         try {
-            array = JSON.parseArray(jsonString);
+            array = JsonTools.parseAsMaps(jsonString);
         } catch (Exception e) {
             log.warn("CopyConcatFieldsError, json string format error: " + jsonString, e);
             return null;
         }
         // 逐一解析
         List<CopyConcat> copyConcatFields = new ArrayList<>();
-        for (Object i : array) {
-            if (!(i instanceof JSONObject)) {
-                continue;
-            }
-            JSONObject json = (JSONObject) i;
+        for (Map<String, Object> json : array) {
             if (json.isEmpty()) {
                 continue;
             }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java b/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java
index 28989c0..c2a96ae 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java
@@ -8,11 +8,11 @@ import org.apache.http.client.CookieStore;
 import org.apache.http.impl.client.BasicCookieStore;
 import org.apache.http.message.BasicHeader;
 import org.apache.http.message.HeaderGroup;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 public class BaseHttpHandler implements IHttpHandler {
@@ -133,12 +133,12 @@ public class BaseHttpHandler implements IHttpHandler {
      * @throws Exception 解析异常
      */
     public ResponseMessage parseResult(HttpUrl hurl, String string) throws RemoteServiceException, Exception {
-        JSONObject json = JSON.parseObject(string);
+        Map<String, Object> json = JsonTools.parseAsMap(string);
         ResponseMessage result = new ResponseMessage();
-        result.setCode(json.getString("code"));
-        result.setMessage(json.getString("message"));
+        result.setCode(MapTools.getString(json, "code"));
+        result.setMessage(MapTools.getString(json, "message"));
         result.setBody(json.get("body"));
-        result.setExtra(json.getJSONObject("extra"));
+        result.setExtra(MapTools.getSubMap(json, "extra"));
 
         if (ResultCode.SUCCESS.name().equals(result.getCode())) {
             return result;
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java b/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
index e6b9f93..ed69864 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
@@ -9,13 +9,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.util.TypeUtils;
-import com.gitee.qdbp.able.beans.KeyString;
-import com.gitee.qdbp.able.exception.ServiceException;
-import com.gitee.qdbp.able.result.ResponseMessage;
-import com.gitee.qdbp.able.result.ResultCode;
-import com.gitee.qdbp.tools.utils.VerifyTools;
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpMessage;
@@ -33,6 +26,12 @@ import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.util.EntityUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResponseMessage;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
  * HTTP请求工具类<pre>
@@ -148,7 +147,7 @@ public class HttpTools {
         } catch (RemoteServiceException e) {
             throw e;
         } catch (Exception e) {
-            throw new ResultParseException("Http request success, but JSON.parseObject error. " + hurl, e);
+            throw new ResultParseException("Http request success, but parseJsonObject error. " + hurl, e);
         }
 
         return result;
@@ -185,9 +184,9 @@ public class HttpTools {
         }
 
         try {
-            return TypeUtils.castToJavaBean(body, type);
+            return JsonTools.convert(body, type);
         } catch (Exception e) {
-            throw new ResultParseException("Http request success, but TypeUtils.castToJavaBean error. " + hurl, e);
+            throw new ResultParseException("Http request success, but convertToJavaBean error. " + hurl, e);
         }
     }
 
@@ -223,9 +222,9 @@ public class HttpTools {
 
         if (body instanceof String) {
             try {
-                return JSON.parseArray((String) body, type);
+                return JsonTools.parseAsObjects((String) body, type);
             } catch (Exception e) {
-                throw new ResultParseException("Http request success, but JSON.parseArray error. " + hurl, e);
+                throw new ResultParseException("Http request success, but parseJsonArray error. " + hurl, e);
             }
         }
 
@@ -247,7 +246,7 @@ public class HttpTools {
         List<T> results = new ArrayList<>();
         for (Object item : objects) {
             try {
-                results.add(TypeUtils.castToJavaBean(item, type));
+                results.add(JsonTools.convert(item, type));
             } catch (Exception e) {
                 throw new ResultParseException("Http request success, but TypeUtils.castToJavaBean error. " + hurl, e);
             }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForForm.java b/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForForm.java
index 6f3a24d..3f5b016 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForForm.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForForm.java
@@ -7,8 +7,8 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import com.alibaba.fastjson.JSON;
 import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import org.apache.http.HttpEntity;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.entity.UrlEncodedFormEntity;
@@ -129,7 +129,7 @@ public class ParamSetterForForm implements IParamSetter {
             string = value.toString();
             builder.addTextBody(key, string, contentType);
         } else {
-            string = JSON.toJSONString(value);
+            string = JsonTools.toJsonString(value);
             builder.addTextBody(key, string, contentType);
         }
         if (logs != null) {
@@ -144,7 +144,7 @@ public class ParamSetterForForm implements IParamSetter {
         } else if (value instanceof CharSequence) {
             string = value.toString();
         } else {
-            string = JSON.toJSONString(value);
+            string = JsonTools.toJsonString(value);
         }
         if (logs != null) {
             logs.add(new KeyString(key, string));
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForJson.java b/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForJson.java
index e8f7575..69274b9 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForJson.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForJson.java
@@ -2,12 +2,12 @@ package com.gitee.qdbp.tools.http;
 
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.gitee.qdbp.able.beans.KeyString;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.utils.URIBuilder;
 import org.apache.http.entity.ContentType;
 import org.apache.http.entity.StringEntity;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.tools.utils.JsonTools;
 
 /**
  * 以application/json的方式提交请求参数
@@ -29,7 +29,7 @@ public class ParamSetterForJson implements IParamSetter {
 
     @Override
     public <P> void setPostParams(HttpPost method, Map<String, P> params, List<KeyString> logs) {
-        String json = JSON.toJSONString(params);
+        String json = JsonTools.toJsonString(params);
         if (logs != null) {
             logs.add(new KeyString(null, json));
         }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/QueryTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/QueryTools.java
index d353a77..c52ef1f 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/QueryTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/QueryTools.java
@@ -7,7 +7,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.regex.Pattern;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.jdbc.ordering.Orderings;
 import com.gitee.qdbp.able.jdbc.paging.PageList;
 import com.gitee.qdbp.able.jdbc.paging.Paging;
@@ -125,7 +124,6 @@ public class QueryTools {
         }
     }
 
-    private static final Pattern NUMBER = Pattern.compile("^[+\\-]?[\\d,]*(\\.\\d+)?$");
     private static final Pattern DATE = Pattern.compile("^[\\d\\-:. ]+$");
 
     /**
@@ -211,8 +209,11 @@ public class QueryTools {
                 }
                 Object realActualValue = actualValue; // 只支持Date
                 try {
-                    if (actualValue instanceof CharSequence && DATE.matcher(actualValue.toString()).matches()) {
-                        realActualValue = TypeUtils.castToDate(actualValue);
+                    if (actualValue instanceof CharSequence) {
+                        String string = actualValue.toString();
+                        if (DATE.matcher(string).matches()) {
+                            realActualValue = DateTools.parse(string);
+                        }
                     }
                 } catch (Exception e) {
                     continue;
@@ -231,7 +232,7 @@ public class QueryTools {
                 Object realActualValue = actualValue; // 只支持Date
                 try {
                     if (actualValue instanceof CharSequence && DATE.matcher(actualValue.toString()).matches()) {
-                        realActualValue = TypeUtils.castToDate(actualValue);
+                        realActualValue = DateTools.parse(actualValue.toString());
                     }
                 } catch (Exception e) {
                     continue;
@@ -318,10 +319,11 @@ public class QueryTools {
 
     private static Object convertToNumberOrDate(Object actualValue) {
         if (actualValue instanceof CharSequence) {
-            if (NUMBER.matcher(actualValue.toString()).matches()) {
-                return TypeUtils.castToDouble(actualValue);
-            } else if (DATE.matcher(actualValue.toString()).matches()) {
-                return TypeUtils.castToDate(actualValue);
+            String string = actualValue.toString();
+            if (StringTools.isNumber(string)) {
+                return ConvertTools.toNumber(string);
+            } else if (DATE.matcher(string).matches()) {
+                return DateTools.parse(string);
             }
         }
         return actualValue;
@@ -433,8 +435,8 @@ public class QueryTools {
         } else {
             try {
                 double actual = actualValue.doubleValue();
-                Number minNumber = VerifyTools.isBlank(minValue) ? null : TypeUtils.castToDouble(minValue);
-                Number maxNumber = VerifyTools.isBlank(maxValue) ? null : TypeUtils.castToDouble(maxValue);
+                Number minNumber = VerifyTools.isBlank(minValue) ? null : JsonTools.convert(minValue, Double.class);
+                Number maxNumber = VerifyTools.isBlank(maxValue) ? null : JsonTools.convert(maxValue, Double.class);
                 if (minNumber != null && maxNumber != null) {
                     return actual >= minNumber.doubleValue() && actual < maxNumber.doubleValue();
                 } else if (maxNumber != null) {
@@ -467,8 +469,8 @@ public class QueryTools {
         } else {
             try {
                 long actual = actualValue.getTime();
-                Date minDate = VerifyTools.isBlank(minValue) ? null : TypeUtils.castToDate(minValue);
-                Date maxDate = VerifyTools.isBlank(maxValue) ? null : TypeUtils.castToDate(maxValue);
+                Date minDate = VerifyTools.isBlank(minValue) ? null : JsonTools.convert(minValue, Date.class);
+                Date maxDate = VerifyTools.isBlank(maxValue) ? null : JsonTools.convert(maxValue, Date.class);
                 if (minDate != null && maxDate != null) {
                     return actual >= minDate.getTime() && actual < maxDate.getTime();
                 } else if (maxDate != null) {
@@ -631,7 +633,7 @@ public class QueryTools {
             return false;
         } else {
             double actualNumber = actualValue.getTime();
-            double expectNumber = TypeUtils.castToDate(expectValue).getTime();
+            double expectNumber = JsonTools.convert(expectValue, Date.class).getTime();
             return judgeValue(actualNumber, flag, expectNumber);
         }
     }
@@ -650,7 +652,7 @@ public class QueryTools {
             return false;
         } else {
             double actualNumber = actualValue.getTime();
-            Date expectDate = TypeUtils.castToDate(expectValue);
+            Date expectDate = JsonTools.convert(expectValue, Date.class);
             double expectNumber = DateTools.toStartTime(expectDate).getTime();
             return judgeValue(actualNumber, flag, expectNumber);
         }
@@ -670,7 +672,7 @@ public class QueryTools {
             return false;
         } else {
             double actualNumber = actualValue.getTime();
-            Date expectDate = TypeUtils.castToDate(expectValue);
+            Date expectDate = JsonTools.convert(expectValue, Date.class);
             double expectNumber = DateTools.toEndTime(expectDate).getTime();
             return judgeValue(actualNumber, flag, expectNumber);
         }
@@ -690,7 +692,7 @@ public class QueryTools {
             return false;
         } else {
             double actualNumber = actualValue.doubleValue();
-            double expectNumber = TypeUtils.castToDouble(expectValue);
+            double expectNumber = JsonTools.convert(expectValue, Double.class);
             return judgeValue(actualNumber, flag, expectNumber);
         }
     }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/compare/SortTest.java b/tools/src/test/java/com/gitee/qdbp/tools/compare/SortTest.java
index f517a96..68c10e2 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/compare/SortTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/compare/SortTest.java
@@ -4,7 +4,6 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
 import com.gitee.qdbp.able.instance.ComplexComparator;
 import com.gitee.qdbp.able.instance.MapFieldComparator;
 import com.gitee.qdbp.able.jdbc.ordering.Orderings;
@@ -17,18 +16,18 @@ public class SortTest {
     public static void main(String[] args) {
 
         List<Map<String, Object>> list = new ArrayList<>();
-        list.add(JSON.parseObject("{id:6,name:'kkk',dept:'BB'}"));
+        list.add(JsonTools.parseAsMap("{id:6,name:'kkk',dept:'BB'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-09-09"));
-        list.add(JSON.parseObject("{id:1,name:'bbb',dept:'AA'}"));
+        list.add(JsonTools.parseAsMap("{id:1,name:'bbb',dept:'AA'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-08-08"));
-        list.add(JSON.parseObject("{id:3,name:'hhh',dept:'AA'}"));
+        list.add(JsonTools.parseAsMap("{id:3,name:'hhh',dept:'AA'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-11-11"));
-        list.add(JSON.parseObject("{id:5,name:'eee',dept:'CC'}"));
+        list.add(JsonTools.parseAsMap("{id:5,name:'eee',dept:'CC'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-10-10"));
-        list.add(JSON.parseObject("{id:4,name:'ddd',dept:'BB'}"));
+        list.add(JsonTools.parseAsMap("{id:4,name:'ddd',dept:'BB'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-08-08"));
-        list.add(JSON.parseObject("{id:7,name:'fff'}"));
-        list.add(JSON.parseObject("{id:2,name:'aaa'}"));
+        list.add(JsonTools.parseAsMap("{id:7,name:'fff'}"));
+        list.add(JsonTools.parseAsMap("{id:2,name:'aaa'}"));
         System.out.println(JsonTools.toJsonString(list));
 
         {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java
index 8a5083d..5ce00da 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java
@@ -11,7 +11,6 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
-import com.alibaba.fastjson.JSONException;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.excel.utils.MetadataTools;
@@ -41,7 +40,7 @@ public class ExcelInOutTest {
             try {
                 model = JsonTools.mapToBean(map, EmployeeInfo.class);
                 employees.add(model);
-            } catch (JSONException e) {
+            } catch (Exception e) {
                 throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
             }
             System.out.println(index + "\timport: " + JsonTools.toLogString(model));
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java
index e061770..33bedb1 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java
@@ -5,8 +5,6 @@ import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.codec.DigestTools;
@@ -14,6 +12,7 @@ import com.gitee.qdbp.tools.files.PathTools;
 import com.gitee.qdbp.tools.http.HostUrlConfig.KeyedHttpUrl;
 import com.gitee.qdbp.tools.http.HttpTools.HttpJsonImpl;
 import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
 import com.gitee.qdbp.tools.utils.RandomTools;
 
 public class HttpExecutorTest extends HttpExecutor {
@@ -74,19 +73,19 @@ public class HttpExecutorTest extends HttpExecutor {
         @Override
         public ResponseMessage parseResult(HttpUrl hurl, String string) throws Exception {
             // { resultCode:int, resultJson:{ msgCode:string, message:string, data:json } }
-            JSONObject json = JSON.parseObject(string);
+            Map<String, Object> json = JsonTools.parseAsMap(string);
             ResponseMessage result = new ResponseMessage();
-            String resultCode = json.getString("resultCode");
-            if (!resultCode.equals(config.getString("cim.interface.success"))) {
+            String resultCode = MapTools.getString(json, "resultCode");
+            if (resultCode == null || !resultCode.equals(config.getString("cim.interface.success"))) {
                 // 这一类异常不应该抛上去给用户看到了, 直接返回接口调用失败, 日志HttpTools会统一记录的
                 ResultCode code = ResultCode.REMOTE_SERVICE_FAIL;
                 throw new RemoteServiceException(code.getCode(), code.getMessage());
             } else {
-                String resultString = json.getString("resultJson");
-                JSONObject resultJson = JSON.parseObject(resultString);
-                result.setCode(resultJson.getString("msgCode"));
-                result.setMessage(resultJson.getString("message"));
-                result.setBody(resultJson.getString("data"));
+                String resultString = MapTools.getString(json, "resultJson");
+                Map<String, Object> resultJson = JsonTools.parseAsMap(resultString);
+                result.setCode(MapTools.getString(resultJson, "msgCode"));
+                result.setMessage(MapTools.getString(resultJson, "message"));
+                result.setBody(MapTools.getString(resultJson, "data"));
 
                 String key = ((KeyedHttpUrl) hurl).getKey();
                 if (result.getCode().equals(config.getString(key + ".success"))) {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpToolsTest.java
index a99a7d1..4f51830 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpToolsTest.java
@@ -1,7 +1,7 @@
 package com.gitee.qdbp.tools.http;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
+import java.util.Map;
+import com.gitee.qdbp.tools.utils.JsonTools;
 
 
 public class HttpToolsTest {
@@ -10,7 +10,7 @@ public class HttpToolsTest {
         String url = "http://cimqas.cttq.com/cim-clinic-gwy/operate/clinic/online/doc/cancel/record";
         String text =
                 "{\"createTime\":\"2016-05-10\",\"sourceType\":\"pc\",\"ts\":34370,\"appCode\":\"A000\",\"accountType\":1,\"account\":\"13913001382\",\"digest\":\"25AE1ED2CDF19CD5A36CE58E1B2158CE\",\"imeiuuid\":\"359596063773059\",\"jsonData\":{\"causeContext\":\"诊室异常\",\"bussiType\":5,\"clinicDate\":\"2016-05-10 09:12:00\",\"clinicCid\":\"20160510085941749062992ee3a8ec60\"}}";
-        JSONObject json = JSON.parseObject(text);
+        Map<String, Object> json = JsonTools.parseAsMap(text);
         try {
             System.out.println(HttpTools.json.post(url, json));
         } catch (HttpException e) {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java b/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java
index 7bfac54..7a236c6 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java
@@ -4,14 +4,14 @@ import java.net.URL;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.codec.DigestTools;
 import com.gitee.qdbp.tools.files.PathTools;
 import com.gitee.qdbp.tools.http.HttpTools.HttpJsonImpl;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
 import com.gitee.qdbp.tools.utils.RandomTools;
 
 /**
@@ -93,23 +93,23 @@ public class XxxAuthHttpExecutor extends HttpExecutor {
             // xxx平台的响应报文是一个两层结构
             // { code:int, json:{ resultCode:string, resultMessage:string, resultData:json } }
             // code是系统异常, 0=成功; returnCode是业务异常, 000000=成功
-            JSONObject json = JSON.parseObject(string);
-            String sysCode = json.getString("code");
-            if (!sysCode.equals(config.getString("xxx.auth.code.sys.success"))) {
+            Map<String, Object> json = JsonTools.parseAsMap(string);
+            String sysCode = MapTools.getString(json, "code");
+            if (sysCode == null || !sysCode.equals(config.getString("xxx.auth.code.sys.success"))) {
                 // xxx.auth.code.sys.success = 0, 不等于0的都是失败
                 // 这一类异常不应该抛上去给用户看到了, 直接返回接口调用失败, 日志HttpTools会统一记录的
                 ResultCode resultCode = ResultCode.REMOTE_SERVICE_FAIL;
                 throw new RemoteServiceException(resultCode.getCode(), resultCode.getMessage());
             } else {
-                String jsonString = json.getString("json");
-                JSONObject jsonObject = JSON.parseObject(jsonString);
-                String bizCode = jsonObject.getString("resultCode");
-                if (!bizCode.equals(config.getString("xxx.auth.code.biz.success"))) {
+                String jsonString = MapTools.getString(json, "json");
+                Map<String, Object> jsonObject =JsonTools.parseAsMap(jsonString);
+                String bizCode = MapTools.getString(jsonObject, "resultCode");
+                if (bizCode == null || !bizCode.equals(config.getString("xxx.auth.code.biz.success"))) {
                     // xxx.auth.code.biz.success = 000000, 不等于000000的都是失败
-                    throw new RemoteServiceException(sysCode, jsonObject.getString("resultMessage"));
+                    throw new RemoteServiceException(sysCode, MapTools.getString(jsonObject, "resultMessage"));
                 } else {
                     ResponseMessage result = new ResponseMessage();
-                    result.setBody(jsonObject.getString("resultData"));
+                    result.setBody(MapTools.getString(jsonObject, "resultData"));
                     return result;
                 }
             }
@@ -137,7 +137,7 @@ class UserAuthService {
 
     public UserInfo queryUserInfo(UserQueryParams contidion) throws ServiceException {
         try {
-            JSONObject params = (JSONObject) JSON.toJSON(contidion);
+            Map<String, Object> params = JsonTools.beanToMap(contidion);
             return XxxAuthHttpExecutor.me.query("xxx.auth.user.baseinfo", params, UserInfo.class);
         } catch (RemoteServiceException e) {
             throw new ServiceException(e, e); // ServiceException(IResultMessage, Throwable)
@@ -148,7 +148,7 @@ class UserAuthService {
 
     public List<UserInfo> listUserInfo(UserQueryParams contidion) throws ServiceException {
         try {
-            JSONObject params = (JSONObject) JSON.toJSON(contidion);
+            Map<String, Object> params = JsonTools.beanToMap(contidion);
             return XxxAuthHttpExecutor.me.list("xxx.auth.user.list", params, UserInfo.class);
         } catch (RemoteServiceException e) {
             throw new ServiceException(e, e); // ServiceException(IResultMessage, Throwable)
@@ -160,11 +160,11 @@ class UserAuthService {
     /** 注册并返回用户ID **/
     public String register(UserInfo userInfo) throws ServiceException {
         try {
-            JSONObject params = (JSONObject) JSON.toJSON(userInfo);
+            Map<String, Object> params = JsonTools.beanToMap(userInfo);
             // 注册接口返回的resultData = { userId:string }
             ResponseMessage resp = XxxAuthHttpExecutor.me.execute("xxx.auth.user.register", params);
-            JSONObject result = JSON.parseObject((String) resp.getBody());
-            return result.getString("userId");
+            Map<String, Object> result = JsonTools.parseAsMap((String) resp.getBody());
+            return MapTools.getString(result, "userId");
         } catch (RemoteServiceException e) {
             throw new ServiceException(e, e); // ServiceException(IResultMessage, Throwable)
         } catch (Exception e) {
@@ -179,8 +179,8 @@ class UserAuthService {
             params.put("userId", userId);
             // 余额接口返回的resultData = { balance:double }
             ResponseMessage resp = XxxAuthHttpExecutor.me.execute("xxx.auth.user.balance", params);
-            JSONObject result = JSON.parseObject((String) resp.getBody());
-            return result.getDouble("balance");
+            Map<String, Object> result = JsonTools.parseAsMap((String) resp.getBody());
+            return MapTools.getValue(result, "balance", null, Double.class);
         } catch (RemoteServiceException e) {
             throw new ServiceException(e, e); // ServiceException(IResultMessage, Throwable)
         } catch (Exception e) {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/KeyValueTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/KeyValueTest.java
index f4c4134..6d89dcb 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/KeyValueTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/KeyValueTest.java
@@ -1,7 +1,6 @@
 package com.gitee.qdbp.tools.utils;
 
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
 
 public class KeyValueTest {
 
@@ -10,7 +9,7 @@ public class KeyValueTest {
         KeyValue<String> item = new KeyValue<>("kkk", "vvv");
         // fastjson-1.2.23.jar 输出: {"key":"kkk","value":"vvv"}
         // fastjson-1.2.24.jar 输出: {"kkk":"vvv"}
-        System.out.println(JSON.toJSONString(item));
+        System.out.println(JsonTools.toLogString(item));
     }
 
     protected static class KeyValue<V> implements Map.Entry<String, V> {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java
index 090c634..95e8e39 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java
@@ -5,7 +5,6 @@ import java.util.List;
 import java.util.Map;
 import org.testng.Assert;
 import org.testng.annotations.Test;
-import com.alibaba.fastjson.JSONObject;
 
 @Test
 public class OgnlToolsTest {
@@ -32,7 +31,7 @@ public class OgnlToolsTest {
         String result1 = JsonTools.toLogString(map);
         System.out.println(result1);
         Map<String, Object> actual1 = JsonTools.beanToMap(map);
-        Map<String, Object> expected1 = (JSONObject) JSONObject.parse("{userBean:{address:{id:\"A0001\",name:\"Home\"}}}");
+        Map<String, Object> expected1 = JsonTools.parseAsMap("{userBean:{address:{id:\"A0001\",name:\"Home\"}}}");
         AssertTools.assertDeepEquals(actual1, expected1);
         OgnlTools.setValue(map, "userBean.address.details", "Beijing China");
         OgnlTools.setValue(map, "test.address.id", "A0009");
@@ -40,7 +39,7 @@ public class OgnlToolsTest {
         String result2 = JsonTools.toLogString(map);
         System.out.println(result2);
         Map<String, Object> actual2 = JsonTools.beanToMap(map);
-        Map<String, Object> expected2 = (JSONObject) JSONObject.parse(
+        Map<String, Object> expected2 = JsonTools.parseAsMap(
                 "{test:{address:{id:\"A0009\",details:\"Nanjing China\"}},userBean:{address:{details:\"Beijing China\",id:\"A0001\",name:\"Home\"}}}");
         AssertTools.assertDeepEquals(actual2, expected2);
     }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
index d98f7e3..11f7c4e 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
@@ -2,7 +2,6 @@ package com.gitee.qdbp.tools.utils;
 
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
 import com.gitee.qdbp.able.beans.KeyValue;
 import org.testng.Assert;
 import org.testng.annotations.Test;
@@ -14,7 +13,7 @@ public class ReflectToolsTest {
     public void testGetDepthValue1() {
         String json = "{domain:{text:'baidu',url:'https://baidu.com'}}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "domain.text";
             String text = ReflectTools.getDepthValue(object, key);
@@ -40,7 +39,7 @@ public class ReflectToolsTest {
         String json =
                 "[{domain:{text:'baidu',url:'https://baidu.com'}},{domain:{text:'bing',url:'https://cn.bing.com'}}]";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMaps(json);
         {
             String key = "[1].domain.text";
             String text = ReflectTools.getDepthValue(object, key);
@@ -72,7 +71,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[1].domain";
             Map<String, Object> domain = ReflectTools.getDepthValue(object, key);
@@ -94,7 +93,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[1].domain.text";
             String text = ReflectTools.getDepthValue(object, key);
@@ -125,7 +124,7 @@ public class ReflectToolsTest {
     public void testGetLastListIndex() {
         String json = "{data:[1,2,3,4,5,6]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[0]";
             Integer result = ReflectTools.getDepthValue(object, key);
@@ -174,7 +173,7 @@ public class ReflectToolsTest {
     public void testContainsDepthFields1() {
         String json = "{data:[1,2,3,4,5,6]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[0]";
             boolean contains = ReflectTools.containsDepthFields(object, key);
@@ -224,7 +223,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[1].domain";
             boolean contains = ReflectTools.containsDepthFields(object, key);
@@ -261,7 +260,7 @@ public class ReflectToolsTest {
     public void testFindDepthValue11() {
         String json = "{domain:{text:'baidu',url:'https://baidu.com'}}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "domain.text";
             List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
@@ -293,7 +292,7 @@ public class ReflectToolsTest {
         String json =
                 "[{domain:{text:'baidu',url:'https://baidu.com'}},{domain:{text:'bing',url:'https://cn.bing.com'}}]";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMaps(json);
         {
             String key = "[1].domain.text";
             List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
@@ -331,7 +330,7 @@ public class ReflectToolsTest {
         String json =
                 "[{domain:{text:'baidu',url:'https://baidu.com'}},{domain:{text:'bing',url:'https://cn.bing.com'}}]";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMaps(json);
         String key = "domain.text";
         List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
         System.out.println(key + " = " + values);
@@ -347,7 +346,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[1].domain";
             List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
@@ -373,7 +372,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         String key = "data.domain";
         List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
         Assert.assertEquals(values.size(), 2, key + ".size");
@@ -404,7 +403,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[1].domain.text";
             List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
@@ -444,7 +443,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com',address:[{city:'Guangzhou'},{city:'Nanjing'}]}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com',address:[{city:'Beijing'},{city:'Shanghai'},{city:'Shenzhen'}]}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         String key = "data.domain.address.city";
         List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
         System.out.println(key + " = " + values);
@@ -465,7 +464,7 @@ public class ReflectToolsTest {
     public void testFindDepthValue61() {
         String json = "{ data: [ { domain:{} }, { domain:null } ] }";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         String key = "data.domain.text";
         List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
         System.out.println(key + " = " + values);
@@ -478,7 +477,7 @@ public class ReflectToolsTest {
     public void testFindDepthValue62() {
         String json = "{ data: [] }";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         String key = "data.domain.text";
         List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
         System.out.println(key + " = " + values);
@@ -489,7 +488,7 @@ public class ReflectToolsTest {
     public void testFindLastListIndex() {
         String json = "{data:[1,2,3,4,5,6]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[0]";
             List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
-- 
Gitee


From ba7e283da15fedb2d7c31d9de115d4e4fcaf76cd Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 1 Feb 2023 00:56:01 +0800
Subject: [PATCH 088/160] =?UTF-8?q?qdbp-tools=E5=BC=BA=E5=BC=95=E7=94=A8ab?=
 =?UTF-8?q?le=E5=92=8Cjson?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 tools/pom.xml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/tools/pom.xml b/tools/pom.xml
index b263c84..a6bb414 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -25,14 +25,12 @@
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
 			<version>${project.version}</version>
-			<optional>true</optional>
 		</dependency>
 
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-json</artifactId>
 			<version>${project.version}</version>
-			<optional>true</optional>
 		</dependency>
 
 		<dependency>
-- 
Gitee


From fe5b4bbda3c0406a2e30762f87f7db7b03eadd4d Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 1 Feb 2023 22:49:44 +0800
Subject: [PATCH 089/160] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B5=8B=E8=AF=95?=
 =?UTF-8?q?=E7=94=A8=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
index 11f7c4e..05eaa84 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
@@ -533,13 +533,17 @@ public class ReflectToolsTest {
             String key = "data[+7]";
             List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
             System.out.println(key + " = " + values);
-            Assert.assertEquals(values.size(), 0, key + ".size");
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertNull(values.get(0).getValue(), key);
         }
         {
             String key = "data[-7]";
             List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
             System.out.println(key + " = " + values);
-            Assert.assertEquals(values.size(), 0, key + ".size");
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertNull(values.get(0).getValue(), key);
         }
     }
 
-- 
Gitee


From 6f8ebbbaea2c72a4830063d2ba445bfbec76268b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 1 Feb 2023 22:52:39 +0800
Subject: [PATCH 090/160] 5.5.1

---
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 tools/pom.xml                        | 2 +-
 8 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index b95cf4f..e0917f6 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.0</version>
+	<version>5.5.1</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index f6c1eaf..305ddcf 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.0</version>
+	<version>5.5.1</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.0</version>
+			<version>5.5.1</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index 5135b23..35a2dbb 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.0</version>
+	<version>5.5.1</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index 4761829..09c9b83 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.0</version>
+		<version>5.5.1</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index aa6b837..154d913 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.0</version>
+		<version>5.5.1</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index e60ef8c..1df5996 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.0</version>
+		<version>5.5.1</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index 4f989c5..bef47c5 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.0</version>
+		<version>5.5.1</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index a6bb414..aabda4f 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.0</version>
+	<version>5.5.1</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From ed506ce1b36774d1b4f3becbbf2513ef0efe7ec6 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 5 Feb 2023 10:58:32 +0800
Subject: [PATCH 091/160] =?UTF-8?q?MapTools=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/ConvertTools.java  |  2 +-
 .../com/gitee/qdbp/tools/utils/MapTools.java  | 57 +++++++++++--------
 2 files changed, 35 insertions(+), 24 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index 3c9fe54..7c4217e 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -1388,7 +1388,7 @@ public class ConvertTools {
      * @deprecated move to {@link MapTools#clearBlankValue(Map)}
      */
     @Deprecated
-    public static void clearBlankValue(Map<String, Object> map) {
+    public static void clearBlankValue(Map<?, ?> map) {
         MapTools.clearBlankValue(map);
     }
 
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index 5bb8289..caceab2 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -31,18 +31,29 @@ public class MapTools {
     }
 
     /**
-     * 将Map转换为JsonMap (key转换为String)
+     * 将Map转换为JsonMap (key转换为String)<br>
+     * 如果map本就是以String为Key, 则返回原对象
      *
      * @param map Map对象
      * @return JsonMap
+     * @since 5.5.0
      */
+    @SuppressWarnings("unchecked")
     public static Map<String, Object> toJsonMap(Map<?, ?> map) {
-        Map<String, Object> result = new LinkedHashMap<>();
+        Map<String, Object> result = map instanceof LinkedHashMap ? new LinkedHashMap<>() : new HashMap<>();
+        if (map == null) {
+            return result;
+        }
+        boolean changes = false;
         for (Map.Entry<?, ?> entry : map.entrySet()) {
-            String key = entry.getKey() == null ? null : entry.getKey().toString();
-            result.put(key, entry.getValue());
+            if (entry.getKey() == null || entry.getKey() instanceof String) {
+                result.put((String) entry.getKey(), entry.getValue());
+            } else {
+                result.put(entry.getKey().toString(), entry.getValue());
+                changes = true;
+            }
         }
-        return result;
+        return changes ? result : (Map<String, Object>) map;
     }
 
     /**
@@ -50,6 +61,7 @@ public class MapTools {
      *
      * @param list 列表
      * @return JsonMap列表
+     * @since 5.5.0
      */
     public static List<Map<String, Object>> toJsonMaps(Collection<?> list) {
         return toJsonMaps(list, true);
@@ -60,6 +72,7 @@ public class MapTools {
      *
      * @param list 列表
      * @return JsonMap列表
+     * @since 5.5.0
      */
     private static List<Map<String, Object>> toJsonMaps(Collection<?> list, boolean throwOnError) {
         List<Map<String, Object>> results = new ArrayList<>();
@@ -116,35 +129,34 @@ public class MapTools {
      * 将Map的KEY转换为驼峰名
      *
      * @param data 数据列表
-     * @return 转换后的数据
+     * @since 5.5.2
      */
-    public static Map<String, Object> toCamelKeyMap(Map<String, Object> data) {
+    public static void mapKeyToCamel(Map<String, Object> data) {
         if (data == null) {
-            return null;
+            return;
         }
-        Map<String, Object> map = new LinkedHashMap<>();
+        Map<String, Object> temp = data instanceof LinkedHashMap ? new LinkedHashMap<>() : new HashMap<>();
         for (Map.Entry<String, Object> entry : data.entrySet()) {
             String key = entry.getKey();
-            map.put(NamingTools.toCamelString(key), entry.getValue());
+            temp.put(NamingTools.toCamelString(key), entry.getValue());
         }
-        return map;
+        data.clear();
+        data.putAll(temp);
     }
 
     /**
      * 将Map的KEY转换为驼峰名
      *
      * @param data 数据列表
-     * @return 转换后的数据
+     * @since 5.5.2
      */
-    public static List<Map<String, Object>> toCamelKeyMaps(List<Map<String, Object>> data) {
+    public static void mapKeyToCamel(List<Map<String, Object>> data) {
         if (data == null) {
-            return null;
+            return;
         }
-        List<Map<String, Object>> result = new ArrayList<>();
         for (Map<String, Object> item : data) {
-            result.add(toCamelKeyMap(item));
+            mapKeyToCamel(item);
         }
-        return result;
     }
 
     /**
@@ -152,24 +164,23 @@ public class MapTools {
      *
      * @param map Map对象
      */
-    @SuppressWarnings("unchecked")
-    public static void clearBlankValue(Map<String, Object> map) {
+    public static void clearBlankValue(Map<?, ?> map) {
         if (map == null) {
             return;
         }
-        Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
+        Iterator<? extends Map.Entry<?, ?>> iterator = map.entrySet().iterator();
         while (iterator.hasNext()) {
-            Map.Entry<String, Object> entry = iterator.next();
+            Map.Entry<?, ?> entry = iterator.next();
             Object value = entry.getValue();
             if (VerifyTools.isBlank(value)) {
                 iterator.remove();
             } else if (value instanceof Map) {
-                clearBlankValue((Map<String, Object>) value);
+                clearBlankValue((Map<?, ?>) value);
             } else if (value instanceof Collection) {
                 Collection<?> collection = (Collection<?>) value;
                 for (Object item : collection) {
                     if (item instanceof Map) {
-                        clearBlankValue((Map<String, Object>) item);
+                        clearBlankValue((Map<?, ?>) item);
                     }
                 }
             }
-- 
Gitee


From b272f20af6b3fa2cba56c0ffbff8c34c45f2ff6f Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 11 Feb 2023 19:43:50 +0800
Subject: [PATCH 092/160] =?UTF-8?q?MapPlus=E5=A2=9E=E5=8A=A0=E9=94=81?=
 =?UTF-8?q?=E5=AE=9A=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/beans/MapPlus.java   | 21 ++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java b/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
index 5e3ae2d..0b704ad 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
@@ -1,10 +1,10 @@
 package com.gitee.qdbp.tools.beans;
 
-import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.able.beans.ModifyStatus;
 import com.gitee.qdbp.tools.utils.MapTools;
 import com.gitee.qdbp.tools.utils.ReflectTools;
 import com.gitee.qdbp.tools.utils.StringTools;
@@ -15,9 +15,11 @@ import com.gitee.qdbp.tools.utils.StringTools;
  * @author zhaohuihua
  * @version 20221207
  */
-public class MapPlus extends HashMap<String, Object> {
+public class MapPlus extends LinkedHashMap<String, Object> {
     private static final long serialVersionUID = 4479911245968765771L;
 
+    private final ModifyStatus modifyStatus = new ModifyStatus();
+
     public MapPlus() {
     }
 
@@ -36,8 +38,9 @@ public class MapPlus extends HashMap<String, Object> {
 
     @Override
     public void putAll(Map<? extends String, ?> m) {
+        this.modifyStatus.checkModifiable();
         if (m != null) {
-            for (Entry<? extends String, ?> entry : m.entrySet()) {
+            for (Map.Entry<? extends String, ?> entry : m.entrySet()) {
                 put(entry.getKey(), entry.getValue());
             }
         }
@@ -46,6 +49,7 @@ public class MapPlus extends HashMap<String, Object> {
     @Override
     @SuppressWarnings("unchecked")
     public Object put(String key, Object value) {
+        this.modifyStatus.checkModifiable();
         if (key == null) {
             return super.put(null, value);
         } else if (key.indexOf('.') <= 0) {
@@ -80,6 +84,7 @@ public class MapPlus extends HashMap<String, Object> {
 
     @Override
     public Object remove(Object key) {
+        this.modifyStatus.checkModifiable();
         if (key == null) {
             return super.remove(null);
         } else if (key instanceof String && ((String) key).indexOf('.') > 0) {
@@ -187,6 +192,12 @@ public class MapPlus extends HashMap<String, Object> {
         return MapTools.getSubMaps(this, key);
     }
 
+    /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
+    public MapPlus lockToUnmodifiable() {
+        this.modifyStatus.setModifiable(false);
+        return this;
+    }
+
     protected boolean isComplexKey(String key) {
         return key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0;
     }
@@ -199,7 +210,7 @@ public class MapPlus extends HashMap<String, Object> {
      */
     public static MapPlus of(Map<?, ?> map) {
         MapPlus result = new MapPlus();
-        for (Entry<?, ?> entry : map.entrySet()) {
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
             Object key = entry.getKey();
             if (key == null) {
                 result.put(null, entry.getValue());
-- 
Gitee


From c493cfd12a6694ad6b1fecf5c281e94df306d4a7 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 11 Feb 2023 19:53:52 +0800
Subject: [PATCH 093/160] NullPointerException

---
 .../main/java/com/gitee/qdbp/tools/utils/ConvertTools.java | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index 7c4217e..2c824a5 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -1231,7 +1231,7 @@ public class ConvertTools {
                 } catch (NumberFormatException ignore) {
                 }
             }
-        } else {
+        } else if (fieldName != null) {
             try {
                 if (fieldName.endsWith("OfMinInDay") || fieldName.endsWith("OfMinDay")) {
                     Date date = DateTools.parse(fieldValue);
@@ -1248,6 +1248,11 @@ public class ConvertTools {
                 }
             } catch (IllegalArgumentException ignore) {
             }
+        } else if (isLikeDateFormat(fieldValue)) {
+            try {
+                return DateTools.parse(fieldValue);
+            } catch (IllegalArgumentException ignore) {
+            }
         }
 
         return fieldValue;
-- 
Gitee


From 49cbde9fdf64d4f28f14b4c16152d6ff59136a73 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 11 Feb 2023 23:06:30 +0800
Subject: [PATCH 094/160] =?UTF-8?q?JsonTools.convert=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E6=95=B0=E5=AD=97=E7=89=B9=E6=AE=8A=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/ConvertTools.java  | 54 ++++++++++++++++---
 .../com/gitee/qdbp/tools/utils/DateTools.java |  2 +-
 .../com/gitee/qdbp/tools/utils/JsonTools.java | 15 ++++++
 3 files changed, 63 insertions(+), 8 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index 2c824a5..19517eb 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -20,6 +20,7 @@ import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -634,14 +635,17 @@ public class ConvertTools {
         } else {
             String[] values = StringTools.split(value, '*');
             for (String string : values) {
+                if (string.isEmpty()) {
+                    return defaults; // 格式错误
+                }
                 try {
                     if (number == null) {
-                        number = new BigInteger(string);
+                        number = parseBigInteger(string);
                     } else {
-                        number = number.multiply(new BigInteger(string));
+                        number = number.multiply(parseBigInteger(string));
                     }
                 } catch (NumberFormatException e) {
-                    return defaults;
+                    return defaults; // 格式错误
                 }
             }
         }
@@ -654,6 +658,32 @@ public class ConvertTools {
         }
     }
 
+    private static BigInteger parseBigInteger(String s) {
+        if (s.indexOf(':') > 0 && s.indexOf('.') > 0) {
+            try {
+                return BigInteger.valueOf(DateTools.parseTime(s));
+            } catch (Exception e) {
+                throw new NumberFormatException(s);
+            }
+        } else if (StringTools.isNumber(s)) {
+            return new BigInteger(s);
+        } else if (s.startsWith("P") || s.startsWith("p") || s.startsWith("-P") || s.startsWith("-p")) {
+            try {
+                // P2DT3H4M / PT15H / PT15M
+                return BigInteger.valueOf(java.time.Duration.parse(s).toMillis());
+            } catch (Exception e) {
+                throw new NumberFormatException(s);
+            }
+        } else {
+            Long number = tryParseByteString(s);
+            if (number == null) {
+                throw new NumberFormatException(s);
+            } else {
+                return BigInteger.valueOf(number);
+            }
+        }
+    }
+
     /**
      * 转换为数字
      *
@@ -925,7 +955,7 @@ public class ConvertTools {
     private static final long tebibyte = gibibyte * kibibyte;
     private static final long pebibyte = tebibyte * kibibyte;
     private static final long exbibyte = pebibyte * kibibyte;
-    private static final Map<String, Long> BYTE_UNITS = new HashMap<>();
+    private static final Map<String, Long> BYTE_UNITS = new LinkedHashMap<>();
 
     static { // 单位对应的倍数
         BYTE_UNITS.put("B", 1L);
@@ -976,6 +1006,15 @@ public class ConvertTools {
      */
     public static long parseByteString(String string) {
         VerifyTools.requireNotBlank(string, "ByteString");
+        Long number = tryParseByteString(string);
+        if (number == null) {
+            throw new NumberFormatException(string);
+        } else {
+            return number;
+        }
+    }
+
+    private static Long tryParseByteString(String string) {
         // 截取数字和单位
         String number = string.trim();
         String unit = null;
@@ -991,7 +1030,7 @@ public class ConvertTools {
             }
         }
         if (VerifyTools.isBlank(number)) {
-            throw new NumberFormatException(string);
+            return null;
         }
         // 计算数值
         if (unit == null) {
@@ -999,9 +1038,10 @@ public class ConvertTools {
         } else {
             Long rate = BYTE_UNITS.get(unit.toUpperCase());
             if (rate == null) {
-                throw new NumberFormatException(string);
+                return null;
+            } else {
+                return Long.parseLong(number) * rate;
             }
-            return Long.parseLong(number) * rate;
         }
     }
 
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
index 625ccf1..8b4a3e5 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
@@ -485,7 +485,7 @@ public class DateTools {
      * @param string 时间字符串
      * @return 时间毫秒数
      */
-    private static long parseTime(String string) {
+    static long parseTime(String string) {
         if (string == null || string.trim().length() == 0) {
             return 0;
         }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
index f2b8fb5..84d8da3 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
@@ -94,6 +94,21 @@ public class JsonTools extends JsonMaps {
         if (clazz == Date.class) {
             return (T) DateTools.parse(string);
         }
+        if (clazz == int.class || clazz == Integer.class) {
+            return (T) Integer.valueOf(ConvertTools.toInteger(string));
+        }
+        if (clazz == long.class || clazz == Long.class) {
+            return (T) Long.valueOf(ConvertTools.toLong(string));
+        }
+        if (clazz == double.class || clazz == Double.class) {
+            return (T) Double.valueOf(ConvertTools.toDouble(string));
+        }
+        if (clazz == float.class || clazz == Float.class) {
+            return (T) Float.valueOf(ConvertTools.toFloat(string));
+        }
+        if (clazz == boolean.class || clazz == Boolean.class) {
+            return (T) Boolean.valueOf(ConvertTools.toBoolean(string));
+        }
         if (VerifyTools.isJsonObjectString(string)) {
             if (clazz == Map.class) {
                 return (T) parseAsMap(string);
-- 
Gitee


From 3d318f0c2c99921e088e902474eb4b715138c602 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 11 Feb 2023 23:08:10 +0800
Subject: [PATCH 095/160] =?UTF-8?q?isWordString=E5=A2=9E=E5=8A=A0allowChar?=
 =?UTF-8?q?s=E5=8F=82=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/StringTools.java   | 82 ++++++++++++++++---
 1 file changed, 70 insertions(+), 12 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index 0cf1135..0a59f3d 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -294,6 +294,27 @@ public class StringTools {
         return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_' || c == '$';
     }
 
+    /**
+     * 是不是单词字符
+     *
+     * @param c 字符
+     * @param allowChars 除字母数字以外的允许字符
+     * @return 是不是单词字符
+     */
+    private static boolean isWordChar(char c, char... allowChars) {
+        if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9') {
+            return true;
+        }
+        if (allowChars != null) {
+            for (int i = 0; i < allowChars.length; i++) {
+                if (c == allowChars[i]) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * 是不是单词
      *
@@ -304,30 +325,64 @@ public class StringTools {
         if (string == null || string.length() == 0) {
             return false;
         }
-        char fc = string.charAt(0);
-        if (fc >= '0' && fc <= '9') {
-            return false; // 以数字开头的, 不是单词
+        char[] allowChars = { '_', '$' };
+        return checkWordString(string, allowChars);
+    }
+
+    /**
+     * 是不是单词
+     *
+     * @param string 字符串
+     * @return 是不是单词
+     */
+    public static boolean isWordString(CharSequence string) {
+        if (string == null || string.length() == 0) {
+            return false;
         }
-        for (int i = 0, z = string.length(); i < z; i++) {
-            char c = string.charAt(i);
-            if (!isWordChar(c)) {
-                return false;
-            }
+        char[] allowChars = { '_', '$' };
+        return checkWordString(string.toString(), allowChars);
+    }
+
+    /**
+     * 是不是单词
+     *
+     * @param string 字符串
+     * @param allowChars 除字母数字以外的允许字符
+     * @return 是不是单词
+     */
+    public static boolean isWordString(String string, char... allowChars) {
+        if (string == null || string.length() == 0) {
+            return false;
         }
-        return true;
+        return checkWordString(string, allowChars);
     }
 
     /**
      * 是不是单词
      *
      * @param string 字符串
+     * @param allowChars 除字母数字以外的允许字符
      * @return 是不是单词
      */
-    public static boolean isWordString(CharSequence string) {
+    public static boolean isWordString(CharSequence string, char... allowChars) {
         if (string == null || string.length() == 0) {
             return false;
         }
-        return isWordString(string.toString());
+        return checkWordString(string.toString(), allowChars);
+    }
+
+    private static boolean checkWordString(String string, char... allowChars) {
+        char fc = string.charAt(0);
+        if (fc >= '0' && fc <= '9') {
+            return false; // 以数字开头的, 不是单词
+        }
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (!isWordChar(c, allowChars)) {
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
@@ -1031,6 +1086,9 @@ public class StringTools {
     }
 
     private static String removeLeftRight(String string, boolean left, boolean right, char... chars) {
+        if (string == null) {
+            return null;
+        }
         char[] value = string.toCharArray();
         int length = value.length;
         int start = 0;
@@ -1394,7 +1452,7 @@ public class StringTools {
 
     /**
      * 替换相同长度的后缀
-     * 
+     *
      * @param string 源字符串
      * @param suffix 新的后缀
      * @return 替换后的字符串
-- 
Gitee


From 01789ff5972ef07a71d90b08eeb56d1c1aa6c506 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 11 Feb 2023 23:08:46 +0800
Subject: [PATCH 096/160] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/tools/utils/ReflectTools.java  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
index 5f99c15..7e6dca7 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
@@ -356,13 +356,13 @@ public abstract class ReflectTools {
         }
     }
 
-    // 是否需要展开 (有下一层且下一层是数字时,不需要展开)
+    // 根据字段名判断是否需要展开 (有下一层且下一层是数字时,说明是取数组的其中一项,不需要展开)
     private static boolean needExpandList(List<String> fields, int index) {
         if (index >= fields.size()) {
             return true;
         } else {
-            String value = fields.get(index);
-            return !StringTools.isNumber(value) || value.indexOf('.') >= 0;
+            String field = fields.get(index);
+            return !StringTools.isNumber(field) || field.indexOf('.') >= 0;
         }
     }
 
-- 
Gitee


From df157c165f376d69f711dd3badb9ee9dc607993f Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 11 Feb 2023 23:09:34 +0800
Subject: [PATCH 097/160] =?UTF-8?q?XmlTools=E7=9A=84array=E5=A2=9E?=
 =?UTF-8?q?=E5=8A=A0StringMatcher=E5=8C=B9=E9=85=8D=E8=A7=84=E5=88=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/utils/XmlTools.java    | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
index 7b009cc..2725de3 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
@@ -17,6 +17,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.i18n.LocaleTools;
+import com.gitee.qdbp.able.matches.StringMatcher;
 import com.gitee.qdbp.able.result.IResultMessage;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -95,7 +96,7 @@ public class XmlTools {
         return container;
     }
 
-    // { parent: { array: [ {aa:1},{aa:2} ] } } 提供为 { parent: { aa:[1,2] } }
+    // { parent: { array: [ {aa:1},{aa:2} ] } } 提升为 { parent: { aa:[1,2] } }
     @SuppressWarnings("unchecked")
     private static void upgradeArraySameNodes(Map<String, Object> map, XmlToJsonOptions o) {
         for (Map.Entry<String, Object> entry : map.entrySet()) {
@@ -419,6 +420,8 @@ public class XmlTools {
         private String textContentName = TEXT_NAME;
         /** 数组节点名称 (不区分大小写) **/
         private String arrayNodeName = ARRAY_NAME;
+        /** 数组节点匹配接口 **/
+        private StringMatcher arrayNodeMatcher;
 
         /** 是否使用驼峰命名 **/
         public boolean isUseCamelNaming() {
@@ -475,6 +478,16 @@ public class XmlTools {
             this.arrayNodeName = arrayNodeName;
         }
 
+        /** 数组节点匹配接口 **/
+        public StringMatcher getArrayNodeMatcher() {
+            return arrayNodeMatcher;
+        }
+
+        /** 数组节点匹配接口 **/
+        public void setArrayNodeMatcher(StringMatcher arrayNodeMatcher) {
+            this.arrayNodeMatcher = arrayNodeMatcher;
+        }
+
         /** 判断是不是数组节点名称 (不区分大小写) **/
         public boolean isArrayNodeName(String nodeName) {
             List<String> arrayNodeNames = StringTools.splits(this.arrayNodeName, ',');
@@ -483,7 +496,7 @@ public class XmlTools {
                     return true;
                 }
             }
-            return false;
+            return arrayNodeMatcher != null && arrayNodeMatcher.matches(nodeName);
         }
     }
 
-- 
Gitee


From 1b3bdb53b453378930ea52be213a7fc6a6d5e9cc Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 12 Feb 2023 00:00:55 +0800
Subject: [PATCH 098/160] =?UTF-8?q?ConvertTools.toLong=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E6=95=B0=E5=AD=97=E7=89=B9=E6=AE=8A=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/tools/utils/ConvertTools.java     | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index 19517eb..b3123e6 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -628,7 +628,7 @@ public class ConvertTools {
         BigInteger number = null;
         if (flagIndex < 0) {
             try {
-                number = new BigInteger(value);
+                number = parseBigInteger(value);
             } catch (NumberFormatException e) {
                 return defaults;
             }
@@ -668,6 +668,12 @@ public class ConvertTools {
         } else if (StringTools.isNumber(s)) {
             return new BigInteger(s);
         } else if (s.startsWith("P") || s.startsWith("p") || s.startsWith("-P") || s.startsWith("-p")) {
+            try {
+                // Duration since jdk8
+                Class.forName("java.time.Duration");
+            } catch (ClassNotFoundException e) {
+                throw new NumberFormatException(s);
+            }
             try {
                 // P2DT3H4M / PT15H / PT15M
                 return BigInteger.valueOf(java.time.Duration.parse(s).toMillis());
-- 
Gitee


From dc044f99fd7a21c16f8b8356e371db6467d19a6a Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 12 Feb 2023 00:01:23 +0800
Subject: [PATCH 099/160] =?UTF-8?q?=E5=85=BC=E5=AE=B9jdk7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/pom.xml                                         |  2 +-
 .../java/com/gitee/qdbp/tools/utils/MapTools.java    | 12 ++++++++++--
 2 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index e0917f6..c39950f 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.1</version>
+	<version>5.5.2</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index caceab2..d0e7206 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -40,7 +40,7 @@ public class MapTools {
      */
     @SuppressWarnings("unchecked")
     public static Map<String, Object> toJsonMap(Map<?, ?> map) {
-        Map<String, Object> result = map instanceof LinkedHashMap ? new LinkedHashMap<>() : new HashMap<>();
+        Map<String, Object> result = newMap(map);
         if (map == null) {
             return result;
         }
@@ -56,6 +56,14 @@ public class MapTools {
         return changes ? result : (Map<String, Object>) map;
     }
 
+    private static Map<String, Object> newMap(Map<?, ?> old) {
+        if (old instanceof LinkedHashMap) {
+            return new LinkedHashMap<>();
+        } else {
+            return new HashMap<>();
+        }
+    }
+
     /**
      * 将对象列表转换为JsonMap列表
      *
@@ -135,7 +143,7 @@ public class MapTools {
         if (data == null) {
             return;
         }
-        Map<String, Object> temp = data instanceof LinkedHashMap ? new LinkedHashMap<>() : new HashMap<>();
+        Map<String, Object> temp = newMap(data);
         for (Map.Entry<String, Object> entry : data.entrySet()) {
             String key = entry.getKey();
             temp.put(NamingTools.toCamelString(key), entry.getValue());
-- 
Gitee


From 5c9460dc2d4c16db42c27614c80d66250cbdf9ff Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 12 Feb 2023 00:10:33 +0800
Subject: [PATCH 100/160] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B5=8B=E8=AF=95?=
 =?UTF-8?q?=E7=B1=BB=E5=8C=85=E8=B7=AF=E5=BE=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/{ => com/gitee/qdbp/json}/test/TestCase1ForGson.java  | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)
 rename test/qdbp-json-test-gson/src/test/java/{ => com/gitee/qdbp/json}/test/TestCase1ForGson.java (90%)

diff --git a/test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java b/test/qdbp-json-test-gson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForGson.java
similarity index 90%
rename from test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java
rename to test/qdbp-json-test-gson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForGson.java
index 90b991b..c40bc99 100644
--- a/test/qdbp-json-test-gson/src/test/java/test/TestCase1ForGson.java
+++ b/test/qdbp-json-test-gson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForGson.java
@@ -1,7 +1,6 @@
-package test;
+package com.gitee.qdbp.json.test;
 
 import org.testng.annotations.Test;
-import com.gitee.qdbp.json.test.TestCase1;
 
 /**
  * TestCase1ForGson
-- 
Gitee


From d2bffcdb99574c23c2246ab5cee6f4b3dc835e79 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 12 Feb 2023 00:11:14 +0800
Subject: [PATCH 101/160] ConvertToolsTest

---
 test/qdbp-tools-test-jdk7/pom.xml             | 94 +++++++++++++++++++
 .../qdbp/tools/test/ConvertToolsTest.java     | 59 ++++++++++++
 .../src/test/resources/logback.xml            | 51 ++++++++++
 .../src/test/resources/testng.xml             |  9 ++
 4 files changed, 213 insertions(+)
 create mode 100644 test/qdbp-tools-test-jdk7/pom.xml
 create mode 100644 test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
 create mode 100644 test/qdbp-tools-test-jdk7/src/test/resources/logback.xml
 create mode 100644 test/qdbp-tools-test-jdk7/src/test/resources/testng.xml

diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
new file mode 100644
index 0000000..54b22b0
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.2</version>
+	</parent>
+
+	<artifactId>qdbp-tools-test-jdk7</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp tools test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-able</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-tools</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+			<version>1.2.16</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>jcl-over-slf4j</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>log4j-over-slf4j</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-core</artifactId>
+			<version>1.2.3</version>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-classic</artifactId>
+			<version>1.2.3</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.testng</groupId>
+			<artifactId>testng</artifactId>
+			<version>6.14.3</version>
+		</dependency>
+
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.12.4</version>
+				<configuration>
+					<suiteXmlFiles>
+						<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
+					</suiteXmlFiles>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
new file mode 100644
index 0000000..ce8d14a
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
@@ -0,0 +1,59 @@
+package com.gitee.qdbp.tools.test;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+
+/**
+ * 数据转换工具类
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class ConvertToolsTest {
+
+    @Test
+    public void test1() {
+        long value = ConvertTools.toLong("1024 * 1024");
+        Assert.assertEquals(value, 1024 * 1024);
+    }
+
+    @Test
+    public void test2() {
+        long value = ConvertTools.toLong("10:20:30.456");
+        Assert.assertEquals(value, 10*60*60*1000+20*60*1000+30*1000+456);
+    }
+
+    @Test
+    public void test3() {
+        long value = ConvertTools.toLong("16KB");
+        Assert.assertEquals(value, 16*1024);
+    }
+
+    @Test
+    public void test4() {
+        long value = ConvertTools.toLong("1024");
+        Assert.assertEquals(value, 1024);
+    }
+
+    @Test
+    public void test5() {
+        long value = ConvertTools.toLong("-1024");
+        Assert.assertEquals(value, -1024);
+    }
+
+    @Test
+    public void test6() {
+        try {
+            // Duration since jdk8
+            Class.forName("java.time.Duration");
+
+            long value = ConvertTools.toLong("PT15H");
+            Assert.assertEquals(value, 15*60*60*1000);
+        } catch (ClassNotFoundException ignore) {
+            System.out.println("test6 skipped on jdk7 ......");
+        }
+    }
+
+}
diff --git a/test/qdbp-tools-test-jdk7/src/test/resources/logback.xml b/test/qdbp-tools-test-jdk7/src/test/resources/logback.xml
new file mode 100644
index 0000000..acff406
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/src/test/resources/logback.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<configuration>
+	<property name="pattern.stdout" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\)%n" />
+	<property name="pattern.normal" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\) | %d{yyyy-MM-dd}%n" />
+
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>${pattern.stdout}</pattern>
+		</encoder>
+	</appender>
+	<appender name="ManagerFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>../logs/qdbp-json-test-jackson.log</file>
+		<!-- 按天滚动 保存30天-->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>../logs/qdbp-json-test-jackson-%d{yyyyMMdd}-%i.log</FileNamePattern>
+			<MaxHistory>30</MaxHistory>
+			<!-- 超过指定大小滚动当天日志 -->
+			<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<MaxFileSize>50MB</MaxFileSize>
+			</TimeBasedFileNamingAndTriggeringPolicy>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${pattern.normal}</pattern>
+		</encoder>
+	</appender>
+	<appender name ="ManagerAppender" class= "ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+		<queueSize>1024</queueSize>
+		<!-- 添加附加的appender,最多只能添加一个 -->
+	 	<appender-ref ref ="ManagerFile"/>
+	</appender>
+
+	<logger name="com.gitee.qdbp" level="DEBUG" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.sql.parse" level="ALL" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.plugins.impl.BaseSqlFileScanner" level="ALL" additivity="true" />
+	<!-- <logger name="org.springframework.orm.jpa" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="true" /> -->
+	<!-- <logger name="com.alibaba.druid.pool" level="DEBUG" additivity="true" /> -->
+
+	<!-- 只要使用了BeanPostProcessor的子类就很难避免此日志: is not eligible for getting processed by all BeanPostProcessors -->
+	<logger name="org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker" level="WARN" additivity="true" />
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ManagerAppender" />
+	</root>
+
+</configuration>
\ No newline at end of file
diff --git a/test/qdbp-tools-test-jdk7/src/test/resources/testng.xml b/test/qdbp-tools-test-jdk7/src/test/resources/testng.xml
new file mode 100644
index 0000000..3f55d76
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/src/test/resources/testng.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
+<suite name="QdbpToolsSuite" parallel="classes">
+	<test name="QdbpToolsTest" verbose="2" preserve-order="true">
+		<packages>
+			<package name="com.gitee.qdbp.tools.test" />
+		</packages>
+	</test>
+</suite>
-- 
Gitee


From e51f06ae0fcba115d249bf583bd833bce68212ee Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 12 Feb 2023 00:11:39 +0800
Subject: [PATCH 102/160] 5.5.2

---
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 3 ++-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 tools/pom.xml                        | 2 +-
 7 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/json/pom.xml b/json/pom.xml
index 305ddcf..1e1c1d6 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.1</version>
+	<version>5.5.2</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.1</version>
+			<version>5.5.2</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index 35a2dbb..4e55089 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.1</version>
+	<version>5.5.2</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
@@ -24,5 +24,6 @@
 		<module>qdbp-json-test-fastjson</module>
 		<module>qdbp-json-test-gson</module>
 		<module>qdbp-json-test-jackson</module>
+		<module>qdbp-tools-test-jdk7</module>
 	</modules>
 </project>
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index 09c9b83..b6ad139 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.1</version>
+		<version>5.5.2</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index 154d913..77f0e8c 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.1</version>
+		<version>5.5.2</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index 1df5996..8975d60 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.1</version>
+		<version>5.5.2</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index bef47c5..5217e85 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.1</version>
+		<version>5.5.2</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index aabda4f..464b0cb 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.1</version>
+	<version>5.5.2</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 93eb615ed7f848048af96a727ae2f2d09cfe5486 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 13 Feb 2023 20:43:57 +0800
Subject: [PATCH 103/160] =?UTF-8?q?ConvertTools.toLong=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E6=95=B0=E5=AD=97=E7=89=B9=E6=AE=8A=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/tools/utils/ConvertTools.java | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index b3123e6..4e05eee 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -659,14 +659,15 @@ public class ConvertTools {
     }
 
     private static BigInteger parseBigInteger(String s) {
-        if (s.indexOf(':') > 0 && s.indexOf('.') > 0) {
+        if (StringTools.isNumber(s)) {
+            return new BigInteger(s);
+        } else if (StringTools.countCharacter(s, ':') == 2) {
             try {
+                // 时间, 带毫秒或不带毫秒, 10:20:30, 10:5:8.360
                 return BigInteger.valueOf(DateTools.parseTime(s));
             } catch (Exception e) {
                 throw new NumberFormatException(s);
             }
-        } else if (StringTools.isNumber(s)) {
-            return new BigInteger(s);
         } else if (s.startsWith("P") || s.startsWith("p") || s.startsWith("-P") || s.startsWith("-p")) {
             try {
                 // Duration since jdk8
-- 
Gitee


From 88b063a761a6a2ef39ed42f4bb8af77832e2a932 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 13 Feb 2023 20:44:52 +0800
Subject: [PATCH 104/160] =?UTF-8?q?mapToBeans=E6=B3=9B=E5=9E=8B=E4=BC=A0?=
 =?UTF-8?q?=E5=80=BC=E9=97=AE=E9=A2=98=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
index 84d8da3..69c905c 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
@@ -235,7 +235,7 @@ public class JsonTools extends JsonMaps {
      * @param clazz 目标Java类
      * @return Java对象
      */
-    public static <T> List<T> mapToBeans(Collection<Map<String, ?>> maps, Class<T> clazz) {
+    public static <T, V> List<T> mapToBeans(Collection<Map<String, V>> maps, Class<T> clazz) {
         return findDefaultJsonService().mapToBeans(maps, clazz);
     }
 
-- 
Gitee


From 344deb05d4864c536694793abd419e9303a0501c Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 13 Feb 2023 20:45:22 +0800
Subject: [PATCH 105/160] =?UTF-8?q?mapToBeans=E6=B3=9B=E5=9E=8B=E4=BC=A0?=
 =?UTF-8?q?=E5=80=BC=E9=97=AE=E9=A2=98=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
index 218a1e6..bf901c5 100644
--- a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
@@ -77,7 +77,7 @@ public abstract class JsonServiceForBase implements JsonService {
      * @return Java对象
      */
     @Override
-    public <T> List<T> mapToBeans(Collection<Map<String, ?>> maps, Class<T> clazz) {
+    public <T, V> List<T> mapToBeans(Collection<Map<String, V>> maps, Class<T> clazz) {
         if (maps == null) {
             return null;
         }
-- 
Gitee


From 62323109537d7a9168dfb6e266c39ee124a1accc Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 13 Feb 2023 20:46:01 +0800
Subject: [PATCH 106/160] =?UTF-8?q?jackson=E5=A2=9E=E5=8A=A0=E4=BD=8E?=
 =?UTF-8?q?=E7=89=88=E6=9C=AC=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/json/JsonServiceForJackson.java      | 54 +++++++++++++++----
 1 file changed, 43 insertions(+), 11 deletions(-)

diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java
index 43dfda3..dafb4b3 100644
--- a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java
@@ -1,11 +1,11 @@
 package com.gitee.qdbp.json;
 
+import java.io.IOException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import com.fasterxml.jackson.core.JacksonException;
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.type.TypeReference;
@@ -15,6 +15,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.ReflectTools;
 
 /**
  * JsonService的Jackson实现类
@@ -38,7 +39,7 @@ public class JsonServiceForJackson extends JsonServiceForBase {
         ObjectMapper mapper = generateObjectMapper(feature, deserializationFeature);
         try {
             return mapper.writeValueAsString(object);
-        } catch (JacksonException e) {
+        } catch (IOException e) {
             throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e)
                     .setDetails("{} to json string error", object.getClass().getSimpleName());
         }
@@ -52,7 +53,7 @@ public class JsonServiceForJackson extends JsonServiceForBase {
         ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
         try {
             return mapper.readValue(jsonString, new TypeReference<Map<String, Object>>() {});
-        } catch (JacksonException e) {
+        } catch (IOException e) {
             throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json object string error");
         }
     }
@@ -65,7 +66,7 @@ public class JsonServiceForJackson extends JsonServiceForBase {
         ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
         try {
             return mapper.readValue(jsonString, new TypeReference<List<Map<String, Object>>>() {});
-        } catch (JacksonException e) {
+        } catch (IOException e) {
             throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json array string error");
         }
     }
@@ -78,7 +79,7 @@ public class JsonServiceForJackson extends JsonServiceForBase {
         ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
         try {
             return mapper.readValue(jsonString, clazz);
-        } catch (JacksonException e) {
+        } catch (IOException e) {
             throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json object string error");
         }
     }
@@ -92,7 +93,7 @@ public class JsonServiceForJackson extends JsonServiceForBase {
         JavaType targetType = mapper.getTypeFactory().constructCollectionType(List.class, clazz);
         try {
             return mapper.readValue(jsonString, targetType);
-        } catch (JacksonException e) {
+        } catch (IOException e) {
             throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json array string error");
         }
     }
@@ -101,8 +102,14 @@ public class JsonServiceForJackson extends JsonServiceForBase {
     protected ObjectMapper generateObjectMapper(JsonFeature.Serialization sf, JsonFeature.Deserialization df) {
         ObjectMapper mapper = new ObjectMapper();
         // 是否处理循环引用 (设置为true时不应该报错)
-        mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
-        mapper.configure(SerializationFeature.WRITE_SELF_REFERENCES_AS_NULL, sf.isHandleCircularReference());
+        // Since: 2.4
+        if (supportFailOnSelfReferences) {
+            mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
+        }
+        // Since: 2.11
+        if (supportWriteSelfReferencesAsNull) {
+            mapper.configure(SerializationFeature.WRITE_SELF_REFERENCES_AS_NULL, sf.isHandleCircularReference());
+        }
         // 字段名是否使用引号括起来
         // mapper.configure(JsonWriteFeature.QUOTE_FIELD_NAMES, feature.isQuoteFieldNames());
         // 是否使用单引号 (jackson不支持)
@@ -131,8 +138,10 @@ public class JsonServiceForJackson extends JsonServiceForBase {
 
         }
         // 序列化BigDecimal类型数据时是否使用toPlainString()
-        // Since: 1.2.16
-        mapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, sf.isWriteBigDecimalAsPlain());
+        // Since: 2.3
+        if (supportWriteBigDecimalAsPlain) {
+            mapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, sf.isWriteBigDecimalAsPlain());
+        }
         // 是否跳过transient修饰的字段
         if (sf.isSkipTransientField()) {
 
@@ -157,10 +166,33 @@ public class JsonServiceForJackson extends JsonServiceForBase {
         // 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象
         mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, df.isUseBigDecimalForFloats());
         // 遇到未知属性时是否抛出异常
-        mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, !df.isFailOnUnknownEnumValues());
+        // Since: 2.0
+        if (supportReadUnknownEnumValuesAsNull) {
+            mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, !df.isFailOnUnknownEnumValues());
+        }
         // 是否将null字段串初始化为空字符串
         // feature.isReadNullStringAsEmpty()
 
         return mapper;
     }
+
+    private static boolean existField(Class<?> clazz, String fieldName) {
+        return ReflectTools.findField(clazz, fieldName) != null;
+    }
+
+    // Since: 2.4
+    private static final boolean supportFailOnSelfReferences;
+    // Since: 2.11
+    private static final boolean supportWriteSelfReferencesAsNull;
+    // Since: 2.3
+    private static final boolean supportWriteBigDecimalAsPlain;
+    // Since: 2.0
+    private static final boolean supportReadUnknownEnumValuesAsNull;
+
+    static {
+        supportFailOnSelfReferences = existField(SerializationFeature.class, "FAIL_ON_SELF_REFERENCES");
+        supportWriteSelfReferencesAsNull = existField(SerializationFeature.class, "WRITE_SELF_REFERENCES_AS_NULL");
+        supportWriteBigDecimalAsPlain = existField(JsonGenerator.Feature.class, "WRITE_BIGDECIMAL_AS_PLAIN");
+        supportReadUnknownEnumValuesAsNull = existField(DeserializationFeature.class, "READ_UNKNOWN_ENUM_VALUES_AS_NULL");
+    }
 }
-- 
Gitee


From a6694dcd5f4bcd7c1b035d346a4dd1357295fc87 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 13 Feb 2023 20:46:35 +0800
Subject: [PATCH 107/160] =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=90=8D=E4=BC=98?=
 =?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/json/JsonServiceForFastjson.java      | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java
index fbaad55..0473bf0 100644
--- a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java
@@ -497,7 +497,7 @@ public class JsonServiceForFastjson extends JsonServiceForBase {
     protected static Feature[] generateParserFeatures(JsonFeature.Deserialization feature) {
         List<Feature> config = new ArrayList<>();
         // Since: 1.2.42
-        if (hasNonStringKeyAsString) {
+        if (supportNonStringKeyAsString) {
             config.add(Feature.NonStringKeyAsString);
         }
         // 是否自动关闭流
@@ -524,7 +524,7 @@ public class JsonServiceForFastjson extends JsonServiceForBase {
         // if (feature.isFailOnUnknownFields()) {
         // }
         // 遇到未知的枚举值时是否抛出异常
-        if (feature.isFailOnUnknownEnumValues() && hasErrorOnEnumNotMatch) {
+        if (feature.isFailOnUnknownEnumValues() && supportErrorOnEnumNotMatch) {
             // Since: 1.2.55
             config.add(Feature.ErrorOnEnumNotMatch);
         }
@@ -535,17 +535,21 @@ public class JsonServiceForFastjson extends JsonServiceForBase {
         return config.toArray(new Feature[0]);
     }
 
+    private static boolean existField(Class<?> clazz, String fieldName) {
+        return ReflectTools.findField(clazz, fieldName) != null;
+    }
+
     // Since: 1.2.42
-    private static final boolean hasErrorOnEnumNotMatch;
+    private static final boolean supportErrorOnEnumNotMatch;
     // Since: 1.2.55
-    private static final boolean hasNonStringKeyAsString;
+    private static final boolean supportNonStringKeyAsString;
     // Since: 1.2.76
     private static final boolean serializerHasGetJsonType;
 
     static {
-        hasErrorOnEnumNotMatch = ReflectTools.findField(Feature.class, "ErrorOnEnumNotMatch") != null;
-        hasNonStringKeyAsString = ReflectTools.findField(Feature.class, "NonStringKeyAsString") != null;
-        serializerHasGetJsonType = ReflectTools.findMethod(JavaBeanSerializer.class, "getJSONType") != null;
+        supportErrorOnEnumNotMatch = existField(Feature.class, "ErrorOnEnumNotMatch");
+        supportNonStringKeyAsString = existField(Feature.class, "NonStringKeyAsString");
+        serializerHasGetJsonType = existField(JavaBeanSerializer.class, "getJSONType");
     }
 
     private static class MapFillToBeanHandler extends JavaBeanDeserializer {
-- 
Gitee


From 88db2ac6db321829ca850b613eb02e082f59c28b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 13 Feb 2023 20:47:29 +0800
Subject: [PATCH 108/160] =?UTF-8?q?mapToBeans=E6=B3=9B=E5=9E=8B=E4=BC=A0?=
 =?UTF-8?q?=E5=80=BC=E9=97=AE=E9=A2=98=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/src/main/java/com/gitee/qdbp/json/JsonService.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/json/JsonService.java b/able/src/main/java/com/gitee/qdbp/json/JsonService.java
index 0f99ae0..e014d63 100644
--- a/able/src/main/java/com/gitee/qdbp/json/JsonService.java
+++ b/able/src/main/java/com/gitee/qdbp/json/JsonService.java
@@ -90,7 +90,7 @@ public interface JsonService {
      * @param clazz 目标Java类
      * @return Java对象
      */
-    <T> List<T> mapToBeans(Collection<Map<String, ?>> maps, Class<T> clazz);
+    <T, V> List<T> mapToBeans(Collection<Map<String, V>> maps, Class<T> clazz);
 
     /**
      * 将Java对象转换为Map列表<br>
-- 
Gitee


From f2e93386fccd73b063ed63cdb9c1b64a12d6d40a Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 13 Feb 2023 20:48:10 +0800
Subject: [PATCH 109/160] =?UTF-8?q?ServiceException.setCode=E5=A2=9E?=
 =?UTF-8?q?=E5=8A=A0=E9=93=BE=E5=BC=8F=E6=93=8D=E4=BD=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/able/exception/ServiceException.java   | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
index 3668684..a98e3ae 100644
--- a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
+++ b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
@@ -97,8 +97,9 @@ public class ServiceException extends RuntimeException implements IResultExcepti
      * 
      * @param code 错误返回码
      */
-    public void setCode(String code) {
+    public ServiceException setCode(String code) {
         this.code = code;
+        return this;
     }
 
     /**
-- 
Gitee


From a068023df5a814dea01054aa3686e25b797e7ba2 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 13 Feb 2023 20:48:44 +0800
Subject: [PATCH 110/160] 5.5.3

---
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 5 +++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 5 ++---
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 9 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index c39950f..2da879a 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.2</version>
+	<version>5.5.3</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index 1e1c1d6..3705b60 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.2</version>
+	<version>5.5.3</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.2</version>
+			<version>5.5.3</version>
 		</dependency>
 
 		<dependency>
@@ -36,6 +36,7 @@
 		<dependency>
 			<groupId>com.fasterxml.jackson.core</groupId>
 			<artifactId>jackson-databind</artifactId>
+			<!--<version>2.7.9.7</version>-->
 			<version>2.14.2</version>
 			<optional>true</optional>
 		</dependency>
diff --git a/test/pom.xml b/test/pom.xml
index 4e55089..0527749 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.2</version>
+	<version>5.5.3</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index b6ad139..4a49d40 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.2</version>
+		<version>5.5.3</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index 77f0e8c..8bbf709 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.2</version>
+		<version>5.5.3</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index 8975d60..e0afa5e 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.2</version>
+		<version>5.5.3</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index 5217e85..f2243dc 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.2</version>
+		<version>5.5.3</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
@@ -29,8 +29,7 @@
 		<dependency>
 			<groupId>com.fasterxml.jackson.core</groupId>
 			<artifactId>jackson-databind</artifactId>
-			<version>2.14.2</version>
-			<optional>true</optional>
+			<version>2.7.9</version>
 		</dependency>
 
 	</dependencies>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index 54b22b0..2463f13 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.2</version>
+		<version>5.5.3</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index 464b0cb..c1d45c5 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.2</version>
+	<version>5.5.3</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 5b498f831315affc96cc6fd68ef82450fe999db7 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 14 Feb 2023 21:01:10 +0800
Subject: [PATCH 111/160] =?UTF-8?q?=E5=8D=95=E8=AF=8D=E6=8B=BC=E5=86=99?=
 =?UTF-8?q?=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index d0e7206..96ac9ae 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -684,7 +684,7 @@ public class MapTools {
         if (VerifyTools.isAnyBlank(data, fields)) {
             return data;
         }
-        Map<String, Set<String>> conditions = parseConditioin(fields);
+        Map<String, Set<String>> conditions = parseCondition(fields);
         DepthMap result = new DepthMap();
         for (Map.Entry<String, Set<String>> entry : conditions.entrySet()) {
             String group = entry.getKey();
@@ -749,7 +749,7 @@ public class MapTools {
         return list;
     }
 
-    private static Map<String, Set<String>> parseConditioin(String... fields) {
+    private static Map<String, Set<String>> parseCondition(String... fields) {
         Map<String, Set<String>> conditions = new HashMap<>();
         for (String item : fields) {
             String[] array = StringTools.split(item, ';');
-- 
Gitee


From 97b7a20259bad94dd1a2d6823e164186de831fb0 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 14 Feb 2023 21:01:47 +0800
Subject: [PATCH 112/160] =?UTF-8?q?parseList=E5=B0=BD=E9=87=8F=E4=BF=9D?=
 =?UTF-8?q?=E6=8C=81=E5=8E=9F=E5=AF=B9=E8=B1=A1=E8=BF=94=E5=9B=9E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/tools/utils/ConvertTools.java  | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index 4e05eee..5b1180a 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -50,7 +50,11 @@ public class ConvertTools {
         if (object == null) {
             return new ArrayList<>();
         }
-        if (object instanceof Collection) {
+        if (object instanceof List) {
+            @SuppressWarnings("unchecked")
+            List<Object> list = (List<Object>) object;
+            return list;
+        } else if (object instanceof Collection) {
             return new ArrayList<>((Collection<?>) object);
         } else if (object.getClass() == int[].class) {
             List<Object> list = new ArrayList<>();
-- 
Gitee


From d936a86ffdc0ef73f2e5ff42e966bc30efc96008 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 14 Feb 2023 21:02:13 +0800
Subject: [PATCH 113/160] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/eachfile/process/FileProcessor.java     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java b/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java
index 3c7a97f..0c1842c 100644
--- a/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java
+++ b/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java
@@ -29,7 +29,7 @@ public interface FileProcessor {
      * 
      * @param root 根目录
      * @param folder 待处理的文件夹
-     * @return 是否继续, false表示跳过后面的文件
+     * @return 是否继续, false表示跳过文件夹下面的文件
      */
     boolean folderProcess(String root, File folder);
 }
-- 
Gitee


From 3a83a14bfc8f9a029e53ce7f7f4abdd1dda172de Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 14 Feb 2023 23:05:37 +0800
Subject: [PATCH 114/160] =?UTF-8?q?MapTools.each=E6=B7=B1=E5=BA=A6?=
 =?UTF-8?q?=E9=81=8D=E5=8E=86Map=E5=86=85=E5=AE=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/utils/MapTools.java  | 236 ++++++++++++++++++
 1 file changed, 236 insertions(+)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index 96ac9ae..468eaf9 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -837,4 +837,240 @@ public class MapTools {
     protected static boolean isComplexKey(String key) {
         return key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0;
     }
+
+    /**
+     * 深度遍历Map内容 (注意: 此方法会改变map的内容, 如对象变成Map,数组变成List)
+     *
+     * @param map 根Map对象
+     * @param interceptor 拦截处理器
+     */
+    public static void each(Map<String, Object> map, EachInterceptor interceptor) {
+        doEachMap(null, map, interceptor);
+    }
+
+    /**
+     * 深层遍历Map内容 (注意: 此方法会改变map的内容, 如对象变成Map,数组变成List)
+     *
+     * @param list 根Map对象数组
+     * @param interceptor 拦截处理器
+     */
+    public static void each(List<Map<String, Object>> list, EachInterceptor interceptor) {
+        List<Object> objects = new ArrayList<>(list);
+        ListEntry listEntry = new ListEntry(objects);
+        for (int i = 0; i < list.size(); i++) {
+            String childName = "[" + i + "]";
+            Object childValue = list.get(i);
+            listEntry.init(childName, childName, i, childValue);
+            interceptor.intercept(listEntry);
+            if (listEntry.enter && listEntry.fieldValue != null) {
+                doEachHandleEntry(listEntry, interceptor);
+            }
+        }
+    }
+
+    private static void doEachMap(String parentPath, Map<String, Object> map, EachInterceptor interceptor) {
+        MapEntry mapEntry = new MapEntry(map);
+        for (Map.Entry<String, Object> item : map.entrySet()) {
+            if (item.getKey() == null || item.getKey().trim().length() == 0) {
+                continue;
+            }
+            String fieldName = item.getKey();
+            String fieldPath = StringTools.concat('.', parentPath, fieldName);
+            Object fieldValue = item.getValue();
+            mapEntry.init(fieldPath, fieldName, fieldValue);
+            interceptor.intercept(mapEntry);
+            if (mapEntry.enter && mapEntry.fieldValue != null) {
+                doEachHandleEntry(mapEntry, interceptor);
+            }
+        }
+    }
+
+    private static void doEachHandleEntry(FieldEntry mapEntry, EachInterceptor interceptor) {
+        String fieldPath = mapEntry.getFieldPath();
+        String fieldName = mapEntry.getFieldName();
+        Object fieldValue = mapEntry.getFieldValue();
+        List<Object> list = ConvertTools.parseList(fieldValue);
+        if (list.size() == 1 && list.get(0) == fieldValue) {
+            // 不是数组
+            if (fieldValue instanceof Map) {
+                Map<String, Object> child = MapTools.toJsonMap((Map<?, ?>) fieldValue);
+                doEachMap(fieldPath, child, interceptor);
+            } else if (!ReflectTools.isPrimitive(fieldValue.getClass(), false)) {
+                Map<String, Object> child = JsonTools.beanToMap(fieldValue);
+                mapEntry.setValue(child);
+                doEachMap(fieldPath, child, interceptor);
+            }
+        } else {
+            mapEntry.setValue(list);
+            ListEntry listEntry = new ListEntry(list);
+            for (int i = 0; i < list.size(); i++) {
+                String suffix = "[" + i + "]";
+                String childName = fieldName + suffix;
+                String childPath = fieldPath + suffix;
+                Object childValue = list.get(i);
+                listEntry.init(childPath, childName, i, childValue);
+                interceptor.intercept(listEntry);
+                if (listEntry.enter && listEntry.fieldValue != null) {
+                    doEachHandleEntry(listEntry, interceptor);
+                }
+            }
+        }
+    }
+
+    /** 遍历Map的拦截处理器 **/
+    public interface EachInterceptor {
+
+        /**
+         * 拦截处理方法
+         *
+         * @param entry 当前值
+         */
+        void intercept(FieldEntry entry);
+    }
+
+    /** 遍历Map的字段项变量 **/
+    public interface FieldEntry {
+
+        String getFieldPath();
+
+        String getFieldName();
+
+        Object getFieldValue();
+
+        void setValue(Object value);
+
+        /** 设置额外的参数 (如果父对象是数组, 则该操作无效) **/
+        void putExtraValue(String key, Object value);
+
+        /** 是否已处理 (只要调用了setValue/putExtraValue就是已处理) **/
+        boolean isHandled();
+
+        /** 是否进入容器, 如果设置为false则不继续处理容器内容 (只在容器值时有效) **/
+        void setEnter(boolean enter);
+    }
+
+    private static class MapEntry implements FieldEntry {
+
+        private final Map<String, Object> map;
+        private String fieldPath;
+        private String fieldName;
+        private Object fieldValue;
+        /** 是否已处理 (只要调用了setValue/putExtraValue就是已处理) **/
+        private boolean handled;
+        /** 是否进入容器, 如果设置为false则不继续处理容器内容 (只在容器值时有效) **/
+        private boolean enter;
+
+        private MapEntry(Map<String, Object> map) {
+            this.map = map;
+        }
+
+        public void init(String fieldPath, String fieldName, Object fieldValue) {
+            this.fieldPath = fieldPath;
+            this.fieldName = fieldName;
+            this.fieldValue = fieldValue;
+            this.handled = false;
+            this.enter = true;
+        }
+
+        @Override
+        public String getFieldPath() {
+            return fieldPath;
+        }
+
+        @Override
+        public String getFieldName() {
+            return fieldName;
+        }
+
+        @Override
+        public Object getFieldValue() {
+            return fieldValue;
+        }
+
+        @Override
+        public void setValue(Object value) {
+            this.map.put(this.fieldName, value);
+            this.fieldValue = value;
+            this.handled = true;
+        }
+
+        @Override
+        public void putExtraValue(String key, Object value) {
+            this.map.put(key, value);
+            this.handled = true;
+        }
+
+        @Override
+        public boolean isHandled() {
+            return this.handled;
+        }
+
+        @Override
+        public void setEnter(boolean enter) {
+            this.enter = enter;
+        }
+    }
+
+    private static class ListEntry implements FieldEntry {
+
+        private final List<Object> list;
+        private String fieldPath;
+        private String fieldName;
+        private int index;
+        private Object fieldValue;
+        /** 是否已处理 (只要调用了setValue/putExtraValue就是已处理) **/
+        private boolean handled;
+        /** 是否进入容器, 如果设置为false则不继续处理容器内容 (只在容器值时有效) **/
+        private boolean enter;
+
+        private ListEntry(List<Object> list) {
+            this.list = list;
+        }
+
+        public void init(String fieldPath, String fieldName, int index, Object fieldValue) {
+            this.fieldPath = fieldPath;
+            this.fieldName = fieldName;
+            this.index = index;
+            this.fieldValue = fieldValue;
+            this.handled = false;
+            this.enter = true;
+        }
+
+        @Override
+        public String getFieldPath() {
+            return fieldPath;
+        }
+
+        @Override
+        public String getFieldName() {
+            return fieldName;
+        }
+
+        @Override
+        public Object getFieldValue() {
+            return fieldValue;
+        }
+
+        @Override
+        public void setValue(Object value) {
+            this.list.set(this.index, value);
+            this.fieldValue = value;
+            this.handled = true;
+        }
+
+        @Override
+        public void putExtraValue(String key, Object value) {
+            // 父对象是数组, 则该操作无效
+        }
+
+        @Override
+        public boolean isHandled() {
+            return this.handled;
+        }
+
+        @Override
+        public void setEnter(boolean enter) {
+            this.enter = enter;
+        }
+    }
 }
-- 
Gitee


From 7b2f2751852a9626eeed3f0cc90d23c73124c5ea Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 14 Feb 2023 23:06:00 +0800
Subject: [PATCH 115/160] 5.5.4

---
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 9 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/able/pom.xml b/able/pom.xml
index 2da879a..77bcf4b 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.3</version>
+	<version>5.5.4</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index 3705b60..5eba04e 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.3</version>
+	<version>5.5.4</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.3</version>
+			<version>5.5.4</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index 0527749..8c9e716 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.3</version>
+	<version>5.5.4</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index 4a49d40..a4b68fa 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.3</version>
+		<version>5.5.4</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index 8bbf709..58d91a1 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.3</version>
+		<version>5.5.4</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index e0afa5e..c2050a9 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.3</version>
+		<version>5.5.4</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index f2243dc..4162433 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.3</version>
+		<version>5.5.4</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index 2463f13..1c93068 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.3</version>
+		<version>5.5.4</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index c1d45c5..ce8ab86 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.3</version>
+	<version>5.5.4</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 16fac15da3b5729350ba7d556dc037cd124a683b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 23 Feb 2023 07:33:21 +0800
Subject: [PATCH 116/160] =?UTF-8?q?MapPlus=E4=B8=8A=E7=A7=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java         | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename {tools => able}/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java (100%)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java b/able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
similarity index 100%
rename from tools/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
rename to able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
-- 
Gitee


From 3f13f49aa425bcb3c5a13ba50a55f1c1d7695b68 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 23 Feb 2023 07:33:42 +0800
Subject: [PATCH 117/160] =?UTF-8?q?MapTools=E5=B0=BD=E9=87=8F=E8=BF=94?=
 =?UTF-8?q?=E5=9B=9E=E5=8E=9F=E5=AF=B9=E8=B1=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/utils/MapTools.java  | 57 ++++++++++++++-----
 1 file changed, 44 insertions(+), 13 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index 468eaf9..ecae8e9 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -15,6 +15,7 @@ import com.gitee.qdbp.able.beans.DepthMap;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.model.reusable.ExpressionMap;
 import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.beans.MapPlus;
 
 /**
  * Map工具类<br>
@@ -38,7 +39,6 @@ public class MapTools {
      * @return JsonMap
      * @since 5.5.0
      */
-    @SuppressWarnings("unchecked")
     public static Map<String, Object> toJsonMap(Map<?, ?> map) {
         Map<String, Object> result = newMap(map);
         if (map == null) {
@@ -53,7 +53,14 @@ public class MapTools {
                 changes = true;
             }
         }
-        return changes ? result : (Map<String, Object>) map;
+        if (changes) {
+            return result;
+        } else {
+            // 如果完全未做转换, 则返回原对象
+            @SuppressWarnings("unchecked")
+            Map<String, Object> older = (Map<String, Object>) map;
+            return older;
+        }
     }
 
     private static Map<String, Object> newMap(Map<?, ?> old) {
@@ -85,17 +92,30 @@ public class MapTools {
     private static List<Map<String, Object>> toJsonMaps(Collection<?> list, boolean throwOnError) {
         List<Map<String, Object>> results = new ArrayList<>();
         int i = -1;
+        boolean changes = !(list instanceof List);
         for (Object item : list) {
             i++;
             if (item == null) {
                 results.add(null);
             } else if (item instanceof Map) {
-                results.add(toJsonMap((Map<?, ?>) item));
+                Map<String, Object> newer = toJsonMap((Map<?, ?>) item);
+                results.add(newer);
+                if (item != newer) {
+                    changes = true;
+                }
             } else {
                 results.add(parseListItem(item, i, throwOnError));
+                changes = true;
             }
         }
-        return results;
+        if (changes) {
+            return results;
+        } else {
+            // 如果完全未做转换, 则返回原对象
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> older = (List<Map<String, Object>>) list;
+            return older;
+        }
     }
 
     private static Map<String, Object> parseListItem(Object item, int i, boolean throwOnError) {
@@ -501,14 +521,7 @@ public class MapTools {
      */
     public static Map<String, Object> getSubMap(Map<?, ?> map, String key) {
         Map<?, ?> value = getValue(map, key, Map.class);
-        Map<String, Object> result = new HashMap<>();
-        if (value != null) {
-            for (Map.Entry<?, ?> entry : value.entrySet()) {
-                String fieldKey = entry.getKey() == null ? null : entry.getKey().toString();
-                result.put(fieldKey, entry.getValue());
-            }
-        }
-        return result;
+        return toJsonMap(value);
     }
 
     /**
@@ -855,7 +868,9 @@ public class MapTools {
      * @param interceptor 拦截处理器
      */
     public static void each(List<Map<String, Object>> list, EachInterceptor interceptor) {
-        List<Object> objects = new ArrayList<>(list);
+        // new ListEntry(list)语法检查报错
+        List<Object> objects = new ArrayList<>();
+        objects.addAll(list);
         ListEntry listEntry = new ListEntry(objects);
         for (int i = 0; i < list.size(); i++) {
             String childName = "[" + i + "]";
@@ -942,6 +957,9 @@ public class MapTools {
         /** 设置额外的参数 (如果父对象是数组, 则该操作无效) **/
         void putExtraValue(String key, Object value);
 
+        /** 获取额外的参数 **/
+        MapPlus getExtraValues();
+
         /** 是否已处理 (只要调用了setValue/putExtraValue就是已处理) **/
         boolean isHandled();
 
@@ -955,6 +973,7 @@ public class MapTools {
         private String fieldPath;
         private String fieldName;
         private Object fieldValue;
+        private final MapPlus extras = new MapPlus();
         /** 是否已处理 (只要调用了setValue/putExtraValue就是已处理) **/
         private boolean handled;
         /** 是否进入容器, 如果设置为false则不继续处理容器内容 (只在容器值时有效) **/
@@ -968,6 +987,7 @@ public class MapTools {
             this.fieldPath = fieldPath;
             this.fieldName = fieldName;
             this.fieldValue = fieldValue;
+            this.extras.clear();
             this.handled = false;
             this.enter = true;
         }
@@ -997,9 +1017,15 @@ public class MapTools {
         @Override
         public void putExtraValue(String key, Object value) {
             this.map.put(key, value);
+            this.extras.put(key, value);
             this.handled = true;
         }
 
+        @Override
+        public MapPlus getExtraValues() {
+            return new MapPlus(this.extras);
+        }
+
         @Override
         public boolean isHandled() {
             return this.handled;
@@ -1063,6 +1089,11 @@ public class MapTools {
             // 父对象是数组, 则该操作无效
         }
 
+        @Override
+        public MapPlus getExtraValues() {
+            return null;
+        }
+
         @Override
         public boolean isHandled() {
             return this.handled;
-- 
Gitee


From 487688666b6732ac4253431b0979f25ef81c719b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 23 Feb 2023 07:33:58 +0800
Subject: [PATCH 118/160] =?UTF-8?q?=E6=B7=B1=E5=BA=A6=E9=81=8D=E5=8E=86Map?=
 =?UTF-8?q?=E6=B5=8B=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/test/EachMapTest.java    | 74 +++++++++++++++++++
 1 file changed, 74 insertions(+)
 create mode 100644 test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/EachMapTest.java

diff --git a/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/EachMapTest.java b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/EachMapTest.java
new file mode 100644
index 0000000..92506e2
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/EachMapTest.java
@@ -0,0 +1,74 @@
+package com.gitee.qdbp.tools.test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.beans.MapPlus;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
+import com.gitee.qdbp.tools.utils.ReflectTools;
+
+/**
+ * 深度遍历Map测试
+ *
+ * @author zhaohuihua
+ * @version 20230215
+ */
+@Test
+public class EachMapTest {
+
+    @Test
+    public void test() {
+        List<Map<String, Object>> addresses = new ArrayList<>();
+        MapPlus address = new MapPlus();
+        address.put("city", "nanjing");
+        address.put("phones", Arrays.asList("1301112222", "13044445555"));
+        addresses.add(address);
+
+        MapPlus root = new MapPlus();
+        root.put("entity.user.userName", "test1");
+        root.put("entity.user.address", addresses);
+        System.out.println(JsonTools.toLogString(root));
+        Assert.assertEquals(root.size(), 1);
+        Map<String, Object> entity = root.getSubMap("entity");
+        System.out.println(entity.getClass());
+        Assert.assertEquals(entity.getClass(), MapPlus.class);
+
+        List<Map<String, Object>> list = root.getSubMaps("entity.user.address");
+        System.out.println(list.getClass());
+        Assert.assertEquals(list.size(), 1);
+        Assert.assertSame(list, addresses);
+        Map<String, Object> address1 = list.get(0);
+        System.out.println(address1.getClass());
+        Assert.assertEquals(address1.getClass(), MapPlus.class);
+        Assert.assertSame(address1, address);
+
+        final Map<String, Object> result = new LinkedHashMap<>();
+        MapTools.each(root, new MapTools.EachInterceptor() {
+            @Override
+            public void intercept(MapTools.FieldEntry entry) {
+                String fieldPath = entry.getFieldPath();
+                Object fieldValue = entry.getFieldValue();
+                if (fieldValue == null) {
+                    result.put(fieldPath, null);
+                } else if (ReflectTools.isPrimitive(fieldValue.getClass(), false)) {
+                    result.put(fieldPath, fieldValue);
+                }
+            }
+        });
+        System.out.println(JsonTools.toLogString(result));
+        Assert.assertTrue(result.containsKey("entity.user.userName"));
+        Assert.assertTrue(result.containsKey("entity.user.address[0].city"));
+        Assert.assertTrue(result.containsKey("entity.user.address[0].phones[0]"));
+        Assert.assertTrue(result.containsKey("entity.user.address[0].phones[1]"));
+
+        Assert.assertEquals(result.get("entity.user.userName"), "test1");
+        Assert.assertEquals(result.get("entity.user.address[0].city"), "nanjing");
+        Assert.assertEquals(result.get("entity.user.address[0].phones[0]"), "1301112222");
+        Assert.assertEquals(result.get("entity.user.address[0].phones[1]"), "13044445555");
+    }
+}
-- 
Gitee


From 89435c1a0573c3542a1088c950d0de042cf09c31 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 23 Feb 2023 07:41:50 +0800
Subject: [PATCH 119/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/tools/utils/MapTools.java    | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index ecae8e9..ade0c17 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -285,10 +285,7 @@ public class MapTools {
     }
 
     private static List<String> splitParams(String string) {
-        if (string == null) {
-            return null;
-        }
-        if (string.length() == 0) {
+        if (string == null || string.length() == 0) {
             return new ArrayList<>();
         }
         List<String> list = new ArrayList<>();
@@ -867,8 +864,9 @@ public class MapTools {
      * @param list 根Map对象数组
      * @param interceptor 拦截处理器
      */
+    @SuppressWarnings("all")
     public static void each(List<Map<String, Object>> list, EachInterceptor interceptor) {
-        // new ListEntry(list)语法检查报错
+        // new ListEntry(list) jdk7语法检查报错
         List<Object> objects = new ArrayList<>();
         objects.addAll(list);
         ListEntry listEntry = new ListEntry(objects);
-- 
Gitee


From cf7723b730edaad17b8931804915f5582af13d7a Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 23 Feb 2023 20:22:43 +0800
Subject: [PATCH 120/160] =?UTF-8?q?=E6=B5=81=E5=BC=8F=E5=AF=B9=E6=AF=94?=
 =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=A2=9E=E5=8A=A0=E5=85=88=E8=AE=A1=E7=AE=97?=
 =?UTF-8?q?=E5=86=8D=E5=AF=B9=E6=AF=94=E7=9A=84=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/tools/compare/CompareStream.java     | 118 +++++++++++++++---
 .../qdbp/tools/compare/CompareTools.java      |   1 +
 2 files changed, 105 insertions(+), 14 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java
index 798512b..cf76848 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java
@@ -2,15 +2,16 @@ package com.gitee.qdbp.tools.compare;
 
 import java.util.ArrayList;
 import java.util.List;
+import com.gitee.qdbp.able.function.BaseFunction;
 
 /**
- * CompareStream
+ * 流式比较容器
  *
  * @author zhaohuihua
  * @version 20220113
  */
 public class CompareStream {
-    private final List<CompareItem<?>> items = new ArrayList<>();
+    private final List<CompareItem> items = new ArrayList<>();
 
     protected CompareStream() {
     }
@@ -24,8 +25,7 @@ public class CompareStream {
      * @return 返回对象自身用于链式调用
      */
     public <T extends Comparable<T>> CompareStream asc(T o1, T o2) {
-        CompareItem<T> item = new CompareItem<T>(o1, o2, true, false);
-        this.items.add(item);
+        this.items.add(new CompareItemForObject<>(o1, o2, true, false));
         return this;
     }
 
@@ -39,8 +39,7 @@ public class CompareStream {
      * @return 返回对象自身用于链式调用
      */
     public <T extends Comparable<T>> CompareStream asc(T o1, T o2, boolean nullsLow) {
-        CompareItem<T> item = new CompareItem<T>(o1, o2, true, nullsLow);
-        this.items.add(item);
+        this.items.add(new CompareItemForObject<>(o1, o2, true, nullsLow));
         return this;
     }
 
@@ -53,8 +52,7 @@ public class CompareStream {
      * @return 返回对象自身用于链式调用
      */
     public <T extends Comparable<T>> CompareStream desc(T o1, T o2) {
-        CompareItem<T> item = new CompareItem<T>(o1, o2, false, false);
-        this.items.add(item);
+        this.items.add(new CompareItemForObject<>(o1, o2, false, false));
         return this;
     }
 
@@ -63,13 +61,76 @@ public class CompareStream {
      *
      * @param o1 对象1
      * @param o2 对象2
-     * @param <T> 对象类型
      * @param nullsLow 空值优先级: true=空值排最前, false=空值排最后
+     * @param <T> 对象类型
      * @return 返回对象自身用于链式调用
      */
     public <T extends Comparable<T>> CompareStream desc(T o1, T o2, boolean nullsLow) {
-        CompareItem<T> item = new CompareItem<T>(o1, o2, false, false);
-        this.items.add(item);
+        this.items.add(new CompareItemForObject<>(o1, o2, false, false));
+        return this;
+    }
+
+    /**
+     * 先进行计算, 再对计算结果升序比较 (空值排最后)
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param function 计算函数 (先对o1,o2进行计算再比较)
+     * @param <T> 对象类型
+     * @param <C> 中间计算结果类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T, C extends Comparable<C>> CompareStream asc(T o1, T o2, BaseFunction<T, C> function) {
+        this.items.add(new CompareItemForFunction<>(o1, o2, true, false, function));
+        return this;
+    }
+
+    /**
+     * 先进行计算, 再对计算结果升序比较
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param nullsLow 空值优先级: true=空值排最前, false=空值排最后
+     * @param function 计算函数 (先对o1,o2进行计算再比较)
+     * @param <T> 对象类型
+     * @param <C> 中间计算结果类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T, C extends Comparable<C>> CompareStream asc(T o1, T o2, boolean nullsLow,
+            BaseFunction<T, C> function) {
+        this.items.add(new CompareItemForFunction<>(o1, o2, true, nullsLow, function));
+        return this;
+    }
+
+    /**
+     * 先进行计算, 再对计算结果降序比较 (空值排最后)
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param function 计算函数 (先对o1,o2进行计算再比较)
+     * @param <T> 对象类型
+     * @param <C> 中间计算结果类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T, C extends Comparable<C>> CompareStream desc(T o1, T o2, BaseFunction<T, C> function) {
+        this.items.add(new CompareItemForFunction<>(o1, o2, false, false, function));
+        return this;
+    }
+
+    /**
+     * 先进行计算, 再对计算结果降序比较
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param nullsLow 空值优先级: true=空值排最前, false=空值排最后
+     * @param function 计算函数 (先对o1,o2进行计算再比较)
+     * @param <T> 对象类型
+     * @param <C> 中间计算结果类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T, C extends Comparable<C>> CompareStream desc(T o1, T o2, boolean nullsLow,
+            BaseFunction<T, C> function) {
+        this.items.add(new CompareItemForFunction<>(o1, o2, false, false, function));
         return this;
     }
 
@@ -79,7 +140,7 @@ public class CompareStream {
      * @return 小于返回1/等于返回0/大于返回-1
      */
     public int compare() {
-        for (CompareItem<?> item : items) {
+        for (CompareItem item : items) {
             int result = item.compare();
             if (result != 0) {
                 return result;
@@ -88,21 +149,50 @@ public class CompareStream {
         return 0;
     }
 
-    private static class CompareItem<T extends Comparable<T>> {
+    private interface CompareItem {
+        int compare();
+    }
+
+    private static class CompareItemForObject<T extends Comparable<T>> implements CompareItem {
         private final T o1;
         private final T o2;
         private final boolean ascending;
         private final boolean nullsLow;
 
-        private CompareItem(T o1, T o2, boolean ascending, boolean nullsLow) {
+        private CompareItemForObject(T o1, T o2, boolean ascending, boolean nullsLow) {
             this.o1 = o1;
             this.o2 = o2;
             this.ascending = ascending;
             this.nullsLow = nullsLow;
         }
 
+        @Override
         public int compare() {
             return CompareTools.compare(o1, o2, ascending, nullsLow);
         }
     }
+
+    private static class CompareItemForFunction<T, C extends Comparable<C>> implements CompareItem {
+        private final T o1;
+        private final T o2;
+        private final boolean ascending;
+        private final boolean nullsLow;
+        private final BaseFunction<T, C> function;
+
+        private CompareItemForFunction(T o1, T o2, boolean ascending, boolean nullsLow,
+                BaseFunction<T, C> function) {
+            this.o1 = o1;
+            this.o2 = o2;
+            this.ascending = ascending;
+            this.nullsLow = nullsLow;
+            this.function = function;
+        }
+
+        @Override
+        public int compare() {
+            C c1 = function.apply(o1);
+            C c2 = function.apply(o2);
+            return CompareTools.compare(c1, c2, ascending, nullsLow);
+        }
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
index eefb20b..db30486 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
@@ -50,6 +50,7 @@ public class CompareTools {
      * @param list 列表数据
      * @param orderings 排序规则, e.g. Orderings.of("dept desc, birthday asc, name desc");
      */
+    @SuppressWarnings("all")
     public static void sort(List<Map<String, Object>> list, Orderings orderings) {
         if (list == null || list.isEmpty() || orderings == null || orderings.isEmpty()) {
             return;
-- 
Gitee


From c2b9ae0662fdae0a4d14b1f6d7a457b88ec5c946 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 23 Feb 2023 20:24:09 +0800
Subject: [PATCH 121/160] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/src/main/java/com/gitee/qdbp/json/JsonFeature.java    | 2 +-
 able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java b/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java
index 2a257de..96c76f9 100644
--- a/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java
+++ b/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java
@@ -259,7 +259,7 @@ public class JsonFeature {
 
         /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
         public Serialization lockToUnmodifiable() {
-            this.modifyStatus.setModifiable(false);
+            this.modifyStatus.lockToUnmodifiable();
             return this;
         }
 
diff --git a/able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java b/able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
index 0b704ad..9fc63e4 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
@@ -194,7 +194,7 @@ public class MapPlus extends LinkedHashMap<String, Object> {
 
     /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
     public MapPlus lockToUnmodifiable() {
-        this.modifyStatus.setModifiable(false);
+        this.modifyStatus.lockToUnmodifiable();
         return this;
     }
 
-- 
Gitee


From 95a5ccd6b0e8d6c38e5b22d3222f62c866865a53 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 23 Feb 2023 20:24:30 +0800
Subject: [PATCH 122/160] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=8C=E5=85=83?=
 =?UTF-8?q?=E5=87=BD=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/able/function/BaseFunction.java      |  6 +++++
 .../qdbp/able/function/BaseSupplier.java      |  1 +
 .../qdbp/able/function/BinaryConsumer.java    | 27 +++++++++++++++++++
 .../qdbp/able/function/BinaryFunction.java    | 27 +++++++++++++++++++
 4 files changed, 61 insertions(+)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/function/BinaryConsumer.java
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/function/BinaryFunction.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java b/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java
index 4b9dac7..5a29ace 100644
--- a/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java
@@ -11,5 +11,11 @@ package com.gitee.qdbp.able.function;
  */
 public interface BaseFunction<T, R> {
 
+    /**
+     * Applies this function to the given argument.
+     *
+     * @param t the function argument
+     * @return the function result
+     */
     R apply(T t);
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java b/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java
index 0d1c5b7..b906d99 100644
--- a/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java
@@ -1,6 +1,7 @@
 package com.gitee.qdbp.able.function;
 
 /**
+ * JDK8中的Supplier在7中的替换接口<br>
  * Represents a supplier of results.
  *
  * <p>There is no requirement that a new or distinct result be returned each
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BinaryConsumer.java b/able/src/main/java/com/gitee/qdbp/able/function/BinaryConsumer.java
new file mode 100644
index 0000000..287bf3d
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BinaryConsumer.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.able.function;
+
+import java.util.function.Consumer;
+
+/**
+ * JDK8中的BiConsumer在7中的替换接口<br>
+ * Represents an operation that accepts two input arguments and returns no
+ * result.  This is the two-arity specialization of {@link Consumer}.
+ * Unlike most other functional interfaces, {@code BiConsumer} is expected
+ * to operate via side-effects.
+ *
+ * <p>This is a <a href="package-summary.html">functional interface</a>
+ * whose functional method is {@link #accept(Object, Object)}.</p>
+ *
+ * @param <T> the type of the first argument to the operation
+ * @param <U> the type of the second argument to the operation
+ */
+public interface BinaryConsumer<T, U> {
+
+    /**
+     * Performs this operation on the given arguments.
+     *
+     * @param t the first input argument
+     * @param u the second input argument
+     */
+    void accept(T t, U u);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BinaryFunction.java b/able/src/main/java/com/gitee/qdbp/able/function/BinaryFunction.java
new file mode 100644
index 0000000..3f83870
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BinaryFunction.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.able.function;
+
+import java.util.function.Function;
+
+/**
+ * JDK8中的BiFunction在7中的替换接口<br>
+ * Represents a function that accepts two arguments and produces a result.
+ * This is the two-arity specialization of {@link Function}.
+ *
+ * <p>This is a <a href="package-summary.html">functional interface</a>
+ * whose functional method is {@link #apply(Object, Object)}.</p>
+ *
+ * @param <T> the type of the first argument to the function
+ * @param <U> the type of the second argument to the function
+ * @param <R> the type of the result of the function
+ */
+public interface BinaryFunction<T, U, R> {
+
+    /**
+     * Applies this function to the given arguments.
+     *
+     * @param t the first function argument
+     * @param u the second function argument
+     * @return the function result
+     */
+    R apply(T t, U u);
+}
-- 
Gitee


From e43725090ac007a8218792ebebfc91f8c5bd2b86 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 23 Feb 2023 20:25:05 +0800
Subject: [PATCH 123/160] qdbp 5.5.5

---
 README.md                            | 4 ++--
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 10 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index da978f2..9182cfb 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.4.20</version>
+        <version>5.5.5</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.4.20</version>
+        <version>5.5.5</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index 77bcf4b..8692fbe 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.4</version>
+	<version>5.5.5</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index 5eba04e..ee452ed 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.4</version>
+	<version>5.5.5</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.4</version>
+			<version>5.5.5</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index 8c9e716..a554543 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.4</version>
+	<version>5.5.5</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index a4b68fa..cb9bf0f 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.4</version>
+		<version>5.5.5</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index 58d91a1..044c297 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.4</version>
+		<version>5.5.5</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index c2050a9..aea71a7 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.4</version>
+		<version>5.5.5</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index 4162433..ab9df3e 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.4</version>
+		<version>5.5.5</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index 1c93068..495935b 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.4</version>
+		<version>5.5.5</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index ce8ab86..847d8bd 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.4</version>
+	<version>5.5.5</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 9729f7be5439a34f5ccb6c902062d94d76027fb7 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 27 Feb 2023 19:21:53 +0800
Subject: [PATCH 124/160] =?UTF-8?q?=E5=AF=B9=E8=A7=A3=E6=9E=90=E4=B8=BAmap?=
 =?UTF-8?q?=E7=9A=84Yaml=E7=9A=84=E6=95=B0=E7=BB=84=E7=9A=84=E7=89=B9?=
 =?UTF-8?q?=E6=AE=8A=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/ConvertTools.java  | 21 +++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index 5b1180a..cec9555 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -106,6 +106,8 @@ public class ConvertTools {
             return list;
         } else if (object.getClass().isArray()) {
             return new ArrayList<>(Arrays.asList((Object[]) object));
+        } else if (object instanceof Map && isListLikeness((Map<?, ?>) object)) {
+            return new ArrayList<>(((Map<?, ?>) object).values());
         } else if (object instanceof Iterable) {
             List<Object> values = new ArrayList<>();
             Iterable<?> iterable = (Iterable<?>) object;
@@ -132,6 +134,25 @@ public class ConvertTools {
         }
     }
 
+    // 所有key是从0开始的连续整数, 就判定为类似数组
+    // Yaml的数组有时会解析为key为序号的map, 因此加一个特殊处理
+    private static boolean isListLikeness(Map<?, ?> map) {
+        if (map.isEmpty() || !(map instanceof LinkedHashMap)) {
+            return false;
+        }
+        int index = 0;
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+            if (entry.getKey() instanceof Integer) {
+                if (entry.getKey().equals(index++)) {
+                    continue;
+                }
+            }
+            return false;
+        }
+        // 所有key是从0开始的连续整数
+        return true;
+    }
+
     /**
      * 将对象解析为列表
      *
-- 
Gitee


From 5833bb65c31073e0dcc278d7d1f9f39f19f98210 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 27 Feb 2023 19:26:03 +0800
Subject: [PATCH 125/160] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=B1=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../qdbp/tools/test/ConvertToolsTest.java     | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
index ce8d14a..aa1866d 100644
--- a/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
+++ b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
@@ -1,5 +1,9 @@
 package com.gitee.qdbp.tools.test;
 
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 import com.gitee.qdbp.tools.utils.ConvertTools;
@@ -56,4 +60,28 @@ public class ConvertToolsTest {
         }
     }
 
+    @Test
+    public void test7() {
+        Map<Integer, Object> map = new LinkedHashMap<>();
+        map.put(0, "1");
+        map.put(1, "2");
+        map.put(2, "3");
+        List<Object> objects = ConvertTools.parseList(map);
+        Assert.assertEquals(objects.size(), 3);
+        Assert.assertEquals(objects.get(0), "1");
+        System.out.println(objects);
+    }
+
+    @Test
+    public void test8() {
+        Map<Integer, Object> map = new HashMap<>();
+        map.put(0, "1");
+        map.put(1, "2");
+        map.put(2, "3");
+        List<Object> objects = ConvertTools.parseList(map);
+        Assert.assertEquals(objects.size(), 1);
+        Assert.assertSame(objects.get(0), map);
+        Assert.assertEquals(objects.get(0).getClass(), HashMap.class);
+        System.out.println(objects);
+    }
 }
-- 
Gitee


From 3024635e2907fc5e2cbe89457ea64c8c3f8417e0 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 27 Feb 2023 19:27:07 +0800
Subject: [PATCH 126/160] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=B1=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/compare/CompareTest.java | 30 +++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java b/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java
index bd89dd5..1422aec 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java
@@ -7,6 +7,7 @@ import java.util.Comparator;
 import java.util.List;
 import org.testng.Assert;
 import org.testng.annotations.Test;
+import com.gitee.qdbp.able.function.BaseFunction;
 import com.gitee.qdbp.tools.utils.AssertTools;
 
 /**
@@ -85,6 +86,35 @@ public class CompareTest {
     }
 
     @Test(priority = 6)
+    public void testObjects2() {
+        List<Item1> items = new ArrayList<>();
+        items.add(new Item1(10, 15));
+        items.add(new Item2(null, 8));
+        items.add(new Item2(5, null));
+        items.add(new Item1(10, 13));
+        items.add(new Item2(5, 8));
+        items.add(new Item1(10, null));
+        items.add(new Item1(10, 8));
+        Collections.sort(items, new Comparator<Item1>() {
+            public int compare(Item1 a, Item1 b) {
+                return CompareTools.stream()
+                        .asc(a, b, new BaseFunction<Item1, Integer>() {
+                            public Integer apply(Item1 o) {
+                                return o.start;
+                            }
+                        })
+                        .asc(a, b, new BaseFunction<Item1, Integer>() {
+                            public Integer apply(Item1 o) {
+                                return o.end;
+                            }
+                        })
+                        .compare();
+            }
+        });
+        System.out.println(items);
+    }
+
+    @Test(priority = 7)
     public void testAscCompares() {
         {
             int result = CompareTools.stream()
-- 
Gitee


From d142a2f377aeb49e51cf2d53f92d73a5dbc6433d Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 27 Feb 2023 19:27:55 +0800
Subject: [PATCH 127/160] qdbp 5.5.6

---
 README.md                            | 4 ++--
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 10 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index 9182cfb..87aa50d 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.5.5</version>
+        <version>5.5.6</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.5.5</version>
+        <version>5.5.6</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index 8692fbe..2923b92 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.5</version>
+	<version>5.5.6</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index ee452ed..34a54c5 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.5</version>
+	<version>5.5.6</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.5</version>
+			<version>5.5.6</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index a554543..fc46b25 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.5</version>
+	<version>5.5.6</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index cb9bf0f..561b1b6 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.5</version>
+		<version>5.5.6</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index 044c297..e2cde4a 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.5</version>
+		<version>5.5.6</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index aea71a7..2281340 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.5</version>
+		<version>5.5.6</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index ab9df3e..31f3527 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.5</version>
+		<version>5.5.6</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index 495935b..9601dd2 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.5</version>
+		<version>5.5.6</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index 847d8bd..6825b87 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.5</version>
+	<version>5.5.6</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 3a102332033f947582e0293d417e6a475193b13f Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 28 Feb 2023 21:48:51 +0800
Subject: [PATCH 128/160] =?UTF-8?q?=E5=AF=B9=E8=A7=A3=E6=9E=90=E4=B8=BAmap?=
 =?UTF-8?q?=E7=9A=84Yaml=E7=9A=84=E6=95=B0=E7=BB=84=E7=9A=84=E7=89=B9?=
 =?UTF-8?q?=E6=AE=8A=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/ConvertTools.java  |  9 +++++--
 .../qdbp/tools/test/ConvertToolsTest.java     | 26 ++++++++++++++-----
 2 files changed, 26 insertions(+), 9 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index cec9555..a6e6af2 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -142,8 +142,13 @@ public class ConvertTools {
         }
         int index = 0;
         for (Map.Entry<?, ?> entry : map.entrySet()) {
-            if (entry.getKey() instanceof Integer) {
-                if (entry.getKey().equals(index++)) {
+            Object key = entry.getKey();
+            if (key instanceof Integer) {
+                if (key.equals(index++)) {
+                    continue;
+                }
+            } else if (key instanceof String && StringTools.isDigit((String) key)) {
+                if (Integer.parseInt((String) key) == index++) {
                     continue;
                 }
             }
diff --git a/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
index aa1866d..9beb3da 100644
--- a/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
+++ b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
@@ -63,21 +63,33 @@ public class ConvertToolsTest {
     @Test
     public void test7() {
         Map<Integer, Object> map = new LinkedHashMap<>();
-        map.put(0, "1");
-        map.put(1, "2");
-        map.put(2, "3");
+        map.put(0, "A");
+        map.put(1, "B");
+        map.put(2, "C");
         List<Object> objects = ConvertTools.parseList(map);
         Assert.assertEquals(objects.size(), 3);
-        Assert.assertEquals(objects.get(0), "1");
+        Assert.assertEquals(objects.get(0), "A");
         System.out.println(objects);
     }
 
     @Test
     public void test8() {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("0", "A");
+        map.put("1", "B");
+        map.put("2", "C");
+        List<Object> objects = ConvertTools.parseList(map);
+        Assert.assertEquals(objects.size(), 3);
+        Assert.assertEquals(objects.get(0), "A");
+        System.out.println(objects);
+    }
+
+    @Test
+    public void test9() {
         Map<Integer, Object> map = new HashMap<>();
-        map.put(0, "1");
-        map.put(1, "2");
-        map.put(2, "3");
+        map.put(0, "A");
+        map.put(1, "B");
+        map.put(2, "C");
         List<Object> objects = ConvertTools.parseList(map);
         Assert.assertEquals(objects.size(), 1);
         Assert.assertSame(objects.get(0), map);
-- 
Gitee


From b6ee720c48c6883113dda5d2795f8db1f3ddf31e Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Tue, 28 Feb 2023 21:49:06 +0800
Subject: [PATCH 129/160] qdbp 5.5.7

---
 README.md                            | 4 ++--
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 10 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index 87aa50d..23828ee 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.5.6</version>
+        <version>5.5.7</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.5.6</version>
+        <version>5.5.7</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index 2923b92..aa8b99e 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.6</version>
+	<version>5.5.7</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index 34a54c5..8bc10f3 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.6</version>
+	<version>5.5.7</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.6</version>
+			<version>5.5.7</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index fc46b25..8d60b2d 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.6</version>
+	<version>5.5.7</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index 561b1b6..4173581 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.6</version>
+		<version>5.5.7</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index e2cde4a..9b66b33 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.6</version>
+		<version>5.5.7</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index 2281340..2d94b61 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.6</version>
+		<version>5.5.7</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index 31f3527..5c4fab2 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.6</version>
+		<version>5.5.7</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index 9601dd2..fdf1fa7 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.6</version>
+		<version>5.5.7</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index 6825b87..9ccc77c 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.6</version>
+	<version>5.5.7</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From a898b3af7a971fd06a11108224ccdf04aae009af Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Wed, 8 Mar 2023 22:18:38 +0800
Subject: [PATCH 130/160] AesCipher

---
 .../gitee/qdbp/tools/crypto/AesCipher.java    | 289 ++++++++++++++++++
 .../gitee/qdbp/tools/crypto/AesEcbCipher.java |   2 +
 .../com/gitee/qdbp/tools/crypto/AesTools.java | 170 +++++++++--
 .../qdbp/tools/crypto/AesCipherTest.java      | 235 ++++++++++++++
 4 files changed, 675 insertions(+), 21 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/crypto/AesCipherTest.java

diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java
new file mode 100644
index 0000000..00e9c22
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java
@@ -0,0 +1,289 @@
+package com.gitee.qdbp.tools.crypto;
+
+import javax.crypto.SecretKey;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.codec.bytes.ByteCodec;
+import com.gitee.qdbp.tools.codec.bytes.HexCodec;
+import com.gitee.qdbp.tools.codec.bytes.TextCodec;
+import com.gitee.qdbp.tools.utils.RandomTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * AES加解密实例
+ *
+ * @author zhaohuihua
+ * @version 20200510
+ */
+public class AesCipher {
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** 生成安全密钥 **/
+    public static byte[] generateSecretKey() {
+        return AesTools.generateSecretKey();
+    }
+
+    /** 生成随机CBC初始向量 **/
+    public static byte[] generateCbcInitVector() {
+        return AesTools.generateCbcInitVector();
+    }
+
+    private static class Options {
+        /** 输入输出文本的编解码方式 **/
+        private TextCodec textCodec = TextCodec.UTF8;
+        /** 输入输出Byte的编解码方式 **/
+        private ByteCodec byteCodec = HexCodec.INSTANCE;
+        /** 密钥对象 **/
+        private SecretKey secretKey;
+        /** 填充方式, 默认为 PKCS5Padding **/
+        private String padding;
+        /** 增加几位随机字符参与加密 **/
+        private int randomChars;
+    }
+
+    public static class Builder {
+
+        private final Options options = new Options();
+
+        /** 输入输出文本的编解码方式 **/
+        public Builder textCodec(TextCodec textCodec) {
+            this.options.textCodec = textCodec;
+            return this;
+        }
+
+        /** 输入输出Byte的编解码方式 **/
+        public Builder byteCodec(ByteCodec byteCodec) {
+            this.options.byteCodec = byteCodec;
+            return this;
+        }
+
+        /** 密码字符串 **/
+        public Builder secretKey(String stringKey) {
+            return secretKey(stringKey, TextCodec.UTF8, true);
+        }
+
+        /** 密码字符串 **/
+        public Builder secretKey(String stringKey, ByteCodec keyCodec) {
+            // TextCodec相当于string.getBytes(), 即表明密钥是字符串, 长度一般不符合要求
+            // HexCodec/Base64Codec/Base58Codec, 一般说明密钥是byte[]
+            boolean keyPadding = keyCodec instanceof TextCodec;
+            return secretKey(stringKey, keyCodec, keyPadding);
+        }
+
+        /** 密码字符串 **/
+        public Builder secretKey(String stringKey, ByteCodec keyCodec, boolean keyPadding) {
+            this.options.secretKey = AesTools.generateSecretKey(keyCodec.decode(stringKey), keyPadding);
+            return this;
+        }
+
+        /** 密码Bytes, 长度必须为16/24/32 **/
+        public Builder secretKey(byte[] secretKey) {
+            return secretKey(secretKey, false);
+        }
+
+        /** 密码Bytes, 长度不限 **/
+        public Builder secretKey(byte[] secretKey, boolean keyPadding) {
+            this.options.secretKey = AesTools.generateSecretKey(secretKey, keyPadding);
+            return this;
+        }
+
+        /** 密钥对象 **/
+        public Builder secretKey(SecretKey secretKey) {
+            this.options.secretKey = secretKey;
+            return this;
+        }
+
+        /** 填充方式, 默认为 PKCS5Padding **/
+        public Builder padding(String padding) {
+            this.options.padding = padding;
+            return this;
+        }
+
+        /** 增加几位随机字符参与加密 **/
+        public Builder randomChars(int randomChars) {
+            this.options.randomChars = randomChars;
+            return this;
+        }
+
+        public EcbCipher buildEcbCipher() {
+            return new EcbCipher(options);
+        }
+
+        public CbcDefault buildCbcCipher() {
+            return new CbcDefault(options);
+        }
+    }
+
+    /** 默认CbcCipher, 固定使用0作为初始向量 **/
+    public static class CbcDefault extends BaseCipher {
+
+        private CbcDefault(Options options) {
+            super(options);
+        }
+
+        /** 使用随机初始向量 **/
+        public CbcCipher initVector() {
+            byte[] initVector = generateCbcInitVector();
+            return new CbcCipher(options, initVector);
+        }
+
+        /** 使用指定的初始向量 (长度必须为16位) **/
+        public CbcCipher initVector(byte[] initVector) {
+            return new CbcCipher(options, initVector);
+        }
+
+        /** 使用字符串初始向量 (长度必须为16位) **/
+        public CbcCipher initVector(String stringKey) {
+            byte[] initVector = TextCodec.UTF8.decode(stringKey);
+            return new CbcCipher(options, initVector);
+        }
+
+        /** 使用字符串初始向量 (解析为byte[]后长度必须为16位) **/
+        public CbcCipher initVector(String stringKey, ByteCodec keyCodec) {
+            byte[] initVector = keyCodec.decode(stringKey);
+            return new CbcCipher(options, initVector);
+        }
+
+        @Override
+        protected byte[] doEncrypt(byte[] plaintext) {
+            byte[] initVector = new byte[16];
+            return AesTools.cbcEncrypt(plaintext, initVector, options.padding, options.secretKey);
+        }
+
+        @Override
+        protected byte[] doDecrypt(byte[] ciphertext) {
+            byte[] initVector = new byte[16];
+            return AesTools.cbcDecrypt(ciphertext, initVector, options.padding, options.secretKey);
+        }
+    }
+
+    /** AES/CBC加密解密类 **/
+    public static class CbcCipher extends BaseCipher {
+
+        private final byte[] initVector;
+
+        private CbcCipher(Options options, byte[] initVector) {
+            super(options);
+            this.initVector = initVector;
+        }
+
+        public String getInitVector() {
+            return options.byteCodec.encode(initVector);
+        }
+
+        @Override
+        protected byte[] doEncrypt(byte[] plaintext) {
+            return AesTools.cbcEncrypt(plaintext, initVector, options.padding, options.secretKey);
+        }
+
+        @Override
+        protected byte[] doDecrypt(byte[] ciphertext) {
+            return AesTools.cbcDecrypt(ciphertext, initVector, options.padding, options.secretKey);
+        }
+    }
+
+    /** AES/ECB加密解密类 **/
+    public static class EcbCipher extends BaseCipher {
+
+        private EcbCipher(Options options) {
+            super(options);
+        }
+
+        @Override
+        protected byte[] doEncrypt(byte[] plaintext) {
+            return AesTools.ecbEncrypt(plaintext, options.padding, options.secretKey);
+        }
+
+        @Override
+        protected byte[] doDecrypt(byte[] ciphertext) {
+            return AesTools.ecbDecrypt(ciphertext, options.padding, options.secretKey);
+        }
+    }
+
+    private static abstract class BaseCipher implements CipherService {
+
+        protected final Options options;
+
+        private BaseCipher(Options options) {
+            this.options = options;
+        }
+
+        protected abstract byte[] doEncrypt(byte[] plaintext);
+
+        protected abstract byte[] doDecrypt(byte[] ciphertext);
+
+        /**
+         * 加密, 使用密钥加密
+         *
+         * @param plaintext 待加密的明文
+         * @return 加密后的密文
+         */
+        @Override
+        public String encrypt(String plaintext) {
+            if (options.secretKey == null) {
+                throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails("secretKey must not be null.");
+            }
+            VerifyTools.requireNonNull(plaintext, "plaintext");
+            String text = plaintext;
+            int randomChars = options.randomChars;
+            if (randomChars > 0) {
+                text = RandomTools.generateString(randomChars) + plaintext + RandomTools.generateString(randomChars);
+            }
+            byte[] input = options.textCodec.decode(text);
+            byte[] output = doEncrypt(input);
+            return options.byteCodec.encode(output);
+        }
+
+        /**
+         * 解密, 使用密钥解密
+         *
+         * @param ciphertext 待解密的密文
+         * @return 解密后的明文
+         */
+        @Override
+        public String decrypt(String ciphertext) {
+            if (options.secretKey == null) {
+                throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails("secretKey must not be null.");
+            }
+            VerifyTools.requireNonNull(ciphertext, "ciphertext");
+            byte[] input = options.byteCodec.decode(ciphertext);
+            byte[] output = doDecrypt(input);
+            String text = options.textCodec.encode(output);
+            int randomChars = options.randomChars;
+            return randomChars <= 0 ? text : text.substring(randomChars, text.length() - randomChars);
+        }
+
+        /**
+         * 加密, 使用密钥加密
+         *
+         * @param plaintext 待加密的明文
+         * @return 加密后的密文
+         */
+        @Override
+        public byte[] encrypt(byte[] plaintext) {
+            if (options.secretKey == null) {
+                throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails("secretKey must not be null.");
+            }
+            VerifyTools.requireNonNull(plaintext, "plaintext");
+            return doEncrypt(plaintext);
+        }
+
+        /**
+         * 解密, 使用密钥解密
+         *
+         * @param ciphertext 待解密的密文
+         * @return 解密后的明文
+         */
+        @Override
+        public byte[] decrypt(byte[] ciphertext) {
+            if (options.secretKey == null) {
+                throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails("secretKey must not be null.");
+            }
+            VerifyTools.requireNonNull(ciphertext, "ciphertext");
+            return doDecrypt(ciphertext);
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java
index 81fe1e6..a23508a 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java
@@ -12,7 +12,9 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  *
  * @author zhaohuihua
  * @version 20200510
+ * @deprecated 改为 AesCipher
  */
+@Deprecated
 public class AesEcbCipher {
 
     /** 输入输出文本的编解码方式 **/
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesTools.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesTools.java
index 122b2b8..16f765d 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesTools.java
@@ -1,15 +1,19 @@
 package com.gitee.qdbp.tools.crypto;
 
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.Key;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.security.spec.InvalidParameterSpecException;
 import javax.crypto.BadPaddingException;
 import javax.crypto.Cipher;
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.KeyGenerator;
 import javax.crypto.NoSuchPaddingException;
 import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
@@ -24,35 +28,119 @@ public class AesTools {
 
     private final static String ALGORITHM = "AES";
     private final static String SHA1PRNG = "SHA1PRNG";
+
     private final static String ECB_CIPHER = "AES/ECB/PKCS5Padding";
+    private final static String CBC_CIPHER = "AES/CBC/PKCS5Padding";
 
     /**
-     * 数据加密
-     * 
+     * ECB数据加密
+     *
      * @param data 待加密的数据
      * @param key 密钥
      * @return 加密后的数据
      */
     public static byte[] ecbEncrypt(byte[] data, SecretKey key) {
-        Cipher cipher = getEcbCipherInstance(key, Cipher.ENCRYPT_MODE);
-        try {
-            return cipher.doFinal(data);
-        } catch (BadPaddingException e) {
-            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
-        } catch (IllegalBlockSizeException e) {
-            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
-        }
+        Cipher cipher = getEcbCipherInstance(ECB_CIPHER, key, Cipher.ENCRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * ECB数据加密
+     *
+     * @param data 待加密的数据
+     * @param padding 填充方式
+     * @param key 密钥
+     * @return 加密后的数据
+     */
+    public static byte[] ecbEncrypt(byte[] data, String padding, SecretKey key) {
+        String algorithm = padding == null ? ECB_CIPHER : "AES/ECB/" + padding;
+        Cipher cipher = getEcbCipherInstance(algorithm, key, Cipher.ENCRYPT_MODE);
+        return cipherDoFinal(data, cipher);
     }
 
     /**
-     * 数据解密
-     * 
+     * ECB数据解密
+     *
      * @param data 待解密的数据
      * @param key 密钥
      * @return 解密后的数据
      */
     public static byte[] ecbDecrypt(byte[] data, SecretKey key) {
-        Cipher cipher = getEcbCipherInstance(key, Cipher.DECRYPT_MODE);
+        Cipher cipher = getEcbCipherInstance(ECB_CIPHER, key, Cipher.DECRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * ECB数据解密
+     *
+     * @param data 待解密的数据
+     * @param padding 填充方式
+     * @param key 密钥
+     * @return 解密后的数据
+     */
+    public static byte[] ecbDecrypt(byte[] data, String padding, SecretKey key) {
+        String algorithm = padding == null ? ECB_CIPHER : "AES/ECB/" + padding;
+        Cipher cipher = getEcbCipherInstance(algorithm, key, Cipher.DECRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * CBC数据加密
+     *
+     * @param data 待加密的数据
+     * @param iv 初始向量
+     * @param key 密钥
+     * @return 加密后的数据
+     */
+    public static byte[] cbcEncrypt(byte[] data, byte[] iv, SecretKey key) {
+        Cipher cipher = getCbcCipherInstance(CBC_CIPHER, key, iv, Cipher.ENCRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * CBC数据加密
+     *
+     * @param data 待加密的数据
+     * @param iv 初始向量
+     * @param padding 填充方式
+     * @param key 密钥
+     * @return 加密后的数据
+     */
+    public static byte[] cbcEncrypt(byte[] data, byte[] iv, String padding, SecretKey key) {
+        String algorithm = padding == null ? CBC_CIPHER : "AES/CBC/" + padding;
+        Cipher cipher = getCbcCipherInstance(algorithm, key, iv, Cipher.ENCRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * CBC数据解密
+     *
+     * @param data 待解密的数据
+     * @param iv 初始向量
+     * @param key 密钥
+     * @return 解密后的数据
+     */
+    public static byte[] cbcDecrypt(byte[] data, byte[] iv, SecretKey key) {
+        Cipher cipher = getCbcCipherInstance(CBC_CIPHER, key, iv, Cipher.DECRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * CBC数据解密
+     *
+     * @param data 待解密的数据
+     * @param iv 初始向量
+     * @param key 密钥
+     * @param padding 填充方式
+     * @return 解密后的数据
+     */
+    public static byte[] cbcDecrypt(byte[] data, byte[] iv, String padding, SecretKey key) {
+        String algorithm = padding == null ? CBC_CIPHER : "AES/CBC/" + padding;
+        Cipher cipher = getCbcCipherInstance(algorithm, key, iv, Cipher.DECRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    private static byte[] cipherDoFinal(byte[] data, Cipher cipher) {
         try {
             return cipher.doFinal(data);
         } catch (BadPaddingException e) {
@@ -62,9 +150,43 @@ public class AesTools {
         }
     }
 
+    private static Cipher getEcbCipherInstance(String algorithm, Key secretKey, int cipherMode) {
+        try {
+            Cipher cipher = Cipher.getInstance(algorithm);
+            cipher.init(cipherMode, secretKey);
+            return cipher;
+        } catch (NoSuchAlgorithmException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (NoSuchPaddingException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (InvalidKeyException e) {
+            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
+        }
+    }
+
+    private static Cipher getCbcCipherInstance(String algorithm, Key secretKey, byte[] ivBytes, int cipherMode) {
+        try {
+            Cipher cipher = Cipher.getInstance(algorithm);
+            AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
+            parameters.init(new IvParameterSpec(ivBytes));
+            cipher.init(cipherMode, secretKey, parameters);
+            return cipher;
+        } catch (NoSuchAlgorithmException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (NoSuchPaddingException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (InvalidKeyException e) {
+            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
+        } catch (InvalidAlgorithmParameterException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (InvalidParameterSpecException e) {
+            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
+        }
+    }
+
     /**
      * 根据密钥数据生成密钥对象
-     * 
+     *
      * @param keyBytes 密钥数据
      * @param keyPadding key是否需要补全(如果key是32位的16进制字符串则不需要)
      * @return SecretKey对象
@@ -88,17 +210,23 @@ public class AesTools {
         }
     }
 
-    private static Cipher getEcbCipherInstance(Key secretKey, int cipherMode) {
+    /** 生成安全密钥 **/
+    public static byte[] generateSecretKey() {
         try {
-            Cipher cipher = Cipher.getInstance(ECB_CIPHER);
-            cipher.init(cipherMode, secretKey);
-            return cipher;
+            SecureRandom sr = SecureRandom.getInstanceStrong();
+            return sr.generateSeed(32);
         } catch (NoSuchAlgorithmException e) {
             throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
-        } catch (NoSuchPaddingException e) {
+        }
+    }
+
+    /** 生成随机CBC初始向量 **/
+    public static byte[] generateCbcInitVector() {
+        try {
+            SecureRandom sr = SecureRandom.getInstanceStrong();
+            return sr.generateSeed(16);
+        } catch (NoSuchAlgorithmException e) {
             throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
-        } catch (InvalidKeyException e) {
-            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
         }
     }
 
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/crypto/AesCipherTest.java b/tools/src/test/java/com/gitee/qdbp/tools/crypto/AesCipherTest.java
new file mode 100644
index 0000000..ff2fe98
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/crypto/AesCipherTest.java
@@ -0,0 +1,235 @@
+package com.gitee.qdbp.tools.crypto;
+
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.codec.bytes.Base58Codec;
+import com.gitee.qdbp.tools.codec.bytes.Base64Codec;
+import com.gitee.qdbp.tools.utils.RandomTools;
+
+/**
+ * AesCipherTest
+ *
+ * @author zhaohuihua
+ * @version 20230308
+ */
+@Test
+public class AesCipherTest {
+
+    @Test
+    public void test() {
+        System.out.println("-- AES CBC ---------------------------");
+        String ciphertext="nohc1YmDPtMGG3DtxkeoJiUb+OuCOq8UAahU8VZ5WrVGC2u7kxamfTjpeN1KeQ2NHuAvAozSoOWzwy+Yy4id0h2dnST+HGTiyWCwpo+ZSfhvKZHr+3vr5TvFayq4ve5uXEUKjW03OALMJTnB7b59V5SGNcGX9K420Ms7bVR+9zDkgT3GBAeCxUpS2vcvSdk1DYwaTD/wOKUN+zCNMsXGNqK4bLkxRzVbLL5nr/CZxvoLExbnTAKQq62aSxazLFRtfP5iB57soCA2ihCjQGSKDpZwGawO4HF78jptW7PzRl2oIDtN4z1vKfy/owVeFZERe60fWHHTCm56ZLbEiU/HShTP+hPrUc3ktZVXZXNZVdQ8Np9wify5l6sR/M4ew4LmGVr1GZldQ0ohK1LAc/2oZbfG0P/YVCI8oaW38RCJMfWpqHKw8nNnjqUsbn1QLLOm6DpA8Gwq/3GG23lSqazG0g==";
+        String secretKey="DZO3/fF/kztY43IIhHOwxw==";
+        String initVector="dd8GXfeZMzOr1U9CL+R80w==";
+        System.out.println("ciphertext = " + ciphertext);
+        System.out.println("secretKey  = " + secretKey);
+        System.out.println("initVector = " + initVector);
+        String plaintext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(secretKey, Base64Codec.INSTANCE)
+                    .byteCodec(Base64Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector, Base64Codec.INSTANCE);
+            // 解密测试
+            plaintext = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + plaintext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(secretKey, Base64Codec.INSTANCE)
+                    .byteCodec(Base64Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector, Base64Codec.INSTANCE);
+            // 加密测试
+            String output = aesCipher.encrypt(plaintext);
+            System.out.println("加密: " + output);
+        }
+    }
+
+    @Test
+    public void testEcbCipher1() {
+        System.out.println("-- AES ECB 1 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        // 生成随机AES密码
+        String aesKey = RandomTools.generateString(20);
+        System.out.println("密钥: " + aesKey);
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildEcbCipher();
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildEcbCipher();
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+    @Test
+    public void testEcbCipher2() {
+        System.out.println("-- AES ECB 2 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        byte[] keyBytes = new byte[32];
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(keyBytes)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildEcbCipher();
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(keyBytes)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildEcbCipher();
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+
+    @Test
+    public void testCbcCipher1() {
+        System.out.println("-- AES CBC 1 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        // 生成随机AES密码
+        String aesKey = RandomTools.generateString(20);
+        System.out.println("密钥: " + aesKey);
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher();
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher();
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+    @Test
+    public void testCbcCipher2() {
+        System.out.println("-- AES CBC 2 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        byte[] keyBytes = new byte[32];
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(keyBytes)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher();
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(keyBytes)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher();
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+    @Test
+    public void testCbcCipher3() {
+        System.out.println("-- AES CBC 3 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        // 生成随机AES密码
+        String aesKey = RandomTools.generateString(20);
+        String initVector = RandomTools.generateString(16);
+        System.out.println("密钥: " + aesKey);
+        System.out.println("向量: " + initVector);
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector);
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector);
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+    @Test
+    public void testCbcCipher4() {
+        System.out.println("-- AES CBC 4 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        // 生成随机AES密码
+        byte[] aesKey = AesCipher.generateSecretKey();
+        byte[] initVector = AesCipher.generateCbcInitVector();
+        System.out.println("密钥: " + Base58Codec.INSTANCE.encode(aesKey));
+        System.out.println("向量: " + Base58Codec.INSTANCE.encode(initVector));
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector);
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector);
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+}
-- 
Gitee


From e950135febbbbcd27efd937a603ce736477094cb Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 9 Mar 2023 00:16:30 +0800
Subject: [PATCH 131/160] =?UTF-8?q?RsaCipher=E6=94=B9=E4=B8=BABuild?=
 =?UTF-8?q?=E6=A8=A1=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/crypto/AesCipher.java    | 161 ++++++----
 .../gitee/qdbp/tools/crypto/AesEcbCipher.java |   2 +-
 .../qdbp/tools/crypto/DefaultRsaCipher.java   |   2 +-
 .../gitee/qdbp/tools/crypto/RsaCipher.java    | 289 +++++++++++++++---
 .../com/gitee/qdbp/tools/crypto/RsaPool.java  |   5 +-
 .../com/gitee/qdbp/tools/crypto/RsaTest.java  |   5 +-
 6 files changed, 361 insertions(+), 103 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java
index 00e9c22..9ac3dca 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java
@@ -1,8 +1,6 @@
 package com.gitee.qdbp.tools.crypto;
 
 import javax.crypto.SecretKey;
-import com.gitee.qdbp.able.exception.ServiceException;
-import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.codec.bytes.ByteCodec;
 import com.gitee.qdbp.tools.codec.bytes.HexCodec;
 import com.gitee.qdbp.tools.codec.bytes.TextCodec;
@@ -31,7 +29,7 @@ public class AesCipher {
         return AesTools.generateCbcInitVector();
     }
 
-    private static class Options {
+    public static class Options {
         /** 输入输出文本的编解码方式 **/
         private TextCodec textCodec = TextCodec.UTF8;
         /** 输入输出Byte的编解码方式 **/
@@ -42,78 +40,137 @@ public class AesCipher {
         private String padding;
         /** 增加几位随机字符参与加密 **/
         private int randomChars;
+
+        /** 输入输出文本的编解码方式 **/
+        public TextCodec getTextCodec() {
+            return textCodec;
+        }
+
+        /** 输入输出Byte的编解码方式 **/
+        public ByteCodec getByteCodec() {
+            return byteCodec;
+        }
+
+        /** 密钥Bytes **/
+        public byte[] getSecretBytes() {
+            return secretKey.getEncoded();
+        }
+
+        /** 密钥字符串 **/
+        public String getSecretKey() {
+            return byteCodec.encode(secretKey.getEncoded());
+        }
+
+        /** 密钥字符串 **/
+        public String getSecretKey(ByteCodec byteCodec) {
+            return byteCodec.encode(secretKey.getEncoded());
+        }
+
+        /** 填充方式, 默认为 PKCS5Padding **/
+        public String getPadding() {
+            return padding;
+        }
+
+        /** 增加几位随机字符参与加密 **/
+        public int getRandomChars() {
+            return randomChars;
+        }
     }
 
     public static class Builder {
 
-        private final Options options = new Options();
+        private final Options o = new Options();
 
         /** 输入输出文本的编解码方式 **/
         public Builder textCodec(TextCodec textCodec) {
-            this.options.textCodec = textCodec;
+            VerifyTools.requireNonNull(textCodec, "textCodec");
+            this.o.textCodec = textCodec;
             return this;
         }
 
         /** 输入输出Byte的编解码方式 **/
         public Builder byteCodec(ByteCodec byteCodec) {
-            this.options.byteCodec = byteCodec;
+            VerifyTools.requireNonNull(byteCodec, "byteCodec");
+            this.o.byteCodec = byteCodec;
+            return this;
+        }
+
+        /** 生成密钥 **/
+        public Builder generateKey() {
+            byte[] secretKey = generateSecretKey();
+            this.o.secretKey = AesTools.generateSecretKey(secretKey, false);
             return this;
         }
 
-        /** 密码字符串 **/
+        /** 密钥字符串 **/
         public Builder secretKey(String stringKey) {
+            VerifyTools.requireNotBlank(stringKey, "stringKey");
             return secretKey(stringKey, TextCodec.UTF8, true);
         }
 
-        /** 密码字符串 **/
+        /** 密钥字符串 **/
         public Builder secretKey(String stringKey, ByteCodec keyCodec) {
+            VerifyTools.requireNotBlank(stringKey, "stringKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
             // TextCodec相当于string.getBytes(), 即表明密钥是字符串, 长度一般不符合要求
             // HexCodec/Base64Codec/Base58Codec, 一般说明密钥是byte[]
             boolean keyPadding = keyCodec instanceof TextCodec;
             return secretKey(stringKey, keyCodec, keyPadding);
         }
 
-        /** 密码字符串 **/
+        /** 密钥字符串 **/
         public Builder secretKey(String stringKey, ByteCodec keyCodec, boolean keyPadding) {
-            this.options.secretKey = AesTools.generateSecretKey(keyCodec.decode(stringKey), keyPadding);
+            VerifyTools.requireNotBlank(stringKey, "stringKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
+            this.o.secretKey = AesTools.generateSecretKey(keyCodec.decode(stringKey), keyPadding);
             return this;
         }
 
-        /** 密码Bytes, 长度必须为16/24/32 **/
+        /** 密钥Bytes, 长度必须为16/24/32 **/
         public Builder secretKey(byte[] secretKey) {
+            VerifyTools.requireNonNull(secretKey, "secretKey");
             return secretKey(secretKey, false);
         }
 
-        /** 密码Bytes, 长度不限 **/
+        /** 密钥Bytes, 长度不限 **/
         public Builder secretKey(byte[] secretKey, boolean keyPadding) {
-            this.options.secretKey = AesTools.generateSecretKey(secretKey, keyPadding);
+            VerifyTools.requireNonNull(secretKey, "secretKey");
+            this.o.secretKey = AesTools.generateSecretKey(secretKey, keyPadding);
             return this;
         }
 
         /** 密钥对象 **/
         public Builder secretKey(SecretKey secretKey) {
-            this.options.secretKey = secretKey;
+            VerifyTools.requireNonNull(secretKey, "secretKey");
+            this.o.secretKey = secretKey;
             return this;
         }
 
         /** 填充方式, 默认为 PKCS5Padding **/
         public Builder padding(String padding) {
-            this.options.padding = padding;
+            VerifyTools.requireNotBlank(padding, "padding");
+            this.o.padding = padding;
             return this;
         }
 
         /** 增加几位随机字符参与加密 **/
         public Builder randomChars(int randomChars) {
-            this.options.randomChars = randomChars;
+            this.o.randomChars = randomChars;
             return this;
         }
 
         public EcbCipher buildEcbCipher() {
-            return new EcbCipher(options);
+            if (o.secretKey == null) {
+                throw new IllegalStateException("SecretKey not configured");
+            }
+            return new EcbCipher(o);
         }
 
         public CbcDefault buildCbcCipher() {
-            return new CbcDefault(options);
+            if (o.secretKey == null) {
+                throw new IllegalStateException("SecretKey not configured");
+            }
+            return new CbcDefault(o);
         }
     }
 
@@ -124,39 +181,39 @@ public class AesCipher {
             super(options);
         }
 
-        /** 使用随机初始向量 **/
-        public CbcCipher initVector() {
+        /** 生成随机初始向量 **/
+        public CbcCipher generateInitVector() {
             byte[] initVector = generateCbcInitVector();
-            return new CbcCipher(options, initVector);
+            return new CbcCipher(o, initVector);
         }
 
         /** 使用指定的初始向量 (长度必须为16位) **/
         public CbcCipher initVector(byte[] initVector) {
-            return new CbcCipher(options, initVector);
+            return new CbcCipher(o, initVector);
         }
 
         /** 使用字符串初始向量 (长度必须为16位) **/
         public CbcCipher initVector(String stringKey) {
             byte[] initVector = TextCodec.UTF8.decode(stringKey);
-            return new CbcCipher(options, initVector);
+            return new CbcCipher(o, initVector);
         }
 
         /** 使用字符串初始向量 (解析为byte[]后长度必须为16位) **/
         public CbcCipher initVector(String stringKey, ByteCodec keyCodec) {
             byte[] initVector = keyCodec.decode(stringKey);
-            return new CbcCipher(options, initVector);
+            return new CbcCipher(o, initVector);
         }
 
         @Override
         protected byte[] doEncrypt(byte[] plaintext) {
             byte[] initVector = new byte[16];
-            return AesTools.cbcEncrypt(plaintext, initVector, options.padding, options.secretKey);
+            return AesTools.cbcEncrypt(plaintext, initVector, o.padding, o.secretKey);
         }
 
         @Override
         protected byte[] doDecrypt(byte[] ciphertext) {
             byte[] initVector = new byte[16];
-            return AesTools.cbcDecrypt(ciphertext, initVector, options.padding, options.secretKey);
+            return AesTools.cbcDecrypt(ciphertext, initVector, o.padding, o.secretKey);
         }
     }
 
@@ -170,18 +227,26 @@ public class AesCipher {
             this.initVector = initVector;
         }
 
-        public String getInitVector() {
-            return options.byteCodec.encode(initVector);
+        public byte[] getInitVectorBytes() {
+            return initVector;
+        }
+
+        public String getInitVectorString() {
+            return o.byteCodec.encode(initVector);
+        }
+
+        public String getInitVectorString(ByteCodec byteCodec) {
+            return byteCodec.encode(initVector);
         }
 
         @Override
         protected byte[] doEncrypt(byte[] plaintext) {
-            return AesTools.cbcEncrypt(plaintext, initVector, options.padding, options.secretKey);
+            return AesTools.cbcEncrypt(plaintext, initVector, o.padding, o.secretKey);
         }
 
         @Override
         protected byte[] doDecrypt(byte[] ciphertext) {
-            return AesTools.cbcDecrypt(ciphertext, initVector, options.padding, options.secretKey);
+            return AesTools.cbcDecrypt(ciphertext, initVector, o.padding, o.secretKey);
         }
     }
 
@@ -194,27 +259,31 @@ public class AesCipher {
 
         @Override
         protected byte[] doEncrypt(byte[] plaintext) {
-            return AesTools.ecbEncrypt(plaintext, options.padding, options.secretKey);
+            return AesTools.ecbEncrypt(plaintext, o.padding, o.secretKey);
         }
 
         @Override
         protected byte[] doDecrypt(byte[] ciphertext) {
-            return AesTools.ecbDecrypt(ciphertext, options.padding, options.secretKey);
+            return AesTools.ecbDecrypt(ciphertext, o.padding, o.secretKey);
         }
     }
 
     private static abstract class BaseCipher implements CipherService {
 
-        protected final Options options;
+        protected final Options o;
 
         private BaseCipher(Options options) {
-            this.options = options;
+            this.o = options;
         }
 
         protected abstract byte[] doEncrypt(byte[] plaintext);
 
         protected abstract byte[] doDecrypt(byte[] ciphertext);
 
+        public Options options() {
+            return o;
+        }
+
         /**
          * 加密, 使用密钥加密
          *
@@ -223,18 +292,15 @@ public class AesCipher {
          */
         @Override
         public String encrypt(String plaintext) {
-            if (options.secretKey == null) {
-                throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails("secretKey must not be null.");
-            }
             VerifyTools.requireNonNull(plaintext, "plaintext");
             String text = plaintext;
-            int randomChars = options.randomChars;
+            int randomChars = o.randomChars;
             if (randomChars > 0) {
                 text = RandomTools.generateString(randomChars) + plaintext + RandomTools.generateString(randomChars);
             }
-            byte[] input = options.textCodec.decode(text);
+            byte[] input = o.textCodec.decode(text);
             byte[] output = doEncrypt(input);
-            return options.byteCodec.encode(output);
+            return o.byteCodec.encode(output);
         }
 
         /**
@@ -245,14 +311,11 @@ public class AesCipher {
          */
         @Override
         public String decrypt(String ciphertext) {
-            if (options.secretKey == null) {
-                throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails("secretKey must not be null.");
-            }
             VerifyTools.requireNonNull(ciphertext, "ciphertext");
-            byte[] input = options.byteCodec.decode(ciphertext);
+            byte[] input = o.byteCodec.decode(ciphertext);
             byte[] output = doDecrypt(input);
-            String text = options.textCodec.encode(output);
-            int randomChars = options.randomChars;
+            String text = o.textCodec.encode(output);
+            int randomChars = o.randomChars;
             return randomChars <= 0 ? text : text.substring(randomChars, text.length() - randomChars);
         }
 
@@ -264,9 +327,6 @@ public class AesCipher {
          */
         @Override
         public byte[] encrypt(byte[] plaintext) {
-            if (options.secretKey == null) {
-                throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails("secretKey must not be null.");
-            }
             VerifyTools.requireNonNull(plaintext, "plaintext");
             return doEncrypt(plaintext);
         }
@@ -279,9 +339,6 @@ public class AesCipher {
          */
         @Override
         public byte[] decrypt(byte[] ciphertext) {
-            if (options.secretKey == null) {
-                throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails("secretKey must not be null.");
-            }
             VerifyTools.requireNonNull(ciphertext, "ciphertext");
             return doDecrypt(ciphertext);
         }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java
index a23508a..d3b197b 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java
@@ -12,7 +12,7 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  *
  * @author zhaohuihua
  * @version 20200510
- * @deprecated 改为 AesCipher
+ * @deprecated 改为 {@linkplain AesCipher}
  */
 @Deprecated
 public class AesEcbCipher {
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/DefaultRsaCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/DefaultRsaCipher.java
index 3ce07e4..65c3620 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/DefaultRsaCipher.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/DefaultRsaCipher.java
@@ -16,7 +16,7 @@ public class DefaultRsaCipher extends RsaCipher {
             "MP38SNHbnVWjkcky22qMDoFbcXapv3fJcfQk1Zjs1UWwKGapFDuW2Dos64PpBefHy4n4p1RdksgXNJmtT1KLBRaUUjnQ79HRrazdGcq2rrCiKjEEN56D2HCQouUXrpA6weSE6p5y3GAZ69bjBW8CbQPy6YNtf18Jor3iJZ5foJJhn6oauY2mxQXNJ4j2ajuFvsAWhNRLLubhFQrJj9HePWfLF1K4fGBFn6x3nPG38xvVmBRoetqT6tH4z8UgchUwr2Zx9rscvYfyShAwGiM8ncudkYzyD1UxUQiuuJJFWEv2iMDEARJ89d36x1C885gqrrFaYYC3bPMAxJ4gWuLQWYxqJjCx7vv4YU1TyigAwAr6MqgdkGMXZen4sGefLS7PkKyGmN16ecjS4qm9WEwhqoGq9vx85NFNqHAUMWd5Y1KgzsHBCZVhdTxXdkZUWrz8xCeVZMHZqAStJ46wKuDsgZX";
 
     public DefaultRsaCipher() {
-        super(DEFAULT_PUBLIC_KEY, DEFAULT_PRIVATE_KEY, Base58Codec.INSTANCE);
+        super(newOptions(DEFAULT_PUBLIC_KEY, DEFAULT_PRIVATE_KEY, Base58Codec.INSTANCE));
     }
 
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java
index 7d553f4..80df0a6 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java
@@ -14,124 +14,323 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  */
 public class RsaCipher implements CipherService {
 
-    /** 输入输出文本的编解码方式 **/
-    private final TextCodec textCodec;
-    /** 输入输出Byte的编解码方式 **/
-    private final ByteCodec byteCodec;
-    /** 私钥 **/
-    private byte[] privateKey;
-    /** 公钥 **/
-    private byte[] publicKey;
+    private final Options o;
 
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    protected RsaCipher(Options options) {
+        this.o = options;
+    }
+
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(int keySize) {
         this(keySize, HexCodec.INSTANCE);
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(int keySize, ByteCodec byteCodec) {
-        this.textCodec = TextCodec.UTF8;
-        this.byteCodec = byteCodec;
-        KeyPair keyPair = RsaTools.generateKeyPair(keySize);
-        this.privateKey = keyPair.getPrivate().getEncoded();
-        this.publicKey = keyPair.getPublic().getEncoded();
+        this(newOptions(RsaTools.generateKeyPair(keySize), byteCodec));
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(String publicKey, String privateKey) {
         this(publicKey, privateKey, HexCodec.INSTANCE);
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(String publicKey, String privateKey, ByteCodec byteCodec) {
-        this.textCodec = TextCodec.UTF8;
-        this.byteCodec = byteCodec;
-        if (publicKey != null) {
-            this.publicKey = byteCodec.decode(publicKey);
-        }
-        if (privateKey != null) {
-            this.privateKey = byteCodec.decode(privateKey);
-        }
+        this(newOptions(publicKey, privateKey, byteCodec));
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(byte[] publicKey, byte[] privateKey) {
         this(publicKey, privateKey, HexCodec.INSTANCE);
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(byte[] publicKey, byte[] privateKey, ByteCodec byteCodec) {
-        this.textCodec = TextCodec.UTF8;
-        this.byteCodec = byteCodec;
-        this.publicKey = publicKey;
-        this.privateKey = privateKey;
+        this(newOptions(publicKey, privateKey, byteCodec));
     }
 
-    /** 公钥 **/
+    protected static Options newOptions(KeyPair keyPair, ByteCodec byteCodec) {
+        Options o = new Options();
+        o.byteCodec = byteCodec;
+        o.publicKey = keyPair.getPublic().getEncoded();
+        o.privateKey = keyPair.getPrivate().getEncoded();
+        return o;
+    }
+
+    protected static Options newOptions(String publicKey, String privateKey, ByteCodec byteCodec) {
+        Options o = new Options();
+        o.byteCodec = byteCodec;
+        o.publicKey = byteCodec.decode(publicKey);
+        o.privateKey = byteCodec.decode(privateKey);
+        return o;
+    }
+
+    private static Options newOptions(byte[] publicKey, byte[] privateKey, ByteCodec byteCodec) {
+        Options o = new Options();
+        o.byteCodec = byteCodec;
+        o.publicKey = publicKey;
+        o.privateKey = privateKey;
+        return o;
+    }
+
+    public Options options() {
+        return o;
+    }
+
+    /**
+     * 获取公钥
+     *
+     * @deprecated 改为 options().getPublicKey() {@link Options#getPublicKey()}
+     */
+    @Deprecated
     public String getPublicKey() {
-        return publicKey == null ? null : byteCodec.encode(publicKey);
+        return o.publicKey == null ? null : o.byteCodec.encode(o.publicKey);
     }
 
-    /** 私钥 **/
+    /**
+     * 获取私钥
+     *
+     * @deprecated 改为 options().getPrivateKey() {@link Options#getPrivateKey()}
+     */
+    @Deprecated
     public String getPrivateKey() {
-        return privateKey == null ? null : byteCodec.encode(privateKey);
+        return o.privateKey == null ? null : o.byteCodec.encode(o.privateKey);
     }
 
     /**
      * 加密, 使用公钥加密
-     * 
+     *
      * @param plaintext 待加密的明文
      * @return 加密后的密文
      */
     @Override
     public String encrypt(String plaintext) {
         VerifyTools.requireNonNull(plaintext, "plaintext");
-        if (publicKey == null) {
+        if (o.publicKey == null) {
             throw new IllegalStateException("PublicKey not configured");
         }
-        byte[] input = textCodec.decode(plaintext);
-        byte[] output = RsaTools.encrypt(input, publicKey);
-        return byteCodec.encode(output);
+        byte[] input = o.textCodec.decode(plaintext);
+        byte[] output = RsaTools.encrypt(input, o.publicKey);
+        return o.byteCodec.encode(output);
     }
 
     /**
      * 加密, 使用公钥加密
-     * 
+     *
      * @param plaintext 待加密的明文
      * @return 加密后的密文
      */
     @Override
     public byte[] encrypt(byte[] plaintext) {
         VerifyTools.requireNonNull(plaintext, "plaintext");
-        if (publicKey == null) {
+        if (o.publicKey == null) {
             throw new IllegalStateException("PublicKey not configured");
         }
-        return RsaTools.encrypt(plaintext, publicKey);
+        return RsaTools.encrypt(plaintext, o.publicKey);
     }
 
     /**
      * 解密, 使用私钥解密
-     * 
+     *
      * @param ciphertext 待解密的密文
      * @return 解密后的明文
      */
     @Override
     public String decrypt(String ciphertext) {
         VerifyTools.requireNonNull(ciphertext, "ciphertext");
-        if (privateKey == null) {
+        if (o.privateKey == null) {
             throw new IllegalStateException("PrivateKey not configured");
         }
-        byte[] input = byteCodec.decode(ciphertext);
-        byte[] output = RsaTools.decrypt(input, privateKey);
-        return textCodec.encode(output);
+        byte[] input = o.byteCodec.decode(ciphertext);
+        byte[] output = RsaTools.decrypt(input, o.privateKey);
+        return o.textCodec.encode(output);
     }
 
     /**
      * 解密, 使用私钥解密
-     * 
+     *
      * @param ciphertext 待解密的密文
      * @return 解密后的明文
      */
     @Override
     public byte[] decrypt(byte[] ciphertext) {
         VerifyTools.requireNonNull(ciphertext, "ciphertext");
-        if (privateKey == null) {
+        if (o.privateKey == null) {
             throw new IllegalStateException("PrivateKey not configured");
         }
-        return RsaTools.decrypt(ciphertext, privateKey);
+        return RsaTools.decrypt(ciphertext, o.privateKey);
+    }
+
+
+    public static class Options {
+        /** 输入输出文本的编解码方式 **/
+        private TextCodec textCodec = TextCodec.UTF8;
+        /** 输入输出Byte的编解码方式 **/
+        private ByteCodec byteCodec = HexCodec.INSTANCE;
+        /** 私钥 **/
+        private byte[] privateKey;
+        /** 公钥 **/
+        private byte[] publicKey;
+
+        /** 输入输出文本的编解码方式 **/
+        public TextCodec getTextCodec() {
+            return textCodec;
+        }
+
+        /** 输入输出Byte的编解码方式 **/
+        public ByteCodec getByteCodec() {
+            return byteCodec;
+        }
+
+        /** 公钥 **/
+        public byte[] getPublicBytes() {
+            return publicKey;
+        }
+
+        /** 私钥 **/
+        public byte[] getPrivateBytes() {
+            return privateKey;
+        }
+
+        /** 公钥 **/
+        public String getPublicKey() {
+            return publicKey == null ? null : byteCodec.encode(publicKey);
+        }
+
+        /** 私钥 **/
+        public String getPrivateKey() {
+            return privateKey == null ? null : byteCodec.encode(privateKey);
+        }
+
+        /** 公钥 **/
+        public String getPublicKey(ByteCodec byteCodec) {
+            return publicKey == null ? null : byteCodec.encode(publicKey);
+        }
+
+        /** 私钥 **/
+        public String getPrivateKey(ByteCodec byteCodec) {
+            return privateKey == null ? null : byteCodec.encode(privateKey);
+        }
+    }
+
+    public static class Builder {
+
+        private final Options o = new Options();
+
+        /** 输入输出文本的编解码方式 **/
+        public Builder textCodec(TextCodec textCodec) {
+            VerifyTools.requireNonNull(textCodec, "textCodec");
+            this.o.textCodec = textCodec;
+            return this;
+        }
+
+        /** 输入输出Byte的编解码方式 **/
+        public Builder byteCodec(ByteCodec byteCodec) {
+            VerifyTools.requireNonNull(byteCodec, "byteCodec");
+            this.o.byteCodec = byteCodec;
+            return this;
+        }
+
+        /** 生成1024位随机密钥 **/
+        public Builder generateKey() {
+            KeyPair keyPair = RsaTools.generateKeyPair();
+            this.o.privateKey = keyPair.getPrivate().getEncoded();
+            this.o.publicKey = keyPair.getPublic().getEncoded();
+            return this;
+        }
+
+        /** 生成指定长度的随机密钥 **/
+        public Builder generateKey(int keySize) {
+            KeyPair keyPair = RsaTools.generateKeyPair(keySize);
+            this.o.privateKey = keyPair.getPrivate().getEncoded();
+            this.o.publicKey = keyPair.getPublic().getEncoded();
+            return this;
+        }
+
+        /** 密钥字符串 **/
+        public Builder secretKey(byte[] publicKey, byte[] privateKey) {
+            VerifyTools.requireNonNull(publicKey, "publicKey");
+            VerifyTools.requireNonNull(privateKey, "privateKey");
+            this.o.publicKey = publicKey;
+            this.o.privateKey = privateKey;
+            return this;
+        }
+
+        /** 密钥字符串 **/
+        public Builder secretKey(String publicKey, String privateKey, ByteCodec keyCodec) {
+            VerifyTools.requireNotBlank(publicKey, "publicKey");
+            VerifyTools.requireNotBlank(privateKey, "privateKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
+            return secretKey(keyCodec.decode(publicKey), keyCodec.decode(privateKey));
+        }
+
+        /** 公钥字符串 (只做加密可以只设公钥) **/
+        public Builder publicKey(byte[] publicKey) {
+            VerifyTools.requireNonNull(publicKey, "publicKey");
+            this.o.publicKey = publicKey;
+            return this;
+        }
+
+        /** 公钥字符串 (只做加密可以只设公钥) **/
+        public Builder publicKey(String publicKey, ByteCodec keyCodec) {
+            VerifyTools.requireNotBlank(publicKey, "publicKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
+            this.o.publicKey = keyCodec.decode(publicKey);
+            return this;
+        }
+
+        /** 私钥字符串 (只做解密可以只设私钥) **/
+        public Builder privateKey(byte[] privateKey) {
+            VerifyTools.requireNotBlank(privateKey, "privateKey");
+            this.o.privateKey = privateKey;
+            return this;
+        }
+
+        /** 私钥字符串 (只做解密可以只设私钥) **/
+        public Builder privateKey(String privateKey, ByteCodec keyCodec) {
+            VerifyTools.requireNotBlank(privateKey, "privateKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
+            this.o.privateKey = keyCodec.decode(privateKey);
+            return this;
+        }
+
+        public RsaCipher build() {
+            return new RsaCipher(o);
+        }
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaPool.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaPool.java
index 51615cf..3714274 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaPool.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaPool.java
@@ -13,13 +13,14 @@ public class RsaPool {
     private static final int KEY_SIZE_DEFAULT = 1024;
     public static final RsaPool instance = new RsaPool();
     /** 主实例 **/
-    private RsaCipher majorCipher = new RsaCipher(KEY_SIZE_DEFAULT, new Base58Codec());
+    private RsaCipher majorCipher = RsaCipher.builder()
+            .byteCodec(Base58Codec.INSTANCE).generateKey(KEY_SIZE_DEFAULT).build();
     /** 备用实例, 更换主实例后, 原先的主实例降为备用实例继续服务一段时间 **/
     private RsaCipher minorCipher = null;
 
     /** 获取当前公钥 **/
     public String getPublicKey() {
-        return majorCipher.getPublicKey();
+        return majorCipher.options().getPublicKey();
     }
 
     /** 使用私钥解密 **/
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/crypto/RsaTest.java b/tools/src/test/java/com/gitee/qdbp/tools/crypto/RsaTest.java
index 3d74168..f538284 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/crypto/RsaTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/crypto/RsaTest.java
@@ -10,8 +10,9 @@ public class RsaTest {
     }
 
     protected static void testCipher(String string) {
-        RsaCipher rsaCipher = new RsaCipher(512, Base58Codec.INSTANCE);
-        System.out.println("公钥: " + rsaCipher.getPublicKey());
+        RsaCipher rsaCipher = RsaCipher.builder()
+                .byteCodec(Base58Codec.INSTANCE).generateKey(512).build();
+        System.out.println("公钥: " + rsaCipher.options().getPublicKey());
         String ciphertext = rsaCipher.encrypt(string);
         System.out.println("加密: " + ciphertext);
         // A用B的公钥+自己的私钥解密
-- 
Gitee


From b4ad31b8ed79f6c2768c7fd07feb350e3dcc9451 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Thu, 9 Mar 2023 00:19:36 +0800
Subject: [PATCH 132/160] 3.5.8

---
 README.md                            | 4 ++--
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 10 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index 23828ee..b0523cc 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.5.7</version>
+        <version>5.5.8</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.5.7</version>
+        <version>5.5.8</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index aa8b99e..3d2959f 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.7</version>
+	<version>5.5.8</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index 8bc10f3..0ec7a34 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.7</version>
+	<version>5.5.8</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.7</version>
+			<version>5.5.8</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index 8d60b2d..29ebfac 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.7</version>
+	<version>5.5.8</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index 4173581..b2ef860 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.7</version>
+		<version>5.5.8</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index 9b66b33..6ce1ac6 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.7</version>
+		<version>5.5.8</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index 2d94b61..1c392ac 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.7</version>
+		<version>5.5.8</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index 5c4fab2..40b34ff 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.7</version>
+		<version>5.5.8</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index fdf1fa7..c96d357 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.7</version>
+		<version>5.5.8</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index 9ccc77c..33ed5ab 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.7</version>
+	<version>5.5.8</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From e5c933e2c7f0c0562ba3c0ef5447df8f29eebb78 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@joyintech.com>
Date: Thu, 16 Mar 2023 22:07:47 +0800
Subject: [PATCH 133/160] =?UTF-8?q?=E5=A2=9E=E5=8A=A0SFTP=E5=B7=A5?=
 =?UTF-8?q?=E5=85=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/able/enums/FileErrorCode.java  |  31 +-
 .../gitee/qdbp/able/model/file/PathInfo.java  |  40 +
 .../i18n/FileErrorCode_zh_CN.properties       |  16 +-
 .../gitee/qdbp/tools/sftp/SftpChannel.java    | 805 ++++++++++++++++++
 .../com/gitee/qdbp/tools/sftp/SftpClient.java | 127 +++
 .../com/gitee/qdbp/tools/sftp/SftpPath.java   | 307 +++++++
 6 files changed, 1323 insertions(+), 3 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/enums/FileErrorCode.java b/able/src/main/java/com/gitee/qdbp/able/enums/FileErrorCode.java
index 8f5b571..fbb1e98 100644
--- a/able/src/main/java/com/gitee/qdbp/able/enums/FileErrorCode.java
+++ b/able/src/main/java/com/gitee/qdbp/able/enums/FileErrorCode.java
@@ -15,12 +15,39 @@ public enum FileErrorCode implements IResultMessage {
     FILE_NOT_FOUND,
     /** 文件读取失败 **/
     FILE_READ_ERROR,
+    /** 文件查询失败 **/
+    FILE_QUERY_ERROR,
     /** 文件写入失败 **/
     FILE_WRITE_ERROR,
+    /** 文件保存失败 **/
+    FILE_SAVE_ERROR,
+    /** 文件导入失败 **/
+    FILE_IMPORT_ERROR,
+    /** 文件导出失败 **/
+    FILE_EXPORT_ERROR,
+    /** 文件上传失败 **/
+    FILE_UPLOAD_ERROR,
+    /** 文件下载失败 **/
+    FILE_DOWNLOAD_ERROR,
+    /** 文件重命名失败 **/
+    FILE_RENAME_ERROR,
+    /** 文件删除失败 **/
+    FILE_DELETE_ERROR,
+    /** 文件处理失败 **/
+    FILE_HANDLE_ERROR,
+
+    /** 文件模板格式错误 **/
+    FILE_TEMPLATE_ERROR,
     /** 文件格式错误 **/
     FILE_FORMAT_ERROR,
-    /** 文件模板格式错误 **/
-    FILE_TEMPLATE_ERROR;
+    /** 文件路径错误 **/
+    FILE_PATH_ERROR,
+    /** 文件大小超出限制 **/
+    FILE_SIZE_OVER_LIMIT,
+    /** 缺少文件扩展名 **/
+    FILE_EXTENSION_LOSE,
+    /** 不支持的文件类型; **/
+    FILE_EXTENSION_ERROR;
 
     /** {@inheritDoc} **/
     @Override
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java b/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java
new file mode 100644
index 0000000..ea6d58c
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java
@@ -0,0 +1,40 @@
+package com.gitee.qdbp.able.model.file;
+
+/**
+ * 路径信息
+ *
+ * @author zhaohuihua
+ * @version 20230312
+ */
+public interface PathInfo {
+
+    /** 文件所在目录 **/
+    String getDirectory();
+
+    /** 文件在目录下的相对路径 **/
+    String getRelativePath();
+
+    /** 完整路径 **/
+    String getPath();
+
+    /** 上级路径 **/
+    String getParent();
+
+    /** 文件名 **/
+    String getName();
+
+    /** 是不是文件 **/
+    boolean isFile();
+
+    /** 是不是目录 **/
+    boolean isDirectory();
+
+    /** 最后修改时间 **/
+    long lastModified();
+
+    /** 是否存在 **/
+    boolean exists();
+
+    /** 文件大小 **/
+    long length();
+}
diff --git a/able/src/main/resources/settings/i18n/FileErrorCode_zh_CN.properties b/able/src/main/resources/settings/i18n/FileErrorCode_zh_CN.properties
index 10ee81c..253494b 100644
--- a/able/src/main/resources/settings/i18n/FileErrorCode_zh_CN.properties
+++ b/able/src/main/resources/settings/i18n/FileErrorCode_zh_CN.properties
@@ -1,6 +1,20 @@
 
 FILE_NOT_FOUND = 文件不存在
 FILE_READ_ERROR = 文件读取失败
+FILE_QUERY_ERROR = 文件查询失败
 FILE_WRITE_ERROR = 文件写入失败
-FILE_FORMAT_ERROR = 文件格式错误
+FILE_SAVE_ERROR = 文件保存失败
+FILE_IMPORT_ERROR = 文件导入失败
+FILE_EXPORT_ERROR = 文件导出失败
+FILE_UPLOAD_ERROR = 文件上传失败
+FILE_DOWNLOAD_ERROR = 文件下载失败
+FILE_RENAME_ERROR = 文件重命名失败
+FILE_DELETE_ERROR = 文件删除失败
+FILE_HANDLE_ERROR = 文件处理失败
+
 FILE_TEMPLATE_ERROR = 文件模板格式错误
+FILE_FORMAT_ERROR = 文件格式错误
+FILE_PATH_ERROR = 文件路径错误
+FILE_SIZE_OVER_LIMIT = 文件大小超出限制
+FILE_EXTENSION_LOSE = 缺少文件扩展名
+FILE_EXTENSION_ERROR = 不支持的文件类型
\ No newline at end of file
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
new file mode 100644
index 0000000..3996477
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
@@ -0,0 +1,805 @@
+package com.gitee.qdbp.tools.sftp;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Vector;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.enums.FileErrorCode;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.matches.FileMatcher;
+import com.gitee.qdbp.able.model.file.PathInfo;
+import com.gitee.qdbp.tools.files.FileTools;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.Session;
+import com.jcraft.jsch.SftpATTRS;
+import com.jcraft.jsch.SftpException;
+
+/**
+ * SFTP操作类<br>
+ * 注意: 以/开头的路径表示当前SFTP用户的根路径, 而不是系统的根路径
+ *
+ * @author zhaohuihua
+ * @version 20230311
+ */
+public class SftpChannel implements AutoCloseable {
+
+    private final Session session;
+    private final ChannelSftp channel;
+
+    public SftpChannel(Session session, ChannelSftp channel) {
+        this.session = session;
+        this.channel = channel;
+    }
+
+    @Override
+    public void close() {
+        if (channel != null) {
+            channel.disconnect();
+        }
+        if (session != null) {
+            session.disconnect();
+        }
+    }
+
+    /** 当前目录 **/
+    public String pwd() {
+        return doGetPwd(channel, this.home());
+    }
+
+    /** 返回SFTP根目录 (系统绝对路径) **/
+    public String home() {
+        return doGetHome(channel);
+    }
+
+    /** 返回SFTP根目录 (系统绝对路径) **/
+    private static String doGetHome(ChannelSftp channel) {
+        try {
+            return channel.getHome();
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails("channel.getHome()");
+        }
+    }
+
+    /** 当前目录 **/
+    private static String doGetPwd(ChannelSftp channel, String home) {
+        String pwd;
+        try {
+            pwd = channel.pwd();
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails("channel.pwd()");
+        }
+        if (pwd.equals(home)) {
+            return "/";
+        } else {
+            return PathTools.concat("/", StringTools.removePrefix(pwd, home), "/");
+        }
+    }
+
+    /**
+     * 文件下载
+     *
+     * @param remotePath 远程文件路径: 既可以是文件名(下载当前目录下的文件), 也可以是全路径+文件名(根据绝对路径下载)
+     * @return 返回文件流
+     * @throws ServiceException 下载失败
+     */
+    public InputStream downloadFile(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            // 拆分文件目录和文件名
+            if (rp.endWithSlash) {
+                throw new ServiceException(FileErrorCode.FILE_PATH_ERROR)
+                        .setDetails("remotePath=" + rp.original);
+            }
+
+            // 下载文件
+            try {
+                return channel.get(rp.effectivePath());
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_DOWNLOAD_ERROR, e)
+                        .setDetails("remotePath=" + rp.original);
+            }
+        }
+    }
+
+    /**
+     * 文件下载并保存到指定位置
+     *
+     * @param remotePath 远程文件路径: 既可以是文件名(下载当前目录下的文件), 也可以是全路径+文件名(根据绝对路径下载)
+     * @param clientPath 本地文件路径: 既可以是文件目录(以/结尾,保存为远程文件名), 也可以是全路径+文件名(保存到绝对路径)
+     * @throws ServiceException 下载失败<br>
+     * FILE_DOWNLOAD_ERROR 文件下载失败<br>
+     * FILE_SAVE_ERROR 文件保存失败
+     */
+    public void downloadSave(String remotePath, String clientPath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            // 拆分远程路径为文件目录和文件名
+            if (rp.endWithSlash || rp.name == null) {
+                throw new ServiceException(FileErrorCode.FILE_PATH_ERROR).setDetails("remotePath=" + rp);
+            }
+            // 拆分本地路径为文件目录和文件名
+            ClientPath cp = ClientPath.of(clientPath);
+            if (cp.endWithSlash) {
+                cp = cp.enter(rp.name);
+            }
+
+            // 拼接路径
+            // 判断目标文件是否存在
+            SftpPath remoteFile = getFileInfo(rp);
+            if (remoteFile == null || !remoteFile.isFile()) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails("remotePath=" + rp);
+            }
+            // 如果本地目录不存在则创建
+            FileTools.mkdirsIfNotExists(cp.fullPathOnRoot());
+            try (OutputStream output = new FileOutputStream(cp.fullPathOnRoot())) {
+                // 下载文件并保存
+                try {
+                    channel.get(rp.effectivePath(), output);
+                } catch (SftpException e) {
+                    throw new ServiceException(FileErrorCode.FILE_DOWNLOAD_ERROR, e)
+                            .setDetails("remotePath=" + rp);
+                }
+            } catch (IOException e) {
+                throw new ServiceException(FileErrorCode.FILE_SAVE_ERROR, e).setDetails("clientPath=" + cp);
+            }
+        }
+    }
+
+    /**
+     * 文件上传
+     *
+     * @param input 输入流
+     * @param remotePath 远程文件路径: 既可以是文件名(上传到当前目录), 也可以是全路径+文件名(保存到绝对路径)
+     */
+    public void uploadFile(InputStream input, String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            if (rp.endWithSlash) {
+                throw new ServiceException(FileErrorCode.FILE_PATH_ERROR).setDetails("remotePath=" + rp);
+            }
+
+            // 创建上传目录
+            doMakeDirectory(rp.folder);
+
+            // 文件上传
+            try {
+                channel.put(input, rp.effectivePath());
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR, e).setDetails("remotePath=" + rp);
+            }
+        }
+    }
+
+    /**
+     * 文件上传 (保存到当前目录)
+     *
+     * @param clientPath 本地文件路径: 全路径+文件名
+     */
+    public void uploadFile(String clientPath) {
+        uploadFile(clientPath, null);
+    }
+
+    /**
+     * 文件上传
+     *
+     * @param clientPath 本地文件路径: 全路径+文件名
+     * @param remotePath 远程文件路径: 既可以是文件目录(以/结尾,保存为本地文件名), 也可以是全路径+文件名(保存到绝对路径)
+     */
+    public void uploadFile(String clientPath, String remotePath) {
+        // 拆分本地路径为文件目录和文件名
+        ClientPath cp = ClientPath.of(clientPath);
+        if (cp.endWithSlash || cp.name == null) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR).setDetails("clientPath=" + cp);
+        }
+        // 判断文件是否存在
+        File clientFile = new File(cp.fullPathOnRoot());
+        if (!clientFile.exists()) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND)
+                    .setDetails("clientPath=" + cp);
+        }
+        if (clientFile.isDirectory()) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR)
+                    .setDetails("clientPath=" + cp);
+        }
+        // 拆分远程路径为文件目录和文件名
+        try (RemotePath rp = parseRemotePath(remotePath, cp.name)) {
+            // 创建上传目录
+            doMakeDirectory(rp.folder);
+
+            // 文件上传
+            try (InputStream input = new FileInputStream(clientFile)) {
+                try {
+                    channel.put(input, rp.effectivePath());
+                } catch (SftpException e) {
+                    throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR, e).setDetails("remotePath=" + rp);
+                }
+            } catch (IOException e) {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails("clientPath=" + cp);
+            }
+        }
+    }
+
+    private RemotePath parseRemotePath(String remotePath, String clientFileName) {
+        RemotePath rp;
+        if (VerifyTools.isBlank(remotePath)) {
+            rp = RemotePath.of(channel, clientFileName);
+        } else {
+            rp = RemotePath.of(channel, remotePath);
+            if (rp.endWithSlash) {
+                rp = rp.enter(clientFileName);
+            }
+        }
+        return rp;
+    }
+
+    private boolean isFileNotFound(SftpException e) {
+        String message = e.getMessage().toLowerCase();
+        return message.contains("no such") || message.contains("not found") || message.contains("not find");
+    }
+
+    public SftpPath info(String remotePath) {
+        return getFileInfo(RemotePath.of(channel, remotePath));
+    }
+
+    private SftpPath getFileInfo(RemotePath rp) {
+        try {
+            SftpATTRS attrs = channel.lstat(rp.effectivePath());
+            return newSftpFile(rp, attrs);
+        } catch (SftpException e) {
+            if (isFileNotFound(e)) {
+                return null;
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_HANDLE_ERROR, e)
+                        .setDetails("remotePath=" + rp);
+            }
+        }
+    }
+
+    /**
+     * 判断指定的文件或目录是否存在
+     *
+     * @param remotePath 远程文件路径: 可以是相对路径文件名/相对路径目录/绝对路径文件名/绝对路径目录
+     * @return 是否存在
+     */
+    public boolean exists(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            try {
+                SftpATTRS attrs = channel.lstat(rp.effectivePath());
+                return attrs != null;
+            } catch (SftpException e) {
+                if (isFileNotFound(e)) {
+                    return false;
+                } else {
+                    throw new ServiceException(FileErrorCode.FILE_HANDLE_ERROR, e).setDetails("remotePath=" + rp);
+                }
+            }
+        }
+    }
+
+    /**
+     * 判断指定路径是不是文件
+     *
+     * @param remotePath 远程文件路径: 可以是相对路径文件名/相对路径目录/绝对路径文件名/绝对路径目录
+     * @return 是否存在
+     */
+    public boolean isFile(String remotePath) {
+        return !isDirectory(remotePath);
+    }
+
+    /**
+     * 判断指定路径是不是目录
+     *
+     * @param remotePath 远程文件路径: 可以是相对路径文件名/相对路径目录/绝对路径文件名/绝对路径目录
+     * @return 是否存在
+     */
+    public boolean isDirectory(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            try {
+                SftpATTRS attrs = channel.lstat(rp.effectivePath());
+                return attrs.isDir() || attrs.isLink();
+            } catch (SftpException e) {
+                if (isFileNotFound(e)) {
+                    return false;
+                } else {
+                    throw new ServiceException(FileErrorCode.FILE_HANDLE_ERROR, e).setDetails("remotePath=" + rp);
+                }
+            }
+        }
+    }
+
+    public void mkdir(String remotePath) {
+        makeDirectory(remotePath);
+    }
+
+    public void makeDirectory(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            doMakeDirectory(rp.effectivePath());
+        }
+    }
+
+    private void doMakeDirectory(String remoteFolder) {
+        if (VerifyTools.isBlank(remoteFolder)) {
+            return;
+        }
+        List<String> names = StringTools.splits(remoteFolder, '/');
+        for (int i = 0; i < names.size(); i++) {
+            String name = names.get(i);
+            if (VerifyTools.isBlank(name)) {
+                continue;
+            }
+            String path = ConvertTools.joinToString(names.subList(0, i + 1), '/');
+            // 判断是否存在
+            SftpATTRS attrs = null;
+            try {
+                attrs = channel.lstat(path);
+            } catch (SftpException ignore) {
+            }
+            if (attrs != null) {
+                if (attrs.isDir() || attrs.isLink()) {
+                    continue;
+                } else {
+                    throw new ServiceException(FileErrorCode.FILE_PATH_ERROR).setDetails("remoteFolder=" + path);
+                }
+            }
+            // 创建目录
+            try {
+                channel.mkdir(path);
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_HANDLE_ERROR, e).setDetails("remoteFolder=" + path);
+            }
+        }
+    }
+
+    public void cd(String remoteFolder) {
+        doChangeDirectory(channel, remoteFolder);
+    }
+
+    public void toHome() {
+        String remoteFolder = this.home();
+        try {
+            channel.cd(remoteFolder);
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR, e)
+                    .setDetails("remoteFolder=" + remoteFolder);
+        }
+    }
+
+    private static void doChangeDirectory(ChannelSftp channel, String remoteFolder) {
+        if (VerifyTools.isBlank(remoteFolder)) {
+            return;
+        }
+        // 如果是绝对路径, 先跳转至home目录, 再按相对路径处理
+        String relativeFolder = remoteFolder;
+        if (PathTools.isAbsolutePath(remoteFolder)) {
+            doChangeToHome(channel);
+            relativeFolder = StringTools.trimLeft(remoteFolder, '/');
+        }
+        try {
+            channel.cd(relativeFolder);
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR, e).setDetails("remoteFolder=" + remoteFolder);
+        }
+    }
+
+    private static void doChangeToHome(ChannelSftp channel) {
+        String home = doGetHome(channel);
+        try {
+            channel.cd(home);
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR, e).setDetails("remoteFolder=/");
+        }
+    }
+
+    /**
+     * 列出指定目录下所有文件
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @return 文件列表
+     */
+    public List<PathInfo> listFiles(String remotePath) {
+        return listFiles(remotePath, null);
+    }
+
+    /**
+     * 列出指定目录下所有文件
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param matcher 过滤条件
+     * @return 文件列表
+     */
+    public List<PathInfo> listFiles(String remotePath, FileMatcher matcher) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            List<PathInfo> paths = doScanPaths(rp, 1, false);
+            return matcher == null ? paths : filterPaths(paths, matcher);
+        }
+    }
+
+    /**
+     * 递归扫描指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @return 文件列表
+     */
+    public List<PathInfo> scanFiles(String remotePath) {
+        return scanFiles(remotePath, null);
+    }
+
+    /**
+     * 递归扫描指定目录下所有文件
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param matcher 过滤条件
+     * @return 文件列表
+     */
+    public List<PathInfo> scanFiles(String remotePath, FileMatcher matcher) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            List<PathInfo> paths = doScanPaths(rp, 1, true);
+            return matcher == null ? paths : filterPaths(paths, matcher);
+        }
+    }
+
+    /**
+     * 列出指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @return 文件列表
+     */
+    public List<PathInfo> listPaths(String remotePath) {
+        return listPaths(remotePath, null);
+    }
+
+    /**
+     * 列出指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param matcher 过滤条件
+     * @return 文件列表
+     */
+    public List<PathInfo> listPaths(String remotePath, FileMatcher matcher) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            List<PathInfo> paths = doScanPaths(rp, 0, false);
+            return matcher == null ? paths : filterPaths(paths, matcher);
+        }
+    }
+
+    /**
+     * 递归扫描指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @return 文件列表
+     */
+    public List<PathInfo> scanPaths(String remotePath) {
+        return scanPaths(remotePath, null);
+    }
+
+    /**
+     * 递归扫描指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param matcher 过滤条件
+     * @return 文件列表
+     */
+    public List<PathInfo> scanPaths(String remotePath, FileMatcher matcher) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            List<PathInfo> paths = doScanPaths(rp, 0, true);
+            return matcher == null ? paths : filterPaths(paths, matcher);
+        }
+    }
+
+    private List<PathInfo> filterPaths(List<PathInfo> paths, FileMatcher matcher) {
+        List<PathInfo> result = new ArrayList<>();
+        for (PathInfo path : paths) {
+            if (path instanceof SftpPath) {
+                SftpPath file = (SftpPath) path;
+                if (matcher.matches(file)) {
+                    result.add(file);
+                }
+            }
+        }
+        return result;
+    }
+
+    // fileType: 0=全部|1=文件|2=目录
+    private List<PathInfo> doScanPaths(RemotePath rp, int fileType, boolean recursive) {
+        Vector<?> vector;
+        try {
+            vector = channel.ls(VerifyTools.nvl(rp.effectivePath(), "."));
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails("remotePath=" + rp);
+        }
+        List<PathInfo> result = new ArrayList<>();
+        for (Object item : vector) {
+            ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) item;
+            String fileName = entry.getFilename();
+            if (".".equals(fileName) || "..".equals(fileName)) {
+                continue;
+            }
+            // 判断是文件还是目录
+            boolean isDirectory = isDirectory(entry.getAttrs());
+            // 判断文件类型是否匹配
+            boolean matches;
+            if (isDirectory) {
+                matches = fileType == 0 || fileType == 2;
+            } else {
+                matches = fileType == 0 || fileType == 1;
+            }
+            // 下级对象
+            RemotePath next = rp.enter(fileName + (isDirectory ? "/" : ""));
+            // 文件类型匹配则加入结果集
+            if (matches) {
+                SftpPath file = newSftpFile(next, entry.getAttrs());
+                result.add(file);
+            }
+            // 递归且是目录则继续处理下级对象
+            if (recursive && isDirectory) {
+                result.addAll(doScanPaths(next, fileType, true));
+            }
+        }
+        return result;
+    }
+
+    private SftpPath newSftpFile(BasePath path, SftpATTRS attrs) {
+        String directory = path.invisiblePath();
+        String relativePath = path.effectivePath();
+
+        SftpPath file = new SftpPath(directory, relativePath);
+        if (attrs != null) {
+            boolean isDirectory = isDirectory(attrs);
+            file.setIsFile(!isDirectory);
+            file.setIsDirectory(isDirectory);
+            file.setLength(attrs.getSize());
+            file.setPermissions(attrs.getPermissions());
+            file.setUpdateTime(new Date(attrs.getMTime() * 1000L));
+        }
+        return file;
+    }
+
+    private boolean isDirectory(SftpATTRS attrs) {
+        if (attrs == null) {
+            return false;
+        } else {
+            return attrs.isDir() || attrs.isLink();
+        }
+    }
+
+    /**
+     * 文件重命名
+     *
+     * @param remotePath 远程文件路径: 既可以是文件名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param newName 新文件名
+     */
+    public void rename(String remotePath, String newName) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            try {
+                channel.rename(rp.effectivePath(), newName);
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_RENAME_ERROR, e)
+                        .setDetails("remotePath={}, newName={}", rp, newName);
+            }
+        }
+    }
+
+    /**
+     * 删除文件
+     *
+     * @param remotePath 远程文件路径: 既可以是文件名(当前目录), 也可以是全路径+文件名(绝对路径)
+     */
+    public boolean deleteFile(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            // 判断目标文件是否存在
+            SftpPath remoteFile = getFileInfo(rp);
+            if (remoteFile == null || !remoteFile.isFile()) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails("remotePath=" + rp.original);
+            }
+
+            try {
+                channel.rm(rp.effectivePath());
+                return true;
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_DELETE_ERROR, e).setDetails("remotePath=" + rp);
+            }
+        }
+    }
+
+    /**
+     * 规范远程路径, 将远程路径统一切分为/home/pwd/folder/name共4个部分<br>
+     * home = SFTP的home目录<br>
+     * pwd = 当前所在路径; 如果传入的是以/开头的绝对路径, 此部分为空<br>
+     * folder = 入参的目录部分; 如果传入的是文件名, 此部分为空<br>
+     * name = 入参的文件名部分; 如果传入的是以/结尾的目录, 此部分也取目录的最后一段, 但isDirectory=true<br>
+     */
+    protected static class BasePath {
+        protected final String original;
+        protected final String home;
+        protected final String pwd;
+        protected final String folder;
+        protected final String name;
+        /** 入参是不是以/结尾 (如果明确以/结尾就是目录, 不是/结尾有可能是文件也有可能是目录) **/
+        protected final boolean endWithSlash;
+
+        protected BasePath(String original, String home, String pwd, String folder, String name,
+                boolean endWithSlash) {
+            this.original = original;
+            this.home = VerifyTools.isBlank(home) ? null : home;
+            this.pwd = trimPath(pwd);
+            this.folder = trimPath(folder);
+            this.name = trimPath(name);
+            this.endWithSlash = endWithSlash;
+        }
+
+        /** 相对于根目录的地址, 即全路径, 返回 /home/pwd/folder/name **/
+        public String fullPathOnRoot() {
+            return PathTools.concat(home, pwd, folder, name, endWithSlash ? "/" : "");
+        }
+
+        /** 相对于Home目录的地址, 返回 pwd/folder/name **/
+        public String fullPathOnHome() {
+            return PathTools.concat(pwd, folder, name, endWithSlash ? "/" : "");
+        }
+
+        /** 相对于根目录的地址, 即全路径, 返回 /home/pwd/folder/ **/
+        public String folderOnRoot() {
+            return PathTools.concat(home, pwd, folder, "/");
+        }
+
+        /** 相对于Home目录的地址, 返回 /pwd/folder/ **/
+        public String folderOnHome() {
+            return PathTools.concat(pwd, folder, "/");
+        }
+
+        /** 隐含目录, 不是参数传入的前缀路径 **/
+        public String invisiblePath() {
+            return PathTools.concat(home, pwd, "/");
+        }
+
+        /** 有效路径, 参数传入的路径 **/
+        public String effectivePath() {
+            return PathTools.concat(folder, name, endWithSlash ? "/" : "");
+        }
+
+        private static String trimPath(String path) {
+            if (VerifyTools.isBlank(path)) {
+                return null;
+            } else {
+                return StringTools.trimLeft(path, '/');
+            }
+        }
+
+        protected static KeyString splitFolderAndName(String path) {
+            if (VerifyTools.isBlank(path)) {
+                return new KeyString();
+            }
+            String tempPath = StringTools.trimRight(path, '/');
+            String folder;
+            String name;
+            if (tempPath.indexOf('/') < 0) {
+                folder = null;
+                name = tempPath;
+            } else {
+                folder = PathTools.removeFileName(tempPath);
+                name = PathTools.getFileName(tempPath);
+            }
+            return new KeyString(folder, name);
+        }
+    }
+
+    protected static class ClientPath extends BasePath {
+
+        protected ClientPath(String original, String home, String pwd, String folder, String name,
+                boolean endWithSlash) {
+            super(original, home, pwd, folder, name, endWithSlash);
+        }
+
+        /** 进入下级路径 **/
+        public ClientPath enter(String path) {
+            boolean endWithSlash = path.charAt(path.length() - 1) == '/';
+            String original = PathTools.concat(this.original, path);
+            KeyString temp = splitFolderAndName(path);
+            String folder = PathTools.concat(this.folder, this.name, temp.getKey());
+            String name = temp.getValue();
+            return new ClientPath(original, home, pwd, folder, name, endWithSlash);
+        }
+
+        @Override
+        public String toString() {
+            return fullPathOnRoot();
+        }
+
+        public static ClientPath of(String path) {
+            VerifyTools.requireNotBlank(path, "path");
+            String formatted = (".".equals(path) || "./".equals(path)) ? path : PathTools.formatPath(path);
+            boolean isAbsolute = formatted.length() > 0 && PathTools.isAbsolutePath(formatted);
+            boolean endWithSlash = formatted.length() > 0 && formatted.charAt(formatted.length() - 1) == '/';
+
+            String home;
+            if (formatted.startsWith("/")) {
+                String osName = System.getProperty("os.name");
+                if (osName != null && osName.startsWith("Windows")) {
+                    home = PathTools.formatPath(new File("/").getAbsolutePath());
+                } else {
+                    home = "/";
+                }
+                formatted = formatted.substring(1);
+            } else if (isAbsolute) {
+                // D:/home/file-center/ 拆分为 D:/ 和 home/file-center/
+                int index = formatted.indexOf('/');
+                home = formatted.substring(0, index + 1);
+                formatted = formatted.substring(index + 1);
+            } else {
+                home = PathTools.formatPath(new File("./").getAbsolutePath());
+            }
+
+            KeyString temp = splitFolderAndName(formatted);
+            String folder = temp.getKey();
+            String name = temp.getValue();
+            return new ClientPath(path, home, null, folder, name, endWithSlash);
+        }
+    }
+
+    protected static class RemotePath extends BasePath implements AutoCloseable {
+
+        private final ChannelSftp channel;
+        private final String oldPwd;
+
+        protected RemotePath(ChannelSftp channel, String oldPwd,
+                String original, String home, String pwd, String folder, String name, boolean endWithSlash) {
+            super(original, home, pwd, folder, name, endWithSlash);
+            this.channel = channel;
+            this.oldPwd = oldPwd;
+            if (oldPwd != null && !"/".equals(oldPwd)) {
+                doChangeToHome(channel);
+            }
+        }
+
+        /** 进入下级路径 **/
+        public RemotePath enter(String path) {
+            boolean endWithSlash = path.charAt(path.length() - 1) == '/';
+            String original = PathTools.concat(this.original, path);
+            KeyString temp = splitFolderAndName(path);
+            String folder = PathTools.concat(this.folder, this.name, temp.getKey());
+            String name = temp.getValue();
+            return new RemotePath(channel, null, original, home, pwd, folder, name, endWithSlash);
+        }
+
+        @Override
+        public void close() {
+            if (oldPwd != null && !"/".equals(oldPwd)) {
+                doChangeDirectory(channel, oldPwd);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder("{")
+                    .append("realPath:").append(effectivePath()).append(",")
+                    .append("original:").append(original).append(",")
+                    .append("absolute:").append(fullPathOnRoot()).append("}")
+                    .toString();
+        }
+
+        public static RemotePath of(ChannelSftp channel, String path) {
+            VerifyTools.requireNotBlank(path, "path");
+
+            String formatted = (".".equals(path) || "./".equals(path)) ? path : PathTools.formatPath(path);
+            boolean isAbsolute = formatted.length() > 0 && formatted.charAt(0) == '/';
+            boolean endWithSlash = formatted.length() > 0 && formatted.charAt(formatted.length() - 1) == '/';
+            String home = doGetHome(channel);
+            String pwd = doGetPwd(channel, home);
+            String pathPwd = isAbsolute ? null : pwd;
+            String oldPwd = isAbsolute ? pwd : null;
+
+            KeyString temp = splitFolderAndName(formatted);
+            String folder = temp.getKey();
+            String name = temp.getValue();
+            return new RemotePath(channel, oldPwd, path, home, pathPwd, folder, name, endWithSlash);
+        }
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
new file mode 100644
index 0000000..7f46513
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
@@ -0,0 +1,127 @@
+package com.gitee.qdbp.tools.sftp;
+
+import java.util.Properties;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+import com.jcraft.jsch.Channel;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+/**
+ * SFTP工具类
+ *
+ * @author zhaohuihua
+ * @version 20230310
+ */
+public class SftpClient {
+
+    private final String host;
+    private final int port;
+    private final String account;
+    private final String password;
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public SftpClient(String host, int port) {
+        this(host, port, null, null);
+    }
+
+    private SftpClient(String host, int port, String account, String password) {
+        VerifyTools.requireNotBlank(host, "host");
+        this.host = host;
+        this.port = port;
+        this.account = account;
+        this.password = password;
+    }
+
+    public SftpChannel connect() {
+        JSch jsch = new JSch();
+        Session session;
+        try {
+            session = jsch.getSession(account, host, port);
+        } catch (JSchException e) {
+            throw new ServiceException(ResultCode.REMOTE_SERVICE_ERROR, e)
+                    .setDetails("sftp --> {}:{}", host, port);
+        }
+
+        if (VerifyTools.isNotBlank(password)) {
+            session.setPassword(password);
+        }
+
+        Properties sshConfig = new Properties();
+        // ssh会把每个访问过的主机公钥记录在~/.ssh/known_hosts
+        // 当下次访问相同主机时, OpensSSH会核对公钥
+        // 如果公钥不同, ssh会发出警告, 以避免受到DNSHijack之类的攻击
+        // 设置StrictHostKeyChecking=no关闭该警告
+        sshConfig.put("StrictHostKeyChecking", "no");
+        session.setConfig(sshConfig);
+        try {
+            session.connect();
+        } catch (JSchException e) {
+            session.disconnect();
+            throw new ServiceException(ResultCode.REMOTE_SERVICE_ERROR, e)
+                    .setDetails("sftp --> {}:{}", host, port);
+        }
+
+        Channel channel;
+        try {
+            channel = session.openChannel("sftp");
+        } catch (JSchException e) {
+            session.disconnect();
+            throw new ServiceException(ResultCode.REMOTE_SERVICE_ERROR, e)
+                    .setDetails("sftp --> {}:{}", host, port);
+        }
+        try {
+            channel.connect();
+        } catch (JSchException e) {
+            session.disconnect();
+            channel.disconnect();
+            throw new ServiceException(ResultCode.REMOTE_SERVICE_ERROR, e)
+                    .setDetails("sftp --> {}:{}", host, port);
+        }
+
+        return new SftpChannel(session, (ChannelSftp) channel);
+    }
+
+    public static class Builder {
+
+        private String host;
+        private int port = 22;
+        private String account;
+        private String password;
+
+        protected Builder() {
+        }
+
+        public Builder host(String host) {
+            this.host = host;
+            return this;
+        }
+
+        public Builder port(int port) {
+            this.port = port;
+            return this;
+        }
+
+        public Builder account(String account) {
+            this.account = account;
+            return this;
+        }
+
+        public Builder password(String password) {
+            this.password = password;
+            return this;
+        }
+
+        public SftpClient build() {
+            VerifyTools.requireNotBlank(host, "host");
+            return new SftpClient(host, port, account, password);
+        }
+    }
+}
+
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
new file mode 100644
index 0000000..372b8e6
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
@@ -0,0 +1,307 @@
+package com.gitee.qdbp.tools.sftp;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FilenameFilter;
+import java.io.Serializable;
+import java.nio.file.Path;
+import java.util.Date;
+import com.gitee.qdbp.able.model.file.PathInfo;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * SFTP文件信息
+ *
+ * @author zhaohuihua
+ * @version 20230311
+ */
+public class SftpPath extends File implements Serializable, PathInfo {
+
+    private static final long serialVersionUID = 5852748641005853109L;
+
+    private final String fileName;
+    private final String directory;
+    private final String relativePath;
+    private final String absolutePath;
+    private long fileSize;
+    private int permissions;
+    private boolean isFile;
+    private boolean isDirectory;
+    private Date updateTime;
+
+    public SftpPath(String directory, String relativePath) {
+        super(directory, relativePath);
+        this.directory = PathTools.formatPath(directory);
+        this.relativePath = PathTools.formatPath(relativePath);
+        this.absolutePath = PathTools.concat(this.directory, this.relativePath);
+        this.fileName = PathTools.getFileName(StringTools.trimRight(relativePath, '/'));
+    }
+
+    @Override
+    public String getDirectory() {
+        return directory;
+    }
+
+    @Override
+    public String getRelativePath() {
+        return relativePath;
+    }
+
+    @Override
+    public String getName() {
+        return fileName;
+    }
+
+    public int getPermissions() {
+        return permissions;
+    }
+
+    public void setPermissions(int permissions) {
+        this.permissions = permissions;
+    }
+
+    @Override
+    public boolean isFile() {
+        return isFile;
+    }
+
+    protected void setIsFile(boolean isFile) {
+        this.isFile = isFile;
+    }
+
+    @Override
+    public boolean isDirectory() {
+        return isDirectory;
+    }
+
+    protected void setIsDirectory(boolean isDirectory) {
+        this.isDirectory = isDirectory;
+    }
+
+    @Override
+    public long lastModified() {
+        return this.updateTime == null ? -1 : this.updateTime.getTime();
+    }
+
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    protected void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    @Override
+    public String getParent() {
+        String parent = PathTools.concat(true, absolutePath, "..");
+        return parent.startsWith("..") ? null : parent;
+    }
+
+    @Override
+    public SftpPath getParentFile() {
+        return null;
+    }
+
+    @Override
+    public String getPath() {
+        return absolutePath;
+    }
+
+    @Override
+    public boolean isAbsolute() {
+        return true;
+    }
+
+    @Override
+    public String getAbsolutePath() {
+        return absolutePath;
+    }
+
+    @Override
+    public SftpPath getAbsoluteFile() {
+        return this;
+    }
+
+    @Override
+    public String getCanonicalPath() {
+        return absolutePath;
+    }
+
+    @Override
+    public SftpPath getCanonicalFile() {
+        return this;
+    }
+
+    @Override
+    public boolean canRead() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canWrite() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isHidden() {
+        // 输出json时会访问此方法
+        // throw new UnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean exists() {
+        return true;
+    }
+
+    @Override
+    public long length() {
+        return this.fileSize;
+    }
+
+    protected void setLength(long fileSize) {
+        this.fileSize = fileSize;
+    }
+
+    @Override
+    public int compareTo(File file) {
+        return this.absolutePath.compareTo(file.getAbsolutePath());
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        return object instanceof SftpPath && compareTo((SftpPath) object) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return this.absolutePath.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return this.absolutePath;
+    }
+
+    @Override
+    public boolean createNewFile() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean delete() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void deleteOnExit() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] list() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] list(FilenameFilter filter) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public File[] listFiles() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public File[] listFiles(FilenameFilter filter) {
+        throw new UnsupportedOperationException();
+    }
+
+    
+    @Override
+    public File[] listFiles( FileFilter filter) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean mkdir() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean mkdirs() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean renameTo(File dest) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setLastModified(long time) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setReadOnly() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setWritable(boolean writable, boolean ownerOnly) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setWritable(boolean writable) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setReadable(boolean readable, boolean ownerOnly) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setReadable(boolean readable) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setExecutable(boolean executable, boolean ownerOnly) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setExecutable(boolean executable) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canExecute() {
+        return super.canExecute();
+    }
+
+    @Override
+    public long getTotalSpace() {
+        return -1;
+    }
+
+    @Override
+    public long getFreeSpace() {
+        return -1;
+    }
+
+    @Override
+    public long getUsableSpace() {
+        return -1;
+    }
+
+    @Override
+    public Path toPath() {
+        throw new UnsupportedOperationException();
+    }
+}
-- 
Gitee


From 38d35b8aa07380a3e5e2e5351e52fc92e136bc71 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@joyintech.com>
Date: Thu, 16 Mar 2023 22:08:37 +0800
Subject: [PATCH 134/160] =?UTF-8?q?XML=E5=B7=A5=E5=85=B7=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=89=8D=E7=BC=80=E5=A4=84?=
 =?UTF-8?q?=E7=90=86=E7=AD=96=E7=95=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/utils/XmlTools.java  | 232 +++++++++++++++---
 1 file changed, 193 insertions(+), 39 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
index 2725de3..d7ce192 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
@@ -35,40 +35,11 @@ import org.xml.sax.SAXException;
 public class XmlTools {
 
     public static Map<String, Object> xmlToJson(String xml) {
-        return xmlToJson(xml, "array,list");
+        return xmlToJson(xml, ARRAY_NAME);
     }
 
     public static Map<String, Object> xmlToJson(String xml, String arrayNodeNames) {
-        XmlToJsonOptions options = new XmlToJsonOptions();
-
-        // 是否使用驼峰命名
-        // 如果为true, 则XML中的<USER_LIST>将会转换为userList
-        options.setUseCamelNaming(true);
-
-        // 是否使用属性容器: 
-        // 如果为true, 属性都集中放在一个容器中; 
-        // 如果为false则不加容器, 与子元素放在一起; 此时如果属性名与子节点名相同, 子节点会覆盖属性名
-        options.setUseAttrContainer(false);
-        // options.setAttrContainerName("$attrs"); // useAttrContainer=false时无效
-
-        // 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称
-        // <node x="111" y="222">333</node> --> { node:{ x:111, y:222, text:333 } }
-        // 上例, 由于node有属性, 于是node变成了一个map节点, 那么这里的333必须要有字段名, 否则无法保存
-        options.setTextContentName("text");
-
-        // 数组节点名称
-        // <array> <aa>1</aa> <aa>2</aa> </array>
-        // 上例: array下全是同名节点, 将会合并为数组 { aa:[1,2] }
-        // <array> <aa>1</aa> </array>
-        // 上例: array下只有一个节点, 也会合并为数组 { aa:[1] }
-        // <array> <aa>1</aa> <aa>2</aa> <bb>3</bb> 444 </array>
-        // 上例: array下存在不同名的节点, 将会包装为数组 { array:[ {aa:1}, {aa:2}, {bb:3}, 444 ] }
-        // <array> 1 </array>
-        // 上例: array下只有一个文本节点, 也会包装为数组 { array:[1] }
-        if (VerifyTools.isNotBlank(arrayNodeNames)) {
-            options.setArrayNodeName(arrayNodeNames);
-        }
-
+        XmlToJsonOptions options = XmlToJsonOptions.defaults().setArrayNodeName(arrayNodeNames);
         return xmlToJson(xml, options);
     }
 
@@ -153,10 +124,29 @@ public class XmlTools {
     }
 
     private static String resolveMapKey(String nodeName, XmlToJsonOptions options) {
-        if (options == null || !options.isUseCamelNaming()) {
+        if (options == null) {
             return nodeName;
+        } else if (!options.isUseCamelNaming()) {
+            return handleNamespacePrefix(nodeName, options);
         } else {
-            return NamingTools.toCamelString(nodeName);
+            return NamingTools.toCamelString(handleNamespacePrefix(nodeName, options));
+        }
+    }
+
+    private static String handleNamespacePrefix(String nodeName, XmlToJsonOptions options) {
+        NamespacePrefixStrategy strategy = options == null ? null : options.getNamespacePrefixStrategy();
+        if (strategy == null) {
+            return nodeName;
+        }
+        switch (strategy) {
+            case REMOTE:
+                return StringTools.removePrefixAt(nodeName, ":");
+            case NORMALIZING:
+                return StringTools.replace(nodeName, ":", "_");
+            case DOLLAR_SEPARATOR:
+                return StringTools.replace(nodeName, ":", "$");
+            default:
+                return nodeName;
         }
     }
 
@@ -410,6 +400,133 @@ public class XmlTools {
 
     public static class XmlToJsonOptions {
 
+        /**
+         * 推荐选项 (解析结果有可能与旧版本不一致)<br>
+         * 1. 字段使用驼峰命名<br>
+         * -- &lt;USER_LIST>将会转换为userList<br>
+         * 2. 属性不使用容器, 属性与子元素放在一起<br>
+         * -- 如果属性名与子节点名相同, 子节点会覆盖属性名<br>
+         * 3. 文本内容的字段名称为text (如果节点有属性, 则节点中的文本内容需要有字段名称)<br>
+         * -- &lt;node x="111" y="222">333&lt;/node> --> { node:{ x:111, y:222, text:333 } }<br>
+         * 4. 数组节点名称为array,list<br>
+         * -- &lt;array> &lt;aa>1&lt;/aa> &lt;aa>2&lt;/aa> &lt;/array> --> { aa:[1,2] }<br>
+         * 5. 命名空间前缀处理策略为移除<br>
+         * -- &lt;soap:Body>转换为为body
+         * @return 推荐选项
+         */
+        public static XmlToJsonOptions recommend() {
+            XmlToJsonOptions options = defaults();
+            // REMOTE: 移除, <soap:Body>转换为body
+            options.setNamespacePrefixStrategy(NamespacePrefixStrategy.REMOTE);
+            return defaults();
+        }
+
+        /**
+         * 默认选项 (与旧版本保持一致)<br>
+         * 1. 字段使用驼峰命名<br>
+         * -- &lt;USER_LIST>将会转换为userList<br>
+         * 2. 属性不使用容器, 属性与子元素放在一起<br>
+         * -- 如果属性名与子节点名相同, 子节点会覆盖属性名<br>
+         * 3. 文本内容的字段名称为text (如果节点有属性, 则节点中的文本内容需要有字段名称)<br>
+         * -- &lt;node x="111" y="222">333&lt;/node> --> { node:{ x:111, y:222, text:333 } }<br>
+         * 4. 数组节点名称为array,list<br>
+         * -- &lt;array> &lt;aa>1&lt;/aa> &lt;aa>2&lt;/aa> &lt;/array> --> { aa:[1,2] }<br>
+         * 5. 命名空间前缀处理策略为不处理<br>
+         * -- &lt;soap:Body>转换为为soap:body
+         *
+         * @return 默认选项
+         */
+        public static XmlToJsonOptions defaults() {
+            XmlToJsonOptions options = new XmlToJsonOptions();
+
+            // 是否使用驼峰命名
+            // 如果为true, 则XML中的<USER_LIST>将会转换为userList
+            options.setUseCamelNaming(true);
+
+            // 是否使用属性容器:
+            // 如果为true, 属性都集中放在一个容器中;
+            // 如果为false则不加容器, 与子元素放在一起; 此时如果属性名与子节点名相同, 子节点会覆盖属性名
+            options.setUseAttrContainer(false);
+            // options.setAttrContainerName("$attrs"); // useAttrContainer=false时无效
+
+            // 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称
+            // <node x="111" y="222">333</node> --> { node:{ x:111, y:222, text:333 } }
+            // 上例, 由于node有属性, 于是node变成了一个map节点, 那么这里的333必须要有字段名, 否则无法保存
+            options.setTextContentName("text");
+
+            // 数组节点名称
+            // <array> <aa>1</aa> <aa>2</aa> </array>
+            // 上例: array下全是同名节点, 将会合并为数组 { aa:[1,2] }
+            // <array> <aa>1</aa> </array>
+            // 上例: array下只有一个节点, 也会合并为数组 { aa:[1] }
+            // <array> <aa>1</aa> <aa>2</aa> <bb>3</bb> 444 </array>
+            // 上例: array下存在不同名的节点, 将会包装为数组 { array:[ {aa:1}, {aa:2}, {bb:3}, 444 ] }
+            // <array> 1 </array>
+            // 上例: array下只有一个文本节点, 也会包装为数组 { array:[1] }
+            options.setArrayNodeName(ARRAY_NAME);
+
+            // NONE: 不处理, <soap:Body>转换为soap:body
+            // REMOTE: 移除, <soap:Body>转换为body
+            // NORMALIZING: 标准化处理, <soap:Body>转换为soapBody
+            // DOLLAR_SEPARATOR: 使用$分隔符, <soap:Body>转换为soap$Body
+            options.setNamespacePrefixStrategy(NamespacePrefixStrategy.NONE);
+
+            return options;
+        }
+
+        /**
+         * 保守选项 (尽量少转换多保留)<br>
+         * 1. 字段不使用驼峰命名<br>
+         * -- &lt;USER_LIST>将会转换为USER_LIST<br>
+         * 2. 属性使用容器, 容器名为'$attrs'<br>
+         * -- 如果属性名与子节点名相同, 子节点会覆盖属性名<br>
+         * 3. 文本内容的字段名称为$text (如果节点有属性, 则节点中的文本内容需要有字段名称)<br>
+         * -- &lt;node x="111" y="222">333&lt;/node> --> { node:{ x:111, y:222, $text:333 } }<br>
+         * 4. 数组节点名称为array,list<br>
+         * -- &lt;array> &lt;aa>1&lt;/aa> &lt;aa>2&lt;/aa> &lt;/array> --> { aa:[1,2] }<br>
+         * 5. 命名空间前缀处理策略为不处理<br>
+         * -- &lt;soap:Body>转换为为soap:body
+         *
+         * @return 保守选项
+         */
+        public static XmlToJsonOptions conservative() {
+            XmlToJsonOptions options = new XmlToJsonOptions();
+
+            // 是否使用驼峰命名
+            // 如果为true, 则XML中的<USER_LIST>将会转换为userList
+            options.setUseCamelNaming(false);
+
+            // 是否使用属性容器:
+            // 如果为true, 属性都集中放在一个容器中;
+            // 如果为false则不加容器, 与子元素放在一起; 此时如果属性名与子节点名相同, 子节点会覆盖属性名
+            options.setUseAttrContainer(true);
+            options.setAttrContainerName(ATTR_NAME);
+
+            // 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称
+            // <node x="111" y="222">333</node> --> { node:{ x:111, y:222, text:333 } }
+            // 上例, 由于node有属性, 于是node变成了一个map节点, 那么这里的333必须要有字段名, 否则无法保存
+            options.setTextContentName(TEXT_NAME);
+
+            // 数组节点名称
+            // <array> <aa>1</aa> <aa>2</aa> </array>
+            // 上例: array下全是同名节点, 将会合并为数组 { aa:[1,2] }
+            // <array> <aa>1</aa> </array>
+            // 上例: array下只有一个节点, 也会合并为数组 { aa:[1] }
+            // <array> <aa>1</aa> <aa>2</aa> <bb>3</bb> 444 </array>
+            // 上例: array下存在不同名的节点, 将会包装为数组 { array:[ {aa:1}, {aa:2}, {bb:3}, 444 ] }
+            // <array> 1 </array>
+            // 上例: array下只有一个文本节点, 也会包装为数组 { array:[1] }
+            options.setArrayNodeName(ARRAY_NAME);
+
+            // NONE: 不处理, <soap:Body>转换为soap:body
+            // REMOTE: 移除, <soap:Body>转换为body
+            // NORMALIZING: 标准化处理, <soap:Body>转换为soapBody
+            // DOLLAR_SEPARATOR: 使用$分隔符, <soap:Body>转换为soap$Body
+            options.setNamespacePrefixStrategy(NamespacePrefixStrategy.NONE);
+
+            return options;
+        }
+
         /** 是否使用驼峰命名 **/
         private boolean useCamelNaming = true;
         /** 是否使用属性容器: 如果为true, 属性都集中放在一个容器中; 如果为false则不加容器, 与子元素放在一起 **/
@@ -422,6 +539,8 @@ public class XmlTools {
         private String arrayNodeName = ARRAY_NAME;
         /** 数组节点匹配接口 **/
         private StringMatcher arrayNodeMatcher;
+        /** 命名空间前缀处理策略 **/
+        private NamespacePrefixStrategy namespacePrefixStrategy = NamespacePrefixStrategy.NONE;
 
         /** 是否使用驼峰命名 **/
         public boolean isUseCamelNaming() {
@@ -429,8 +548,9 @@ public class XmlTools {
         }
 
         /** 是否使用驼峰命名 **/
-        public void setUseCamelNaming(boolean useCamelNaming) {
+        public XmlToJsonOptions setUseCamelNaming(boolean useCamelNaming) {
             this.useCamelNaming = useCamelNaming;
+            return this;
         }
 
         /** 是否使用属性容器: 如果为true, 属性都集中放在一个容器中; 如果为false则不加容器, 与子元素放在一起 **/
@@ -442,8 +562,9 @@ public class XmlTools {
         }
 
         /** 是否使用属性容器: 如果为true, 属性都集中放在一个容器中; 如果为false则不加容器, 与子元素放在一起 **/
-        public void setUseAttrContainer(boolean useAttrContainer) {
+        public XmlToJsonOptions setUseAttrContainer(boolean useAttrContainer) {
             this.useAttrContainer = useAttrContainer;
+            return this;
         }
 
         /** 属性容器名称 **/
@@ -452,8 +573,9 @@ public class XmlTools {
         }
 
         /** 属性容器名称 **/
-        public void setAttrContainerName(String attrContainerName) {
+        public XmlToJsonOptions setAttrContainerName(String attrContainerName) {
             this.attrContainerName = VerifyTools.nvl(attrContainerName, ATTR_NAME);
+            return this;
         }
 
         /** 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称 **/
@@ -464,8 +586,9 @@ public class XmlTools {
         }
 
         /** 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称 **/
-        public void setTextContentName(String textContentName) {
+        public XmlToJsonOptions setTextContentName(String textContentName) {
             this.textContentName = VerifyTools.nvl(textContentName, TEXT_NAME);
+            return this;
         }
 
         /** 数组节点名称 (不区分大小写) **/
@@ -474,8 +597,15 @@ public class XmlTools {
         }
 
         /** 数组节点名称 (不区分大小写) **/
-        public void setArrayNodeName(String arrayNodeName) {
+        public XmlToJsonOptions setArrayNodeName(String arrayNodeName) {
             this.arrayNodeName = arrayNodeName;
+            return this;
+        }
+
+        /** 数组节点名称 (不区分大小写) **/
+        public XmlToJsonOptions addArrayNodeName(String arrayNodeName) {
+            this.arrayNodeName = StringTools.concat(',', this.arrayNodeName, arrayNodeName);
+            return this;
         }
 
         /** 数组节点匹配接口 **/
@@ -484,8 +614,9 @@ public class XmlTools {
         }
 
         /** 数组节点匹配接口 **/
-        public void setArrayNodeMatcher(StringMatcher arrayNodeMatcher) {
+        public XmlToJsonOptions setArrayNodeMatcher(StringMatcher arrayNodeMatcher) {
             this.arrayNodeMatcher = arrayNodeMatcher;
+            return this;
         }
 
         /** 判断是不是数组节点名称 (不区分大小写) **/
@@ -498,6 +629,17 @@ public class XmlTools {
             }
             return arrayNodeMatcher != null && arrayNodeMatcher.matches(nodeName);
         }
+
+        /** 命名空间前缀处理策略 **/
+        public NamespacePrefixStrategy getNamespacePrefixStrategy() {
+            return namespacePrefixStrategy;
+        }
+
+        /** 命名空间前缀处理策略 **/
+        public XmlToJsonOptions setNamespacePrefixStrategy(NamespacePrefixStrategy namespacePrefixStrategy) {
+            this.namespacePrefixStrategy = namespacePrefixStrategy;
+            return this;
+        }
     }
 
     private final static Charset UTF8 = StandardCharsets.UTF_8;
@@ -508,4 +650,16 @@ public class XmlTools {
     /** 静态工具类私有构造方法 **/
     private XmlTools() {
     }
+
+    /** 命名空间前缀处理策略 **/
+    public enum NamespacePrefixStrategy {
+        /** 不处理, <soap:Body>转换为soap:body **/
+        NONE,
+        /** 移除, <soap:Body>转换为body **/
+        REMOTE,
+        /** 标准化处理, <soap:Body>转换为soapBody **/
+        NORMALIZING,
+        /** 使用$分隔符, <soap:Body>转换为soap$Body **/
+        DOLLAR_SEPARATOR
+    }
 }
-- 
Gitee


From 42ebef3a8c0e32d5f7855448d08ee7ab74107c76 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@joyintech.com>
Date: Thu, 16 Mar 2023 22:09:18 +0800
Subject: [PATCH 135/160] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B0=86List?=
 =?UTF-8?q?=E4=BD=9C=E4=B8=BAMap=E5=AF=B9=E8=B1=A1=E8=BF=94=E5=9B=9E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/able/beans/LinkedListMap.java  | 27 +++++++++++++++++++
 .../com/gitee/qdbp/tools/utils/MapTools.java  | 23 ++++++++++++----
 2 files changed, 45 insertions(+), 5 deletions(-)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/beans/LinkedListMap.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/LinkedListMap.java b/able/src/main/java/com/gitee/qdbp/able/beans/LinkedListMap.java
new file mode 100644
index 0000000..cc08ca3
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/LinkedListMap.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.utils.MapTools;
+
+/**
+ * 用来装List对象的容器
+ *
+ * @author zhaohuihua
+ * @version 20230315
+ */
+public class LinkedListMap extends LinkedHashMap<String, Object> {
+
+    private static final long serialVersionUID = -1985897783368043967L;
+
+    public LinkedListMap(List<Map<String, Object>> list) {
+        for (int i = 0; i < list.size(); i++) {
+            this.put(String.valueOf(i), list.get(i));
+        }
+    }
+
+    public List<Map<String, Object>> content() {
+        return MapTools.toJsonMaps(this.values());
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index ade0c17..cdbe93b 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -12,6 +12,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import com.gitee.qdbp.able.beans.DepthMap;
+import com.gitee.qdbp.able.beans.LinkedListMap;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.model.reusable.ExpressionMap;
 import com.gitee.qdbp.able.result.ResultCode;
@@ -446,15 +447,27 @@ public class MapTools {
         return convertValue(value, null, clazz);
     }
 
+    @SuppressWarnings("unchecked")
     private static <T> T convertValue(Object value, T defaults, Class<T> clazz) {
         if (value == null && defaults != null) {
             return defaults;
         } else {
-            try {
-                T result = JsonTools.convert(value, clazz);
-                return result != null ? result : defaults;
-            } catch (Exception e) {
-                return defaults;
+            if (clazz == Map.class) {
+                List<Object> objects = ConvertTools.parseList(value);
+                if (objects.size() == 1 && objects.get(0) == value) {
+                    // 如果列表容器只有value一个元素, 说明value不是列表
+                    return (T) JsonTools.beanToMap(value);
+                } else {
+                    // 如果结果是数组, 包装成Map返回
+                    return (T) new LinkedListMap(JsonTools.beanToMaps(objects));
+                }
+            } else {
+                try {
+                    T result = JsonTools.convert(value, clazz);
+                    return result != null ? result : defaults;
+                } catch (Exception e) {
+                    return defaults;
+                }
             }
         }
     }
-- 
Gitee


From 68272bc70901ab04dd397961091a1caa4a30d0c9 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@joyintech.com>
Date: Thu, 16 Mar 2023 22:20:13 +0800
Subject: [PATCH 136/160] =?UTF-8?q?XML=E5=B7=A5=E5=85=B7=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=89=8D=E7=BC=80=E5=A4=84?=
 =?UTF-8?q?=E7=90=86=E7=AD=96=E7=95=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/tools/utils/XmlTools.java    | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
index d7ce192..905967e 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
@@ -139,7 +139,7 @@ public class XmlTools {
             return nodeName;
         }
         switch (strategy) {
-            case REMOTE:
+            case REMOVE:
                 return StringTools.removePrefixAt(nodeName, ":");
             case NORMALIZING:
                 return StringTools.replace(nodeName, ":", "_");
@@ -417,8 +417,8 @@ public class XmlTools {
         public static XmlToJsonOptions recommend() {
             XmlToJsonOptions options = defaults();
             // REMOTE: 移除, <soap:Body>转换为body
-            options.setNamespacePrefixStrategy(NamespacePrefixStrategy.REMOTE);
-            return defaults();
+            options.setNamespacePrefixStrategy(NamespacePrefixStrategy.REMOVE);
+            return options;
         }
 
         /**
@@ -656,7 +656,7 @@ public class XmlTools {
         /** 不处理, <soap:Body>转换为soap:body **/
         NONE,
         /** 移除, <soap:Body>转换为body **/
-        REMOTE,
+        REMOVE,
         /** 标准化处理, <soap:Body>转换为soapBody **/
         NORMALIZING,
         /** 使用$分隔符, <soap:Body>转换为soap$Body **/
-- 
Gitee


From 673456cd5ba4f9c7d845bbff52fed3114bff7ba8 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@joyintech.com>
Date: Thu, 16 Mar 2023 22:30:13 +0800
Subject: [PATCH 137/160] =?UTF-8?q?XML=E5=B7=A5=E5=85=B7=E5=A2=9E=E5=8A=A0?=
 =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=89=8D=E7=BC=80=E5=A4=84?=
 =?UTF-8?q?=E7=90=86=E7=AD=96=E7=95=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
index 905967e..e37daac 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
@@ -413,6 +413,7 @@ public class XmlTools {
          * 5. 命名空间前缀处理策略为移除<br>
          * -- &lt;soap:Body>转换为为body
          * @return 推荐选项
+         * @since 5.5.10
          */
         public static XmlToJsonOptions recommend() {
             XmlToJsonOptions options = defaults();
@@ -435,6 +436,7 @@ public class XmlTools {
          * -- &lt;soap:Body>转换为为soap:body
          *
          * @return 默认选项
+         * @since 5.5.10
          */
         public static XmlToJsonOptions defaults() {
             XmlToJsonOptions options = new XmlToJsonOptions();
@@ -488,6 +490,7 @@ public class XmlTools {
          * -- &lt;soap:Body>转换为为soap:body
          *
          * @return 保守选项
+         * @since 5.5.10
          */
         public static XmlToJsonOptions conservative() {
             XmlToJsonOptions options = new XmlToJsonOptions();
-- 
Gitee


From 3acbfbf84fbb901650fa966a177496bf9e0f980c Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@joyintech.com>
Date: Thu, 16 Mar 2023 22:31:27 +0800
Subject: [PATCH 138/160] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java | 1 +
 tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java | 1 +
 tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java  | 1 +
 tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java    | 3 ++-
 4 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java b/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java
index ea6d58c..3ed4af6 100644
--- a/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java
+++ b/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java
@@ -5,6 +5,7 @@ package com.gitee.qdbp.able.model.file;
  *
  * @author zhaohuihua
  * @version 20230312
+ * @since 5.5.10
  */
 public interface PathInfo {
 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
index 3996477..9988f1d 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
@@ -31,6 +31,7 @@ import com.jcraft.jsch.SftpException;
  *
  * @author zhaohuihua
  * @version 20230311
+ * @since 5.5.10
  */
 public class SftpChannel implements AutoCloseable {
 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
index 7f46513..0982329 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
@@ -15,6 +15,7 @@ import com.jcraft.jsch.Session;
  *
  * @author zhaohuihua
  * @version 20230310
+ * @since 5.5.10
  */
 public class SftpClient {
 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
index 372b8e6..edc1409 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
@@ -15,8 +15,9 @@ import com.gitee.qdbp.tools.utils.StringTools;
  *
  * @author zhaohuihua
  * @version 20230311
+ * @since 5.5.10
  */
-public class SftpPath extends File implements Serializable, PathInfo {
+public class SftpPath extends File implements PathInfo, Serializable {
 
     private static final long serialVersionUID = 5852748641005853109L;
 
-- 
Gitee


From 02b3a17e3383e36b4f46fca63a10aa56e5b4111c Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@joyintech.com>
Date: Thu, 16 Mar 2023 22:33:29 +0800
Subject: [PATCH 139/160] 5.5.10

---
 README.md                            |  4 ++--
 able/pom.xml                         |  2 +-
 json/pom.xml                         |  4 ++--
 test/pom.xml                         |  2 +-
 test/qdbp-json-test-base/pom.xml     |  2 +-
 test/qdbp-json-test-fastjson/pom.xml |  2 +-
 test/qdbp-json-test-gson/pom.xml     |  2 +-
 test/qdbp-json-test-jackson/pom.xml  |  2 +-
 test/qdbp-tools-test-jdk7/pom.xml    |  2 +-
 tools/pom.xml                        | 10 +++++++++-
 10 files changed, 20 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index b0523cc..6dec2d1 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.5.8</version>
+        <version>5.5.10</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.5.8</version>
+        <version>5.5.10</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index 3d2959f..19aeb14 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.8</version>
+	<version>5.5.10</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index 0ec7a34..43066ad 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.8</version>
+	<version>5.5.10</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.8</version>
+			<version>5.5.10</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index 29ebfac..094a0af 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.8</version>
+	<version>5.5.10</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index b2ef860..e1be19e 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.8</version>
+		<version>5.5.10</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index 6ce1ac6..9f31f0e 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.8</version>
+		<version>5.5.10</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index 1c392ac..71210ff 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.8</version>
+		<version>5.5.10</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index 40b34ff..24fc8e9 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.8</version>
+		<version>5.5.10</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index c96d357..7cb0a52 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.8</version>
+		<version>5.5.10</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index 33ed5ab..4334c55 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.8</version>
+	<version>5.5.10</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
@@ -47,6 +47,14 @@
 			<optional>true</optional>
 		</dependency>
 
+		<!-- SFTP -->
+		<dependency>
+			<groupId>com.jcraft</groupId>
+			<artifactId>jsch</artifactId>
+			<version>0.1.55</version>
+			<optional>true</optional>
+		</dependency>
+
 		<dependency>
 			<groupId>ognl</groupId>
 			<artifactId>ognl</artifactId>
-- 
Gitee


From 7b26704d7ba77bab3b8d895c0835b6a749f756ca Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@joyintech.com>
Date: Sun, 19 Mar 2023 22:53:58 +0800
Subject: [PATCH 140/160] =?UTF-8?q?Debugger=E5=A2=9E=E5=BC=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/able/debug/BaseDebugger.java   | 30 +++++++++++++++++--
 .../com/gitee/qdbp/able/debug/Debugger.java   | 22 +++++++++++++-
 2 files changed, 49 insertions(+), 3 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/able/debug/BaseDebugger.java b/able/src/main/java/com/gitee/qdbp/able/debug/BaseDebugger.java
index f8e2a6a..678b411 100644
--- a/able/src/main/java/com/gitee/qdbp/able/debug/BaseDebugger.java
+++ b/able/src/main/java/com/gitee/qdbp/able/debug/BaseDebugger.java
@@ -1,5 +1,7 @@
 package com.gitee.qdbp.able.debug;
 
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
 import java.util.Date;
 import java.util.List;
 import com.gitee.qdbp.able.matches.StringMatcher;
@@ -44,15 +46,39 @@ public abstract class BaseDebugger implements Debugger {
         log(buffer.toString());
     }
 
+    @Override
+    public void debug(Formatted formatted) {
+        log(formatted.getMessage());
+    }
+
+    private static final String DECIMAL_FORMAT = "#.##################";
+
     protected void makeMessage(StringBuffer buffer, String format, Object... args) {
+        makeMessage(buffer, new Date(), format, args);
+    }
+
+    protected void makeMessage(StringBuffer buffer, Date time, String format, Object... args) {
         if (timePattern != null) {
-            buffer.append(DateTools.format(new Date(), timePattern)).append(' ');
+            buffer.append(DateTools.format(time, timePattern)).append(' ');
         }
         if (args == null || args.length == 0) {
             buffer.append(format);
         } else {
             String fmt = StringTools.replace(format, "{}", "%s");
-            buffer.append(String.format(fmt, args));
+            Object[] params = new String[args.length];
+            for (int i = 0; i < args.length; i++) {
+                Object item = args[i];
+                if (item instanceof Date) {
+                    params[i] = DateTools.toAutoString((Date) item);
+                } else if (item instanceof Double) {
+                    params[i] = new DecimalFormat(DECIMAL_FORMAT).format(item);
+                } else if (item instanceof BigDecimal) {
+                    params[i] = ConvertTools.toPlainString((BigDecimal) item);
+                } else {
+                    params[i] = item;
+                }
+            }
+            buffer.append(String.format(fmt, params));
             // 如果参数比占位符多, 且最后一个参数是异常类, 则输出异常堆栈
             Object last = args[args.length - 1];
             if (last instanceof Throwable) {
diff --git a/able/src/main/java/com/gitee/qdbp/able/debug/Debugger.java b/able/src/main/java/com/gitee/qdbp/able/debug/Debugger.java
index 3c4ed0b..376af95 100644
--- a/able/src/main/java/com/gitee/qdbp/able/debug/Debugger.java
+++ b/able/src/main/java/com/gitee/qdbp/able/debug/Debugger.java
@@ -10,12 +10,32 @@ public interface Debugger {
 
     /**
      * 记录日志
-     * 
+     *
      * @param message 消息内容
      * @param args 占位符参数
      */
     void debug(String message, Object... args);
 
+    /**
+     * 记录日志
+     *
+     * @param formatted 已格式化的消息内容
+     */
+    void debug(Formatted formatted);
+
+    class Formatted {
+
+        private final String message;
+
+        public Formatted(String message) {
+            this.message = message;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+    }
+
     interface Aware {
 
         void setDebugger(Debugger debugger);
-- 
Gitee


From 0fcac2c6979551aa6b389350c25300664f17a35c Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Mar 2023 22:12:20 +0800
Subject: [PATCH 141/160] ExtensionFileMatcher

---
 .../able/matches/ExtensionFileMatcher.java    | 65 +++++++++++++++++++
 .../qdbp/able/matches/WrapFileMatcher.java    |  6 ++
 2 files changed, 71 insertions(+)
 create mode 100644 able/src/main/java/com/gitee/qdbp/able/matches/ExtensionFileMatcher.java

diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/ExtensionFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/ExtensionFileMatcher.java
new file mode 100644
index 0000000..f718f35
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/ExtensionFileMatcher.java
@@ -0,0 +1,65 @@
+package com.gitee.qdbp.able.matches;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.able.matches.StringMatcher.Matches;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 文件后缀匹配规则<br>
+ * new ExtensionFileMatcher("doc,docx,xls,xlsx");<br>
+ *
+ * @author zhaohuihua
+ * @version 20230324
+ */
+public class ExtensionFileMatcher implements FileMatcher {
+
+    /** 是否反转判断结果 **/
+    private final boolean reverse;
+    private final String pattern;
+    private final Map<String, ?> extensions = new HashMap<>();
+
+    public ExtensionFileMatcher(String extensions) {
+        this(extensions, Matches.Positive);
+    }
+
+    public ExtensionFileMatcher(String extensions, Matches mode) {
+        this.reverse = mode == Matches.Negative;
+        this.pattern = extensions;
+        List<String> items = StringTools.splits(extensions, ',');
+        for (String extension : items) {
+            this.extensions.put(extension.toLowerCase(), null);
+        }
+    }
+
+    public ExtensionFileMatcher(List<String> extensions) {
+        this(extensions, Matches.Positive);
+    }
+
+    public ExtensionFileMatcher(List<String> extensions, Matches mode) {
+        this.reverse = mode == Matches.Negative;
+        this.pattern = ConvertTools.joinToString(extensions);
+        for (String extension : extensions) {
+            this.extensions.put(extension.toLowerCase(), null);
+        }
+    }
+
+    @Override
+    public boolean matches(File source) {
+        String extension = PathTools.getExtension(source.getName(), false).toLowerCase();
+        return this.extensions.containsKey(extension) != reverse;
+    }
+
+    @Override
+    public String toString() {
+        if (reverse) {
+            return "extension!:" + pattern;
+        } else {
+            return "extension:" + pattern;
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java
index 9e46629..e32352a 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java
@@ -5,6 +5,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import com.gitee.qdbp.able.matches.StringMatcher.LogicType;
+import com.gitee.qdbp.able.matches.StringMatcher.Matches;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
@@ -194,6 +195,11 @@ public class WrapFileMatcher implements FileMatcher {
      */
     public static FileMatcher parseMatcher(String pattern, String defaultTarget, String defaultMode) {
         VerifyTools.requireNotBlank(pattern, "pattern");
+        if (pattern.startsWith("extension:")) {
+            return new ExtensionFileMatcher(pattern.substring("extension:".length()));
+        } else if (pattern.startsWith("extension!:")) {
+            return new ExtensionFileMatcher(pattern.substring("extension!:".length()), Matches.Negative);
+        }
         Target target;
         if (pattern.startsWith("name:")) {
             target = Target.FileName;
-- 
Gitee


From fb0a53af29669c64fc4c7be0f3f8f23ab32531c9 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Mar 2023 22:16:02 +0800
Subject: [PATCH 142/160] =?UTF-8?q?FileMatcher=E5=A2=9E=E5=8A=A0of?=
 =?UTF-8?q?=E7=B3=BB=E5=88=97=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/able/matches/AntFileMatcher.java  | 16 ++++++++++++++++
 .../gitee/qdbp/able/matches/EndsFileMatcher.java | 15 +++++++++++++++
 .../qdbp/able/matches/EqualsFileMatcher.java     | 16 ++++++++++++++++
 .../qdbp/able/matches/RegexpFileMatcher.java     | 16 ++++++++++++++++
 .../qdbp/able/matches/StartsFileMatcher.java     | 15 +++++++++++++++
 5 files changed, 78 insertions(+)

diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/AntFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/AntFileMatcher.java
index 8362eb9..ad75b91 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/AntFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/AntFileMatcher.java
@@ -31,4 +31,20 @@ public class AntFileMatcher extends BaseFileMatcher {
     public AntFileMatcher(String pattern, Target target, Matches mode) {
         super(new AntStringMatcher(pattern, true, mode), target);
     }
+
+    public static AntFileMatcher ofNameMatches(String pattern) {
+        return new AntFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static AntFileMatcher ofPathMatches(String pattern) {
+        return new AntFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static AntFileMatcher ofNameNotMatches(String pattern) {
+        return new AntFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static AntFileMatcher ofPathNotMatches(String pattern) {
+        return new AntFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/EndsFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/EndsFileMatcher.java
index 7b36215..75ff976 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/EndsFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/EndsFileMatcher.java
@@ -32,4 +32,19 @@ public class EndsFileMatcher extends BaseFileMatcher {
         super(new EndsStringMatcher(pattern, mode), target);
     }
 
+    public static EndsFileMatcher ofNameMatches(String pattern) {
+        return new EndsFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static EndsFileMatcher ofPathMatches(String pattern) {
+        return new EndsFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static EndsFileMatcher ofNameNotMatches(String pattern) {
+        return new EndsFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static EndsFileMatcher ofPathNotMatches(String pattern) {
+        return new EndsFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/EqualsFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/EqualsFileMatcher.java
index 8a320f8..4a8f3b1 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/EqualsFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/EqualsFileMatcher.java
@@ -31,4 +31,20 @@ public class EqualsFileMatcher extends BaseFileMatcher {
     public EqualsFileMatcher(String pattern, Target target, Matches mode) {
         super(new EqualsStringMatcher(pattern, mode), target);
     }
+
+    public static EqualsFileMatcher ofNameMatches(String pattern) {
+        return new EqualsFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static EqualsFileMatcher ofPathMatches(String pattern) {
+        return new EqualsFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static EqualsFileMatcher ofNameNotMatches(String pattern) {
+        return new EqualsFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static EqualsFileMatcher ofPathNotMatches(String pattern) {
+        return new EqualsFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpFileMatcher.java
index 86f3304..4ac65dc 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpFileMatcher.java
@@ -53,4 +53,20 @@ public class RegexpFileMatcher extends BaseFileMatcher {
     public RegexpFileMatcher(Pattern pattern, Target target, Matches mode) {
         super(new RegexpStringMatcher(pattern, mode), target);
     }
+
+    public static RegexpFileMatcher ofNameMatches(String pattern) {
+        return new RegexpFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static RegexpFileMatcher ofPathMatches(String pattern) {
+        return new RegexpFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static RegexpFileMatcher ofNameNotMatches(String pattern) {
+        return new RegexpFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static RegexpFileMatcher ofPathNotMatches(String pattern) {
+        return new RegexpFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StartsFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/StartsFileMatcher.java
index 52cb3a4..98823d4 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StartsFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StartsFileMatcher.java
@@ -32,4 +32,19 @@ public class StartsFileMatcher extends BaseFileMatcher {
         super(new StartsStringMatcher(pattern, mode), target);
     }
 
+    public static StartsFileMatcher ofNameMatches(String pattern) {
+        return new StartsFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static StartsFileMatcher ofPathMatches(String pattern) {
+        return new StartsFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static StartsFileMatcher ofNameNotMatches(String pattern) {
+        return new StartsFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static StartsFileMatcher ofPathNotMatches(String pattern) {
+        return new StartsFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
-- 
Gitee


From 7ea6b2574f40583f24280ae621521d82d5d26d67 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Mar 2023 22:21:18 +0800
Subject: [PATCH 143/160] =?UTF-8?q?beanToMap=E4=BC=98=E5=8C=96,=20?=
 =?UTF-8?q?=E4=BF=9D=E7=95=99=E7=A9=BA=E5=80=BC=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/tools/utils/MapTools.java    | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index cdbe93b..7124c3e 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -132,7 +132,7 @@ public class MapTools {
             return null;
         }
         try {
-            return JsonTools.beanToMap(item);
+            return JsonTools.beanToMap(item, true, false);
         } catch (ServiceException e) {
             if (!throwOnError) {
                 return null;
@@ -456,10 +456,10 @@ public class MapTools {
                 List<Object> objects = ConvertTools.parseList(value);
                 if (objects.size() == 1 && objects.get(0) == value) {
                     // 如果列表容器只有value一个元素, 说明value不是列表
-                    return (T) JsonTools.beanToMap(value);
+                    return (T) toJsonMap((Map<?, ?>) value);
                 } else {
                     // 如果结果是数组, 包装成Map返回
-                    return (T) new LinkedListMap(JsonTools.beanToMaps(objects));
+                    return (T) new LinkedListMap(JsonTools.beanToMaps(objects, true, false));
                 }
             } else {
                 try {
@@ -922,7 +922,7 @@ public class MapTools {
                 Map<String, Object> child = MapTools.toJsonMap((Map<?, ?>) fieldValue);
                 doEachMap(fieldPath, child, interceptor);
             } else if (!ReflectTools.isPrimitive(fieldValue.getClass(), false)) {
-                Map<String, Object> child = JsonTools.beanToMap(fieldValue);
+                Map<String, Object> child = JsonTools.beanToMap(fieldValue, true, false);
                 mapEntry.setValue(child);
                 doEachMap(fieldPath, child, interceptor);
             }
-- 
Gitee


From 0abd1e5f27cfd56e2b31a546df4cf4481dd39f8c Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Mar 2023 22:44:20 +0800
Subject: [PATCH 144/160] =?UTF-8?q?sftp=E7=9A=84downloadSave=E5=A2=9E?=
 =?UTF-8?q?=E5=8A=A0=E4=BF=9D=E5=AD=98=E8=B7=AF=E5=BE=84=E8=BF=94=E5=9B=9E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
index 9988f1d..4449081 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
@@ -117,11 +117,12 @@ public class SftpChannel implements AutoCloseable {
      *
      * @param remotePath 远程文件路径: 既可以是文件名(下载当前目录下的文件), 也可以是全路径+文件名(根据绝对路径下载)
      * @param clientPath 本地文件路径: 既可以是文件目录(以/结尾,保存为远程文件名), 也可以是全路径+文件名(保存到绝对路径)
+     * @return 实际保存路径
      * @throws ServiceException 下载失败<br>
      * FILE_DOWNLOAD_ERROR 文件下载失败<br>
      * FILE_SAVE_ERROR 文件保存失败
      */
-    public void downloadSave(String remotePath, String clientPath) {
+    public String downloadSave(String remotePath, String clientPath) {
         try (RemotePath rp = RemotePath.of(channel, remotePath)) {
             // 拆分远程路径为文件目录和文件名
             if (rp.endWithSlash || rp.name == null) {
@@ -140,11 +141,13 @@ public class SftpChannel implements AutoCloseable {
                 throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails("remotePath=" + rp);
             }
             // 如果本地目录不存在则创建
-            FileTools.mkdirsIfNotExists(cp.fullPathOnRoot());
-            try (OutputStream output = new FileOutputStream(cp.fullPathOnRoot())) {
+            String savePath = cp.fullPathOnRoot();
+            FileTools.mkdirsIfNotExists(savePath);
+            try (OutputStream output = new FileOutputStream(savePath)) {
                 // 下载文件并保存
                 try {
                     channel.get(rp.effectivePath(), output);
+                    return savePath;
                 } catch (SftpException e) {
                     throw new ServiceException(FileErrorCode.FILE_DOWNLOAD_ERROR, e)
                             .setDetails("remotePath=" + rp);
-- 
Gitee


From 05370953fc24839d4b6980f4fb74ea21a8d3fb52 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Mar 2023 22:45:11 +0800
Subject: [PATCH 145/160] =?UTF-8?q?sftp=E5=A2=9E=E5=8A=A0Getter,=20?=
 =?UTF-8?q?=E5=AF=86=E7=A0=81=E6=94=B9=E4=B8=BAbyte[]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/gitee/qdbp/tools/sftp/SftpClient.java | 41 ++++++++++++++++---
 1 file changed, 36 insertions(+), 5 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
index 0982329..70187b4 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
@@ -1,5 +1,6 @@
 package com.gitee.qdbp.tools.sftp;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Properties;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
@@ -22,7 +23,7 @@ public class SftpClient {
     private final String host;
     private final int port;
     private final String account;
-    private final String password;
+    private final byte[] password;
 
     public static Builder builder() {
         return new Builder();
@@ -32,7 +33,7 @@ public class SftpClient {
         this(host, port, null, null);
     }
 
-    private SftpClient(String host, int port, String account, String password) {
+    private SftpClient(String host, int port, String account, byte[] password) {
         VerifyTools.requireNotBlank(host, "host");
         this.host = host;
         this.port = port;
@@ -50,7 +51,7 @@ public class SftpClient {
                     .setDetails("sftp --> {}:{}", host, port);
         }
 
-        if (VerifyTools.isNotBlank(password)) {
+        if (password != null) {
             session.setPassword(password);
         }
 
@@ -89,12 +90,33 @@ public class SftpClient {
         return new SftpChannel(session, (ChannelSftp) channel);
     }
 
+    public String getHost() {
+        return host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public String getAccount() { return account; }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder();
+        if (VerifyTools.isNotBlank(account)) {
+            buffer.append(account).append('@');
+        }
+        buffer.append(host);
+        buffer.append(':').append(port);
+        return buffer.toString();
+    }
+
     public static class Builder {
 
         private String host;
         private int port = 22;
         private String account;
-        private String password;
+        private byte[] password;
 
         protected Builder() {
         }
@@ -114,11 +136,20 @@ public class SftpClient {
             return this;
         }
 
-        public Builder password(String password) {
+        public Builder password(byte[] password) {
             this.password = password;
             return this;
         }
 
+        public Builder password(String password) {
+            if (VerifyTools.isBlank(password)) {
+                this.password = null;
+            } else {
+                this.password = password.getBytes(StandardCharsets.UTF_8);
+            }
+            return this;
+        }
+
         public SftpClient build() {
             VerifyTools.requireNotBlank(host, "host");
             return new SftpClient(host, port, account, password);
-- 
Gitee


From 91dc5e93637ccf30b33ce3915410774f1a2d5f6d Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sat, 25 Mar 2023 22:47:43 +0800
Subject: [PATCH 146/160] =?UTF-8?q?qdbp=E7=89=88=E6=9C=AC=E5=8D=87?=
 =?UTF-8?q?=E7=BA=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md                            | 4 ++--
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 10 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index 6dec2d1..e354a10 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.5.10</version>
+        <version>5.5.12</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.5.10</version>
+        <version>5.5.12</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index 19aeb14..7ec4ef6 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.10</version>
+	<version>5.5.12</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index 43066ad..16296ad 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.10</version>
+	<version>5.5.12</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.10</version>
+			<version>5.5.12</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index 094a0af..e09c511 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.10</version>
+	<version>5.5.12</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index e1be19e..97e668c 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.10</version>
+		<version>5.5.12</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index 9f31f0e..b901ebc 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.10</version>
+		<version>5.5.12</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index 71210ff..24a9865 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.10</version>
+		<version>5.5.12</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index 24fc8e9..04b6278 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.10</version>
+		<version>5.5.12</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index 7cb0a52..22e4b55 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.10</version>
+		<version>5.5.12</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index 4334c55..c51ae0b 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.10</version>
+	<version>5.5.12</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 0f10478e27e47e53ad4d3d9d03708a76c6fbe4d8 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 26 Mar 2023 10:33:34 +0800
Subject: [PATCH 147/160] =?UTF-8?q?SftpChannel.info=E7=BB=93=E6=9E=9C?=
 =?UTF-8?q?=E6=94=B9=E4=B8=BAPathInfo=20(SftpPath=E4=BB=85=E4=BE=9B?=
 =?UTF-8?q?=E5=86=85=E9=83=A8=E4=BD=BF=E7=94=A8)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
index 4449081..a6cc7cc 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
@@ -249,7 +249,7 @@ public class SftpChannel implements AutoCloseable {
         return message.contains("no such") || message.contains("not found") || message.contains("not find");
     }
 
-    public SftpPath info(String remotePath) {
+    public PathInfo info(String remotePath) {
         return getFileInfo(RemotePath.of(channel, remotePath));
     }
 
-- 
Gitee


From b8ad9e754470f653f80687f6de110acf104fee0b Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 26 Mar 2023 10:36:34 +0800
Subject: [PATCH 148/160] =?UTF-8?q?SftpPath=E4=BB=85=E4=BE=9B=E5=86=85?=
 =?UTF-8?q?=E9=83=A8=E4=BD=BF=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
index edc1409..cad1294 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
@@ -17,7 +17,7 @@ import com.gitee.qdbp.tools.utils.StringTools;
  * @version 20230311
  * @since 5.5.10
  */
-public class SftpPath extends File implements PathInfo, Serializable {
+class SftpPath extends File implements PathInfo, Serializable {
 
     private static final long serialVersionUID = 5852748641005853109L;
 
-- 
Gitee


From 2a255b06338bdc213e3b93a5164177cc365d17df Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 27 Mar 2023 20:46:29 +0800
Subject: [PATCH 149/160] =?UTF-8?q?isJsonObjectString()=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/VerifyTools.java   | 23 +++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
index 92e5964..050b04e 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
@@ -267,22 +267,37 @@ public class VerifyTools {
 
     /** 是不是JsonObject字符串 **/
     public static boolean isJsonObjectString(String value) {
-        return checkJsonString(value, '{', '}');
+        return checkJsonObjectString(value);
     }
 
     /** 是不是JsonObject字符串 **/
     public static boolean isJsonObjectString(Object value) {
-        return value instanceof String && checkJsonString((String) value, '{', '}');
+        return value instanceof String && checkJsonObjectString((String) value);
+    }
+
+    // 以{开头, 以}结尾, 且中间有冒号
+    private static boolean checkJsonObjectString(String string) {
+        boolean passed = checkJsonString(string, '{', '}');
+        if (!passed) {
+            return false;
+        } else {
+            String content = StringTools.removeLeftRight(string, 1, 1).trim();
+            return content.length() == 0 || content.indexOf(':') > 0;
+        }
     }
 
     /** 是不是JsonArray字符串 **/
     public static boolean isJsonArrayString(String value) {
-        return checkJsonString(value, '[', ']');
+        return checkJsonArrayString(value);
     }
 
     /** 是不是JsonArray字符串 **/
     public static boolean isJsonArrayString(Object value) {
-        return value instanceof String && checkJsonString((String) value, '[', ']');
+        return value instanceof String && checkJsonArrayString((String) value);
+    }
+
+    private static boolean checkJsonArrayString(String string) {
+        return checkJsonString(string, '[', ']');
     }
 
     private static boolean checkJsonString(String value, char first, char last) {
-- 
Gitee


From 3518183622b52ff6283a7de7de548b729d7e5e87 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Mon, 27 Mar 2023 20:46:44 +0800
Subject: [PATCH 150/160] VerifyToolsTest

---
 .../qdbp/tools/utils/VerifyToolsTest.java     | 58 +++++++++++++++++++
 1 file changed, 58 insertions(+)
 create mode 100644 tools/src/test/java/com/gitee/qdbp/tools/utils/VerifyToolsTest.java

diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/VerifyToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/VerifyToolsTest.java
new file mode 100644
index 0000000..0510c5a
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/VerifyToolsTest.java
@@ -0,0 +1,58 @@
+package com.gitee.qdbp.tools.utils;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * VerifyToolsTest
+ *
+ * @author zhaohuihua
+ * @version 20230327
+ */
+@Test
+public class VerifyToolsTest {
+
+    @Test
+    public void testJsonObjectString() {
+        {
+            boolean result = VerifyTools.isJsonObjectString("{}");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonObjectString("{  }");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonObjectString("{a:'11'}");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonObjectString("{aaa}");
+            Assert.assertFalse(result);
+        }
+    }
+
+    @Test
+    public void testJsonArrayString() {
+        {
+            boolean result = VerifyTools.isJsonArrayString("{}");
+            Assert.assertFalse(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonArrayString("[]");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonArrayString("[  ]");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonArrayString("[1,2,3]");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonArrayString("[ {a:'11'} ]");
+            Assert.assertTrue(result);
+        }
+    }
+}
-- 
Gitee


From f1437bf2ceef0d5d74820cbe8eea09f0c0cc27d1 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 2 Apr 2023 21:47:05 +0800
Subject: [PATCH 151/160] FormatTools

---
 .../gitee/qdbp/tools/utils/VerifyTools.java   |  32 ++-
 .../gitee/qdbp/tools/utils/FormatTools.java   | 182 ++++++++++++++++++
 2 files changed, 211 insertions(+), 3 deletions(-)
 create mode 100644 tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
index 050b04e..3baab94 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
@@ -277,7 +277,7 @@ public class VerifyTools {
 
     // 以{开头, 以}结尾, 且中间有冒号
     private static boolean checkJsonObjectString(String string) {
-        boolean passed = checkJsonString(string, '{', '}');
+        boolean passed = checkStringFeature(string, '{', '}');
         if (!passed) {
             return false;
         } else {
@@ -297,10 +297,36 @@ public class VerifyTools {
     }
 
     private static boolean checkJsonArrayString(String string) {
-        return checkJsonString(string, '[', ']');
+        return checkStringFeature(string, '[', ']');
     }
 
-    private static boolean checkJsonString(String value, char first, char last) {
+    /**
+     * 是不是XML字符串 (只是简单判断以&lt;开头且以&gt;结尾)
+     *
+     * @param value 判断目标
+     * @return 判断结果
+     * @since 5.5.13
+     */
+    public static boolean isXmlString(String value) {
+        return checkXmlString(value);
+    }
+
+    /**
+     * 是不是XML字符串 (只是简单判断以&lt;开头且以&gt;结尾)
+     *
+     * @param value 判断目标
+     * @return 判断结果
+     * @since 5.5.13
+     */
+    public static boolean isXmlString(Object value) {
+        return value instanceof String && checkXmlString((String) value);
+    }
+
+    private static boolean checkXmlString(String string) {
+        return checkStringFeature(string, '<', '>');
+    }
+
+    private static boolean checkStringFeature(String value, char first, char last) {
         if (value == null) {
             return false;
         }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
new file mode 100644
index 0000000..e4fc417
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
@@ -0,0 +1,182 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import com.gitee.qdbp.json.JsonFeature;
+import com.gitee.qdbp.json.JsonFeature.Serialization;
+
+/**
+ * 格式化工具
+ *
+ * @author zhaohuihua
+ * @version 20230330
+ * @since 5.5.13
+ */
+public class FormatTools {
+
+    /** 静态工具类私有构造方法 **/
+    private FormatTools() {
+    }
+
+    /** 格式化字符串, 支持json和xml的处理 */
+    public static String formatString(String string) {
+        if (VerifyTools.isXmlString(string)) {
+            return FormatTools.formatXml(string);
+        } else if (VerifyTools.isJsonObjectString(string)) {
+            Object object = JsonTools.parseAsMap(string);
+            return formatJson(object);
+        } else if (VerifyTools.isJsonArrayString(string)) {
+            Object object = JsonTools.parseAsMaps(string);
+            return formatJson(object);
+        } else {
+            return string;
+        }
+    }
+
+    /**
+     * 格式化Json字符串
+     *
+     * @param json 源字符串
+     * @return 格式化后的字符串
+     */
+    public static String formatJson(String json) {
+        if (VerifyTools.isBlank(json)) {
+            return json;
+        }
+        try {
+            if (VerifyTools.isJsonObjectString(json)) {
+                Object object = JsonTools.parseAsMap(json);
+                return formatJson(object);
+            } else if (VerifyTools.isJsonArrayString(json)) {
+                Object object = JsonTools.parseAsMaps(json);
+                return formatJson(object);
+            } else {
+                return json;
+            }
+        } catch (Exception ignore) {
+            return json;
+        }
+    }
+
+    /**
+     * 格式化Json字符串
+     *
+     * @param object 源对象
+     * @return 格式化后的字符串
+     */
+    public static String formatJson(Object object) {
+        Serialization serialization = JsonFeature.serialization()
+                .setQuoteFieldNames(false)
+                .setPrettyFormat(true)
+                .lockToUnmodifiable();
+        return JsonTools.toJsonString(object, serialization);
+    }
+
+    /**
+     * 格式化XML字符串
+     *
+     * @param xml 源字符串
+     * @return 格式化后的字符串
+     */
+    public static String formatXml(String xml) {
+        if (VerifyTools.isBlank(xml) || !VerifyTools.isXmlString(xml)) {
+            return xml;
+        }
+        Reader reader = new StringReader(xml);
+        try (StringWriter writer = new StringWriter()) {
+            formatXml(reader, writer);
+            return writer.getBuffer().toString();
+        } catch (IOException e) {
+            return xml;
+        }
+    }
+
+    public static void formatXml(Reader reader, Writer writer) throws IOException {
+        int indent = 0;
+        // <text>abcd</text> abcd是xxx标签中间的内容, 这里不能换行
+        boolean ignoreIndent = false;
+        boolean lastIsSpace = false;
+        StringBuilder buffer = new StringBuilder();
+        int index;
+        while ((index = reader.read()) >= 0) {
+            char c = (char) index;
+            if (c == '\r' || c == '\n') {
+                continue;
+            }
+            int bufSize = buffer.length();
+            char lastChar = bufSize == 0 ? '\0' : buffer.charAt(bufSize - 1);
+            if ((lastChar == '<' || lastChar == '>') && (c == ' ' || c == '\t' || c == '\f')) {
+                continue;
+            }
+            if (lastChar == '<') {
+                if (c == '/') {
+                    // </xxx> 减少缩进
+                    indent--;
+                    buffer.setLength(bufSize - 1); // 去掉最后的<
+                    if (!ignoreIndent) {
+                        appendIndent(buffer, indent); // 添加缩进
+                    }
+                    buffer.append('<').append(c);
+                } else if (c == '?') {
+                    // <?xml version="1.0" encoding="UTF-8"?>
+                    buffer.append(c);
+                } else {
+                    // <xxx> 增加缩进
+                    buffer.setLength(bufSize - 1); // 去掉最后的<
+                    appendIndent(buffer, indent); // 添加缩进
+                    buffer.append('<').append(c);
+                    indent++;
+                }
+                continue;
+            }
+            if (c == ' ' || c == '\t' || c == '\f') {
+                if (!lastIsSpace) {
+                    buffer.append(' ');
+                }
+                lastIsSpace = true;
+                continue;
+            }
+            lastIsSpace = false;
+            if (c == '<') {
+                if (lastChar == '>') {
+                    ignoreIndent = false;
+                    // 处理掉之前的内容
+                    buffer.append('\n');
+                    writer.write(buffer.toString());
+                    buffer.setLength(0);
+                } else {
+                    ignoreIndent = true;
+                }
+                // 重新开始
+                buffer.append(c);
+                // 下一步要看下一字符是什么
+                continue;
+            }
+            if (c == '>') {
+                buffer.append(c);
+                if (lastChar == '/') {
+                    indent--;
+                }
+            } else {
+                buffer.append(c);
+            }
+        }
+        if (buffer.length() > 0) {
+            writer.write(buffer.toString());
+            buffer.setLength(0);
+        }
+        writer.flush();
+    }
+
+    private static void appendIndent(StringBuilder buffer, int indent) {
+        if (indent <= 0) {
+            return;
+        }
+        for (int i = 0; i < indent; i++) {
+            buffer.append("  ");
+        }
+    }
+}
-- 
Gitee


From d5658d0f52793d4bed4caaba046041a896e1ca51 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 2 Apr 2023 21:49:52 +0800
Subject: [PATCH 152/160] 5.5.13

---
 README.md                            | 4 ++--
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 10 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index e354a10..57ca711 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.5.12</version>
+        <version>5.5.13</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.5.12</version>
+        <version>5.5.13</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index 7ec4ef6..67cf352 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.12</version>
+	<version>5.5.13</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index 16296ad..d5aee24 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.12</version>
+	<version>5.5.13</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.12</version>
+			<version>5.5.13</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index e09c511..c2bcea4 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.12</version>
+	<version>5.5.13</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index 97e668c..03bc3bc 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.12</version>
+		<version>5.5.13</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index b901ebc..f12939a 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.12</version>
+		<version>5.5.13</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index 24a9865..6216551 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.12</version>
+		<version>5.5.13</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index 04b6278..e3012a4 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.12</version>
+		<version>5.5.13</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index 22e4b55..471f04e 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.12</version>
+		<version>5.5.13</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index c51ae0b..afd5117 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.12</version>
+	<version>5.5.13</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 2044f39a6d00348753413d53337ea21ecb6547ad Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 2 Apr 2023 21:50:42 +0800
Subject: [PATCH 153/160] FormatTools

---
 .../com/gitee/qdbp/tools/utils/FormatTools.java   | 15 ---------------
 1 file changed, 15 deletions(-)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
index e4fc417..dcef547 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
@@ -21,21 +21,6 @@ public class FormatTools {
     private FormatTools() {
     }
 
-    /** 格式化字符串, 支持json和xml的处理 */
-    public static String formatString(String string) {
-        if (VerifyTools.isXmlString(string)) {
-            return FormatTools.formatXml(string);
-        } else if (VerifyTools.isJsonObjectString(string)) {
-            Object object = JsonTools.parseAsMap(string);
-            return formatJson(object);
-        } else if (VerifyTools.isJsonArrayString(string)) {
-            Object object = JsonTools.parseAsMaps(string);
-            return formatJson(object);
-        } else {
-            return string;
-        }
-    }
-
     /**
      * 格式化Json字符串
      *
-- 
Gitee


From e943af3b2a25366c1da4723343547b5a0eb94462 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 2 Apr 2023 21:56:58 +0800
Subject: [PATCH 154/160] =?UTF-8?q?FormatTools=E7=A7=BB=E8=87=B3qdbp-able?=
 =?UTF-8?q?=E5=B7=A5=E7=A8=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gitee/qdbp/tools/utils/FormatTools.java   | 30 +++++++++++++++++++
 1 file changed, 30 insertions(+)
 rename {tools => able}/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java (85%)

diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
similarity index 85%
rename from tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
rename to able/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
index dcef547..294ab95 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
@@ -21,11 +21,32 @@ public class FormatTools {
     private FormatTools() {
     }
 
+    /**
+     * 格式化字符串, 支持json和xml的处理
+     * @param string 源字符串
+     * @return 格式化后的字符串
+     * @since 5.5.14
+     */
+    public static String formatString(String string) {
+        if (VerifyTools.isXmlString(string)) {
+            return FormatTools.formatXml(string);
+        } else if (VerifyTools.isJsonObjectString(string)) {
+            Object object = JsonTools.parseAsMap(string);
+            return formatJson(object);
+        } else if (VerifyTools.isJsonArrayString(string)) {
+            Object object = JsonTools.parseAsMaps(string);
+            return formatJson(object);
+        } else {
+            return string;
+        }
+    }
+
     /**
      * 格式化Json字符串
      *
      * @param json 源字符串
      * @return 格式化后的字符串
+     * @since 5.5.13
      */
     public static String formatJson(String json) {
         if (VerifyTools.isBlank(json)) {
@@ -51,6 +72,7 @@ public class FormatTools {
      *
      * @param object 源对象
      * @return 格式化后的字符串
+     * @since 5.5.13
      */
     public static String formatJson(Object object) {
         Serialization serialization = JsonFeature.serialization()
@@ -65,6 +87,7 @@ public class FormatTools {
      *
      * @param xml 源字符串
      * @return 格式化后的字符串
+     * @since 5.5.13
      */
     public static String formatXml(String xml) {
         if (VerifyTools.isBlank(xml) || !VerifyTools.isXmlString(xml)) {
@@ -79,6 +102,13 @@ public class FormatTools {
         }
     }
 
+    /**
+     * 格式化XML字符串
+     *
+     * @param reader 输入
+     * @param writer 输出
+     * @since 5.5.13
+     */
     public static void formatXml(Reader reader, Writer writer) throws IOException {
         int indent = 0;
         // <text>abcd</text> abcd是xxx标签中间的内容, 这里不能换行
-- 
Gitee


From e10d27e75f9a3c907df6e1ccb080b2ecca1b2ce2 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 2 Apr 2023 21:59:40 +0800
Subject: [PATCH 155/160] qdbp-5.5.14

---
 README.md                            | 4 ++--
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 10 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index 57ca711..ebd181e 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.5.13</version>
+        <version>5.5.14</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.5.13</version>
+        <version>5.5.14</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index 67cf352..2cd5ffb 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.13</version>
+	<version>5.5.14</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index d5aee24..f64cfa4 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.13</version>
+	<version>5.5.14</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.13</version>
+			<version>5.5.14</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index c2bcea4..8b0b23f 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.13</version>
+	<version>5.5.14</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index 03bc3bc..4a54e01 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.13</version>
+		<version>5.5.14</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index f12939a..59fe835 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.13</version>
+		<version>5.5.14</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index 6216551..e629a06 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.13</version>
+		<version>5.5.14</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index e3012a4..c860357 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.13</version>
+		<version>5.5.14</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index 471f04e..8a94ca3 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.13</version>
+		<version>5.5.14</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index afd5117..ea352b1 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.13</version>
+	<version>5.5.14</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee


From 0ce20da19974351d174da1e41a621a18e554172a Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 21 May 2023 11:15:59 +0800
Subject: [PATCH 156/160] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
index bf901c5..a6f2f0d 100644
--- a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
@@ -40,14 +40,17 @@ public abstract class JsonServiceForBase implements JsonService {
     public String toJsonString(Object object) {
         return toJsonString(object, serializationFeature);
     }
+
     @Override
     public Map<String, Object> parseAsMap(String jsonString) {
         return parseAsMap(jsonString, deserializationFeature);
     }
+
     @Override
     public <T> List<T> parseAsObjects(String jsonString, Class<T> clazz) {
         return parseAsObjects(jsonString, clazz, deserializationFeature);
     }
+
     @Override
     public List<Map<String, Object>> parseAsMaps(String jsonString) {
         return parseAsMaps(jsonString, deserializationFeature);
-- 
Gitee


From d93decc8ba8c19d71c647c07a68d157d892a8cae Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 21 May 2023 11:17:00 +0800
Subject: [PATCH 157/160] =?UTF-8?q?=E5=8F=96=E5=80=BC=E9=80=BB=E8=BE=91?=
 =?UTF-8?q?=E9=94=99=E8=AF=AFBUG=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
index d38be4f..9b3f925 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
@@ -1048,7 +1048,7 @@ public abstract class PathTools {
             }
             if (!endsWithSeparator(buffer) && !startsWithSeparator(path)) {
                 buffer.append(SLASH).append(path);
-            } else if (endsWithSeparator(folder) && startsWithSeparator(path)) {
+            } else if (endsWithSeparator(buffer) && startsWithSeparator(path)) {
                 buffer.append(path.substring(1));
             } else {
                 buffer.append(path);
-- 
Gitee


From 7bd80d8c2d56d7cee5fefd357ca2ad5e6dbe13e0 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 21 May 2023 11:57:06 +0800
Subject: [PATCH 158/160] JsonTools.toSimpleString

---
 .../main/java/com/gitee/qdbp/tools/utils/JsonTools.java   | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
index 69c905c..7c9de2a 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
@@ -138,7 +138,13 @@ public class JsonTools extends JsonMaps {
     private static final JsonFeature.Serialization UNQUOTE_FIELD_NAME_FEATURE =
             JsonFeature.serialization().setQuoteFieldNames(false).lockToUnmodifiable();
 
-    /** 转换为日志字符串, json的key不会有引号, 可以稍微简化一下结构 **/
+    /** 转换为简化的字符串, json的key不会有引号, 可以稍微简化一下结构 **/
+    // 目前与toLogString相同, 但以后toLogString有可能会更加简化 (因为log不用考虑反序列化)
+    public static String toSimpleString(Object object) {
+        return findDefaultJsonService().toJsonString(object, UNQUOTE_FIELD_NAME_FEATURE);
+    }
+
+    /** 转换为日志字符串, 有可能无法反序列化, json的key不会有引号, 可以稍微简化一下结构 **/
     public static String toLogString(Object object) {
         return findDefaultJsonService().toJsonString(object, UNQUOTE_FIELD_NAME_FEATURE);
     }
-- 
Gitee


From 4e175837a1e13cbf08a3e1ad229c395cd8f9b873 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 21 May 2023 23:05:16 +0800
Subject: [PATCH 159/160] =?UTF-8?q?Json=E7=AE=80=E5=8C=96=E9=80=BB?=
 =?UTF-8?q?=E8=BE=91=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/gitee/qdbp/tools/utils/JsonTools.java    | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
index 7c9de2a..ca02ac1 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
@@ -135,18 +135,21 @@ public class JsonTools extends JsonMaps {
         return findDefaultJsonService().toJsonString(object, feature);
     }
 
-    private static final JsonFeature.Serialization UNQUOTE_FIELD_NAME_FEATURE =
-            JsonFeature.serialization().setQuoteFieldNames(false).lockToUnmodifiable();
+    private static final JsonFeature.Serialization SIMPLIFY_FEATURE = JsonFeature.serialization()
+            .setQuoteFieldNames(false)
+            .setWriteFieldNullValue(false)
+            .setWriteMapNullValue(false)
+            .lockToUnmodifiable();
 
     /** 转换为简化的字符串, json的key不会有引号, 可以稍微简化一下结构 **/
     // 目前与toLogString相同, 但以后toLogString有可能会更加简化 (因为log不用考虑反序列化)
     public static String toSimpleString(Object object) {
-        return findDefaultJsonService().toJsonString(object, UNQUOTE_FIELD_NAME_FEATURE);
+        return findDefaultJsonService().toJsonString(object, SIMPLIFY_FEATURE);
     }
 
     /** 转换为日志字符串, 有可能无法反序列化, json的key不会有引号, 可以稍微简化一下结构 **/
     public static String toLogString(Object object) {
-        return findDefaultJsonService().toJsonString(object, UNQUOTE_FIELD_NAME_FEATURE);
+        return findDefaultJsonService().toJsonString(object, SIMPLIFY_FEATURE);
     }
 
     /** 解析Json字符串对象 **/
-- 
Gitee


From 620ff14895ddef16b38a8c6170c89b200eb88be0 Mon Sep 17 00:00:00 2001
From: zhaohuihua <zhaohuihua@126.com>
Date: Sun, 21 May 2023 23:10:03 +0800
Subject: [PATCH 160/160] 5.5.15

---
 README.md                            | 4 ++--
 able/pom.xml                         | 2 +-
 json/pom.xml                         | 4 ++--
 test/pom.xml                         | 2 +-
 test/qdbp-json-test-base/pom.xml     | 2 +-
 test/qdbp-json-test-fastjson/pom.xml | 2 +-
 test/qdbp-json-test-gson/pom.xml     | 2 +-
 test/qdbp-json-test-jackson/pom.xml  | 2 +-
 test/qdbp-tools-test-jdk7/pom.xml    | 2 +-
 tools/pom.xml                        | 2 +-
 10 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index ebd181e..bbd0d0a 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.5.14</version>
+        <version>5.5.15</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.5.14</version>
+        <version>5.5.15</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index 2cd5ffb..c80e177 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -9,7 +9,7 @@
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.5.14</version>
+	<version>5.5.15</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/json/pom.xml b/json/pom.xml
index f64cfa4..6ef795c 100644
--- a/json/pom.xml
+++ b/json/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.14</version>
+	<version>5.5.15</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json library</description>
 
@@ -24,7 +24,7 @@
 		<dependency>
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
-			<version>5.5.14</version>
+			<version>5.5.15</version>
 		</dependency>
 
 		<dependency>
diff --git a/test/pom.xml b/test/pom.xml
index 8b0b23f..5941e7e 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-json-test</artifactId>
 	<packaging>pom</packaging>
-	<version>5.5.14</version>
+	<version>5.5.15</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp json test</description>
 
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
index 4a54e01..7ca5221 100644
--- a/test/qdbp-json-test-base/pom.xml
+++ b/test/qdbp-json-test-base/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.14</version>
+		<version>5.5.15</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-base</artifactId>
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
index 59fe835..8aa4fcc 100644
--- a/test/qdbp-json-test-fastjson/pom.xml
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.14</version>
+		<version>5.5.15</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-fastjson</artifactId>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
index e629a06..55df0db 100644
--- a/test/qdbp-json-test-gson/pom.xml
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.14</version>
+		<version>5.5.15</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-gson</artifactId>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
index c860357..3b7215f 100644
--- a/test/qdbp-json-test-jackson/pom.xml
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.14</version>
+		<version>5.5.15</version>
 	</parent>
 
 	<artifactId>qdbp-json-test-jackson</artifactId>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
index 8a94ca3..ecebe93 100644
--- a/test/qdbp-tools-test-jdk7/pom.xml
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-json-test</artifactId>
-		<version>5.5.14</version>
+		<version>5.5.15</version>
 	</parent>
 
 	<artifactId>qdbp-tools-test-jdk7</artifactId>
diff --git a/tools/pom.xml b/tools/pom.xml
index ea352b1..057e332 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -10,7 +10,7 @@
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.5.14</version>
+	<version>5.5.15</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
-- 
Gitee