diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/util/CoordinateUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/CoordinateUtil.java similarity index 99% rename from hutool-core/src/main/java/org/dromara/hutool/core/util/CoordinateUtil.java rename to hutool-core/src/main/java/org/dromara/hutool/core/data/CoordinateUtil.java index 4494a7f70..8cf7d7552 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/util/CoordinateUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/CoordinateUtil.java @@ -10,7 +10,7 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.util; +package org.dromara.hutool.core.data; import java.io.Serializable; import java.util.Objects; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/util/CreditCodeUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/CreditCodeUtil.java similarity index 98% rename from hutool-core/src/main/java/org/dromara/hutool/core/util/CreditCodeUtil.java rename to hutool-core/src/main/java/org/dromara/hutool/core/data/CreditCodeUtil.java index 1727dddfe..1be2324be 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/util/CreditCodeUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/CreditCodeUtil.java @@ -10,12 +10,13 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.util; +package org.dromara.hutool.core.data; import org.dromara.hutool.core.map.SafeConcurrentHashMap; import org.dromara.hutool.core.regex.PatternPool; import org.dromara.hutool.core.regex.ReUtil; import org.dromara.hutool.core.text.StrUtil; +import org.dromara.hutool.core.util.RandomUtil; import java.util.Map; import java.util.regex.Pattern; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/MaskingUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java similarity index 99% rename from hutool-core/src/main/java/org/dromara/hutool/core/text/MaskingUtil.java rename to hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java index 1b9b7915f..0c9064ac4 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/MaskingUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/MaskingUtil.java @@ -10,8 +10,9 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.text; +package org.dromara.hutool.core.data; +import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.core.util.CharUtil; /** diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/PasswdStrength.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/PasswdStrength.java similarity index 99% rename from hutool-core/src/main/java/org/dromara/hutool/core/text/PasswdStrength.java rename to hutool-core/src/main/java/org/dromara/hutool/core/data/PasswdStrength.java index 504fc7f86..78e15b28a 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/PasswdStrength.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/PasswdStrength.java @@ -10,7 +10,9 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.text; +package org.dromara.hutool.core.data; + +import org.dromara.hutool.core.text.StrUtil; /** * 检测密码强度
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/util/PhoneUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/PhoneUtil.java similarity index 99% rename from hutool-core/src/main/java/org/dromara/hutool/core/util/PhoneUtil.java rename to hutool-core/src/main/java/org/dromara/hutool/core/data/PhoneUtil.java index 9602bbea7..8b31f1272 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/util/PhoneUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/PhoneUtil.java @@ -10,7 +10,7 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.util; +package org.dromara.hutool.core.data; import org.dromara.hutool.core.regex.PatternPool; import org.dromara.hutool.core.lang.Validator; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/data/package-info.java b/hutool-core/src/main/java/org/dromara/hutool/core/data/package-info.java new file mode 100644 index 000000000..e9b74b3ce --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/data/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +/** + * 数据相关封装和工具类
+ * 在Hutool中,“数据”是指社会属性的内容
+ * 如电话、统一社会信用代码、密码、坐标系、数据脱敏等。 + * + * @author looly + */ +package org.dromara.hutool.core.data; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/lang/Validator.java b/hutool-core/src/main/java/org/dromara/hutool/core/lang/Validator.java index 34bc1b499..df99c2751 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/lang/Validator.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/lang/Validator.java @@ -17,7 +17,7 @@ import org.dromara.hutool.core.exceptions.ValidateException; import org.dromara.hutool.core.regex.PatternPool; import org.dromara.hutool.core.regex.RegexPool; import org.dromara.hutool.core.util.CharsetUtil; -import org.dromara.hutool.core.util.CreditCodeUtil; +import org.dromara.hutool.core.data.CreditCodeUtil; import org.dromara.hutool.core.math.NumberUtil; import org.dromara.hutool.core.util.ObjUtil; import org.dromara.hutool.core.regex.ReUtil; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/CharSequenceUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/CharSequenceUtil.java index eb9054c14..f9fd0d2e5 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/CharSequenceUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/CharSequenceUtil.java @@ -23,6 +23,7 @@ import org.dromara.hutool.core.text.finder.CharFinder; import org.dromara.hutool.core.text.finder.CharMatcherFinder; import org.dromara.hutool.core.text.finder.Finder; import org.dromara.hutool.core.text.finder.StrFinder; +import org.dromara.hutool.core.text.placeholder.StrFormatter; import org.dromara.hutool.core.text.replacer.RangeReplacerByChar; import org.dromara.hutool.core.text.replacer.RangeReplacerByStr; import org.dromara.hutool.core.text.replacer.SearchReplacer; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/StrUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/StrUtil.java index b2460c884..b44a0cbdb 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/StrUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/StrUtil.java @@ -13,6 +13,7 @@ package org.dromara.hutool.core.text; import org.dromara.hutool.core.array.ArrayUtil; +import org.dromara.hutool.core.text.placeholder.StrFormatter; import org.dromara.hutool.core.util.CharsetUtil; import java.io.StringReader; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/PlaceholderParser.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/PlaceholderParser.java similarity index 98% rename from hutool-core/src/main/java/org/dromara/hutool/core/text/PlaceholderParser.java rename to hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/PlaceholderParser.java index 9f5b4b970..7ba625970 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/PlaceholderParser.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/PlaceholderParser.java @@ -10,10 +10,11 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.text; +package org.dromara.hutool.core.text.placeholder; import org.dromara.hutool.core.exceptions.UtilException; import org.dromara.hutool.core.lang.Assert; +import org.dromara.hutool.core.text.StrChecker; import org.dromara.hutool.core.util.CharUtil; import java.util.Objects; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/StrFormatter.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrFormatter.java similarity index 97% rename from hutool-core/src/main/java/org/dromara/hutool/core/text/StrFormatter.java rename to hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrFormatter.java index e543a4eec..ce3f7a24d 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/StrFormatter.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrFormatter.java @@ -10,10 +10,10 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.text; +package org.dromara.hutool.core.text.placeholder; import org.dromara.hutool.core.array.ArrayUtil; -import org.dromara.hutool.core.text.placeholder.StrTemplate; +import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.core.text.placeholder.template.NamedPlaceholderStrTemplate; import java.util.Map; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/StrMatcher.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrMatcher.java similarity index 97% rename from hutool-core/src/main/java/org/dromara/hutool/core/text/StrMatcher.java rename to hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrMatcher.java index b27896fd6..42e115eda 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/StrMatcher.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrMatcher.java @@ -10,9 +10,10 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.text; +package org.dromara.hutool.core.text.placeholder; import org.dromara.hutool.core.map.MapUtil; +import org.dromara.hutool.core.text.StrUtil; import java.util.ArrayList; import java.util.HashMap; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrTemplate.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrTemplate.java index f503ea419..ac3d7af71 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrTemplate.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrTemplate.java @@ -32,1008 +32,1008 @@ import static org.dromara.hutool.core.text.placeholder.StrTemplate.Feature.*; * @since 6.0.0 */ public abstract class StrTemplate { - // region 静态属性和方法 - // ################################################## 静态属性和方法 ################################################## - /** - * 转义符 默认值 - */ - public static final char DEFAULT_ESCAPE = CharPool.BACKSLASH; - - /** - * 全局默认策略,一旦修改,对所有模板对象都生效 - *

该值 是每个模板对象创建时的 策略初始值,因此,修改全局默认策略,不影响已经创建的模板对象

- */ - protected static int globalFeatures = Feature.of(FORMAT_MISSING_KEY_PRINT_WHOLE_PLACEHOLDER, FORMAT_NULL_VALUE_TO_STR, - MATCH_KEEP_DEFAULT_VALUE, MATCH_EMPTY_VALUE_TO_NULL, MATCH_NULL_STR_TO_NULL); - - /** - * 全局默认值处理器,一旦修改,对所有模板对象都生效 - *

根据 占位符变量 返回 默认值

- */ - protected static UnaryOperator globalDefaultValueHandler; - - - /** - * 创建 单占位符模板对象的 Builder - *

例如,"{}", "?", "$$$"

- * - * @param template 字符串模板 - * @return 单占位符 模板对象的 Builder - */ - public static SinglePlaceholderStrTemplate.Builder of(String template) { - return SinglePlaceholderStrTemplate.builder(template); - } - - /** - * 创建 有前缀和后缀的占位符模板对象的 Builder - *

例如,"{0}", "{name}", "#{name}"

- * - * @param template 字符串模板 - * @return 有前缀和后缀的占位符模板对象的 Builder - */ - public static NamedPlaceholderStrTemplate.Builder ofNamed(String template) { - return NamedPlaceholderStrTemplate.builder(template); - } - - /** - * 设置 全局默认策略,一旦修改,对所有模板对象都生效 - *

该值 是每个模板对象创建时的 策略初始值,因此,修改全局默认策略,不影响已经创建的模板对象

- * - * @param globalFeatures 全局默认策略 - */ - public static void setGlobalFeatures(final Feature... globalFeatures) { - StrTemplate.globalFeatures = Feature.of(globalFeatures); - } - - /** - * 设置 全局默认值处理器,一旦修改,对所有模板对象都生效 - * - * @param globalDefaultValueHandler 全局默认处理器,根据 占位符变量 返回 默认值 - */ - public static void setGlobalDefaultValue(final UnaryOperator globalDefaultValueHandler) { - StrTemplate.globalDefaultValueHandler = Objects.requireNonNull(globalDefaultValueHandler); - } - // endregion - - // region 普通属性 - // ################################################## 普通属性 ################################################## - - /** - * 字符串模板 - */ - private final String template; - /** - * 转义符,默认为: {@link CharPool#BACKSLASH} - * - *

转义符如果标记在 占位符的开始或者结束 之前,则该占位符无效,属于普通字符串的一部分
- * 例如,转义符为 {@literal '/'},占位符为 "{}":
- * 当字符串模板为 {@literal "I am /{}"} 时,该模板中没有任何需要替换的占位符,格式化结果为 {@literal "I am {}"} - *

- * - *

如果要打印转义符,使用双转义符即可,例如,转义符为 {@literal '/'},占位符为 "{}":
- * 当字符串模板为 {@literal "I am //{}"} ,格式化参数为 {@literal "student"}, 格式化结果为 {@literal "I am /student"} - *

- */ - protected final char escape; - /** - * 占位符 没有找到 对应的填充值时 使用的默认值,如果没有,则使用 {@link #defaultValueHandler} 提供默认值, - * 如果也没有,使用 {@link #globalDefaultValueHandler},还是没有,则抛出异常 - */ - protected final String defaultValue; - /** - * 当前模板的默认值处理器,根据 占位变量 返回 默认值 - */ - protected final UnaryOperator defaultValueHandler; - /** - * 当前模板的策略值 - */ - private final int features; - /** - * 模板中的所有固定文本和占位符 - */ - protected List segments; - /** - * 所有占位符 - */ - protected List placeholderSegments; - /** - * 模板中的固定文本长度,序列化时用于计算最终文本长度 - */ - protected int fixedTextTotalLength; - // endregion - - protected StrTemplate(final String template, final char escape, final String defaultValue, - final UnaryOperator defaultValueHandler, final int features) { - Assert.notNull(template, "String template cannot be null"); - this.template = template; - this.escape = escape; - this.defaultValue = defaultValue; - this.defaultValueHandler = defaultValueHandler; - this.features = features; - } - - /** - * 获取 模板字符串 - * - * @return 模板字符串 - */ - public String getTemplate() { - return template; - } - - /** - * 获取 当前模板的 策略值 - * - * @return 策略值 - */ - public int getFeatures() { - return features; - } - - /** - * 校验 传入的字符串 是否和模板匹配 - * - * @param str 校验字符串,应该是由格式化方法生成的字符串 - * @return 是否和模板匹配 - */ - public boolean isMatches(final String str) { - if (StrUtil.isEmpty(str)) { - return false; - } - int startIdx = 0, findIdx; - boolean hasPlaceholder = false; - String text; - for (StrTemplateSegment segment : segments) { - if (segment instanceof LiteralSegment) { - text = segment.getText(); - findIdx = str.indexOf(text, startIdx); - // 没有找到固定文本,匹配失败 - if (findIdx == -1) { - return false; - } - // 出现 未匹配 的文本,但是这里却没有占位符,匹配失败 - else if (findIdx != startIdx && !hasPlaceholder) { - return false; - } - startIdx = findIdx + text.length(); - hasPlaceholder = false; - } else { - // 有两个紧密相连的占位符,无法正确地拆分变量值 - if (hasPlaceholder) { - throw new UtilException("There are two closely related placeholders that cannot be split properly!"); - } - hasPlaceholder = true; - } - } - - return true; - } - - /** - * 获取 所有占位变量名称列表 - *

例如,{@literal "{}"->"{}"、"{name}"->"name"}

- * - * @return 所有占位变量名称列表 - */ - public List getPlaceholderVariableNames() { - return this.placeholderSegments.stream() - .map(AbstractPlaceholderSegment::getPlaceholder) - .collect(Collectors.toList()); - } - - /** - * 获取 所有占位符的完整文本列表 - *

例如,{@literal "{}"->"{}"、"{name}"->"{name}"}

- * - * @return 所有占位符的完整文本列表 - */ - public List getPlaceholderTexts() { - return this.placeholderSegments.stream() - .map(AbstractPlaceholderSegment::getText) - .collect(Collectors.toList()); - } - - // region 格式化方法 - // ################################################## 格式化方法 ################################################## - - /** - * 根据 原始数据 生成 格式化字符串 - *

依次遍历模板中的 占位符,根据 占位符 返回 需要序列化的值

- *

不对 占位符 和 参数值 做任何处理,由用户抉择

- * - * @param valueSupplier 根据 占位符 返回 需要序列化的值的字符串形式,例如:
- * {@code key -> map.get(key)} - * @return 模板格式化之后的结果 - */ - public String formatRawByKey(final Function valueSupplier) { - return formatRawBySegment(segment -> valueSupplier.apply(segment.getPlaceholder())); - } - - /** - * 根据 原始数据 生成 格式化字符串 - *

依次遍历模板中的 占位符,根据 占位符 返回 需要序列化的值

- *

不对 占位符 和 参数值 做任何处理,由用户抉择

- * - * @param valueSupplier 根据 占位符 返回 需要序列化的值的字符串形式,例如:
- * {@code segment -> map.get(segment.getPlaceholder())} - * @return 模板格式化之后的结果 - */ - public String formatRawBySegment(final Function valueSupplier) { - // 保存 参数转为字符串的结果 - final List values = new ArrayList<>(placeholderSegments.size()); - // 先统计 固定文本 + 需要格式化的参数的字符串 的总字符数量 - int totalTextLength = this.fixedTextTotalLength; - - String valueStr; - for (AbstractPlaceholderSegment segment : placeholderSegments) { - // 根据 占位符 返回 需要序列化的值 - valueStr = valueSupplier.apply(segment); - if (valueStr == null) { - valueStr = "null"; - } - totalTextLength += valueStr.length(); - values.add(valueStr); - } - - final StringBuilder sb = new StringBuilder(totalTextLength); - final Iterator valueIterator = values.iterator(); - // 构造格式化结果字符串 - for (StrTemplateSegment segment : segments) { - segment.format(sb, valueIterator); - } - return sb.toString(); - } - - /** - * 按顺序使用 迭代器元素 替换 占位符 - * - * @param iterable iterable - * @return 格式化字符串 - */ - protected String formatSequence(final Iterable iterable) { - if (iterable == null) { - return getTemplate(); - } - - final Iterator iterator = iterable.iterator(); - return formatBySegment(segment -> { - if (iterator.hasNext()) { - return iterator.next(); - } else { - return formatMissingKey(segment); - } - }); - } - - /** - * 根据 策略 和 默认值 处理需要序列化的值, 生成 格式化字符串 - *

依次遍历模板中的 占位符,根据 占位符 返回 需要序列化的值

- * - * @param valueSupplier 根据 占位符 返回 需要序列化的值,如果返回值不是 {@link String},则使用 {@link StrUtil#utf8Str(Object)} - * 方法转为字符串 - * @return 模板格式化之后的结果 - */ - protected String formatBySegment(final Function valueSupplier) { - return formatRawBySegment(segment -> { - // 根据 占位符 返回 需要序列化的值 - Object value = valueSupplier.apply(segment); - if (value != null) { - if (value instanceof String) { - return (String) value; - } else { - return StrUtil.utf8Str(value); - } - } else { - // 处理null值 - return formatNullValue(segment); - } - }); - } - - /** - * 根据 策略 返回 格式化参数中 找不到 占位符 时的默认值 - *

例如,map中没有 占位符变量 这个key;基于下标的参数中,找不到 占位符下标 对应的 列表元素

- * - * @param segment 占位符 - * @return 参数中找不到占位符时的默认值 - */ - protected String formatMissingKey(final AbstractPlaceholderSegment segment) { - final int features = getFeatures(); - if (FORMAT_MISSING_KEY_PRINT_WHOLE_PLACEHOLDER.contains(features)) { - return segment.getText(); - } else if (FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE.contains(features)) { - return getDefaultValue(segment); - } else if (FORMAT_MISSING_KEY_PRINT_NULL.contains(features)) { - return "null"; - } else if (FORMAT_MISSING_KEY_PRINT_EMPTY.contains(features)) { - return ""; - } else if (FORMAT_MISSING_KEY_PRINT_VARIABLE_NAME.contains(features)) { - return segment.getPlaceholder(); - } else if (FORMAT_MISSING_KEY_THROWS.contains(features)) { - throw new UtilException("There is no value associated with key: '" + segment.getPlaceholder() + "'"); - } - throw new UtilException("There is no value associated with key: '" + segment.getPlaceholder() + - "'. You should define some Feature for missing key when building."); - } - - /** - * 根据 策略 返回 占位符 对应的值为 {@code null} 时的返回值 - * - * @param segment 占位符 - * @return 占位符对应的值为 {@code null} 时的返回值 - */ - protected String formatNullValue(final AbstractPlaceholderSegment segment) { - final int features = getFeatures(); - if (FORMAT_NULL_VALUE_TO_STR.contains(features)) { - return "null"; - } else if (FORMAT_NULL_VALUE_TO_EMPTY.contains(features)) { - return ""; - } else if (FORMAT_NULL_VALUE_TO_WHOLE_PLACEHOLDER.contains(features)) { - return segment.getText(); - } else if (FORMAT_NULL_VALUE_TO_DEFAULT_VALUE.contains(features)) { - return getDefaultValue(segment); - } - throw new UtilException("There is a NULL value cannot resolve. You should define a Feature for null value when building or filter null value."); - } - // endregion - - // region 解析方法 - // ################################################## 解析方法 ################################################## - - // region 原始数据的解析方法 - // 不对 占位符 和 解析得到的值 做任何处理,由用户抉择 - // ############################# 原始数据的解析方法 ############################# - - /** - * 原始数据的解析方法 - *

不对 占位符 和 解析得到的值 做任何处理,由用户抉择

- * - * @param str 待解析的字符串 - * @param keyValueConsumer 消费 占位符变量名称 和 占位符对应的解析得到的字符串值,例如:
{@code (key, value) -> map.put(key, value)} - */ - public void matchesRawByKey(final String str, final BiConsumer keyValueConsumer) { - if (str == null || keyValueConsumer == null || CollUtil.isEmpty(placeholderSegments)) { - return; - } - matchesRawBySegment(str, (segment, value) -> keyValueConsumer.accept(segment.getPlaceholder(), value)); - } - - /** - * 原始数据的解析方法 - *

不对 占位符 和 解析得到的值 做任何处理,由用户抉择

- * - * @param str 待解析的字符串 - * @param keyValueConsumer 消费 占位符 和 占位符对应的解析得到的字符串值,例如:
{@code (key, value) -> map.put(key, value)} - */ - public void matchesRawBySegment(final String str, final BiConsumer keyValueConsumer) { - if (str == null || keyValueConsumer == null || CollUtil.isEmpty(placeholderSegments)) { - return; - } - - int startIdx = 0, findIdx; - AbstractPlaceholderSegment placeholderSegment = null; - String text; - for (StrTemplateSegment segment : segments) { - if (segment instanceof LiteralSegment) { - text = segment.getText(); - // 查找固定文本 - findIdx = str.indexOf(text, startIdx); - // 没有找到固定文本,匹配失败 - if (findIdx == -1) { - return; - } else if (placeholderSegment != null) { - // 处理 占位符 和 解析得到的字符串值原始值 - keyValueConsumer.accept(placeholderSegment, str.substring(startIdx, findIdx)); - } - // 中间出现 未匹配 的文本,同时还没有占位变量,匹配失败 - else if (findIdx != startIdx) { - return; - } - startIdx = findIdx + text.length(); - placeholderSegment = null; - } else { - // 有两个紧密相连的占位符,无法正确地拆分变量值 - if (placeholderSegment != null) { - throw new UtilException("There are two closely related placeholders that cannot be split properly!"); - } - placeholderSegment = (AbstractPlaceholderSegment) segment; - } - } - - // 结尾有未匹配的 占位变量 - if (placeholderSegment != null) { - keyValueConsumer.accept(placeholderSegment, str.substring(startIdx)); - } - } - // endregion - - // region 普通解析方法 - // 根据 策略 和 默认值 进行解析处理 - // ############################# 普通解析方法 ############################# - - /** - * 将 占位符位置的值 按顺序解析为 字符串列表 - * - * @param str 待解析的字符串,一般是格式化方法的返回值 - * @return 字符串列表 - */ - protected List matchesSequence(final String str) { - if (str == null || placeholderSegments.isEmpty() || !isMatches(str)) { - return ListUtil.zero(); - } - - final List list = new ArrayList<>(placeholderSegments.size()); - matchesByKey(str, (segment, value) -> list.add(value)); - return list; - } - - /** - * 根据 策略 和 默认值 获得最终的 value,由消费者处理该 value - * - * @param str 待解析的字符串 - * @param keyValueConsumer 按占位符顺序 消费 占位符变量 和 最终的value,例如:
{@code (key, value) -> map.put(key, value)} - */ - public void matchesByKey(final String str, final BiConsumer keyValueConsumer) { - if (hasDefaultValue()) { - matchesByKey(str, keyValueConsumer, true, this::getDefaultValue); - } else { - matchesByKey(str, keyValueConsumer, false, null); - } - } - - /** - * 根据 策略 和 默认值 获得最终的 value,由消费者处理该 value - * - * @param str 待解析的字符串 - * @param keyValueConsumer 按占位符顺序 消费 占位符变量 和 最终的value,例如:
{@code (key, value) -> map.put(key, value)} - * @param hasDefaultValue 是否有默认值 - * @param defaultValueSupplier 默认值提供者,根据 占位符 返回 默认值 - */ - protected void matchesByKey(final String str, final BiConsumer keyValueConsumer, final boolean hasDefaultValue, - final Function defaultValueSupplier) { - if (str == null || keyValueConsumer == null || CollUtil.isEmpty(placeholderSegments)) { - return; - } - matchesRawBySegment(str, (segment, value) -> matchByKey( - keyValueConsumer, segment.getPlaceholder(), value, hasDefaultValue, - // 默认值 - () -> hasDefaultValue ? StrUtil.utf8Str(defaultValueSupplier.apply(segment)) : null - )); - } - - /** - * 根据 策略 和 默认值 获得最终的 value,由消费者处理该 value - * - * @param keyValueConsumer 按占位符顺序 消费 占位符变量 和 最终的value,例如:
{@code (key, value) -> map.put(key, value)} - * @param key 占位符变量 - * @param value 解析得到的值,原始值 - * @param hasDefaultValue 是否有默认值 - * @param defaultValueSupplier 默认值提供者 - */ - private void matchByKey(final BiConsumer keyValueConsumer, final String key, final String value, - final boolean hasDefaultValue, final Supplier defaultValueSupplier) { - final int features = getFeatures(); - - // 存在默认值 - if (hasDefaultValue) { - // 保留默认值,则跳过默认值策略处理,由后续策略决定 最终的值 - if (!MATCH_KEEP_DEFAULT_VALUE.contains(features)) { - // 解析到的参数值 是 默认值 - if (value.equals(defaultValueSupplier.get())) { - // 校验 默认值策略 - if (MATCH_IGNORE_DEFAULT_VALUE.contains(features)) { - return; - } else if (MATCH_DEFAULT_VALUE_TO_NULL.contains(features)) { - keyValueConsumer.accept(key, null); - return; - } - } - } - } - - // 解析到的参数值 是 空字符串 - if ("".equals(value)) { - if (MATCH_EMPTY_VALUE_TO_NULL.contains(features)) { - keyValueConsumer.accept(key, null); - } else if (MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE.contains(features)) { - keyValueConsumer.accept(key, defaultValueSupplier.get()); - } else if (MATCH_IGNORE_EMPTY_VALUE.contains(features)) { - return; - } else if (MATCH_KEEP_VALUE_EMPTY.contains(features)) { - keyValueConsumer.accept(key, value); - } - return; - } - - // 解析到的参数值 是 null字符串 - if ("null".equals(value)) { - if (MATCH_NULL_STR_TO_NULL.contains(features)) { - keyValueConsumer.accept(key, null); - } else if (MATCH_KEEP_NULL_STR.contains(features)) { - keyValueConsumer.accept(key, value); - } else if (MATCH_IGNORE_NULL_STR.contains(features)) { - return; - } - return; - } - - // 普通参数值 - keyValueConsumer.accept(key, value); - } - // endregion - // endregion - - /** - * 是否有默认值 - * - * @return 是否有默认值 - */ - protected boolean hasDefaultValue() { - return defaultValue != null || defaultValueHandler != null || globalDefaultValueHandler != null; - } - - /** - * 根据 占位符 返回默认值 - *

根据定义的默认值、默认值提供者、全局默认值提供者,返回默认值

- * - * @param segment 占位符 - * @return 默认值 - */ - protected String getDefaultValue(final AbstractPlaceholderSegment segment) { - if (defaultValue != null) { - return defaultValue; - } else if (defaultValueHandler != null) { - return StrUtil.utf8Str(defaultValueHandler.apply(segment.getPlaceholder())); - } else if (globalDefaultValueHandler != null) { - return StrUtil.utf8Str(globalDefaultValueHandler.apply(segment.getPlaceholder())); - } - throw new UtilException("There is no default value for key: '" + segment.getPlaceholder() + - "'. You should define a 'defaultValue' or 'defaultValueHandler' or 'globalDefaultValueHandler' when building."); - } - - /** - * 一些公共的初始化代码 - *

由于此时子类还没构造完成,所以只能由子类构造方法调用

- */ - protected void afterInit() { - // 解析 并 优化 segment 列表 - this.segments = optimizeSegments(parseSegments(template)); - - // 计算 固定文本segment 的 数量 和 文本总长度 - int literalSegmentSize = 0, fixedTextTotalLength = 0; - for (StrTemplateSegment segment : this.segments) { - if (segment instanceof LiteralSegment) { - ++literalSegmentSize; - fixedTextTotalLength += segment.getText().length(); - } - } - this.fixedTextTotalLength = fixedTextTotalLength; - - // 获取 占位符segment 列表 - final int placeholderSegmentsSize = segments.size() - literalSegmentSize; - if (placeholderSegmentsSize == 0) { - this.placeholderSegments = ListUtil.zero(); - } else { - List placeholderSegments = new ArrayList<>(placeholderSegmentsSize); - for (StrTemplateSegment segment : segments) { - if (segment instanceof AbstractPlaceholderSegment) { - placeholderSegments.add((AbstractPlaceholderSegment) segment); - } - } - this.placeholderSegments = placeholderSegments; - } - } - - - /** - * 将 模板 解析为 Segment 列表 - * - * @param template 字符串模板 - * @return Segment列表 - */ - protected abstract List parseSegments(String template); - - /** - * 获取 模板中 所有segment - * - * @return segment列表 - */ - protected List getSegments() { - return segments; - } - - /** - * 获取 模板中的 占位符 segment - * - * @return 占位符列表 - */ - protected List getPlaceholderSegments() { - return placeholderSegments; - } - - /** - * 优化节点列表 - *

移除空文本节点,合并连续的文本节点

- * - * @param segments 节点列表 - * @return 不占用多余空间的节点列表 - */ - private List optimizeSegments(final List segments) { - if (CollUtil.isEmpty(segments)) { - return segments; - } - - final List list = new ArrayList<>(segments.size()); - StrTemplateSegment last; - for (StrTemplateSegment segment : segments) { - if (segment instanceof LiteralSegment) { - // 空的文本节点,没有任何意义 - if (segment.getText().isEmpty()) { - continue; - } - if (list.isEmpty()) { - list.add(segment); - continue; - } - last = list.get(list.size() - 1); - // 如果是两个连续的文本节点,需要合并 - if (last instanceof LiteralSegment) { - list.set(list.size() - 1, new LiteralSegment(last.getText() + segment.getText())); - } else { - list.add(segment); - } - } else { - list.add(segment); - } - } - // 释放空闲的列表元素 - return list.size() == segments.size() ? list : new ArrayList<>(list); - } - - /** - * 抽象Builder - * - * @param Builder子类 - * @param 模板子类 - */ - protected static abstract class AbstractBuilder, TemplateChild extends StrTemplate> { - /** - * 字符串模板 - */ - protected final String template; - /** - * 默认值 - */ - protected String defaultValue; - /** - * 默认值处理器 - */ - protected UnaryOperator defaultValueHandler; - /** - * 用户是否设置了 转义符 - */ - protected boolean escape$set; - /** - * 转义符 - */ - protected char escape; - /** - * 策略值 - */ - protected int features; - - protected AbstractBuilder(final String template) { - this.template = Objects.requireNonNull(template); - // 策略值 初始为 全局默认策略 - this.features = StrTemplate.globalFeatures; - } - - /** - * 设置 转义符 - * - * @param escape 转义符 - * @return builder子对象 - */ - public BuilderChild escape(final char escape) { - this.escape = escape; - this.escape$set = true; - return self(); - } - - /** - * 设置 新的策略值,完全覆盖旧的策略值 - * - * @param newFeatures 新策略枚举 - * @return builder子对象 - */ - public BuilderChild features(final Feature... newFeatures) { - this.features = Feature.of(newFeatures); - return self(); - } - - /** - * 向 策略值 中 添加策略 - *

同组内的策略是互斥的,一但设置为组内的某个新策略,就会清除之前的同组策略,仅保留新策略

- * - * @param appendFeatures 需要新增的策略 - * @return builder子对象 - */ - public BuilderChild addFeatures(final Feature... appendFeatures) { - if (ArrayUtil.isNotEmpty(appendFeatures)) { - for (Feature feature : appendFeatures) { - this.features = feature.set(this.features); - } - } - return self(); - } - - /** - * 从 策略值 中 删除策略 - *

删除的策略 可以 不存在

- * - * @param removeFeatures 需要删除的策略 - * @return builder子对象 - */ - public BuilderChild removeFeatures(final Feature... removeFeatures) { - if (ArrayUtil.isNotEmpty(removeFeatures)) { - for (Feature feature : removeFeatures) { - this.features = feature.clear(this.features); - } - } - return self(); - } - - /** - * 设置 默认值 - *

不可能为 {@code null},可以为 {@code "null"}

- * - * @param defaultValue 默认值 - * @return builder子对象 - */ - public BuilderChild defaultValue(final String defaultValue) { - this.defaultValue = Objects.requireNonNull(defaultValue); - return self(); - } - - /** - * 设置 默认值处理器 - * - * @param defaultValueHandler 默认值处理器,根据 占位变量 返回 默认值 - * @return builder子对象 - */ - public BuilderChild defaultValue(final UnaryOperator defaultValueHandler) { - this.defaultValueHandler = Objects.requireNonNull(defaultValueHandler); - return self(); - } - - /** - * 创建 模板对象 - * - * @return 模板对象 - */ - public TemplateChild build() { - if (!this.escape$set) { - this.escape = DEFAULT_ESCAPE; - } - return buildInstance(); - } - - /** - * 设置 转义符 - * - * @return builder子对象 - */ - protected abstract BuilderChild self(); - - /** - * 子类Builder 返回 创建的 模板对象 - * - * @return 模板对象 - */ - protected abstract TemplateChild buildInstance(); - } - - /** - * 格式化 和 解析 策略 - *

同组内的策略是互斥的,一但设置为组内的某个新策略,就会清除之前的同组策略,仅保留新策略

- */ - public enum Feature { - // region 格式化策略 - // ======================================== 格式化策略 ======================================== - - // region 占位符没有对应值策略组 - // 传递的格式化参数中找不到 占位变量,例如:占位符有三个,格式化时仅传入两个值;map中不包含占位符变量这个key;按下标格式化时,传入的列表不包含这个下标时; - // ==================== 占位符没有对应值策略组 ==================== - /** - * 格式化时,如果 占位符 没有 对应的值,则打印完整占位符
- * 对于 变量占位符,例如"${name}",原样打印"${name}"
- *

默认策略

- */ - FORMAT_MISSING_KEY_PRINT_WHOLE_PLACEHOLDER(0, 0, 6), - /** - * 格式化时,如果 占位符 没有 对应的值,则打印 默认值,如果 没有默认值,则抛出异常
- */ - FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE(1, 0, 6), - /** - * 格式化时,如果 占位符 没有 对应的值,且没有默认值,则打印 {@code "null"}字符串
- */ - FORMAT_MISSING_KEY_PRINT_NULL(2, 0, 6), - /** - * 格式化时,如果 占位符 没有 对应的值,则打印 空字符串
- *

该策略意味着 模板存在默认值,且为 空字符串

- */ - FORMAT_MISSING_KEY_PRINT_EMPTY(3, 0, 6), - /** - * 格式化时,如果 占位符 没有 对应的值:
- * 对于 单个占位符,例如"?",打印完整占位符"?";
- * 对于 变量占位符,则只打印占位变量,例如"${name}",只打印"name";
- */ - FORMAT_MISSING_KEY_PRINT_VARIABLE_NAME(4, 0, 6), - /** - * 格式化时,如果 占位符 没有 对应的值,则抛出异常
- */ - FORMAT_MISSING_KEY_THROWS(5, 0, 6), - //endregion - - // region null值策略组 - // ==================== null值策略组 ==================== - /** - * 格式化时,如果 占位符 对应的值为 {@code null},则打印 {@code "null"} 字符串 - *

默认策略

- */ - FORMAT_NULL_VALUE_TO_STR(6, 6, 4), - /** - * 格式化时,如果 占位符 对应的值为 {@code null},则打印 {@code ""} 空字符串 - */ - FORMAT_NULL_VALUE_TO_EMPTY(7, 6, 4), - /** - * 格式化时,如果 占位符 对应的值为 {@code null},则原样打印占位符
- * 对于 变量占位符,输出完整占位符,例如"${name}",打印"${name}"
- */ - FORMAT_NULL_VALUE_TO_WHOLE_PLACEHOLDER(8, 6, 4), - /** - * 格式化时,如果 占位符 对应的值为 {@code null},则使用 默认值,如果 没有默认值,则抛出异常
- */ - FORMAT_NULL_VALUE_TO_DEFAULT_VALUE(9, 6, 4), - //endregion - //endregion - - // region 解析策略 - // 解析策略校验顺序: 默认值策略、空字符串策略、null字符串策略 - // ======================================== 解析策略 ======================================== - - // region 默认值策略组 - // ==================== 默认值策略组 ==================== - /** - * 解析时,结果中 包含 默认值,原样返回 - *

默认策略

- */ - MATCH_KEEP_DEFAULT_VALUE(16, 16, 3), - /** - * 解析时,结果中 不包含 默认值,只要等于默认值,都忽略 - *

即,返回的结果 map 中 不会包含 这个key

- *

在 基于下标的解析方法中 不生效,基于下标的解析结果只区分是否为 {@code null},元素数量是固定的

- */ - MATCH_IGNORE_DEFAULT_VALUE(17, 16, 3), - /** - * 解析时,在 结果中 将 默认值 转为 {@code null} - *

返回的结果 map 中 包含 这个key

- */ - MATCH_DEFAULT_VALUE_TO_NULL(18, 16, 3), - // endregion - - // region 空字符串策略组 - // 占位符 位置的值是 空字符串 - // ==================== 空字符串策略组 ==================== - /** - * 解析时,占位符 对应的值为 空字符串,将 这个空字符串 转为 {@code null} - *

默认策略

- */ - MATCH_EMPTY_VALUE_TO_NULL(19, 19, 4), - /** - * 解析时,占位符 对应的值为 空字符串,将 这个空字符串 转为 默认值,如果 没有默认值,则转为 {@code null} - */ - MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE(20, 19, 4), - /** - * 解析时,占位符 对应的值为 空字符串,结果中 不包含 这个空字符串 - *

即,返回的结果 map 中 不会包含 这个key

- *

在 基于下标的解析方法中 不生效,基于下标的解析结果只区分是否为 {@code null},元素数量是固定的

- */ - MATCH_IGNORE_EMPTY_VALUE(21, 19, 4), - /** - * 解析时,占位符 对应的值为 空字符串,结果中 依然保留 这个空字符串 - */ - MATCH_KEEP_VALUE_EMPTY(22, 19, 4), - // endregion - - // region null值策略组 - // ==================== null值策略组 ==================== - /** - * 解析时,占位符 对应的值为 {@code "null"} 字符串,在 结果中 转为 {@code null} - *

默认策略

- */ - MATCH_NULL_STR_TO_NULL(23, 23, 3), - /** - * 解析时,占位符 对应的值为 {@code "null"} 字符串,在 结果中 保留字符串形式 {@code "null"} - */ - MATCH_KEEP_NULL_STR(24, 23, 3), - /** - * 解析时,占位符 对应的值为 {@code "null"} 字符串,结果中 不包含 这个值 - *

即,返回的结果 map 中 不会包含 这个key

- *

在 基于下标的解析方法中 不生效,基于下标的解析结果只区分是否为 {@code null},元素数量是固定的

- */ - MATCH_IGNORE_NULL_STR(25, 23, 3), - // endregion - // endregion - ; - /** - * 掩码 - */ - private final int mask; - /** - * 清除掩码的二进制值 - */ - private final int clearMask; - - /** - * 策略构造方法 - * - * @param bitPos 位数,掩码中哪一位需要置为1,从0开始 - * @param bitStart 同组第一个策略的掩码位数 - * @param bitLen 同组策略数量 - */ - Feature(int bitPos, int bitStart, int bitLen) { - this.mask = 1 << bitPos; - this.clearMask = (-1 << (bitStart + bitLen)) | ((1 << bitStart) - 1); - } - - /** - * 是否为当前策略 - * - * @param features 外部的策略值 - * @return 是否为当前策略 - */ - public boolean contains(int features) { - return (features & mask) != 0; - } - - /** - * 在 策略值 中添加 当前策略 - * - * @param features 外部的策略值 - * @return 添加后的策略值 - */ - public int set(int features) { - return (features & clearMask) | mask; - } - - /** - * 在 策略值 中移除 当前策略 - * - * @param features 外部的策略值 - * @return 移除后的策略值 - */ - public int clear(int features) { - return (features & clearMask); - } - - /** - * 计算 总的策略值 - * - * @param features 策略枚举数组 - * @return 总的策略值 - */ - public static int of(Feature... features) { - if (features == null) { - return 0; - } - - int value = 0; - for (Feature feature : features) { - value = feature.set(value); - } - - return value; - } - } + // region 静态属性和方法 + // ################################################## 静态属性和方法 ################################################## + /** + * 转义符 默认值 + */ + public static final char DEFAULT_ESCAPE = CharPool.BACKSLASH; + + /** + * 全局默认策略,一旦修改,对所有模板对象都生效 + *

该值 是每个模板对象创建时的 策略初始值,因此,修改全局默认策略,不影响已经创建的模板对象

+ */ + protected static int globalFeatures = Feature.of(FORMAT_MISSING_KEY_PRINT_WHOLE_PLACEHOLDER, FORMAT_NULL_VALUE_TO_STR, + MATCH_KEEP_DEFAULT_VALUE, MATCH_EMPTY_VALUE_TO_NULL, MATCH_NULL_STR_TO_NULL); + + /** + * 全局默认值处理器,一旦修改,对所有模板对象都生效 + *

根据 占位符变量 返回 默认值

+ */ + protected static UnaryOperator globalDefaultValueHandler; + + + /** + * 创建 单占位符模板对象的 Builder + *

例如,"{}", "?", "$$$"

+ * + * @param template 字符串模板 + * @return 单占位符 模板对象的 Builder + */ + public static SinglePlaceholderStrTemplate.Builder of(String template) { + return SinglePlaceholderStrTemplate.builder(template); + } + + /** + * 创建 有前缀和后缀的占位符模板对象的 Builder + *

例如,"{0}", "{name}", "#{name}"

+ * + * @param template 字符串模板 + * @return 有前缀和后缀的占位符模板对象的 Builder + */ + public static NamedPlaceholderStrTemplate.Builder ofNamed(String template) { + return NamedPlaceholderStrTemplate.builder(template); + } + + /** + * 设置 全局默认策略,一旦修改,对所有模板对象都生效 + *

该值 是每个模板对象创建时的 策略初始值,因此,修改全局默认策略,不影响已经创建的模板对象

+ * + * @param globalFeatures 全局默认策略 + */ + public static void setGlobalFeatures(final Feature... globalFeatures) { + StrTemplate.globalFeatures = Feature.of(globalFeatures); + } + + /** + * 设置 全局默认值处理器,一旦修改,对所有模板对象都生效 + * + * @param globalDefaultValueHandler 全局默认处理器,根据 占位符变量 返回 默认值 + */ + public static void setGlobalDefaultValue(final UnaryOperator globalDefaultValueHandler) { + StrTemplate.globalDefaultValueHandler = Objects.requireNonNull(globalDefaultValueHandler); + } + // endregion + + // region 普通属性 + // ################################################## 普通属性 ################################################## + + /** + * 字符串模板 + */ + private final String template; + /** + * 转义符,默认为: {@link CharPool#BACKSLASH} + * + *

转义符如果标记在 占位符的开始或者结束 之前,则该占位符无效,属于普通字符串的一部分
+ * 例如,转义符为 {@literal '/'},占位符为 "{}":
+ * 当字符串模板为 {@literal "I am /{}"} 时,该模板中没有任何需要替换的占位符,格式化结果为 {@literal "I am {}"} + *

+ * + *

如果要打印转义符,使用双转义符即可,例如,转义符为 {@literal '/'},占位符为 "{}":
+ * 当字符串模板为 {@literal "I am //{}"} ,格式化参数为 {@literal "student"}, 格式化结果为 {@literal "I am /student"} + *

+ */ + protected final char escape; + /** + * 占位符 没有找到 对应的填充值时 使用的默认值,如果没有,则使用 {@link #defaultValueHandler} 提供默认值, + * 如果也没有,使用 {@link #globalDefaultValueHandler},还是没有,则抛出异常 + */ + protected final String defaultValue; + /** + * 当前模板的默认值处理器,根据 占位变量 返回 默认值 + */ + protected final UnaryOperator defaultValueHandler; + /** + * 当前模板的策略值 + */ + private final int features; + /** + * 模板中的所有固定文本和占位符 + */ + protected List segments; + /** + * 所有占位符 + */ + protected List placeholderSegments; + /** + * 模板中的固定文本长度,序列化时用于计算最终文本长度 + */ + protected int fixedTextTotalLength; + // endregion + + protected StrTemplate(final String template, final char escape, final String defaultValue, + final UnaryOperator defaultValueHandler, final int features) { + Assert.notNull(template, "String template cannot be null"); + this.template = template; + this.escape = escape; + this.defaultValue = defaultValue; + this.defaultValueHandler = defaultValueHandler; + this.features = features; + } + + /** + * 获取 模板字符串 + * + * @return 模板字符串 + */ + public String getTemplate() { + return template; + } + + /** + * 获取 当前模板的 策略值 + * + * @return 策略值 + */ + public int getFeatures() { + return features; + } + + /** + * 校验 传入的字符串 是否和模板匹配 + * + * @param str 校验字符串,应该是由格式化方法生成的字符串 + * @return 是否和模板匹配 + */ + public boolean isMatches(final String str) { + if (StrUtil.isEmpty(str)) { + return false; + } + int startIdx = 0, findIdx; + boolean hasPlaceholder = false; + String text; + for (StrTemplateSegment segment : segments) { + if (segment instanceof LiteralSegment) { + text = segment.getText(); + findIdx = str.indexOf(text, startIdx); + // 没有找到固定文本,匹配失败 + if (findIdx == -1) { + return false; + } + // 出现 未匹配 的文本,但是这里却没有占位符,匹配失败 + else if (findIdx != startIdx && !hasPlaceholder) { + return false; + } + startIdx = findIdx + text.length(); + hasPlaceholder = false; + } else { + // 有两个紧密相连的占位符,无法正确地拆分变量值 + if (hasPlaceholder) { + throw new UtilException("There are two closely related placeholders that cannot be split properly!"); + } + hasPlaceholder = true; + } + } + + return true; + } + + /** + * 获取 所有占位变量名称列表 + *

例如,{@literal "{}"->"{}"、"{name}"->"name"}

+ * + * @return 所有占位变量名称列表 + */ + public List getPlaceholderVariableNames() { + return this.placeholderSegments.stream() + .map(AbstractPlaceholderSegment::getPlaceholder) + .collect(Collectors.toList()); + } + + /** + * 获取 所有占位符的完整文本列表 + *

例如,{@literal "{}"->"{}"、"{name}"->"{name}"}

+ * + * @return 所有占位符的完整文本列表 + */ + public List getPlaceholderTexts() { + return this.placeholderSegments.stream() + .map(AbstractPlaceholderSegment::getText) + .collect(Collectors.toList()); + } + + // region 格式化方法 + // ################################################## 格式化方法 ################################################## + + /** + * 根据 原始数据 生成 格式化字符串 + *

依次遍历模板中的 占位符,根据 占位符 返回 需要序列化的值

+ *

不对 占位符 和 参数值 做任何处理,由用户抉择

+ * + * @param valueSupplier 根据 占位符 返回 需要序列化的值的字符串形式,例如:
+ * {@code key -> map.get(key)} + * @return 模板格式化之后的结果 + */ + public String formatRawByKey(final Function valueSupplier) { + return formatRawBySegment(segment -> valueSupplier.apply(segment.getPlaceholder())); + } + + /** + * 根据 原始数据 生成 格式化字符串 + *

依次遍历模板中的 占位符,根据 占位符 返回 需要序列化的值

+ *

不对 占位符 和 参数值 做任何处理,由用户抉择

+ * + * @param valueSupplier 根据 占位符 返回 需要序列化的值的字符串形式,例如:
+ * {@code segment -> map.get(segment.getPlaceholder())} + * @return 模板格式化之后的结果 + */ + public String formatRawBySegment(final Function valueSupplier) { + // 保存 参数转为字符串的结果 + final List values = new ArrayList<>(placeholderSegments.size()); + // 先统计 固定文本 + 需要格式化的参数的字符串 的总字符数量 + int totalTextLength = this.fixedTextTotalLength; + + String valueStr; + for (AbstractPlaceholderSegment segment : placeholderSegments) { + // 根据 占位符 返回 需要序列化的值 + valueStr = valueSupplier.apply(segment); + if (valueStr == null) { + valueStr = "null"; + } + totalTextLength += valueStr.length(); + values.add(valueStr); + } + + final StringBuilder sb = new StringBuilder(totalTextLength); + final Iterator valueIterator = values.iterator(); + // 构造格式化结果字符串 + for (StrTemplateSegment segment : segments) { + segment.format(sb, valueIterator); + } + return sb.toString(); + } + + /** + * 按顺序使用 迭代器元素 替换 占位符 + * + * @param iterable iterable + * @return 格式化字符串 + */ + protected String formatSequence(final Iterable iterable) { + if (iterable == null) { + return getTemplate(); + } + + final Iterator iterator = iterable.iterator(); + return formatBySegment(segment -> { + if (iterator.hasNext()) { + return iterator.next(); + } else { + return formatMissingKey(segment); + } + }); + } + + /** + * 根据 策略 和 默认值 处理需要序列化的值, 生成 格式化字符串 + *

依次遍历模板中的 占位符,根据 占位符 返回 需要序列化的值

+ * + * @param valueSupplier 根据 占位符 返回 需要序列化的值,如果返回值不是 {@link String},则使用 {@link StrUtil#utf8Str(Object)} + * 方法转为字符串 + * @return 模板格式化之后的结果 + */ + protected String formatBySegment(final Function valueSupplier) { + return formatRawBySegment(segment -> { + // 根据 占位符 返回 需要序列化的值 + Object value = valueSupplier.apply(segment); + if (value != null) { + if (value instanceof String) { + return (String) value; + } else { + return StrUtil.utf8Str(value); + } + } else { + // 处理null值 + return formatNullValue(segment); + } + }); + } + + /** + * 根据 策略 返回 格式化参数中 找不到 占位符 时的默认值 + *

例如,map中没有 占位符变量 这个key;基于下标的参数中,找不到 占位符下标 对应的 列表元素

+ * + * @param segment 占位符 + * @return 参数中找不到占位符时的默认值 + */ + protected String formatMissingKey(final AbstractPlaceholderSegment segment) { + final int features = getFeatures(); + if (FORMAT_MISSING_KEY_PRINT_WHOLE_PLACEHOLDER.contains(features)) { + return segment.getText(); + } else if (FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE.contains(features)) { + return getDefaultValue(segment); + } else if (FORMAT_MISSING_KEY_PRINT_NULL.contains(features)) { + return "null"; + } else if (FORMAT_MISSING_KEY_PRINT_EMPTY.contains(features)) { + return ""; + } else if (FORMAT_MISSING_KEY_PRINT_VARIABLE_NAME.contains(features)) { + return segment.getPlaceholder(); + } else if (FORMAT_MISSING_KEY_THROWS.contains(features)) { + throw new UtilException("There is no value associated with key: '" + segment.getPlaceholder() + "'"); + } + throw new UtilException("There is no value associated with key: '" + segment.getPlaceholder() + + "'. You should define some Feature for missing key when building."); + } + + /** + * 根据 策略 返回 占位符 对应的值为 {@code null} 时的返回值 + * + * @param segment 占位符 + * @return 占位符对应的值为 {@code null} 时的返回值 + */ + protected String formatNullValue(final AbstractPlaceholderSegment segment) { + final int features = getFeatures(); + if (FORMAT_NULL_VALUE_TO_STR.contains(features)) { + return "null"; + } else if (FORMAT_NULL_VALUE_TO_EMPTY.contains(features)) { + return ""; + } else if (FORMAT_NULL_VALUE_TO_WHOLE_PLACEHOLDER.contains(features)) { + return segment.getText(); + } else if (FORMAT_NULL_VALUE_TO_DEFAULT_VALUE.contains(features)) { + return getDefaultValue(segment); + } + throw new UtilException("There is a NULL value cannot resolve. You should define a Feature for null value when building or filter null value."); + } + // endregion + + // region 解析方法 + // ################################################## 解析方法 ################################################## + + // region 原始数据的解析方法 + // 不对 占位符 和 解析得到的值 做任何处理,由用户抉择 + // ############################# 原始数据的解析方法 ############################# + + /** + * 原始数据的解析方法 + *

不对 占位符 和 解析得到的值 做任何处理,由用户抉择

+ * + * @param str 待解析的字符串 + * @param keyValueConsumer 消费 占位符变量名称 和 占位符对应的解析得到的字符串值,例如:
{@code (key, value) -> map.put(key, value)} + */ + public void matchesRawByKey(final String str, final BiConsumer keyValueConsumer) { + if (str == null || keyValueConsumer == null || CollUtil.isEmpty(placeholderSegments)) { + return; + } + matchesRawBySegment(str, (segment, value) -> keyValueConsumer.accept(segment.getPlaceholder(), value)); + } + + /** + * 原始数据的解析方法 + *

不对 占位符 和 解析得到的值 做任何处理,由用户抉择

+ * + * @param str 待解析的字符串 + * @param keyValueConsumer 消费 占位符 和 占位符对应的解析得到的字符串值,例如:
{@code (key, value) -> map.put(key, value)} + */ + public void matchesRawBySegment(final String str, final BiConsumer keyValueConsumer) { + if (str == null || keyValueConsumer == null || CollUtil.isEmpty(placeholderSegments)) { + return; + } + + int startIdx = 0, findIdx; + AbstractPlaceholderSegment placeholderSegment = null; + String text; + for (StrTemplateSegment segment : segments) { + if (segment instanceof LiteralSegment) { + text = segment.getText(); + // 查找固定文本 + findIdx = str.indexOf(text, startIdx); + // 没有找到固定文本,匹配失败 + if (findIdx == -1) { + return; + } else if (placeholderSegment != null) { + // 处理 占位符 和 解析得到的字符串值原始值 + keyValueConsumer.accept(placeholderSegment, str.substring(startIdx, findIdx)); + } + // 中间出现 未匹配 的文本,同时还没有占位变量,匹配失败 + else if (findIdx != startIdx) { + return; + } + startIdx = findIdx + text.length(); + placeholderSegment = null; + } else { + // 有两个紧密相连的占位符,无法正确地拆分变量值 + if (placeholderSegment != null) { + throw new UtilException("There are two closely related placeholders that cannot be split properly!"); + } + placeholderSegment = (AbstractPlaceholderSegment) segment; + } + } + + // 结尾有未匹配的 占位变量 + if (placeholderSegment != null) { + keyValueConsumer.accept(placeholderSegment, str.substring(startIdx)); + } + } + // endregion + + // region 普通解析方法 + // 根据 策略 和 默认值 进行解析处理 + // ############################# 普通解析方法 ############################# + + /** + * 将 占位符位置的值 按顺序解析为 字符串列表 + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @return 字符串列表 + */ + protected List matchesSequence(final String str) { + if (str == null || placeholderSegments.isEmpty() || !isMatches(str)) { + return ListUtil.zero(); + } + + final List list = new ArrayList<>(placeholderSegments.size()); + matchesByKey(str, (segment, value) -> list.add(value)); + return list; + } + + /** + * 根据 策略 和 默认值 获得最终的 value,由消费者处理该 value + * + * @param str 待解析的字符串 + * @param keyValueConsumer 按占位符顺序 消费 占位符变量 和 最终的value,例如:
{@code (key, value) -> map.put(key, value)} + */ + public void matchesByKey(final String str, final BiConsumer keyValueConsumer) { + if (hasDefaultValue()) { + matchesByKey(str, keyValueConsumer, true, this::getDefaultValue); + } else { + matchesByKey(str, keyValueConsumer, false, null); + } + } + + /** + * 根据 策略 和 默认值 获得最终的 value,由消费者处理该 value + * + * @param str 待解析的字符串 + * @param keyValueConsumer 按占位符顺序 消费 占位符变量 和 最终的value,例如:
{@code (key, value) -> map.put(key, value)} + * @param hasDefaultValue 是否有默认值 + * @param defaultValueSupplier 默认值提供者,根据 占位符 返回 默认值 + */ + protected void matchesByKey(final String str, final BiConsumer keyValueConsumer, final boolean hasDefaultValue, + final Function defaultValueSupplier) { + if (str == null || keyValueConsumer == null || CollUtil.isEmpty(placeholderSegments)) { + return; + } + matchesRawBySegment(str, (segment, value) -> matchByKey( + keyValueConsumer, segment.getPlaceholder(), value, hasDefaultValue, + // 默认值 + () -> hasDefaultValue ? StrUtil.utf8Str(defaultValueSupplier.apply(segment)) : null + )); + } + + /** + * 根据 策略 和 默认值 获得最终的 value,由消费者处理该 value + * + * @param keyValueConsumer 按占位符顺序 消费 占位符变量 和 最终的value,例如:
{@code (key, value) -> map.put(key, value)} + * @param key 占位符变量 + * @param value 解析得到的值,原始值 + * @param hasDefaultValue 是否有默认值 + * @param defaultValueSupplier 默认值提供者 + */ + private void matchByKey(final BiConsumer keyValueConsumer, final String key, final String value, + final boolean hasDefaultValue, final Supplier defaultValueSupplier) { + final int features = getFeatures(); + + // 存在默认值 + if (hasDefaultValue) { + // 保留默认值,则跳过默认值策略处理,由后续策略决定 最终的值 + if (!MATCH_KEEP_DEFAULT_VALUE.contains(features)) { + // 解析到的参数值 是 默认值 + if (value.equals(defaultValueSupplier.get())) { + // 校验 默认值策略 + if (MATCH_IGNORE_DEFAULT_VALUE.contains(features)) { + return; + } else if (MATCH_DEFAULT_VALUE_TO_NULL.contains(features)) { + keyValueConsumer.accept(key, null); + return; + } + } + } + } + + // 解析到的参数值 是 空字符串 + if ("".equals(value)) { + if (MATCH_EMPTY_VALUE_TO_NULL.contains(features)) { + keyValueConsumer.accept(key, null); + } else if (MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE.contains(features)) { + keyValueConsumer.accept(key, defaultValueSupplier.get()); + } else if (MATCH_IGNORE_EMPTY_VALUE.contains(features)) { + return; + } else if (MATCH_KEEP_VALUE_EMPTY.contains(features)) { + keyValueConsumer.accept(key, value); + } + return; + } + + // 解析到的参数值 是 null字符串 + if ("null".equals(value)) { + if (MATCH_NULL_STR_TO_NULL.contains(features)) { + keyValueConsumer.accept(key, null); + } else if (MATCH_KEEP_NULL_STR.contains(features)) { + keyValueConsumer.accept(key, value); + } else if (MATCH_IGNORE_NULL_STR.contains(features)) { + return; + } + return; + } + + // 普通参数值 + keyValueConsumer.accept(key, value); + } + // endregion + // endregion + + /** + * 是否有默认值 + * + * @return 是否有默认值 + */ + protected boolean hasDefaultValue() { + return defaultValue != null || defaultValueHandler != null || globalDefaultValueHandler != null; + } + + /** + * 根据 占位符 返回默认值 + *

根据定义的默认值、默认值提供者、全局默认值提供者,返回默认值

+ * + * @param segment 占位符 + * @return 默认值 + */ + protected String getDefaultValue(final AbstractPlaceholderSegment segment) { + if (defaultValue != null) { + return defaultValue; + } else if (defaultValueHandler != null) { + return StrUtil.utf8Str(defaultValueHandler.apply(segment.getPlaceholder())); + } else if (globalDefaultValueHandler != null) { + return StrUtil.utf8Str(globalDefaultValueHandler.apply(segment.getPlaceholder())); + } + throw new UtilException("There is no default value for key: '" + segment.getPlaceholder() + + "'. You should define a 'defaultValue' or 'defaultValueHandler' or 'globalDefaultValueHandler' when building."); + } + + /** + * 一些公共的初始化代码 + *

由于此时子类还没构造完成,所以只能由子类构造方法调用

+ */ + protected void afterInit() { + // 解析 并 优化 segment 列表 + this.segments = optimizeSegments(parseSegments(template)); + + // 计算 固定文本segment 的 数量 和 文本总长度 + int literalSegmentSize = 0, fixedTextTotalLength = 0; + for (StrTemplateSegment segment : this.segments) { + if (segment instanceof LiteralSegment) { + ++literalSegmentSize; + fixedTextTotalLength += segment.getText().length(); + } + } + this.fixedTextTotalLength = fixedTextTotalLength; + + // 获取 占位符segment 列表 + final int placeholderSegmentsSize = segments.size() - literalSegmentSize; + if (placeholderSegmentsSize == 0) { + this.placeholderSegments = ListUtil.zero(); + } else { + List placeholderSegments = new ArrayList<>(placeholderSegmentsSize); + for (StrTemplateSegment segment : segments) { + if (segment instanceof AbstractPlaceholderSegment) { + placeholderSegments.add((AbstractPlaceholderSegment) segment); + } + } + this.placeholderSegments = placeholderSegments; + } + } + + + /** + * 将 模板 解析为 Segment 列表 + * + * @param template 字符串模板 + * @return Segment列表 + */ + protected abstract List parseSegments(String template); + + /** + * 获取 模板中 所有segment + * + * @return segment列表 + */ + protected List getSegments() { + return segments; + } + + /** + * 获取 模板中的 占位符 segment + * + * @return 占位符列表 + */ + protected List getPlaceholderSegments() { + return placeholderSegments; + } + + /** + * 优化节点列表 + *

移除空文本节点,合并连续的文本节点

+ * + * @param segments 节点列表 + * @return 不占用多余空间的节点列表 + */ + private List optimizeSegments(final List segments) { + if (CollUtil.isEmpty(segments)) { + return segments; + } + + final List list = new ArrayList<>(segments.size()); + StrTemplateSegment last; + for (StrTemplateSegment segment : segments) { + if (segment instanceof LiteralSegment) { + // 空的文本节点,没有任何意义 + if (segment.getText().isEmpty()) { + continue; + } + if (list.isEmpty()) { + list.add(segment); + continue; + } + last = list.get(list.size() - 1); + // 如果是两个连续的文本节点,需要合并 + if (last instanceof LiteralSegment) { + list.set(list.size() - 1, new LiteralSegment(last.getText() + segment.getText())); + } else { + list.add(segment); + } + } else { + list.add(segment); + } + } + // 释放空闲的列表元素 + return list.size() == segments.size() ? list : new ArrayList<>(list); + } + + /** + * 抽象Builder + * + * @param Builder子类 + * @param 模板子类 + */ + protected static abstract class AbstractBuilder, TemplateChild extends StrTemplate> { + /** + * 字符串模板 + */ + protected final String template; + /** + * 默认值 + */ + protected String defaultValue; + /** + * 默认值处理器 + */ + protected UnaryOperator defaultValueHandler; + /** + * 用户是否设置了 转义符 + */ + protected boolean escape$set; + /** + * 转义符 + */ + protected char escape; + /** + * 策略值 + */ + protected int features; + + protected AbstractBuilder(final String template) { + this.template = Objects.requireNonNull(template); + // 策略值 初始为 全局默认策略 + this.features = StrTemplate.globalFeatures; + } + + /** + * 设置 转义符 + * + * @param escape 转义符 + * @return builder子对象 + */ + public BuilderChild escape(final char escape) { + this.escape = escape; + this.escape$set = true; + return self(); + } + + /** + * 设置 新的策略值,完全覆盖旧的策略值 + * + * @param newFeatures 新策略枚举 + * @return builder子对象 + */ + public BuilderChild features(final Feature... newFeatures) { + this.features = Feature.of(newFeatures); + return self(); + } + + /** + * 向 策略值 中 添加策略 + *

同组内的策略是互斥的,一但设置为组内的某个新策略,就会清除之前的同组策略,仅保留新策略

+ * + * @param appendFeatures 需要新增的策略 + * @return builder子对象 + */ + public BuilderChild addFeatures(final Feature... appendFeatures) { + if (ArrayUtil.isNotEmpty(appendFeatures)) { + for (Feature feature : appendFeatures) { + this.features = feature.set(this.features); + } + } + return self(); + } + + /** + * 从 策略值 中 删除策略 + *

删除的策略 可以 不存在

+ * + * @param removeFeatures 需要删除的策略 + * @return builder子对象 + */ + public BuilderChild removeFeatures(final Feature... removeFeatures) { + if (ArrayUtil.isNotEmpty(removeFeatures)) { + for (Feature feature : removeFeatures) { + this.features = feature.clear(this.features); + } + } + return self(); + } + + /** + * 设置 默认值 + *

不可能为 {@code null},可以为 {@code "null"}

+ * + * @param defaultValue 默认值 + * @return builder子对象 + */ + public BuilderChild defaultValue(final String defaultValue) { + this.defaultValue = Objects.requireNonNull(defaultValue); + return self(); + } + + /** + * 设置 默认值处理器 + * + * @param defaultValueHandler 默认值处理器,根据 占位变量 返回 默认值 + * @return builder子对象 + */ + public BuilderChild defaultValue(final UnaryOperator defaultValueHandler) { + this.defaultValueHandler = Objects.requireNonNull(defaultValueHandler); + return self(); + } + + /** + * 创建 模板对象 + * + * @return 模板对象 + */ + public TemplateChild build() { + if (!this.escape$set) { + this.escape = DEFAULT_ESCAPE; + } + return buildInstance(); + } + + /** + * 设置 转义符 + * + * @return builder子对象 + */ + protected abstract BuilderChild self(); + + /** + * 子类Builder 返回 创建的 模板对象 + * + * @return 模板对象 + */ + protected abstract TemplateChild buildInstance(); + } + + /** + * 格式化 和 解析 策略 + *

同组内的策略是互斥的,一但设置为组内的某个新策略,就会清除之前的同组策略,仅保留新策略

+ */ + public enum Feature { + // region 格式化策略 + // ======================================== 格式化策略 ======================================== + + // region 占位符没有对应值策略组 + // 传递的格式化参数中找不到 占位变量,例如:占位符有三个,格式化时仅传入两个值;map中不包含占位符变量这个key;按下标格式化时,传入的列表不包含这个下标时; + // ==================== 占位符没有对应值策略组 ==================== + /** + * 格式化时,如果 占位符 没有 对应的值,则打印完整占位符
+ * 对于 变量占位符,例如"${name}",原样打印"${name}"
+ *

默认策略

+ */ + FORMAT_MISSING_KEY_PRINT_WHOLE_PLACEHOLDER(0, 0, 6), + /** + * 格式化时,如果 占位符 没有 对应的值,则打印 默认值,如果 没有默认值,则抛出异常
+ */ + FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE(1, 0, 6), + /** + * 格式化时,如果 占位符 没有 对应的值,且没有默认值,则打印 {@code "null"}字符串
+ */ + FORMAT_MISSING_KEY_PRINT_NULL(2, 0, 6), + /** + * 格式化时,如果 占位符 没有 对应的值,则打印 空字符串
+ *

该策略意味着 模板存在默认值,且为 空字符串

+ */ + FORMAT_MISSING_KEY_PRINT_EMPTY(3, 0, 6), + /** + * 格式化时,如果 占位符 没有 对应的值:
+ * 对于 单个占位符,例如"?",打印完整占位符"?";
+ * 对于 变量占位符,则只打印占位变量,例如"${name}",只打印"name";
+ */ + FORMAT_MISSING_KEY_PRINT_VARIABLE_NAME(4, 0, 6), + /** + * 格式化时,如果 占位符 没有 对应的值,则抛出异常
+ */ + FORMAT_MISSING_KEY_THROWS(5, 0, 6), + //endregion + + // region null值策略组 + // ==================== null值策略组 ==================== + /** + * 格式化时,如果 占位符 对应的值为 {@code null},则打印 {@code "null"} 字符串 + *

默认策略

+ */ + FORMAT_NULL_VALUE_TO_STR(6, 6, 4), + /** + * 格式化时,如果 占位符 对应的值为 {@code null},则打印 {@code ""} 空字符串 + */ + FORMAT_NULL_VALUE_TO_EMPTY(7, 6, 4), + /** + * 格式化时,如果 占位符 对应的值为 {@code null},则原样打印占位符
+ * 对于 变量占位符,输出完整占位符,例如"${name}",打印"${name}"
+ */ + FORMAT_NULL_VALUE_TO_WHOLE_PLACEHOLDER(8, 6, 4), + /** + * 格式化时,如果 占位符 对应的值为 {@code null},则使用 默认值,如果 没有默认值,则抛出异常
+ */ + FORMAT_NULL_VALUE_TO_DEFAULT_VALUE(9, 6, 4), + //endregion + //endregion + + // region 解析策略 + // 解析策略校验顺序: 默认值策略、空字符串策略、null字符串策略 + // ======================================== 解析策略 ======================================== + + // region 默认值策略组 + // ==================== 默认值策略组 ==================== + /** + * 解析时,结果中 包含 默认值,原样返回 + *

默认策略

+ */ + MATCH_KEEP_DEFAULT_VALUE(16, 16, 3), + /** + * 解析时,结果中 不包含 默认值,只要等于默认值,都忽略 + *

即,返回的结果 map 中 不会包含 这个key

+ *

在 基于下标的解析方法中 不生效,基于下标的解析结果只区分是否为 {@code null},元素数量是固定的

+ */ + MATCH_IGNORE_DEFAULT_VALUE(17, 16, 3), + /** + * 解析时,在 结果中 将 默认值 转为 {@code null} + *

返回的结果 map 中 包含 这个key

+ */ + MATCH_DEFAULT_VALUE_TO_NULL(18, 16, 3), + // endregion + + // region 空字符串策略组 + // 占位符 位置的值是 空字符串 + // ==================== 空字符串策略组 ==================== + /** + * 解析时,占位符 对应的值为 空字符串,将 这个空字符串 转为 {@code null} + *

默认策略

+ */ + MATCH_EMPTY_VALUE_TO_NULL(19, 19, 4), + /** + * 解析时,占位符 对应的值为 空字符串,将 这个空字符串 转为 默认值,如果 没有默认值,则转为 {@code null} + */ + MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE(20, 19, 4), + /** + * 解析时,占位符 对应的值为 空字符串,结果中 不包含 这个空字符串 + *

即,返回的结果 map 中 不会包含 这个key

+ *

在 基于下标的解析方法中 不生效,基于下标的解析结果只区分是否为 {@code null},元素数量是固定的

+ */ + MATCH_IGNORE_EMPTY_VALUE(21, 19, 4), + /** + * 解析时,占位符 对应的值为 空字符串,结果中 依然保留 这个空字符串 + */ + MATCH_KEEP_VALUE_EMPTY(22, 19, 4), + // endregion + + // region null值策略组 + // ==================== null值策略组 ==================== + /** + * 解析时,占位符 对应的值为 {@code "null"} 字符串,在 结果中 转为 {@code null} + *

默认策略

+ */ + MATCH_NULL_STR_TO_NULL(23, 23, 3), + /** + * 解析时,占位符 对应的值为 {@code "null"} 字符串,在 结果中 保留字符串形式 {@code "null"} + */ + MATCH_KEEP_NULL_STR(24, 23, 3), + /** + * 解析时,占位符 对应的值为 {@code "null"} 字符串,结果中 不包含 这个值 + *

即,返回的结果 map 中 不会包含 这个key

+ *

在 基于下标的解析方法中 不生效,基于下标的解析结果只区分是否为 {@code null},元素数量是固定的

+ */ + MATCH_IGNORE_NULL_STR(25, 23, 3), + // endregion + // endregion + ; + /** + * 掩码 + */ + private final int mask; + /** + * 清除掩码的二进制值 + */ + private final int clearMask; + + /** + * 策略构造方法 + * + * @param bitPos 位数,掩码中哪一位需要置为1,从0开始 + * @param bitStart 同组第一个策略的掩码位数 + * @param bitLen 同组策略数量 + */ + Feature(int bitPos, int bitStart, int bitLen) { + this.mask = 1 << bitPos; + this.clearMask = (-1 << (bitStart + bitLen)) | ((1 << bitStart) - 1); + } + + /** + * 是否为当前策略 + * + * @param features 外部的策略值 + * @return 是否为当前策略 + */ + public boolean contains(int features) { + return (features & mask) != 0; + } + + /** + * 在 策略值 中添加 当前策略 + * + * @param features 外部的策略值 + * @return 添加后的策略值 + */ + public int set(int features) { + return (features & clearMask) | mask; + } + + /** + * 在 策略值 中移除 当前策略 + * + * @param features 外部的策略值 + * @return 移除后的策略值 + */ + public int clear(int features) { + return (features & clearMask); + } + + /** + * 计算 总的策略值 + * + * @param features 策略枚举数组 + * @return 总的策略值 + */ + public static int of(Feature... features) { + if (features == null) { + return 0; + } + + int value = 0; + for (Feature feature : features) { + value = feature.set(value); + } + + return value; + } + } } diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/package-info.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/package-info.java new file mode 100644 index 000000000..375a40c17 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +/** + * 字符串占位符相关封装,包括占位符替换变量和解析变量 + */ +package org.dromara.hutool.core.text.placeholder; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/NamedPlaceholderStrTemplate.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/NamedPlaceholderStrTemplate.java index e5c954890..9ba13a261 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/NamedPlaceholderStrTemplate.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/NamedPlaceholderStrTemplate.java @@ -7,8 +7,8 @@ import org.dromara.hutool.core.collection.CollUtil; import org.dromara.hutool.core.collection.ListUtil; import org.dromara.hutool.core.convert.Convert; import org.dromara.hutool.core.exceptions.UtilException; +import org.dromara.hutool.core.func.LambdaUtil; import org.dromara.hutool.core.lang.Assert; -import org.dromara.hutool.core.lang.func.LambdaUtil; import org.dromara.hutool.core.math.NumberUtil; import org.dromara.hutool.core.text.StrPool; import org.dromara.hutool.core.text.placeholder.StrTemplate; diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/util/CoordinateUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/data/CoordinateUtilTest.java similarity index 79% rename from hutool-core/src/test/java/org/dromara/hutool/core/util/CoordinateUtilTest.java rename to hutool-core/src/test/java/org/dromara/hutool/core/data/CoordinateUtilTest.java index 7df504613..9cdcbb750 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/util/CoordinateUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/data/CoordinateUtilTest.java @@ -1,4 +1,16 @@ -package org.dromara.hutool.core.util; +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.core.data; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/util/CreditCodeUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/data/CreditCodeUtilTest.java similarity index 61% rename from hutool-core/src/test/java/org/dromara/hutool/core/util/CreditCodeUtilTest.java rename to hutool-core/src/test/java/org/dromara/hutool/core/data/CreditCodeUtilTest.java index 0a81875ce..906fd50c2 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/util/CreditCodeUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/data/CreditCodeUtilTest.java @@ -1,5 +1,18 @@ -package org.dromara.hutool.core.util; +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ +package org.dromara.hutool.core.data; + +import org.dromara.hutool.core.data.CreditCodeUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/util/MaskingUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java similarity index 88% rename from hutool-core/src/test/java/org/dromara/hutool/core/util/MaskingUtilTest.java rename to hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java index 968932f5c..8d3636a96 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/util/MaskingUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/data/MaskingUtilTest.java @@ -1,6 +1,18 @@ -package org.dromara.hutool.core.util; +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ -import org.dromara.hutool.core.text.MaskingUtil; +package org.dromara.hutool.core.data; + +import org.dromara.hutool.core.data.MaskingUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/data/PasswdStrengthTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/data/PasswdStrengthTest.java new file mode 100644 index 000000000..976a88da6 --- /dev/null +++ b/hutool-core/src/test/java/org/dromara/hutool/core/data/PasswdStrengthTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.core.data; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PasswdStrengthTest { + @Test + public void strengthTest(){ + final String passwd = "2hAj5#mne-ix.86H"; + Assertions.assertEquals(13, PasswdStrength.check(passwd)); + } + + @Test + public void strengthNumberTest(){ + final String passwd = "9999999999999"; + Assertions.assertEquals(0, PasswdStrength.check(passwd)); + } +} diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/util/PhoneUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/data/PhoneUtilTest.java similarity index 84% rename from hutool-core/src/test/java/org/dromara/hutool/core/util/PhoneUtilTest.java rename to hutool-core/src/test/java/org/dromara/hutool/core/data/PhoneUtilTest.java index 8bcdc5228..0eeaf1b8c 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/util/PhoneUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/data/PhoneUtilTest.java @@ -1,5 +1,18 @@ -package org.dromara.hutool.core.util; +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ +package org.dromara.hutool.core.data; + +import org.dromara.hutool.core.data.PhoneUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/lang/StrFormatterTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/lang/StrFormatterTest.java index f110fa573..f649d541e 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/lang/StrFormatterTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/lang/StrFormatterTest.java @@ -1,6 +1,6 @@ package org.dromara.hutool.core.lang; -import org.dromara.hutool.core.text.StrFormatter; +import org.dromara.hutool.core.text.placeholder.StrFormatter; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/text/PasswdStrengthTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/text/PasswdStrengthTest.java deleted file mode 100644 index f66987a71..000000000 --- a/hutool-core/src/test/java/org/dromara/hutool/core/text/PasswdStrengthTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.dromara.hutool.core.text; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class PasswdStrengthTest { - @Test - public void strengthTest(){ - final String passwd = "2hAj5#mne-ix.86H"; - Assertions.assertEquals(13, PasswdStrength.check(passwd)); - } - - @Test - public void strengthNumberTest(){ - final String passwd = "9999999999999"; - Assertions.assertEquals(0, PasswdStrength.check(passwd)); - } -} diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/text/PlaceholderParserTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/text/PlaceholderParserTest.java index 23b7baa13..eea266c1e 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/text/PlaceholderParserTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/text/PlaceholderParserTest.java @@ -1,5 +1,6 @@ package org.dromara.hutool.core.text; +import org.dromara.hutool.core.text.placeholder.PlaceholderParser; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/text/StrMatcherTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/text/StrMatcherTest.java index ccf2aee62..373915cbb 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/text/StrMatcherTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/text/StrMatcherTest.java @@ -1,5 +1,6 @@ package org.dromara.hutool.core.text; +import org.dromara.hutool.core.text.placeholder.StrMatcher; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test;