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/PlaceholderParser.java index 83f1ae8f1..9f5b4b970 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/PlaceholderParser.java @@ -129,7 +129,7 @@ public class PlaceholderParser implements UnaryOperator { continue; } - // 记录当前位符的开始符号与上一占位符的结束符号间的字符串 + // 记录当前占位符的开始符号与上一占位符的结束符号间的字符串 result.append(src, closeCursor, openCursor - closeCursor); // 重置结束游标至当前占位符的开始处 @@ -160,7 +160,7 @@ public class PlaceholderParser implements UnaryOperator { result.append(processor.apply(expression.toString())); expression.setLength(0); // 完成当前占位符的处理匹配,寻找下一个 - closeCursor = end + close.length(); + closeCursor = end + closeLength; } // 寻找下一个开始符号 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/StrFormatter.java index 96a3f3b46..e543a4eec 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/StrFormatter.java @@ -13,7 +13,8 @@ package org.dromara.hutool.core.text; import org.dromara.hutool.core.array.ArrayUtil; -import org.dromara.hutool.core.util.CharUtil; +import org.dromara.hutool.core.text.placeholder.StrTemplate; +import org.dromara.hutool.core.text.placeholder.template.NamedPlaceholderStrTemplate; import java.util.Map; @@ -60,50 +61,10 @@ public class StrFormatter { if (StrUtil.isBlank(strPattern) || StrUtil.isBlank(placeHolder) || ArrayUtil.isEmpty(argArray)) { return strPattern; } - final int strPatternLength = strPattern.length(); - final int placeHolderLength = placeHolder.length(); - - // 初始化定义好的长度以获得更好的性能 - final StringBuilder sbuf = new StringBuilder(strPatternLength + 50); - - int handledPosition = 0;// 记录已经处理到的位置 - int delimIndex;// 占位符所在位置 - for (int argIndex = 0; argIndex < argArray.length; argIndex++) { - delimIndex = strPattern.indexOf(placeHolder, handledPosition); - if (delimIndex == -1) {// 剩余部分无占位符 - if (handledPosition == 0) { // 不带占位符的模板直接返回 - return strPattern; - } - // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 - sbuf.append(strPattern, handledPosition, strPatternLength); - return sbuf.toString(); - } - - // 转义符 - if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == CharUtil.BACKSLASH) {// 转义符 - if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == CharUtil.BACKSLASH) {// 双转义符 - // 转义符之前还有一个转义符,占位符依旧有效 - sbuf.append(strPattern, handledPosition, delimIndex - 1); - sbuf.append(StrUtil.utf8Str(argArray[argIndex])); - handledPosition = delimIndex + placeHolderLength; - } else { - // 占位符被转义 - argIndex--; - sbuf.append(strPattern, handledPosition, delimIndex - 1); - sbuf.append(placeHolder.charAt(0)); - handledPosition = delimIndex + 1; - } - } else {// 正常占位符 - sbuf.append(strPattern, handledPosition, delimIndex); - sbuf.append(StrUtil.utf8Str(argArray[argIndex])); - handledPosition = delimIndex + placeHolderLength; - } - } - - // 加入最后一个占位符后所有的字符 - sbuf.append(strPattern, handledPosition, strPatternLength); - - return sbuf.toString(); + return StrTemplate.of(strPattern) + .placeholder(placeHolder) + .build() + .format(argArray); } /** @@ -124,15 +85,12 @@ public class StrFormatter { return template.toString(); } - String template2 = template.toString(); - String value; - for (final Map.Entry entry : map.entrySet()) { - value = StrUtil.utf8Str(entry.getValue()); - if (null == value && ignoreNull) { - continue; - } - template2 = StrUtil.replace(template2, "{" + entry.getKey() + "}", value); + final NamedPlaceholderStrTemplate.Builder builder = StrTemplate.ofNamed(template.toString()); + if (ignoreNull) { + builder.addFeatures(StrTemplate.Feature.FORMAT_NULL_VALUE_TO_WHOLE_PLACEHOLDER); + } else { + builder.addFeatures(StrTemplate.Feature.FORMAT_NULL_VALUE_TO_EMPTY); } - return template2; + return builder.build().format(map); } } 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 new file mode 100644 index 000000000..f503ea419 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrTemplate.java @@ -0,0 +1,1039 @@ +package org.dromara.hutool.core.text.placeholder; + +import org.dromara.hutool.core.array.ArrayUtil; +import org.dromara.hutool.core.collection.CollUtil; +import org.dromara.hutool.core.collection.ListUtil; +import org.dromara.hutool.core.exceptions.UtilException; +import org.dromara.hutool.core.lang.Assert; +import org.dromara.hutool.core.text.CharPool; +import org.dromara.hutool.core.text.StrUtil; +import org.dromara.hutool.core.text.placeholder.segment.AbstractPlaceholderSegment; +import org.dromara.hutool.core.text.placeholder.segment.LiteralSegment; +import org.dromara.hutool.core.text.placeholder.segment.StrTemplateSegment; +import org.dromara.hutool.core.text.placeholder.template.NamedPlaceholderStrTemplate; +import org.dromara.hutool.core.text.placeholder.template.SinglePlaceholderStrTemplate; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import static org.dromara.hutool.core.text.placeholder.StrTemplate.Feature.*; + +/** + * 字符串模板 格式化 和 反解析 抽象父类 + * + * @author emptypoint + * @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; + } + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/AbstractPlaceholderSegment.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/AbstractPlaceholderSegment.java new file mode 100644 index 000000000..c3dba5c18 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/AbstractPlaceholderSegment.java @@ -0,0 +1,39 @@ +package org.dromara.hutool.core.text.placeholder.segment; + +import java.util.Iterator; + +/** + * 字符串模板-占位符-抽象 Segment + *

例如:{@literal "???"->"???", "{}"->"{}", "{name}"->"name"}

