From 850c766213344d57696acfdc3defd36789beed70 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 8 Dec 2020 04:54:44 +0800 Subject: [PATCH] add FoundWord --- CHANGELOG.md | 3 +- .../cn/hutool/core/lang/DefaultSegment.java | 34 +++++ .../java/cn/hutool/core/lang/Segment.java | 41 ++++++ .../java/cn/hutool/core/text/StrBuilder.java | 8 +- .../java/cn/hutool/core/util/ObjectUtil.java | 15 ++- .../java/cn/hutool/core/util/StrUtil.java | 106 ++++++++-------- .../main/java/cn/hutool/dfa/FoundWord.java | 76 ++++++----- .../java/cn/hutool/dfa/SensitiveUtil.java | 118 +++++++++++++++--- .../src/main/java/cn/hutool/dfa/WordTree.java | 93 +++++++++++--- .../cn/hutool/dfa/{test => }/DfaTest.java | 36 +++--- .../dfa/{test => }/SensitiveUtilTest.java | 3 +- .../main/java/cn/hutool/json/JSONUtil.java | 4 +- 12 files changed, 389 insertions(+), 148 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/DefaultSegment.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Segment.java rename hutool-dfa/src/test/java/cn/hutool/dfa/{test => }/DfaTest.java (66%) rename hutool-dfa/src/test/java/cn/hutool/dfa/{test => }/SensitiveUtilTest.java (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6006c1758..97a428e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------------------------------------------- -# 5.5.3 (2020-12-07) +# 5.5.3 (2020-12-08) ### 新特性 * 【core 】 IdcardUtil增加行政区划83(issue#1277@Github) @@ -12,6 +12,7 @@ * 【db 】 Db增加使用sql的page方法(issue#247@Gitee) * 【cache 】 CacheObj的isExpired()逻辑修改(issue#1295@Github) * 【json 】 JSONStrFormater改为JSONStrFormatter +* 【dfa 】 增加FoundWord(pr#1290@Github) ### Bug修复 * 【cache 】 修复Cache中get重复misCount计数问题(issue#1281@Github) diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/DefaultSegment.java b/hutool-core/src/main/java/cn/hutool/core/lang/DefaultSegment.java new file mode 100644 index 000000000..9337ec9cd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/DefaultSegment.java @@ -0,0 +1,34 @@ +package cn.hutool.core.lang; + +/** + * 片段默认实现 + * + * @param 数字类型,用于表示位置index + * @author looly + * @since 5.5.3 + */ +public class DefaultSegment implements Segment { + + protected T startIndex; + protected T endIndex; + + /** + * 构造 + * @param startIndex 起始位置 + * @param endIndex 结束位置 + */ + public DefaultSegment(T startIndex, T endIndex) { + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + @Override + public T getStartIndex() { + return this.startIndex; + } + + @Override + public T getEndIndex() { + return this.endIndex; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Segment.java b/hutool-core/src/main/java/cn/hutool/core/lang/Segment.java new file mode 100644 index 000000000..869386f46 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Segment.java @@ -0,0 +1,41 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.NumberUtil; + +import java.lang.reflect.Type; + +/** + * 片段表示,用于表示文本、集合等数据结构的一个区间。 + * @param 数字类型,用于表示位置index + * + * @author looly + * @since 5.5.3 + */ +public interface Segment { + + /** + * 获取起始位置 + * + * @return 起始位置 + */ + T getStartIndex(); + + /** + * 获取结束位置 + * + * @return 结束位置 + */ + T getEndIndex(); + + /** + * 片段长度,默认计算方法为abs({@link #getEndIndex()} - {@link #getEndIndex()}) + * + * @return 片段长度 + */ + default T length(){ + final T start = Assert.notNull(getStartIndex(), "Start index must be not null!"); + final T end = Assert.notNull(getEndIndex(), "End index must be not null!"); + return Convert.convert((Type) start.getClass(), NumberUtil.sub(end, start).abs()); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java index dae62696b..7fb068a01 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java @@ -523,7 +523,8 @@ public class StrBuilder implements CharSequence, Appendable, Serializable { * @param minimumCapacity 最小容量 */ private void ensureCapacity(int minimumCapacity) { - if (minimumCapacity > value.length) { + // overflow-conscious code + if (minimumCapacity - value.length < 0) { expandCapacity(minimumCapacity); } } @@ -535,8 +536,9 @@ public class StrBuilder implements CharSequence, Appendable, Serializable { * @param minimumCapacity 需要扩展的最小容量 */ private void expandCapacity(int minimumCapacity) { - int newCapacity = value.length * 2 + 2; - if (newCapacity < minimumCapacity) { + int newCapacity = (value.length << 1) + 2; + // overflow-conscious code + if (newCapacity - minimumCapacity < 0) { newCapacity = minimumCapacity; } if (newCapacity < 0) { diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java index 8ae304433..0cd995851 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java @@ -383,7 +383,7 @@ public class ObjectUtil { * 克隆对象
* 如果对象实现Cloneable接口,调用其clone方法
* 如果实现Serializable接口,执行深度克隆
- * 否则返回null + * 否则返回{@code null} * * @param 对象类型 * @param obj 被克隆对象 @@ -606,11 +606,24 @@ public class ObjectUtil { return ArrayUtil.emptyCount(objs); } + /** + * 是否存在{@code null}对象,通过{@link ObjectUtil#isNull(Object)} 判断元素 + * + * @param objs 被检查对象 + * @return 是否存在 + * @since 5.5.3 + * @see ArrayUtil#hasNull(Object[]) + */ + public static boolean hasNull(Object... objs) { + return ArrayUtil.hasNull(objs); + } + /** * 是否存在{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 * * @param objs 被检查对象 * @return 是否存在 + * @see ArrayUtil#hasEmpty(Object...) */ public static boolean hasEmpty(Object... objs) { return ArrayUtil.hasEmpty(objs); diff --git a/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java index cb4cb4b85..868aeca3f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java @@ -105,7 +105,7 @@ public class StrUtil { public static final char C_COLON = CharUtil.COLON; /** - * 字符常量:艾特 '@' + * 字符常量:艾特 {@code '@'} */ public static final char C_AT = CharUtil.AT; @@ -210,7 +210,7 @@ public class StrUtil { public static final String COLON = ":"; /** - * 字符串常量:艾特 "@" + * 字符串常量:艾特 {@code "@"} */ public static final String AT = "@"; @@ -246,7 +246,7 @@ public class StrUtil { public static final String HTML_GT = ">"; /** - * 字符串常量:空 JSON "{}" + * 字符串常量:空 JSON {@code "{}"} */ public static final String EMPTY_JSON = "{}"; @@ -542,7 +542,7 @@ public class StrUtil { } /** - * 如果字符串是 null,则返回指定默认字符串,否则返回字符串本身。 + * 如果字符串是 {@code null},则返回指定默认字符串,否则返回字符串本身。 * *
 	 * nullToDefault(null, "default")  = "default"
@@ -560,7 +560,7 @@ public class StrUtil {
 	}
 
 	/**
-	 * 如果字符串是null或者"",则返回指定默认字符串,否则返回字符串本身。
+	 * 如果字符串是{@code null}或者"",则返回指定默认字符串,否则返回字符串本身。
 	 *
 	 * 
 	 * emptyToDefault(null, "default")  = "default"
@@ -579,7 +579,7 @@ public class StrUtil {
 	}
 
 	/**
-	 * 如果字符串是null或者""或者空白,则返回指定默认字符串,否则返回字符串本身。
+	 * 如果字符串是{@code null}或者""或者空白,则返回指定默认字符串,否则返回字符串本身。
 	 *
 	 * 
 	 * emptyToDefault(null, "default")  = "default"
@@ -598,7 +598,7 @@ public class StrUtil {
 	}
 
 	/**
-	 * 当给定字符串为空字符串时,转换为null
+	 * 当给定字符串为空字符串时,转换为{@code null}
 	 *
 	 * @param str 被转换的字符串
 	 * @return 转换后的字符串
@@ -774,10 +774,10 @@ public class StrUtil {
 	// ------------------------------------------------------------------------ Trim
 
 	/**
-	 * 除去字符串头尾部的空白,如果字符串是null,依然返回null。
+	 * 除去字符串头尾部的空白,如果字符串是{@code null},依然返回{@code null}。
 	 *
 	 * 

- * 注意,和String.trim不同,此方法使用NumberUtil.isBlankChar 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 * *

 	 * trim(null)          = null
@@ -788,7 +788,7 @@ public class StrUtil {
 	 * 
* * @param str 要处理的字符串 - * @return 除去头尾空白的字符串,如果原字串为null,则返回null + * @return 除去头尾空白的字符串,如果原字串为{@code null},则返回{@code null} */ public static String trim(CharSequence str) { return (null == str) ? null : trim(str, 0); @@ -813,7 +813,7 @@ public class StrUtil { } /** - * 除去字符串头尾部的空白,如果字符串是{@code null},返回""。 + * 除去字符串头尾部的空白,如果字符串是{@code null},返回{@code ""}。 * *
 	 * StrUtil.trimToEmpty(null)          = ""
@@ -852,10 +852,10 @@ public class StrUtil {
 	}
 
 	/**
-	 * 除去字符串头部的空白,如果字符串是null,则返回null。
+	 * 除去字符串头部的空白,如果字符串是{@code null},则返回{@code null}。
 	 *
 	 * 

- * 注意,和String.trim不同,此方法使用CharUtil.isBlankChar 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 * *

 	 * trimStart(null)         = null
@@ -867,17 +867,17 @@ public class StrUtil {
 	 * 
* * @param str 要处理的字符串 - * @return 除去空白的字符串,如果原字串为null或结果字符串为"",则返回 null + * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} */ public static String trimStart(CharSequence str) { return trim(str, -1); } /** - * 除去字符串尾部的空白,如果字符串是null,则返回null。 + * 除去字符串尾部的空白,如果字符串是{@code null},则返回{@code null}。 * *

- * 注意,和String.trim不同,此方法使用CharUtil.isBlankChar 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * 注意,和{@link String#trim()}不同,此方法使用{@link CharUtil#isBlankChar(char)} 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 * *

 	 * trimEnd(null)       = null
@@ -889,47 +889,45 @@ public class StrUtil {
 	 * 
* * @param str 要处理的字符串 - * @return 除去空白的字符串,如果原字串为null或结果字符串为"",则返回 null + * @return 除去空白的字符串,如果原字串为{@code null}或结果字符串为{@code ""},则返回 {@code null} */ public static String trimEnd(CharSequence str) { return trim(str, 1); } /** - * 除去字符串头尾部的空白符,如果字符串是null,依然返回null。 + * 除去字符串头尾部的空白符,如果字符串是{@code null},依然返回{@code null}。 * * @param str 要处理的字符串 - * @param mode -1表示trimStart,0表示trim全部, 1表示trimEnd - * @return 除去指定字符后的的字符串,如果原字串为null,则返回null + * @param mode {@code -1}表示trimStart,{@code 0}表示trim全部, {@code 1}表示trimEnd + * @return 除去指定字符后的的字符串,如果原字串为{@code null},则返回{@code null} */ public static String trim(CharSequence str, int mode) { + String result; if (str == null) { - return null; - } - - int length = str.length(); - int start = 0; - int end = length; - - // 扫描字符串头部 - if (mode <= 0) { - while ((start < end) && (CharUtil.isBlankChar(str.charAt(start)))) { - start++; + result = null; + } else { + int length = str.length(); + int start = 0; + int end = length;// 扫描字符串头部 + if (mode <= 0) { + while ((start < end) && (CharUtil.isBlankChar(str.charAt(start)))) { + start++; + } + }// 扫描字符串尾部 + if (mode >= 0) { + while ((start < end) && (CharUtil.isBlankChar(str.charAt(end - 1)))) { + end--; + } + } + if ((start > 0) || (end < length)) { + result = str.toString().substring(start, end); + } else { + result = str.toString(); } } - // 扫描字符串尾部 - if (mode >= 0) { - while ((start < end) && (CharUtil.isBlankChar(str.charAt(end - 1)))) { - end--; - } - } - - if ((start > 0) || (end < length)) { - return str.toString().substring(start, end); - } - - return str.toString(); + return result; } /** @@ -1251,7 +1249,7 @@ public class StrUtil { } /** - * 是否包含特定字符,忽略大小写,如果给定两个参数都为null,返回true + * 是否包含特定字符,忽略大小写,如果给定两个参数都为{@code null},返回true * * @param str 被检测字符串 * @param testStr 被测试是否包含的字符串 @@ -1971,7 +1969,7 @@ public class StrUtil { * abcdefgh 2 3 =》 c
* abcdefgh 2 -3 =》 cde
* - * @param str String + * @param str String * @param fromIndexInclude 开始的index(包括) * @param toIndexExclude 结束的index(不包括) * @return 字串 @@ -2094,7 +2092,7 @@ public class StrUtil { /** * 切割指定位置之前部分的字符串 * - * @param string 字符串 + * @param string 字符串 * @param toIndexExclude 切割到的位置(不包括) * @return 切割后的剩余的前半部分字符串 */ @@ -2406,12 +2404,12 @@ public class StrUtil { final List result = new LinkedList<>(); final String[] split = split(str, prefix); - if(prefix.equals(suffix)){ + if (prefix.equals(suffix)) { // 前后缀字符相同,单独处理 for (int i = 1, length = split.length - 1; i < length; i += 2) { result.add(split[i]); } - } else{ + } else { int suffixIndex; for (String fragment : split) { suffixIndex = fragment.indexOf(suffix.toString()); @@ -2441,7 +2439,7 @@ public class StrUtil { * StrUtil.subBetweenAll("#hello# world#!", "#"); = ["hello"] *
* - * @param str 被切割的字符串 + * @param str 被切割的字符串 * @param prefixAndSuffix 截取开始和结束的字符串标识 * @return 截取后的字符串 * @author gotanks @@ -2620,7 +2618,7 @@ public class StrUtil { * * @param str1 要比较的字符串1 * @param str2 要比较的字符串2 - * @return 如果两个字符串相同,或者都是null,则返回true + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} */ public static boolean equals(CharSequence str1, CharSequence str2) { return equals(str1, str2, false); @@ -2639,7 +2637,7 @@ public class StrUtil { * * @param str1 要比较的字符串1 * @param str2 要比较的字符串2 - * @return 如果两个字符串相同,或者都是null,则返回true + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} */ public static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) { return equals(str1, str2, true); @@ -2651,7 +2649,7 @@ public class StrUtil { * @param str1 要比较的字符串1 * @param str2 要比较的字符串2 * @param ignoreCase 是否忽略大小写 - * @return 如果两个字符串相同,或者都是null,则返回true + * @return 如果两个字符串相同,或者都是{@code null},则返回{@code true} * @since 3.2.0 */ public static boolean equals(CharSequence str1, CharSequence str2, boolean ignoreCase) { @@ -3431,7 +3429,7 @@ public class StrUtil { * StrUtil.padAfter("123", 2, '0');//"23" *
* - * @param str 字符串,如果为null,直接返回null + * @param str 字符串,如果为{@code null},直接返回null * @param minLength 最小长度 * @param padChar 补充的字符 * @return 补充后的字符串 @@ -3459,7 +3457,7 @@ public class StrUtil { * StrUtil.padAfter("123", 2, "ABC");//"23" *
* - * @param str 字符串,如果为null,直接返回null + * @param str 字符串,如果为{@code null},直接返回null * @param minLength 最小长度 * @param padStr 补充的字符 * @return 补充后的字符串 diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java b/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java index e57a0f9ec..6d68a6bd4 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/FoundWord.java @@ -1,49 +1,61 @@ package cn.hutool.dfa; -/** - * @author 肖海斌 - *

- * 匹配到的敏感词,包含敏感词,text中匹配敏感词的内容,以及匹配内容在text中的下标, - * 下标可以用来做敏感词的进一步处理,如果替换成** - */ -public class FoundWord { - /** - * 生效的敏感词 - */ - private String word; - /** - * 敏感词匹配到的内容 - */ - private String foundWord; - /** - * 匹配内容在待分析字符串中的开始位置 - */ - private int startIndex; - /** - * 匹配内容在待分析字符串中的结束位置 - */ - private int endIndex; +import cn.hutool.core.lang.DefaultSegment; - public FoundWord(String word, String foundWord, int start, int end) { +/** + *

+ * 匹配到的单词,包含单词,text中匹配单词的内容,以及匹配内容在text中的下标, + * 下标可以用来做单词的进一步处理,如果替换成** + * + * @author 肖海斌 + */ +public class FoundWord extends DefaultSegment { + /** + * 生效的单词,即单词树中的词 + */ + private final String word; + /** + * 单词匹配到的内容,即文中的单词 + */ + private final String foundWord; + + /** + * 构造 + * + * @param word 生效的单词,即单词树中的词 + * @param foundWord 单词匹配到的内容,即文中的单词 + * @param startIndex 起始位置(包含) + * @param endIndex 结束位置(包含) + */ + public FoundWord(String word, String foundWord, int startIndex, int endIndex) { + super(startIndex, endIndex); this.word = word; this.foundWord = foundWord; - this.startIndex = start; - this.endIndex = end; } + /** + * 获取生效的单词,即单词树中的词 + * + * @return 生效的单词 + */ public String getWord() { return word; } + /** + * 获取单词匹配到的内容,即文中的单词 + * @return 单词匹配到的内容 + */ public String getFoundWord() { return foundWord; } - public int getStartIndex() { - return startIndex; - } - - public int getEndIndex() { - return endIndex; + /** + * 默认的,只输出匹配到的关键字 + * @return 匹配到的关键字 + */ + @Override + public String toString() { + return this.foundWord; } } diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java index 6a396d49d..a7f0e05d5 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java @@ -1,6 +1,6 @@ package cn.hutool.dfa; -import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Filter; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.StrUtil; @@ -25,7 +25,7 @@ public final class SensitiveUtil { * @return 是否已经被初始化 */ public static boolean isInited() { - return !sensitiveTree.isEmpty(); + return false == sensitiveTree.isEmpty(); } /** @@ -117,19 +117,44 @@ public final class SensitiveUtil { * * @param text 文本 * @return 敏感词 + * @deprecated 请使用 {@link #getFoundFirstSensitive(String)} */ - public static FoundWord getFindedFirstSensitive(String text) { + @Deprecated + public static String getFindedFirstSensitive(String text) { return sensitiveTree.match(text); } + /** + * 查找敏感词,返回找到的第一个敏感词 + * + * @param text 文本 + * @return 敏感词 + * @since 5.5.3 + */ + public static FoundWord getFoundFirstSensitive(String text) { + return sensitiveTree.matchWord(text); + } + + /** + * 查找敏感词,返回找到的第一个敏感词 + * + * @param obj bean,会被转为JSON字符串 + * @return 敏感词 + * @deprecated 请使用 {@link #getFoundFirstSensitive(Object)} + */ + @Deprecated + public static String getFindedFirstSensitive(Object obj) { + return sensitiveTree.match(JSONUtil.toJsonStr(obj)); + } + /** * 查找敏感词,返回找到的第一个敏感词 * * @param obj bean,会被转为JSON字符串 * @return 敏感词 */ - public static FoundWord getFindedFirstSensitive(Object obj) { - return sensitiveTree.match(JSONUtil.toJsonStr(obj)); + public static FoundWord getFoundFirstSensitive(Object obj) { + return sensitiveTree.matchWord(JSONUtil.toJsonStr(obj)); } /** @@ -137,11 +162,40 @@ public final class SensitiveUtil { * * @param text 文本 * @return 敏感词 + * @deprecated 请使用 {@link #getFoundAllSensitive(String)} */ - public static List getFindedAllSensitive(String text) { + @Deprecated + public static List getFindedAllSensitive(String text) { return sensitiveTree.matchAll(text); } + /** + * 查找敏感词,返回找到的所有敏感词 + * + * @param text 文本 + * @return 敏感词 + * @since 5.5.3 + */ + public static List getFoundAllSensitive(String text) { + return sensitiveTree.matchAllWords(text); + } + + /** + * 查找敏感词,返回找到的所有敏感词
+ * 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
+ * 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * + * @param text 文本 + * @param isDensityMatch 是否使用密集匹配原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @return 敏感词 + * @deprecated 请使用 {@link #getFoundAllSensitive(String, boolean, boolean)} + */ + @Deprecated + public static List getFindedAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) { + return sensitiveTree.matchAll(text, -1, isDensityMatch, isGreedMatch); + } + /** * 查找敏感词,返回找到的所有敏感词
* 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
@@ -152,8 +206,8 @@ public final class SensitiveUtil { * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 * @return 敏感词 */ - public static List getFindedAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) { - return sensitiveTree.matchAll(text, -1, isDensityMatch, isGreedMatch); + public static List getFoundAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch) { + return sensitiveTree.matchAllWords(text, -1, isDensityMatch, isGreedMatch); } /** @@ -161,11 +215,24 @@ public final class SensitiveUtil { * * @param bean 对象,会被转为JSON * @return 敏感词 + * @deprecated 请使用 {@link #getFoundAllSensitive(Object)} */ - public static List getFindedAllSensitive(Object bean) { + @Deprecated + public static List getFindedAllSensitive(Object bean) { return sensitiveTree.matchAll(JSONUtil.toJsonStr(bean)); } + /** + * 查找敏感词,返回找到的所有敏感词 + * + * @param bean 对象,会被转为JSON + * @return 敏感词 + * @since 5.5.3 + */ + public static List getFoundAllSensitive(Object bean) { + return sensitiveTree.matchAllWords(JSONUtil.toJsonStr(bean)); + } + /** * 查找敏感词,返回找到的所有敏感词
* 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
@@ -175,9 +242,26 @@ public final class SensitiveUtil { * @param isDensityMatch 是否使用密集匹配原则 * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 * @return 敏感词 + * @deprecated 请使用 {@link #getFoundAllSensitive(Object, boolean, boolean)} */ - public static List getFindedAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) { - return getFindedAllSensitive(JSONUtil.toJsonStr(bean), isDensityMatch, isGreedMatch); + @Deprecated + public static List getFindedAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) { + return sensitiveTree.matchAll(JSONUtil.toJsonStr(bean), -1, isDensityMatch, isGreedMatch); + } + + /** + * 查找敏感词,返回找到的所有敏感词
+ * 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
+ * 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * + * @param bean 对象,会被转为JSON + * @param isDensityMatch 是否使用密集匹配原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @return 敏感词 + * @since 5.5.3 + */ + public static List getFoundAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch) { + return getFoundAllSensitive(JSONUtil.toJsonStr(bean), isDensityMatch, isGreedMatch); } /** @@ -191,23 +275,27 @@ public final class SensitiveUtil { */ public static T sensitiveFilter(T bean, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) { String jsonText = JSONUtil.toJsonStr(bean); - Class c = (Class) bean.getClass(); + @SuppressWarnings("unchecked") + final Class c = (Class) bean.getClass(); return JSONUtil.toBean(sensitiveFilter(jsonText, isGreedMatch, sensitiveProcessor), c); } /** + * 处理过滤文本中的敏感词,默认替换成* + * * @param text 文本 * @param isGreedMatch 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] * @param sensitiveProcessor 敏感词处理器,默认按匹配内容的字符数替换成* * @return 敏感词过滤处理后的文本 */ public static String sensitiveFilter(String text, boolean isGreedMatch, SensitiveProcessor sensitiveProcessor) { - if (null == text || text.trim().equals("")) { + if (StrUtil.isEmpty(text)) { return text; } + //敏感词过滤场景下,不需要密集匹配 - List foundWordList = getFindedAllSensitive(text, false, isGreedMatch); - if (CollectionUtil.isEmpty(foundWordList)) { + List foundWordList = getFoundAllSensitive(text, false, isGreedMatch); + if (CollUtil.isEmpty(foundWordList)) { return text; } sensitiveProcessor = sensitiveProcessor == null ? new SensitiveProcessor() { diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java b/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java index 4e05657b9..13e84f589 100644 --- a/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java @@ -1,11 +1,17 @@ package cn.hutool.dfa; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Filter; import cn.hutool.core.text.StrBuilder; import cn.hutool.core.util.StrUtil; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * DFA(Deterministic Finite Automaton 确定有穷自动机) @@ -26,7 +32,7 @@ public class WordTree extends HashMap { private static final long serialVersionUID = -4646423269465809276L; /** - * 敏感词字符末尾标识,用于标识单词末尾字符 + * 单词字符末尾标识,用于标识单词末尾字符 */ private final Set endCharacterSet = new HashSet<>(); /** @@ -62,26 +68,30 @@ public class WordTree extends HashMap { * 增加一组单词 * * @param words 单词集合 + * @return this */ - public void addWords(Collection words) { + public WordTree addWords(Collection words) { if (false == (words instanceof Set)) { words = new HashSet<>(words); } for (String word : words) { addWord(word); } + return this; } /** * 增加一组单词 * * @param words 单词数组 + * @return this */ - public void addWords(String... words) { + public WordTree addWords(String... words) { HashSet wordsSet = CollectionUtil.newHashSet(words); for (String word : wordsSet) { addWord(word); } + return this; } /** @@ -89,7 +99,7 @@ public class WordTree extends HashMap { * * @param word 单词 */ - public void addWord(String word) { + public WordTree addWord(String word) { final Filter charFilter = this.charFilter; WordTree parent = null; WordTree current = this; @@ -112,8 +122,8 @@ public class WordTree extends HashMap { if (null != parent) { parent.setEnd(currentChar); } + return this; } - //------------------------------------------------------------------------------- match /** @@ -126,7 +136,7 @@ public class WordTree extends HashMap { if (null == text) { return false; } - return null != match(text); + return null != matchWord(text); } /** @@ -135,15 +145,24 @@ public class WordTree extends HashMap { * @param text 被检查的文本 * @return 匹配到的关键字 */ - public FoundWord match(String text) { + public String match(String text) { + final FoundWord foundWord = matchWord(text); + return null != foundWord ? foundWord.toString() : null; + } + + /** + * 获得第一个匹配的关键字 + * + * @param text 被检查的文本 + * @return 匹配到的关键字 + * @since 5.5.3 + */ + public FoundWord matchWord(String text) { if (null == text) { return null; } - List matchAll = matchAll(text, 1); - if (CollectionUtil.isNotEmpty(matchAll)) { - return matchAll.get(0); - } - return null; + final List matchAll = matchAllWords(text, 1); + return CollUtil.get(matchAll, 0); } //------------------------------------------------------------------------------- match all @@ -154,10 +173,21 @@ public class WordTree extends HashMap { * @param text 被检查的文本 * @return 匹配的词列表 */ - public List matchAll(String text) { + public List matchAll(String text) { return matchAll(text, -1); } + /** + * 找出所有匹配的关键字 + * + * @param text 被检查的文本 + * @return 匹配的词列表 + * @since 5.5.3 + */ + public List matchAllWords(String text) { + return matchAllWords(text, -1); + } + /** * 找出所有匹配的关键字 * @@ -165,10 +195,22 @@ public class WordTree extends HashMap { * @param limit 限制匹配个数 * @return 匹配的词列表 */ - public List matchAll(String text, int limit) { + public List matchAll(String text, int limit) { return matchAll(text, limit, false, false); } + /** + * 找出所有匹配的关键字 + * + * @param text 被检查的文本 + * @param limit 限制匹配个数 + * @return 匹配的词列表 + * @since 5.5.3 + */ + public List matchAllWords(String text, int limit) { + return matchAllWords(text, limit, false, false); + } + /** * 找出所有匹配的关键字
* 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
@@ -180,7 +222,24 @@ public class WordTree extends HashMap { * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 * @return 匹配的词列表 */ - public List matchAll(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) { + public List matchAll(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) { + final List matchAllWords = matchAllWords(text, limit, isDensityMatch, isGreedMatch); + return CollUtil.map(matchAllWords, FoundWord::toString, true); + } + + /** + * 找出所有匹配的关键字
+ * 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
+ * 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * + * @param text 被检查的文本 + * @param limit 限制匹配个数 + * @param isDensityMatch 是否使用密集匹配原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @return 匹配的词列表 + * @since 5.5.3 + */ + public List matchAllWords(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) { if (null == text) { return null; } @@ -239,8 +298,6 @@ public class WordTree extends HashMap { } return foundWords; } - - //--------------------------------------------------------------------------------------- Private method start /** diff --git a/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java b/hutool-dfa/src/test/java/cn/hutool/dfa/DfaTest.java similarity index 66% rename from hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java rename to hutool-dfa/src/test/java/cn/hutool/dfa/DfaTest.java index 913f10fce..344547476 100644 --- a/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java +++ b/hutool-dfa/src/test/java/cn/hutool/dfa/DfaTest.java @@ -1,13 +1,10 @@ -package cn.hutool.dfa.test; +package cn.hutool.dfa; -import cn.hutool.core.collection.CollectionUtil; -import cn.hutool.dfa.FoundWord; -import cn.hutool.dfa.WordTree; +import cn.hutool.core.collection.CollUtil; import org.junit.Assert; import org.junit.Test; import java.util.List; -import java.util.stream.Collectors; /** * DFA单元测试 @@ -29,8 +26,8 @@ public class DfaTest { // 情况一:标准匹配,匹配到最短关键词,并跳过已经匹配的关键词 // 匹配到【大】,就不再继续匹配了,因此【大土豆】不匹配 // 匹配到【刚出锅】,就跳过这三个字了,因此【出锅】不匹配(由于刚首先被匹配,因此长的被匹配,最短匹配只针对第一个字相同选最短) - List matchAll = tree.matchAll(text, -1, false, false); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅")); + List matchAll = tree.matchAll(text, -1, false, false); + Assert.assertEquals(matchAll, CollUtil.newArrayList("大", "土^豆", "刚出锅")); } /** @@ -45,8 +42,8 @@ public class DfaTest { // 情况二:匹配到最短关键词,不跳过已经匹配的关键词 // 【大】被匹配,最短匹配原则【大土豆】被跳过,【土豆继续被匹配】 // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 - List matchAll = tree.matchAll(text, -1, true, false); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "土^豆", "刚出锅", "出锅")); + List matchAll = tree.matchAll(text, -1, true, false); + Assert.assertEquals(matchAll, CollUtil.newArrayList("大", "土^豆", "刚出锅", "出锅")); } /** @@ -61,8 +58,8 @@ public class DfaTest { // 情况三:匹配到最长关键词,跳过已经匹配的关键词 // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配 // 由于【大土豆】被匹配,【土豆】被跳过,由于【刚出锅】被匹配,【出锅】被跳过 - List matchAll = tree.matchAll(text, -1, false, true); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "大土^豆", "刚出锅")); + List matchAll = tree.matchAll(text, -1, false, true); + Assert.assertEquals(matchAll, CollUtil.newArrayList("大", "大土^豆", "刚出锅")); } @@ -78,8 +75,8 @@ public class DfaTest { // 情况四:匹配到最长关键词,不跳过已经匹配的关键词(最全关键词) // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配,由于不跳过已经匹配的关键词,土豆继续被匹配 // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 - List matchAll = tree.matchAll(text, -1, true, true); - Assert.assertEquals(matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("大", "大土^豆", "土^豆", "刚出锅", "出锅")); + List matchAll = tree.matchAll(text, -1, true, true); + Assert.assertEquals(matchAll, CollUtil.newArrayList("大", "大土^豆", "土^豆", "刚出锅", "出锅")); } @@ -91,21 +88,20 @@ public class DfaTest { WordTree tree = new WordTree(); tree.addWord("tio"); - List all = tree.matchAll("AAAAAAAt-ioBBBBBBB"); - Assert.assertEquals(all.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()), CollectionUtil.newArrayList("t-io")); + List all = tree.matchAll("AAAAAAAt-ioBBBBBBB"); + Assert.assertEquals(all, CollUtil.newArrayList("t-io")); } @Test - public void aTest() { + public void aTest(){ WordTree tree = new WordTree(); tree.addWord("women"); String text = "a WOMEN todo.".toLowerCase(); - List matchAll = tree.matchAll(text, -1, false, false); - Assert.assertEquals("[women]", matchAll.stream().map(fw -> fw.getFoundWord()).collect(Collectors.toList()).toString()); + List matchAll = tree.matchAll(text, -1, false, false); + Assert.assertEquals("[women]", matchAll.toString()); } // ---------------------------------------------------------------------------------------------------------- - /** * 构建查找树 * @@ -121,4 +117,4 @@ public class DfaTest { tree.addWord("出锅"); return tree; } -} +} \ No newline at end of file diff --git a/hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java b/hutool-dfa/src/test/java/cn/hutool/dfa/SensitiveUtilTest.java similarity index 93% rename from hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java rename to hutool-dfa/src/test/java/cn/hutool/dfa/SensitiveUtilTest.java index 8a19fef3e..ba7348b09 100644 --- a/hutool-dfa/src/test/java/cn/hutool/dfa/test/SensitiveUtilTest.java +++ b/hutool-dfa/src/test/java/cn/hutool/dfa/SensitiveUtilTest.java @@ -1,6 +1,5 @@ -package cn.hutool.dfa.test; +package cn.hutool.dfa; -import cn.hutool.dfa.SensitiveUtil; import org.junit.Assert; import org.junit.Test; diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java b/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java index ad3948af2..3afa65cc2 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java @@ -694,12 +694,12 @@ public final class JSONUtil { * 在需要的时候包装对象
* 包装包括: *

    - *
  • null =》 JSONNull.NULL
  • + *
  • {@code null} =》 {@code JSONNull.NULL}
  • *
  • array or collection =》 JSONArray
  • *
  • map =》 JSONObject
  • *
  • standard property (Double, String, et al) =》 原对象
  • *
  • 来自于java包 =》 字符串
  • - *
  • 其它 =》 尝试包装为JSONObject,否则返回null
  • + *
  • 其它 =》 尝试包装为JSONObject,否则返回{@code null}
  • *
* * @param object 被包装的对象