+ * + * @author emptypoint + * @since 6.0.0 + */ +public abstract class AbstractPlaceholderSegment implements StrTemplateSegment { + /** + * 占位符变量 + *

例如:{@literal "???"->"???", "{}"->"{}", "{name}"->"name"}

+ */ + private final String placeholder; + + protected AbstractPlaceholderSegment(final String placeholder) { + this.placeholder = placeholder; + } + + @Override + public String getText() { + return placeholder; + } + + @Override + public void format(final StringBuilder sb, final Iterator valueIterator) { + // 当前是 占位符,直接 替换为 参数值 + if (valueIterator.hasNext()) { + sb.append(valueIterator.next()); + } + } + + public String getPlaceholder() { + return placeholder; + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/IndexedPlaceholderSegment.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/IndexedPlaceholderSegment.java new file mode 100644 index 000000000..29fe41b1d --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/IndexedPlaceholderSegment.java @@ -0,0 +1,24 @@ +package org.dromara.hutool.core.text.placeholder.segment; + +/** + * 基字符串模板-基于下标的占位符 Segment + *

例如,"{1}"

+ * + * @author emptypoint + * @since 6.0.0 + */ +public class IndexedPlaceholderSegment extends NamedPlaceholderSegment { + /** + * 下标值 + */ + private final int index; + + public IndexedPlaceholderSegment(final String idxStr, final String wholePlaceholder) { + super(idxStr, wholePlaceholder); + this.index = Integer.parseInt(idxStr); + } + + public int getIndex() { + return index; + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/LiteralSegment.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/LiteralSegment.java new file mode 100644 index 000000000..3e2ce0efe --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/LiteralSegment.java @@ -0,0 +1,32 @@ +package org.dromara.hutool.core.text.placeholder.segment; + +import java.util.Iterator; + +/** + * 字符串模板-固定文本 Segment + * + * @author emptypoint + * @since 6.0.0 + */ +public class LiteralSegment implements StrTemplateSegment { + /** + * 模板中固定的一段文本 + */ + private final String text; + + public LiteralSegment(final String text) { + this.text = text; + } + + @Override + public String getText() { + return text; + } + + @Override + public void format(final StringBuilder sb, final Iterator valueIterator) { + // 在格式化中 拼接 固定文本 + sb.append(text); + } + +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/NamedPlaceholderSegment.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/NamedPlaceholderSegment.java new file mode 100644 index 000000000..4603595dc --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/NamedPlaceholderSegment.java @@ -0,0 +1,27 @@ +package org.dromara.hutool.core.text.placeholder.segment; + +/** + * 字符串模板-有前后缀的变量占位符 Segment + *

例如,"{1}", "{name}", "#{id}"

+ * + * @author emptypoint + * @since 6.0.0 + */ +public class NamedPlaceholderSegment extends AbstractPlaceholderSegment { + /** + * 占位符完整文本 + *

例如:{@literal "{name}"->"{name}"}

+ */ + private final String wholePlaceholder; + + public NamedPlaceholderSegment(final String name, final String wholePlaceholder) { + super(name); + this.wholePlaceholder = wholePlaceholder; + } + + @Override + public String getText() { + return wholePlaceholder; + } + +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/SinglePlaceholderSegment.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/SinglePlaceholderSegment.java new file mode 100644 index 000000000..eb843903b --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/SinglePlaceholderSegment.java @@ -0,0 +1,19 @@ +package org.dromara.hutool.core.text.placeholder.segment; + +/** + * 字符串模板-单变量占位符 Segment + *

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

+ * + * @author emptypoint + * @since 6.0.0 + */ +public class SinglePlaceholderSegment extends AbstractPlaceholderSegment { + + private SinglePlaceholderSegment(final String placeholder) { + super(placeholder); + } + + public static SinglePlaceholderSegment newInstance(final String placeholder) { + return new SinglePlaceholderSegment(placeholder); + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/StrTemplateSegment.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/StrTemplateSegment.java new file mode 100644 index 000000000..d69c83e47 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/segment/StrTemplateSegment.java @@ -0,0 +1,29 @@ +package org.dromara.hutool.core.text.placeholder.segment; + +import java.util.Iterator; + +/** + * 字符串模板-抽象 Segment + * + * @author emptypoint + * @since 6.0.0 + */ +public interface StrTemplateSegment { + /** + * 在格式化中,按顺序 拼接 参数值 + * + *

如果是固定文本,则直接拼接,如果是占位符,则拼接参数值

+ * + * @param sb 存储格式化结果的变量 + * @param valueIterator 与占位符依次对应的参数值列表 + */ + void format(final StringBuilder sb, final Iterator valueIterator); + + /** + * 获取文本值 + * + * @return 文本值,对于固定文本Segment,返回文本值;对于单占位符Segment,返回占位符;对于有前后缀的占位符Segment,返回占位符完整文本,例如: "{name}" + */ + String getText(); + +} 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 new file mode 100644 index 000000000..e5c954890 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/NamedPlaceholderStrTemplate.java @@ -0,0 +1,622 @@ +package org.dromara.hutool.core.text.placeholder.template; + +import org.dromara.hutool.core.array.ArrayUtil; +import org.dromara.hutool.core.bean.BeanDesc; +import org.dromara.hutool.core.bean.BeanUtil; +import org.dromara.hutool.core.collection.CollUtil; +import org.dromara.hutool.core.collection.ListUtil; +import org.dromara.hutool.core.convert.Convert; +import org.dromara.hutool.core.exceptions.UtilException; +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; +import org.dromara.hutool.core.text.placeholder.segment.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.*; +import java.util.function.*; + +/** + * 有前后缀的字符串模板 + *

例如,"{1}", "{name}", "#{id}"

+ * + * @author emptypoint + * @since 6.0.0 + */ +public class NamedPlaceholderStrTemplate extends StrTemplate { + /** + * 默认前缀 + */ + public static final String DEFAULT_PREFIX = StrPool.DELIM_START; + /** + * 默认后缀 + */ + public static final String DEFAULT_SUFFIX = StrPool.DELIM_END; + + /** + * 占位符前缀,默认为: {@link #DEFAULT_PREFIX} + */ + protected String prefix; + /** + * 占位符后缀,默认为: {@link #DEFAULT_SUFFIX} + */ + protected String suffix; + /** + * 在 下标占位符中,最大的下标值 + */ + protected int indexedSegmentMaxIdx = 0; + + protected NamedPlaceholderStrTemplate(final String template, final int features, final String prefix, + final String suffix, final char escape, final String defaultValue, + final UnaryOperator defaultValueHandler) { + super(template, escape, defaultValue, defaultValueHandler, features); + + Assert.notEmpty(prefix); + Assert.notEmpty(suffix); + this.prefix = prefix; + this.suffix = suffix; + + // 一些初始化后续操作 + afterInit(); + + // 记录 下标占位符 最大的 下标值 + if (!placeholderSegments.isEmpty()) { + for (AbstractPlaceholderSegment segment : placeholderSegments) { + if (segment instanceof IndexedPlaceholderSegment) { + this.indexedSegmentMaxIdx = Math.max(this.indexedSegmentMaxIdx, ((IndexedPlaceholderSegment) segment).getIndex()); + } + } + } + } + + @Override + protected List parseSegments(final String template) { + // 寻找第一个前缀符号 + int openCursor = template.indexOf(prefix); + // 没有任何占位符 + if (openCursor == -1) { + return Collections.singletonList(new LiteralSegment(template)); + } + + final int openLength = prefix.length(); + final int closeLength = suffix.length(); + List segments = new ArrayList<>(); + int closeCursor = 0; + // 开始匹配 + final char[] src = template.toCharArray(); + final StringBuilder expression = new StringBuilder(16); + boolean hasDoubleEscape = false; + // 占位变量名称 + String variableName; + // 完整的占位符 + String wholePlaceholder; + while (openCursor > -1) { + // 开始符号是否被转义,若是则跳过,并寻找下一个开始符号 + if (openCursor > 0 && src[openCursor - 1] == escape) { + // 存在 双转义符,转义符之前还有一个转义符,形如:"\\{",占位符依旧有效 + if (openCursor > 1 && src[openCursor - 2] == escape) { + hasDoubleEscape = true; + } else { + // 开始符号被转义,跳过,寻找下一个开始符号 + segments.add(new LiteralSegment( + template.substring(closeCursor, openCursor - 1) + prefix + )); + closeCursor = openCursor + openLength; + openCursor = template.indexOf(prefix, closeCursor); + continue; + } + } + + // 没有双转义符 + if (!hasDoubleEscape) { + if (closeCursor < openCursor) { + // 完整记录当前占位符的开始符号与上一占位符的结束符号间的字符串 + segments.add(new LiteralSegment(template.substring(closeCursor, openCursor))); + } + } else { + // 存在双转义符,只能保留一个转义符 + hasDoubleEscape = false; + segments.add(new LiteralSegment(template.substring(closeCursor, openCursor - 1))); + } + + // 重置结束游标至当前占位符的开始处 + closeCursor = openCursor + openLength; + + // 寻找结束符号下标 + int end = template.indexOf(suffix, closeCursor); + while (end > -1) { + // 结束符号被转义,寻找下一个结束符号 + if (end > closeCursor && src[end - 1] == escape) { + // 双转义符,保留一个转义符,并且找到了结束符 + if (end > 1 && src[end - 2] == escape) { + expression.append(src, closeCursor, end - closeCursor - 1); + break; + } else { + expression.append(src, closeCursor, end - closeCursor - 1).append(suffix); + closeCursor = end + closeLength; + end = template.indexOf(suffix, closeCursor); + } + } + // 找到结束符号 + else { + expression.append(src, closeCursor, end - closeCursor); + break; + } + } + + // 未能找到结束符号,说明匹配异常 + if (end == -1) { + throw new UtilException("\"{}\" 中字符下标 {} 处的开始符没有找到对应的结束符", template, openCursor); + } + // 找到结束符号,开始到结束符号 之间的字符串 就是占位变量 + else { + // 占位变量名称 + variableName = expression.toString(); + expression.setLength(0); + // 完整的占位符 + wholePlaceholder = expression.append(prefix).append(variableName).append(suffix).toString(); + expression.setLength(0); + // 如果是整数,则当作下标处理 + if (NumberUtil.isInteger(variableName)) { + segments.add(new IndexedPlaceholderSegment(variableName, wholePlaceholder)); + } else { + // 当作变量名称处理 + segments.add(new NamedPlaceholderSegment(variableName, wholePlaceholder)); + } + // 完成当前占位符的处理匹配,寻找下一个 + closeCursor = end + closeLength; + } + + // 寻找下一个开始符号 + openCursor = template.indexOf(prefix, closeCursor); + } + + // 若匹配结束后仍有未处理的字符串,则直接将其拼接到表达式上 + if (closeCursor < src.length) { + segments.add(new LiteralSegment(template.substring(closeCursor, src.length))); + } + return segments; + } + + // region 格式化方法 + // ################################################## 格式化方法 ################################################## + + // region 基于顺序的格式化方法 + // ############################## 基于顺序的格式化方法 ############################## + + /** + * 按顺序使用 数组元素 替换 占位符 + * + * @param args 可变参数 + * @return 格式化字符串 + */ + public String formatSequence(final Object... args) { + return formatArraySequence(args); + } + + /** + * 按顺序使用 原始数组元素 替换 占位符 + * + * @param array 原始类型数组,例如: {@code int[]} + * @return 格式化字符串 + */ + public String formatArraySequence(final Object array) { + return formatArraySequence(ArrayUtil.wrap(array)); + } + + /** + * 按顺序使用 数组元素 替换 占位符 + * + * @param array 数组 + * @return 格式化字符串 + */ + public String formatArraySequence(final Object[] array) { + if (array == null) { + return getTemplate(); + } + return formatSequence(Arrays.asList(array)); + } + + /** + * 按顺序使用 迭代器元素 替换 占位符 + * + * @param iterable iterable + * @return 格式化字符串 + */ + @Override + public String formatSequence(final Iterable iterable) { + return super.formatSequence(iterable); + } + // endregion + + // region 基于下标的格式化方法 + // ############################## 基于下标的格式化方法 ############################## + + /** + * 按 下标 使用 数组元素 替换 占位符 + * + * @param args 可变参数 + * @return 格式化字符串 + */ + public String formatIndexed(final Object... args) { + return formatArrayIndexed(args); + } + + /** + * 按 下标 使用 原始数组元素 替换 占位符 + * + * @param array 原始类型数组 + * @return 格式化字符串 + */ + public String formatArrayIndexed(final Object array) { + return formatArrayIndexed(ArrayUtil.wrap(array)); + } + + /** + * 按 下标 使用 数组元素 替换 占位符 + * + * @param array 数组 + * @return 格式化字符串 + */ + public String formatArrayIndexed(final Object[] array) { + if (array == null) { + return getTemplate(); + } + return formatIndexed(Arrays.asList(array)); + } + + /** + * 按 下标 使用 集合元素 替换 占位符 + * + * @param collection 集合元素 + * @return 格式化字符串 + */ + public String formatIndexed(final Collection collection) { + return formatIndexed(collection, null); + } + + /** + * 按 下标 使用 集合元素 替换 占位符 + * + * @param collection 集合元素 + * @param missingIndexHandler 集合中不存在下标位置时的处理器,根据 下标 返回 代替值 + * @return 格式化字符串 + */ + public String formatIndexed(final Collection collection, IntFunction missingIndexHandler) { + if (collection == null) { + return getTemplate(); + } + + final int size = collection.size(); + final boolean isList = collection instanceof List; + return formatBySegment(segment -> { + int index = ((IndexedPlaceholderSegment) segment).getIndex(); + if (index < 0) { + index += size; + } + if (index >= 0 && index < size) { + if (isList) { + return ((List) collection).get(index); + } + return CollUtil.get(collection, index); + } + // 下标越界,代表 占位符 没有对应值,尝试获取代替值 + else if (missingIndexHandler != null) { + return missingIndexHandler.apply(index); + } else { + return formatMissingKey(segment); + } + }); + } + // endregion + + // region 基于键值的格式化方法 + // ############################## 基于键值的格式化方法 ############################## + + /** + * 使用 占位变量名称 从 Bean 或 Map 中查询值来 替换 占位符 + * + * @param beanOrMap Bean 或 Map 实例 + * @return 格式化字符串 + */ + @SuppressWarnings("unchecked") + public String format(final Object beanOrMap) { + if (beanOrMap == null) { + return getTemplate(); + } + if (beanOrMap instanceof Map) { + return format((Map) beanOrMap); + } else if (BeanUtil.isReadableBean(beanOrMap.getClass())) { + final BeanDesc beanDesc = BeanUtil.getBeanDesc(beanOrMap.getClass()); + return format(fieldName -> { + final Method getterMethod = beanDesc.getGetter(fieldName); + if (getterMethod == null) { + return null; + } + return LambdaUtil.buildGetter(getterMethod).apply(beanOrMap); + }); + } + return format(fieldName -> BeanUtil.getFieldValue(beanOrMap, fieldName)); + } + + /** + * 使用 占位变量名称 从 Map 中查询值来 替换 占位符 + * + * @param map map + * @return 格式化字符串 + */ + public String format(final Map map) { + if (map == null) { + return getTemplate(); + } + return format(map::get, map::containsKey); + } + + /** + * 使用 占位变量名称 从 valueSupplier 中查询值来 替换 占位符 + * + * @param valueSupplier 根据 占位变量名称 返回 值 + * @return 格式化字符串 + */ + public String format(final Function valueSupplier) { + if (valueSupplier == null) { + return getTemplate(); + } + return formatBySegment(segment -> valueSupplier.apply(segment.getPlaceholder())); + } + + /** + * 使用 占位变量名称 从 valueSupplier 中查询值来 替换 占位符 + * + * @param valueSupplier 根据 占位变量名称 返回 值 + * @param containsKey 占位变量名称 是否存在,例如:{@code map.containsKey(key)} + * @return 格式化字符串 + */ + public String format(final Function valueSupplier, final Predicate containsKey) { + if (valueSupplier == null || containsKey == null) { + return getTemplate(); + } + + return formatBySegment(segment -> { + final String placeholder = segment.getPlaceholder(); + if (containsKey.test(placeholder)) { + return valueSupplier.apply(placeholder); + } + return formatMissingKey(segment); + }); + } + // endregion + // endregion + + // region 解析方法 + // ################################################## 解析方法 ################################################## + + // region 基于顺序的解析方法 + // ############################## 基于顺序的解析方法 ############################## + + /** + * 将 占位符位置的值 按顺序解析为 字符串数组 + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @return 字符串数组 + */ + public String[] matchesSequenceToArray(final String str) { + return matchesSequence(str).toArray(new String[0]); + } + + /** + * 将 占位符位置的值 按顺序解析为 字符串列表 + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @return 字符串列表 + */ + @Override + public List matchesSequence(final String str) { + return super.matchesSequence(str); + } + // endregion + + // region 基于下标的解析方法 + // ############################## 基于下标的解析方法 ############################## + + /** + * 将 占位符位置的值 按 占位符下标值 解析为 字符串数组 + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @return 字符串数组 + * @see #matchesIndexed(String, IntFunction) + */ + public String[] matchesIndexedToArray(final String str) { + return matchesIndexed(str, null).toArray(new String[0]); + } + + /** + * 将 占位符位置的值 按 占位符下标值 解析为 字符串数组 + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @param missingIndexHandler 根据 下标 返回 默认值,该参数可以为 {@code null},仅在 {@link Feature#MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE} 策略时生效 + * @return 字符串数组 + * @see #matchesIndexed(String, IntFunction) + */ + public String[] matchesIndexedToArray(final String str, final IntFunction missingIndexHandler) { + return matchesIndexed(str, missingIndexHandler).toArray(new String[0]); + } + + /** + * 将 占位符位置的值 按 占位符下标值 解析为 字符串列表 + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @return 字符串列表 + * @see #matchesIndexed(String, IntFunction) + */ + public List matchesIndexed(final String str) { + return matchesIndexed(str, null); + } + + /** + * 将 占位符位置的值 按 占位符下标值 解析为 字符串列表 + * + *

例如,模板中为 {@literal "This is between {1} and {2}"},格式化结果为 {@literal "This is between 666 and 999"}, + * 由于其最大下标为 2, 则解析结果中固定有 3 个元素,解析结果为 {@code [null, "666", "999"]}

+ * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @param missingIndexHandler 根据 下标 返回 默认值,该参数可以为 {@code null},仅在 {@link Feature#MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE} 策略时生效 + * @return 字符串列表 + */ + public List matchesIndexed(final String str, final IntFunction missingIndexHandler) { + if (str == null || placeholderSegments.isEmpty() || !isMatches(str)) { + return ListUtil.zero(); + } + + final List params = new ArrayList<>(this.indexedSegmentMaxIdx + 1); + // 用null值填充所有位置 + ListUtil.setOrPadding(params, this.indexedSegmentMaxIdx, null, null); + matchesIndexed(str, params::set, missingIndexHandler); + return params; + } + + /** + * 根据 下标 和 下标占位符位置的值,自行提取结果值 + * + *

例如,模板中为 {@literal "This is between {1} and {2}"},格式化结果为 {@literal "This is between 666 and 999"}, + * 由于其最大下标为 2, 则解析结果中固定有 3 个元素,解析结果为 {@code [null, "666", "999"]}

+ * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @param idxValueConsumer 处理 下标 和 下标占位符位置的值 的消费者,例如:
{@code (idx, value) -> list.set(idx, value)} + * @param missingIndexHandler 根据 下标 返回 默认值,该参数可以为 {@code null},仅在 {@link Feature#MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE} 策略时生效 + */ + public void matchesIndexed(final String str, final BiConsumer idxValueConsumer, + final IntFunction missingIndexHandler) { + if (str == null || CollUtil.isEmpty(placeholderSegments) || !isMatches(str)) { + return; + } + + if (missingIndexHandler == null) { + matchesByKey(str, (key, value) -> idxValueConsumer.accept(Integer.parseInt(key), value)); + } else { + matchesByKey(str, (key, value) -> idxValueConsumer.accept(Integer.parseInt(key), value), true, segment -> { + if ((segment instanceof IndexedPlaceholderSegment)) { + return missingIndexHandler.apply(((IndexedPlaceholderSegment) segment).getIndex()); + } + return getDefaultValue(segment); + }); + } + } + // endregion + + // region 基于键值的解析方法 + // ############################## 基于键值的解析方法 ############################## + + /** + * 根据 占位变量 和 对应位置解析值 构造 {@link Map} + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @return {@link Map} + */ + public Map matches(final String str) { + return matches(str, HashMap::new); + } + + /** + * 根据 占位变量 和 对应位置解析值 构造 map 或者 bean 实例 + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @param beanOrMapSupplier 提供一个 bean 或者 map,例如:{@code HashMap::new} + * @param 返回结果对象类型 + * @return map 或者 bean 实例 + */ + public T matches(final String str, final Supplier beanOrMapSupplier) { + Assert.notNull(beanOrMapSupplier, "beanOrMapSupplier cannot be null"); + final T obj = beanOrMapSupplier.get(); + if (str == null || obj == null || placeholderSegments.isEmpty() || !isMatches(str)) { + return obj; + } + + if (obj instanceof Map) { + @SuppressWarnings("unchecked") final Map map = (Map) obj; + matchesByKey(str, map::put); + } else if (BeanUtil.isReadableBean(obj.getClass())) { + final BeanDesc beanDesc = BeanUtil.getBeanDesc(obj.getClass()); + matchesByKey(str, (key, value) -> { + final Field field = beanDesc.getField(key); + final Method setterMethod = beanDesc.getSetter(key); + if (field == null || setterMethod == null) { + return; + } + final Object convert = Convert.convert(field.getType(), value); + LambdaUtil.buildSetter(setterMethod).accept(obj, convert); + }); + } + return obj; + } + // endregion + // endregion + + /** + * 创建 builder + * + * @param template 字符串模板,不能为 {@code null} + * @return builder实例 + */ + public static Builder builder(final String template) { + return new Builder(template); + } + + public static class Builder extends AbstractBuilder { + /** + * 占位符前缀,默认为 {@link NamedPlaceholderStrTemplate#DEFAULT_PREFIX} + *

不能为空字符串

+ */ + protected String prefix; + /** + * 占位符后缀,默认为 {@link NamedPlaceholderStrTemplate#DEFAULT_SUFFIX} + *

不能为空字符串

+ */ + protected String suffix; + + protected Builder(final String template) { + super(template); + } + + /** + * 设置 占位符前缀 + * + * @param prefix 占位符前缀,不能为空字符串 + * @return builder + */ + public Builder prefix(final String prefix) { + this.prefix = prefix; + return this; + } + + /** + * 设置 占位符后缀 + * + * @param suffix 占位符后缀,不能为空字符串 + * @return builder + */ + public Builder suffix(final String suffix) { + this.suffix = suffix; + return this; + } + + @Override + protected NamedPlaceholderStrTemplate buildInstance() { + if (this.prefix == null) { + this.prefix = DEFAULT_PREFIX; + } + if (this.suffix == null) { + this.suffix = DEFAULT_SUFFIX; + } + return new NamedPlaceholderStrTemplate(this.template, this.features, this.prefix, this.suffix, this.escape, this.defaultValue, this.defaultValueHandler); + } + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/SinglePlaceholderStrTemplate.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/SinglePlaceholderStrTemplate.java new file mode 100644 index 000000000..5ca295875 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/SinglePlaceholderStrTemplate.java @@ -0,0 +1,215 @@ +package org.dromara.hutool.core.text.placeholder.template; + +import org.dromara.hutool.core.array.ArrayUtil; +import org.dromara.hutool.core.lang.Assert; +import org.dromara.hutool.core.text.StrPool; +import org.dromara.hutool.core.text.placeholder.StrTemplate; +import org.dromara.hutool.core.text.placeholder.segment.LiteralSegment; +import org.dromara.hutool.core.text.placeholder.segment.SinglePlaceholderSegment; +import org.dromara.hutool.core.text.placeholder.segment.StrTemplateSegment; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +/** + * 单占位符字符串模板 + *

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

+ * + * @author emptypoint + * @since 6.0.0 + */ +public class SinglePlaceholderStrTemplate extends StrTemplate { + /** + * 默认的占位符 + */ + public static final String DEFAULT_PLACEHOLDER = StrPool.EMPTY_JSON; + + /** + * 占位符,默认为: {@link StrPool#EMPTY_JSON} + */ + protected String placeholder; + + protected SinglePlaceholderStrTemplate(final String template, final int features, final String placeholder, final char escape, + final String defaultValue, final UnaryOperator defaultValueHandler) { + super(template, escape, defaultValue, defaultValueHandler, features); + + Assert.notEmpty(placeholder); + this.placeholder = placeholder; + + // 初始化Segment列表 + afterInit(); + } + + @Override + protected List parseSegments(final String template) { + final int placeholderLength = placeholder.length(); + final int strPatternLength = template.length(); + // 记录已经处理到的位置 + int handledPosition = 0; + // 占位符所在位置 + int delimIndex; + // 复用的占位符变量 + final SinglePlaceholderSegment singlePlaceholderSegment = SinglePlaceholderSegment.newInstance(placeholder); + List segments = null; + while (true) { + delimIndex = template.indexOf(placeholder, handledPosition); + if (delimIndex == -1) { + // 整个模板都不带占位符 + if (handledPosition == 0) { + return Collections.singletonList(new LiteralSegment(template)); + } + // 字符串模板剩余部分不再包含占位符 + if (handledPosition < strPatternLength) { + segments.add(new LiteralSegment(template.substring(handledPosition, strPatternLength))); + } + return segments; + } else if (segments == null) { + segments = new ArrayList<>(); + } + + // 存在 转义符 + if (delimIndex > 0 && template.charAt(delimIndex - 1) == escape) { + // 存在 双转义符 + if (delimIndex > 1 && template.charAt(delimIndex - 2) == escape) { + // 转义符之前还有一个转义符,形如:"//{",占位符依旧有效 + segments.add(new LiteralSegment(template.substring(handledPosition, delimIndex - 1))); + segments.add(singlePlaceholderSegment); + handledPosition = delimIndex + placeholderLength; + } else { + // 占位符被转义,形如:"/{",当前字符并不是一个真正的占位符,而是普通字符串的一部分 + segments.add(new LiteralSegment( + template.substring(handledPosition, delimIndex - 1) + placeholder.charAt(0) + )); + handledPosition = delimIndex + 1; + } + } else { + // 正常占位符 + segments.add(new LiteralSegment(template.substring(handledPosition, delimIndex))); + segments.add(singlePlaceholderSegment); + handledPosition = delimIndex + placeholderLength; + } + } + } + + // region 格式化方法 + // ################################################## 格式化方法 ################################################## + + /** + * 按顺序使用 数组元素 替换 占位符 + * + * @param args 可变参数 + * @return 格式化字符串 + */ + public String format(final Object... args) { + return formatArray(args); + } + + /** + * 按顺序使用 原始数组元素 替换 占位符 + * + * @param array 原始类型数组,例如: {@code int[]} + * @return 格式化字符串 + */ + public String formatArray(final Object array) { + return formatArray(ArrayUtil.wrap(array)); + } + + /** + * 按顺序使用 数组元素 替换 占位符 + * + * @param array 数组 + * @return 格式化字符串 + */ + public String formatArray(final Object[] array) { + if (array == null) { + return getTemplate(); + } + return format(Arrays.asList(array)); + } + + /** + * 按顺序使用 迭代器元素 替换 占位符 + * + * @param iterable iterable + * @return 格式化字符串 + */ + public String format(final Iterable iterable) { + return super.formatSequence(iterable); + } + // endregion + + // region 解析方法 + // ################################################## 解析方法 ################################################## + + /** + * 将 占位符位置的值 按顺序解析为 字符串数组 + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @return 参数值数组 + */ + public String[] matchesToArray(final String str) { + return matches(str).toArray(new String[0]); + } + + /** + * 将 占位符位置的值 按顺序解析为 字符串列表 + * + * @param str 待解析的字符串,一般是格式化方法的返回值 + * @return 参数值列表 + */ + public List matches(final String str) { + return super.matchesSequence(str); + } + // endregion + + /** + * 创建 builder + * + * @param template 字符串模板,不能为 {@code null} + * @return builder实例 + */ + public static Builder builder(final String template) { + return new Builder(template); + } + + public static class Builder extends AbstractBuilder { + /** + * 单占位符 + *

例如:"?"、"{}"

+ *

默认为 {@link SinglePlaceholderStrTemplate#DEFAULT_PLACEHOLDER}

+ */ + protected String placeholder; + + protected Builder(final String template) { + super(template); + } + + /** + * 设置 占位符 + * + * @param placeholder 占位符,不能为 {@code null} 和 {@code ""} + * @return builder + */ + public Builder placeholder(final String placeholder) { + this.placeholder = placeholder; + return this; + } + + @Override + protected SinglePlaceholderStrTemplate buildInstance() { + if (this.placeholder == null) { + this.placeholder = DEFAULT_PLACEHOLDER; + } + return new SinglePlaceholderStrTemplate(this.template, this.features, this.placeholder, this.escape, + this.defaultValue, this.defaultValueHandler); + } + + @Override + protected Builder self() { + return this; + } + } +} 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 6a592b9ae..23b7baa13 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 @@ -39,6 +39,13 @@ public class PlaceholderParserTest { "i [a][m] a jvav programmer", parser.apply(text) ); + + text = "select * from #[tableName] where id = #[id]"; + parser = new PlaceholderParser(str -> "?", "#[", "]"); + Assertions.assertEquals( + "select * from ? where id = ?", + parser.apply(text) + ); } } diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/text/StrTemplateTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/text/StrTemplateTest.java new file mode 100644 index 000000000..d69cfdb20 --- /dev/null +++ b/hutool-core/src/test/java/org/dromara/hutool/core/text/StrTemplateTest.java @@ -0,0 +1,671 @@ +package org.dromara.hutool.core.text; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.dromara.hutool.core.collection.ListUtil; +import org.dromara.hutool.core.exceptions.UtilException; +import org.dromara.hutool.core.map.MapUtil; +import org.dromara.hutool.core.text.placeholder.StrTemplate; +import org.dromara.hutool.core.text.placeholder.template.NamedPlaceholderStrTemplate; +import org.dromara.hutool.core.text.placeholder.template.SinglePlaceholderStrTemplate; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * test for {@link StrTemplate} + * + * @author emptypoint + */ +public class StrTemplateTest { + + @Test + public void singlePlaceholderFormatTest() { + // 默认值 + testSinglePlaceholderFormat("{}", '\\'); + + // 自定义占位符 + testSinglePlaceholderFormat("?", '\\'); + + // 自定义多个占位符 + testSinglePlaceholderFormat("$$$", '\\'); + + // 自定义多个占位符和转义符 + testSinglePlaceholderFormat("$$$", '/'); + } + + @Test + public void namedPlaceholderFormatSequenceTest() { + String text = "select * from #[tableName] where id = #[id]"; + NamedPlaceholderStrTemplate strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").build(); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.formatSequence("user", 1001) + ); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.formatArraySequence(new String[]{"user", "1001"}) + ); + Assertions.assertEquals( + "select * from 123 where id = 456", + strTemplate.formatArraySequence(new int[]{123, 456}) + ); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.formatSequence(ListUtil.of("user", 1001)) + ); + } + + @Test + public void namedPlaceholderFormatIndexedTest() { + String text = "select * from #[1] where id = #[2]"; + NamedPlaceholderStrTemplate strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").build(); + + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.formatIndexed("hutool", "user", 1001) + ); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.formatArrayIndexed(new String[]{"hutool", "user", "1001"}) + ); + Assertions.assertEquals( + "select * from 123 where id = 456", + strTemplate.formatArrayIndexed(new int[]{666, 123, 456}) + ); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.formatIndexed(ListUtil.of("hutool", "user", 1001)) + ); + Assertions.assertEquals( + "select * from user where id = ?", + strTemplate.formatIndexed(ListUtil.of("hutool", "user"), idx -> "?") + ); + } + + @Test + public void namedPlaceholderFormatTest() { + String text = "select * from #[tableName] where id = #[id]"; + NamedPlaceholderStrTemplate strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").build(); + + Map map = MapUtil.builder().put("tableName", "user").put("id", 1001).build(); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.format(map) + ); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.format((Object) map) + ); + + FormatEntity entity = new FormatEntity().setTableName("user").setId(1001); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.format(entity) + ); + entity = new FormatEntity().setTableName("user").setId(1001); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.format(entity) + ); + } + + @Data + @Accessors(chain = true) + @NoArgsConstructor + @AllArgsConstructor + public static class FormatEntity { + private String tableName; + private Integer id; + } + + @Test + public void namedPlaceholderFormatDefaultValueTest() { + String text = "i {a}{m} a {jvav} programmer"; + NamedPlaceholderStrTemplate.Builder strTemplate = StrTemplate.ofNamed(text) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE); + Assertions.assertEquals( + "i a programmer", + strTemplate.defaultValue(s -> "") + .build() + .formatSequence() + ); + Assertions.assertEquals( + "i ?? a ? programmer", + strTemplate.defaultValue(s -> "?") + .build() + .formatSequence() + ); + Assertions.assertEquals( + "i $$$$$$ a $$$ programmer", + strTemplate.defaultValue(s -> "$$$") + .build() + .formatSequence() + ); + + text = "select * from #[tableName] where id = #[id]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]"); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.defaultValue(s -> "?") + .build() + .formatSequence("user", 1001) + ); + Assertions.assertEquals( + "select * from user where id = ?", + strTemplate.defaultValue(s -> "?") + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE) + .build() + .formatSequence("user") + ); + Assertions.assertEquals( + "select * from user where id = ?", + strTemplate.defaultValue(s -> "?") + .build() + .formatArraySequence(new String[]{"user"}) + ); + Assertions.assertEquals( + "select * from user where id = ?", + strTemplate.defaultValue(s -> "?") + .build() + .formatSequence(ListUtil.of("user")) + ); + + text = "select * from #[1] where id = #[2]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE); + Assertions.assertEquals( + "select * from user where id = ?", + strTemplate.defaultValue(s -> "?") + .build() + .formatIndexed("hutool", "user") + ); + Assertions.assertEquals( + "select * from user where id = ?", + strTemplate.defaultValue(s -> "?") + .build() + .formatArrayIndexed(new String[]{"hutool", "user"}) + ); + Assertions.assertEquals( + "select * from user where id = ?", + strTemplate.defaultValue(s -> "?") + .build() + .formatIndexed(ListUtil.of("hutool", "user")) + ); + } + + @Test + public void namedPlaceholderEscapeTest() { + Map map = MapUtil.builder().put("tableName", "user").put("id", 1001).build(); + // 转义符 + String text = "select * from \\#[tableName] where id = \\#[id]"; + NamedPlaceholderStrTemplate strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").build(); + Assertions.assertEquals( + "select * from #[tableName] where id = #[id]", + strTemplate.format(map) + ); + text = "select * from \\#[tableName] where id = #[id\\]]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").build(); + Assertions.assertEquals( + "select * from #[tableName] where id = 1001", + strTemplate.format(MapUtil.builder().put("tableName", "user").put("id]", 1001).build()) + ); + + // 转义 转义符 + text = "select * from \\\\#[tableName] where id = #[id]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").build(); + Assertions.assertEquals( + "select * from \\user where id = 1001", + strTemplate.format(map) + ); + text = "select * from \\\\#[tableName] where id = \\\\#[id]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").build(); + Assertions.assertEquals( + "select * from \\user where id = \\1001", + strTemplate.format(map) + ); + text = "select * from \\\\#[tableName] where id = #[id\\\\]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").build(); + Assertions.assertEquals( + "select * from \\user where id = 1001", + strTemplate.format(MapUtil.builder().put("tableName", "user").put("id\\", 1001).build()) + ); + text = "select * from #[tableName\\\\] where id = #[id\\\\]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").build(); + Assertions.assertEquals( + "select * from user where id = 1001", + strTemplate.format(MapUtil.builder().put("tableName\\", "user").put("id\\", 1001).build()) + ); + + // 自定义 转义符 + text = "select * from /#[tableName] where id = /#[id]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").escape('/').build(); + Assertions.assertEquals( + "select * from #[tableName] where id = #[id]", + strTemplate.format(map) + ); + text = "select * from //#[tableName] where id = //#[id]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").escape('/').build(); + Assertions.assertEquals( + "select * from /user where id = /1001", + strTemplate.format(map) + ); + text = "select * from /#[tableName] where id = #[id/]]"; + strTemplate = StrTemplate.ofNamed(text).prefix("#[").suffix("]").escape('/').build(); + Assertions.assertEquals( + "select * from #[tableName] where id = 1001", + strTemplate.format(MapUtil.builder().put("tableName", "user").put("id]", 1001).build()) + ); + } + + private void testSinglePlaceholderFormat(String placeholder, char escape) { + // 通常使用 + String commonTemplate = "this is " + placeholder + " for " + placeholder; + SinglePlaceholderStrTemplate template = StrTemplate.of(commonTemplate) + .placeholder(placeholder) + .escape(escape) + .build(); + + + // 普通使用 + Assertions.assertEquals("this is a for 666", + template.format("a", 666) + ); + Assertions.assertEquals("this is a for 666", + template.format(ListUtil.of("a", 666)) + ); + Assertions.assertEquals("this is 123 for 456", + template.formatArray(new int[]{123, 456}) + ); + Assertions.assertEquals("this is 123 for 456", + template.formatArray(new Integer[]{123, 456}) + ); + + // 转义占位符 + Assertions.assertEquals("this is " + placeholder + " for a", + StrTemplate.of("this is " + escape + placeholder + " for " + placeholder) + .placeholder(placeholder) + .escape(escape) + .build() + .format("a", "b") + ); + // 转义"转义符" + Assertions.assertEquals("this is " + escape + "a for b", + StrTemplate.of("this is " + escape + escape + placeholder + " for " + placeholder) + .placeholder(placeholder) + .escape(escape) + .build() + .format("a", "b") + ); + // 填充null值 + Assertions.assertEquals("this is " + null + " for b", + template.format(null, "b") + ); + Assertions.assertEquals("this is a for null", + template.format("a", null) + ); + + // 序列化参数 小于 占位符数量 + Assertions.assertEquals("this is a for " + placeholder, + template.format("a") + ); + + + SinglePlaceholderStrTemplate.Builder builder = StrTemplate.of(commonTemplate) + .placeholder(placeholder) + .escape(escape) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE); + + Assertions.assertEquals("this is a for ", + builder.defaultValue("") + .build() + .format("a") + ); + Assertions.assertEquals("this is a for 666", + builder.defaultValue("666") + .build() + .format("a") + ); + + builder = StrTemplate.of(commonTemplate) + .placeholder(placeholder) + .escape(escape) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE); + Assertions.assertEquals("this is a for ", + builder.defaultValue(s -> "") + .build() + .format("a") + ); + + Assertions.assertEquals("this is a for 666", + builder.defaultValue(s -> "666") + .build() + .format("a") + ); + + // 序列化参数 超过 占位符数量 + Assertions.assertEquals("this is a for b", + builder.build() + .format("a", "b", "c") + ); + + // 残缺的占位符 + if (placeholder.length() >= 2) { + Assertions.assertEquals("this is " + placeholder.charAt(0) + " for a", + StrTemplate.of("this is " + placeholder.charAt(0) + " for " + placeholder) + .placeholder(placeholder) + .escape(escape) + .build() + .format("a") + ); + Assertions.assertEquals("this is " + placeholder.charAt(1) + " for a", + StrTemplate.of("this is " + placeholder.charAt(1) + " for " + placeholder) + .placeholder(placeholder) + .escape(escape) + .build() + .format("a") + ); + Assertions.assertEquals("this is " + placeholder.charAt(0) + ' ' + placeholder.charAt(1) + " for a", + StrTemplate.of("this is " + placeholder.charAt(0) + ' ' + placeholder.charAt(1) + " for " + placeholder) + .placeholder(placeholder) + .escape(escape) + .build() + .format("a") + ); + } + } + + @Test + public void isMatchesTest() { + SinglePlaceholderStrTemplate strTemplate = StrTemplate.of("this is {} for {}").build(); + Assertions.assertTrue(strTemplate.isMatches("this is a for b")); + Assertions.assertTrue(strTemplate.isMatches("this is aaa for 666")); + Assertions.assertTrue(strTemplate.isMatches("this is a for b ")); + Assertions.assertTrue(strTemplate.isMatches("this is a x for b")); + Assertions.assertTrue(strTemplate.isMatches("this is {} for {}")); + Assertions.assertTrue(strTemplate.isMatches("this is { } for {}")); + Assertions.assertTrue(strTemplate.isMatches("this is { } for { }")); + Assertions.assertTrue(strTemplate.isMatches("this is a for b")); + Assertions.assertTrue(strTemplate.isMatches("this is a for b")); + Assertions.assertTrue(strTemplate.isMatches("this is a for b")); + Assertions.assertTrue(strTemplate.isMatches("this is a for ")); + Assertions.assertTrue(strTemplate.isMatches("this is for b")); + Assertions.assertTrue(strTemplate.isMatches("this is for ")); + + Assertions.assertFalse(strTemplate.isMatches("")); + Assertions.assertFalse(strTemplate.isMatches(" ")); + Assertions.assertFalse(strTemplate.isMatches(" \r\n \n ")); + Assertions.assertFalse(strTemplate.isMatches(" this is a for b")); + Assertions.assertFalse(strTemplate.isMatches("this is a forb")); + Assertions.assertFalse(strTemplate.isMatches("this is a for b")); + Assertions.assertFalse(strTemplate.isMatches("this are a for b")); + Assertions.assertFalse(strTemplate.isMatches("that is a for b")); + + // 占位符在最前和最后 + strTemplate = StrTemplate.of("{}, this is for {}").build(); + Assertions.assertTrue(strTemplate.isMatches("Cleveland, this is for you")); + Assertions.assertTrue(strTemplate.isMatches("Cleveland, this is for you ")); + Assertions.assertTrue(strTemplate.isMatches(" Cleveland, this is for you")); + Assertions.assertTrue(strTemplate.isMatches("Cleveland, this is for you ")); + Assertions.assertTrue(strTemplate.isMatches("Cleveland, this is for you ?")); + Assertions.assertTrue(strTemplate.isMatches("Cleveland , this is for you")); + Assertions.assertTrue(strTemplate.isMatches(":)Cleveland, this is for you")); + + Assertions.assertFalse(strTemplate.isMatches("Cleveland, this is for you")); + Assertions.assertFalse(strTemplate.isMatches("Cleveland, this is for you")); + Assertions.assertFalse(strTemplate.isMatches("Cleveland, this is for you")); + Assertions.assertFalse(strTemplate.isMatches("Cleveland, this is four you")); + Assertions.assertFalse(strTemplate.isMatches("Cleveland, this are for you")); + Assertions.assertFalse(strTemplate.isMatches("Cleveland, that is for you")); + } + + @Test + public void singlePlaceholderMatchesTest() { + SinglePlaceholderStrTemplate strTemplate = StrTemplate.of("this is {} for {}").build(); + Assertions.assertEquals(ListUtil.of("a", "b"), strTemplate.matches("this is a for b")); + Assertions.assertEquals(ListUtil.of("aaa", "666"), strTemplate.matches("this is aaa for 666")); + Assertions.assertEquals(ListUtil.of("a", "b "), strTemplate.matches("this is a for b ")); + Assertions.assertEquals(ListUtil.of("a x", "b"), strTemplate.matches("this is a x for b")); + Assertions.assertEquals(ListUtil.of("{}", "{}"), strTemplate.matches("this is {} for {}")); + Assertions.assertEquals(ListUtil.of("{ }", "{}"), strTemplate.matches("this is { } for {}")); + Assertions.assertEquals(ListUtil.of("{ }", "{ }"), strTemplate.matches("this is { } for { }")); + Assertions.assertEquals(ListUtil.of(" a", "b"), strTemplate.matches("this is a for b")); + Assertions.assertEquals(ListUtil.of(" a", " b"), strTemplate.matches("this is a for b")); + Assertions.assertEquals(ListUtil.of("a ", "b"), strTemplate.matches("this is a for b")); + Assertions.assertEquals(ListUtil.of("a", null), strTemplate.matches("this is a for ")); + Assertions.assertEquals(ListUtil.of(null, "b"), strTemplate.matches("this is for b")); + Assertions.assertEquals(ListUtil.of(null, null), strTemplate.matches("this is for ")); + + final List emptyList = Collections.emptyList(); + Assertions.assertEquals(emptyList, strTemplate.matches("")); + Assertions.assertEquals(emptyList, strTemplate.matches(" ")); + Assertions.assertEquals(emptyList, strTemplate.matches(" \r\n \n ")); + Assertions.assertEquals(emptyList, strTemplate.matches(" this is a for b")); + Assertions.assertEquals(emptyList, strTemplate.matches("this is a forb")); + Assertions.assertEquals(emptyList, strTemplate.matches("this is a for b")); + Assertions.assertEquals(emptyList, strTemplate.matches("this are a for b")); + Assertions.assertEquals(emptyList, strTemplate.matches("that is a for b")); + + strTemplate = StrTemplate.of("{}, this is for {}").build(); + Assertions.assertEquals(ListUtil.of("Cleveland", "you"), strTemplate.matches("Cleveland, this is for you")); + Assertions.assertEquals(ListUtil.of(" Cleveland", "you"), strTemplate.matches(" Cleveland, this is for you")); + Assertions.assertEquals(ListUtil.of("Cleveland ", "you"), strTemplate.matches("Cleveland , this is for you")); + Assertions.assertEquals(ListUtil.of("Cleveland", "you "), strTemplate.matches("Cleveland, this is for you ")); + Assertions.assertEquals(ListUtil.of("Cleveland", " you"), strTemplate.matches("Cleveland, this is for you")); + Assertions.assertEquals(ListUtil.of("Cleveland", " you "), strTemplate.matches("Cleveland, this is for you ")); + Assertions.assertEquals(ListUtil.of("Cleveland", "you ?"), strTemplate.matches("Cleveland, this is for you ?")); + Assertions.assertEquals(ListUtil.of(":)Cleveland", "you:("), strTemplate.matches(":)Cleveland, this is for you:(")); + + Assertions.assertEquals(emptyList, strTemplate.matches("Cleveland, this is for you")); + Assertions.assertEquals(emptyList, strTemplate.matches("Cleveland, this is for you")); + Assertions.assertEquals(emptyList, strTemplate.matches("Cleveland, this is for you")); + Assertions.assertEquals(emptyList, strTemplate.matches("Cleveland, this is four you")); + Assertions.assertEquals(emptyList, strTemplate.matches("Cleveland, this are for you")); + Assertions.assertEquals(emptyList, strTemplate.matches("Cleveland, that is for you")); + } + + @Test + public void namedPlaceholderMatchesSequenceTest() { + NamedPlaceholderStrTemplate strTemplate = StrTemplate.ofNamed("this is {a} for {b}").build(); + Assertions.assertEquals(ListUtil.of("a", "b"), strTemplate.matchesSequence("this is a for b")); + Assertions.assertEquals(ListUtil.of("aaa", "666"), strTemplate.matchesSequence("this is aaa for 666")); + Assertions.assertEquals(ListUtil.of("a", "b "), strTemplate.matchesSequence("this is a for b ")); + Assertions.assertEquals(ListUtil.of("a x", "b"), strTemplate.matchesSequence("this is a x for b")); + Assertions.assertEquals(ListUtil.of("{}", "{}"), strTemplate.matchesSequence("this is {} for {}")); + Assertions.assertEquals(ListUtil.of("{ }", "{}"), strTemplate.matchesSequence("this is { } for {}")); + Assertions.assertEquals(ListUtil.of("{ }", "{ }"), strTemplate.matchesSequence("this is { } for { }")); + Assertions.assertEquals(ListUtil.of(" a", "b"), strTemplate.matchesSequence("this is a for b")); + Assertions.assertEquals(ListUtil.of(" a", " b"), strTemplate.matchesSequence("this is a for b")); + Assertions.assertEquals(ListUtil.of("a ", "b"), strTemplate.matchesSequence("this is a for b")); + Assertions.assertEquals(ListUtil.of("a", null), strTemplate.matchesSequence("this is a for ")); + Assertions.assertEquals(ListUtil.of(null, "b"), strTemplate.matchesSequence("this is for b")); + Assertions.assertEquals(ListUtil.of(null, null), strTemplate.matchesSequence("this is for ")); + + final List emptyList = Collections.emptyList(); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence(" ")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence(" \r\n \n ")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence(" this is a for b")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("this is a forb")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("this is a for b")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("this are a for b")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("that is a for b")); + + strTemplate = StrTemplate.ofNamed("{a}, this is for {b}").build(); + Assertions.assertEquals(ListUtil.of("Cleveland", "you"), strTemplate.matchesSequence("Cleveland, this is for you")); + Assertions.assertEquals(ListUtil.of(" Cleveland", "you"), strTemplate.matchesSequence(" Cleveland, this is for you")); + Assertions.assertEquals(ListUtil.of("Cleveland ", "you"), strTemplate.matchesSequence("Cleveland , this is for you")); + Assertions.assertEquals(ListUtil.of("Cleveland", "you "), strTemplate.matchesSequence("Cleveland, this is for you ")); + Assertions.assertEquals(ListUtil.of("Cleveland", " you"), strTemplate.matchesSequence("Cleveland, this is for you")); + Assertions.assertEquals(ListUtil.of("Cleveland", " you "), strTemplate.matchesSequence("Cleveland, this is for you ")); + Assertions.assertEquals(ListUtil.of("Cleveland", "you ?"), strTemplate.matchesSequence("Cleveland, this is for you ?")); + Assertions.assertEquals(ListUtil.of(":)Cleveland", "you:("), strTemplate.matchesSequence(":)Cleveland, this is for you:(")); + + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("Cleveland, this is for you")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("Cleveland, this is for you")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("Cleveland, this is for you")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("Cleveland, this is four you")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("Cleveland, this are for you")); + Assertions.assertEquals(emptyList, strTemplate.matchesSequence("Cleveland, that is for you")); + } + + @Test + public void namedPlaceholderMatchesIndexedTest() { + NamedPlaceholderStrTemplate strTemplate = StrTemplate.ofNamed("this is {2} for {1}").build(); + Assertions.assertEquals(ListUtil.of(null, "b", "a"), strTemplate.matchesIndexed("this is a for b", null)); + Assertions.assertEquals(ListUtil.of(null, "666", "aaa"), strTemplate.matchesIndexed("this is aaa for 666", null)); + Assertions.assertEquals(ListUtil.of(null, "b", null), strTemplate.matchesIndexed("this is for b", null)); + Assertions.assertEquals(ListUtil.of(null, null, "aaa"), strTemplate.matchesIndexed("this is aaa for ", null)); + Assertions.assertEquals(ListUtil.of(null, null, null), strTemplate.matchesIndexed("this is for ", null)); + + strTemplate = StrTemplate.ofNamed("this is {2} for {1}") + .addFeatures(StrTemplate.Feature.MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE) + .build(); + Assertions.assertEquals(ListUtil.of(null, "b", "a"), strTemplate.matchesIndexed("this is a for b", idx -> "?")); + Assertions.assertEquals(ListUtil.of(null, "666", "aaa"), strTemplate.matchesIndexed("this is aaa for 666", idx -> "?")); + Assertions.assertEquals(ListUtil.of(null, "b", "?"), strTemplate.matchesIndexed("this is for b", idx -> "?")); + Assertions.assertEquals(ListUtil.of(null, "?", "aaa"), strTemplate.matchesIndexed("this is aaa for ", idx -> "?")); + Assertions.assertEquals(ListUtil.of(null, "?", "?"), strTemplate.matchesIndexed("this is for ", idx -> "?")); + + strTemplate = StrTemplate.ofNamed("this is {2} for {1}").build(); + final List emptyList = Collections.emptyList(); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed("", null)); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed(" ", null)); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed(" \r\n \n ", null)); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed(" this is a for b", null)); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed("this is a forb", null)); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed("this is a for b", null)); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed("this are a for b", null)); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed("that is a for b", null)); + + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed(" this is a for b", idx -> "?")); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed("this is a forb", idx -> "?")); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed("this is a for b", idx -> "?")); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed("this are a for b", idx -> "?")); + Assertions.assertEquals(emptyList, strTemplate.matchesIndexed("that is a for b", idx -> "?")); + } + + + @Test + public void namedPlaceholderMatchesTest() { + NamedPlaceholderStrTemplate strTemplate = StrTemplate.ofNamed("this is {tableName} for {id}").build(); + Supplier> mapSupplier = HashMap::new; + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").put("id", "666").build(), strTemplate.matches("this is aaa for 666", mapSupplier)); + Assertions.assertEquals(MapUtil.builder("tableName", null).put("id", "666").build(), strTemplate.matches("this is for 666", mapSupplier)); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").put("id", null).build(), strTemplate.matches("this is aaa for ", mapSupplier)); + Assertions.assertEquals(MapUtil.builder("tableName", null).put("id", null).build(), strTemplate.matches("this is for ", mapSupplier)); + Assertions.assertEquals(Collections.emptyMap(), strTemplate.matches("", mapSupplier)); + + + Supplier beanSupplier = FormatEntity::new; + Assertions.assertEquals(new FormatEntity("aaa", 666), strTemplate.matches("this is aaa for 666", beanSupplier)); + Assertions.assertEquals(new FormatEntity(null, 666), strTemplate.matches("this is for 666", beanSupplier)); + Assertions.assertEquals(new FormatEntity("aaa", null), strTemplate.matches("this is aaa for ", beanSupplier)); + Assertions.assertEquals(new FormatEntity(null, null), strTemplate.matches("this is for ", beanSupplier)); + Assertions.assertEquals(new FormatEntity(), strTemplate.matches("", beanSupplier)); + } + + @Test + public void featureTest() { + // 通常使用 + String commonTemplate = "this is {tableName} for {id}"; + // ##### 使用新的策略 替换 默认策略 ##### + NamedPlaceholderStrTemplate template = StrTemplate.ofNamed(commonTemplate) + .features(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_EMPTY, StrTemplate.Feature.MATCH_IGNORE_EMPTY_VALUE) + .build(); + testFeature(template); + + // 添加新策略,互斥的策略则算作设置新策略,旧策略失效 + template = StrTemplate.ofNamed(commonTemplate) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_EMPTY, StrTemplate.Feature.MATCH_IGNORE_DEFAULT_VALUE, StrTemplate.Feature.MATCH_IGNORE_EMPTY_VALUE) + .build(); + testFeature(template); + + // ##### 删除策略 ##### + NamedPlaceholderStrTemplate template2 = StrTemplate.ofNamed(commonTemplate) + .removeFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_WHOLE_PLACEHOLDER) + .build(); + Assertions.assertEquals("this is aaa for 666", template2.format(MapUtil.builder("tableName", "aaa").put("id", "666").build())); + Assertions.assertThrows(UtilException.class, () -> template2.format(MapUtil.builder("tableName", "aaa").build())); + + // ##### 空字符串策略 ##### + template = StrTemplate.ofNamed(commonTemplate) + // 解析时,空字符串 转为 null值 + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_EMPTY, StrTemplate.Feature.MATCH_EMPTY_VALUE_TO_NULL) + .build(); + Assertions.assertEquals("this is aaa for ", template.format(MapUtil.builder("tableName", "aaa").build())); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").put("id", null).build(), template.matches("this is aaa for null")); + + // 解析时,空字符串 转为 默认值 + template = StrTemplate.ofNamed(commonTemplate) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_EMPTY, StrTemplate.Feature.MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE) + .defaultValue("?") + .build(); + Assertions.assertEquals("this is aaa for ", template.format(MapUtil.builder("tableName", "aaa").build())); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").put("id", "?").build(), template.matches("this is aaa for ")); + + // 默认值 为 空字符串,解析时,空字符串 转为 默认值 + template = StrTemplate.ofNamed(commonTemplate) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_EMPTY, StrTemplate.Feature.MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE) + .defaultValue("") + .build(); + Assertions.assertEquals("this is aaa for ", template.format(MapUtil.builder("tableName", "aaa").build())); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").put("id", "").build(), template.matches("this is aaa for ")); + + // ##### null值策略 ##### + // 解析时,null字符串 转为 null值 + template = StrTemplate.ofNamed(commonTemplate) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_NULL, StrTemplate.Feature.MATCH_NULL_STR_TO_NULL) + .build(); + Assertions.assertEquals("this is aaa for null", template.format(MapUtil.builder("tableName", "aaa").build())); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").put("id", null).build(), template.matches("this is aaa for null")); + // 格式化时,null值 转为 默认值 ;解析时,null字符串 转为 null值 + template = StrTemplate.ofNamed(commonTemplate) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE, StrTemplate.Feature.MATCH_NULL_STR_TO_NULL) + .defaultValue("null") + .build(); + Assertions.assertEquals("this is aaa for null", template.format(MapUtil.builder("tableName", "aaa").build())); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").put("id", null).build(), template.matches("this is aaa for null")); + + // 解析时,忽略 null字符串 + template = StrTemplate.ofNamed(commonTemplate) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_NULL, StrTemplate.Feature.MATCH_IGNORE_NULL_STR) + .build(); + Assertions.assertEquals("this is aaa for null", template.format(MapUtil.builder("tableName", "aaa").build())); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").build(), template.matches("this is aaa for null")); + // 格式化时,null值 转为 默认值 ;解析时,忽略 null字符串 + template = StrTemplate.ofNamed(commonTemplate) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE, StrTemplate.Feature.MATCH_IGNORE_NULL_STR) + .defaultValue("null") + .build(); + Assertions.assertEquals("this is aaa for null", template.format(MapUtil.builder("tableName", "aaa").build())); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").build(), template.matches("this is aaa for null")); + + // 解析时,null字符串 依然为 "null"字符串 + template = StrTemplate.ofNamed(commonTemplate) + .addFeatures(StrTemplate.Feature.FORMAT_MISSING_KEY_PRINT_NULL, StrTemplate.Feature.MATCH_KEEP_NULL_STR) + .build(); + Assertions.assertEquals("this is aaa for null", template.format(MapUtil.builder("tableName", "aaa").build())); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").put("id", "null").build(), template.matches("this is aaa for null")); + } + + private void testFeature(NamedPlaceholderStrTemplate template) { + // 格式化 + Assertions.assertEquals("this is aaa for 666", template.format(MapUtil.builder("tableName", "aaa").put("id", "666").build())); + Assertions.assertEquals("this is aaa for ", template.format(MapUtil.builder("tableName", "aaa").build())); + Assertions.assertEquals("this is for 666", template.format(MapUtil.builder("id", "666").build())); + Assertions.assertEquals("this is for ", template.format(MapUtil.builder().build())); + + // 解析 + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").put("id", "666").build(), template.matches("this is aaa for 666")); + Assertions.assertEquals(MapUtil.builder("tableName", "aaa").build(), template.matches("this is aaa for ")); + Assertions.assertEquals(MapUtil.builder("id", "666").build(), template.matches("this is for 666")); + Assertions.assertEquals(MapUtil.builder().build(), template.matches("this is for ")); + } +}