001/**
002 * Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.gnu.org/licenses/lgpl-3.0.txt
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package dev.tinyflow.core.util;
017
018import com.alibaba.fastjson.JSON;
019import com.alibaba.fastjson.JSONPath;
020import com.alibaba.fastjson.serializer.SerializerFeature;
021
022import java.util.*;
023import java.util.concurrent.ConcurrentHashMap;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027/**
028 * 文本模板引擎,用于将包含 {{xxx}} 占位符的字符串模板,动态渲染为最终文本。
029 * 支持 JSONPath 取值语法与 “??” 空值兜底逻辑。
030 * <p>
031 * 例如:
032 * 模板: "Hello {{ user.name ?? 'Unknown' }}!"
033 * 数据: { "user": { "name": "Alice" } }
034 * 输出: "Hello Alice!"
035 * <p>
036 * 支持缓存模板与 JSONPath 编译结果,提升性能。
037 */
038public class TextTemplate {
039
040    /**
041     * 匹配 {{ expression }} 的正则表达式
042     */
043    private static final Pattern PLACEHOLDER_PATTERN =
044            Pattern.compile("\\{\\{\\s*([^{}]+?)\\s*}}");
045
046    /**
047     * 模板缓存(按原始模板字符串)
048     */
049    private static final Map<String, TextTemplate> TEMPLATE_CACHE = new ConcurrentHashMap<>();
050
051    /**
052     * JSONPath 编译缓存,避免重复编译
053     */
054    private static final Map<String, JSONPath> JSONPATH_CACHE = new ConcurrentHashMap<>();
055
056    /**
057     * 原始模板字符串
058     */
059    private final String originalTemplate;
060
061    /**
062     * 模板中拆分出的静态与动态 token 列表
063     */
064    private final List<TemplateToken> tokens;
065
066    public TextTemplate(String template) {
067        this.originalTemplate = template != null ? template : "";
068        this.tokens = Collections.unmodifiableList(parseTemplate(this.originalTemplate));
069    }
070
071    /**
072     * 从缓存中获取或新建模板实例
073     */
074    public static TextTemplate of(String template) {
075        String finalTemplate = template != null ? template : "";
076        return MapUtil.computeIfAbsent(TEMPLATE_CACHE, finalTemplate, k -> new TextTemplate(finalTemplate));
077    }
078
079    /**
080     * 清空模板与 JSONPath 缓存
081     */
082    public static void clearCache() {
083        TEMPLATE_CACHE.clear();
084        JSONPATH_CACHE.clear();
085    }
086
087
088    public String formatToString(List<Map<String, Object>> rootMaps) {
089        Map<String, Object> rootMap = new HashMap<>();
090        for (Map<String, Object> m : rootMaps) {
091            if (m != null) {
092                rootMap.putAll(m);
093            }
094        }
095        return formatToString(rootMap, false);
096    }
097
098    /**
099     * 将模板格式化为字符串
100     */
101    public String formatToString(Map<String, Object> rootMap) {
102        return formatToString(rootMap, false);
103    }
104
105    /**
106     * 将模板格式化为字符串,可选择是否对结果进行 JSON 转义
107     *
108     * @param rootMap             数据上下文
109     * @param escapeForJsonOutput 是否对结果进行 JSON 字符串转义
110     */
111    public String formatToString(Map<String, Object> rootMap, boolean escapeForJsonOutput) {
112        if (tokens.isEmpty()) return originalTemplate;
113        if (rootMap == null) rootMap = Collections.emptyMap();
114
115        StringBuilder sb = new StringBuilder(originalTemplate.length() + 64);
116
117        for (TemplateToken token : tokens) {
118            if (token.isStatic) {
119                // 静态文本,直接拼接
120                sb.append(token.content);
121                continue;
122            }
123
124            // 动态表达式求值
125            String value = evaluate(token.parseResult, rootMap, escapeForJsonOutput);
126
127            // 没有兜底且值为空时抛出异常
128            if (!token.explicitEmptyFallback && value.isEmpty()) {
129                throw new IllegalArgumentException(String.format(
130                        "Missing value for expression: \"%s\"%nTemplate: %s%nProvided parameters:%n%s",
131                        token.rawExpression,
132                        originalTemplate,
133                        JSON.toJSONString(rootMap, SerializerFeature.PrettyFormat)
134                ));
135            }
136            sb.append(value);
137        }
138
139        return sb.toString();
140    }
141
142    /**
143     * 解析模板字符串,将其拆解为静态文本与动态占位符片段
144     */
145    private List<TemplateToken> parseTemplate(String template) {
146        List<TemplateToken> result = new ArrayList<>(template.length() / 8);
147        if (template == null || template.isEmpty()) return result;
148
149        Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
150        int lastEnd = 0;
151
152        while (matcher.find()) {
153            int start = matcher.start();
154            int end = matcher.end();
155
156            // 处理 {{ 前的静态文本
157            if (start > lastEnd) {
158                result.add(TemplateToken.staticText(template.substring(lastEnd, start)));
159            }
160
161            // 处理 {{ ... }} 动态部分
162            String rawExpr = matcher.group(1);
163            TemplateParseResult parsed = parseTemplateExpression(rawExpr);
164            result.add(TemplateToken.dynamic(parsed.parseResult, rawExpr, parsed.explicitEmptyFallback));
165
166            lastEnd = end;
167        }
168
169        // 末尾剩余静态文本
170        if (lastEnd < template.length()) {
171            result.add(TemplateToken.staticText(template.substring(lastEnd)));
172        }
173
174        return result;
175    }
176
177    /**
178     * 解析单个表达式内容,处理 ?? 空值兜底逻辑。
179     * 例如: user.name ?? user.nick ?? "未知"
180     */
181    private TemplateParseResult parseTemplateExpression(String expr) {
182        // 无 ?? 表示该值必填
183        if (!expr.contains("??")) {
184            return new TemplateParseResult(new ParseResult(expr.trim(), null), false);
185        }
186
187        // 按 ?? 分割,支持链式兜底
188        String[] parts = expr.split("\\s*\\?\\?\\s*", -1);
189        boolean explicitEmptyFallback = parts[parts.length - 1].trim().isEmpty();
190
191        // 从右往左构建兜底链
192        ParseResult result = null;
193        for (int i = parts.length - 1; i >= 0; i--) {
194            String p = parts[i].trim();
195            if (p.isEmpty()) p = "\"\""; // 空串转为 "" 字面量
196            result = new ParseResult(p, result);
197        }
198
199        return new TemplateParseResult(result, explicitEmptyFallback);
200    }
201
202    /**
203     * 递归求值表达式(支持多级兜底)
204     */
205    private String evaluate(ParseResult pr, Map<String, Object> root, boolean escapeForJsonOutput) {
206        if (pr == null) return "";
207
208        // 字面量直接返回
209        if (pr.isLiteral) {
210            String literal = pr.getUnquotedLiteral();
211            return escapeForJsonOutput ? escapeJsonString(literal) : literal;
212        }
213
214        // 尝试从 JSONPath 取值
215        Object value = getValueByJsonPath(root, pr.expression, escapeForJsonOutput);
216        if (value instanceof CharSequence ||
217                value instanceof Number ||
218                value instanceof Boolean ||
219                value instanceof Character) {
220            return value.toString();
221        } else if (value != null) {
222            return com.alibaba.fastjson2.JSON.toJSONString(value);
223        }
224
225        // 若未取到,则尝试 fallback
226        return evaluate(pr.defaultResult, root, escapeForJsonOutput);
227    }
228
229    /**
230     * 根据 JSONPath 获取对象值
231     */
232    private Object getValueByJsonPath(Map<String, Object> root, String path, boolean escapeForJsonOutput) {
233        try {
234            String fullPath = path.startsWith("$") ? path : "$." + path;
235            JSONPath compiled = MapUtil.computeIfAbsent(JSONPATH_CACHE, fullPath, JSONPath::compile);
236            Object value = compiled.eval(root);
237            if (escapeForJsonOutput && value instanceof String) {
238                return escapeJsonString((String) value);
239            }
240            return value;
241        } catch (Exception ignored) {
242            return null;
243        }
244    }
245
246    /**
247     * 将字符串进行 JSON 安全转义
248     */
249    private static String escapeJsonString(String input) {
250        if (input == null || input.isEmpty()) return input;
251        return input
252                .replace("\\", "\\\\")
253                .replace("\"", "\\\"")
254                .replace("\b", "\\b")
255                .replace("\f", "\\f")
256                .replace("\n", "\\n")
257                .replace("\r", "\\r")
258                .replace("\t", "\\t");
259    }
260
261    /**
262     * 去掉字符串两端的引号
263     */
264    private static String unquote(String str) {
265        if (str == null || str.length() < 2) return str;
266        char first = str.charAt(0);
267        char last = str.charAt(str.length() - 1);
268        if ((first == '\'' && last == '\'') || (first == '"' && last == '"')) {
269            return str.substring(1, str.length() - 1);
270        }
271        return str;
272    }
273
274
275    /**
276     * 模板片段对象。
277     * 每个模板字符串会被解析为若干个 TemplateToken:
278     * - 静态文本(isStatic = true)
279     * - 动态表达式(isStatic = false)
280     */
281    private static class TemplateToken {
282        final boolean isStatic;              // 是否为静态文本
283        final String content;                // 静态文本内容
284        final ParseResult parseResult;       // 动态解析结果(表达式树)
285        final String rawExpression;          // 原始表达式字符串
286        final boolean explicitEmptyFallback; // 是否显式声明空兜底(以 ?? 结尾)
287
288        private TemplateToken(boolean isStatic, String content,
289                              ParseResult parseResult, String rawExpression,
290                              boolean explicitEmptyFallback) {
291            this.isStatic = isStatic;
292            this.content = content;
293            this.parseResult = parseResult;
294            this.rawExpression = rawExpression;
295            this.explicitEmptyFallback = explicitEmptyFallback;
296        }
297
298        /**
299         * 创建静态文本 token
300         */
301        static TemplateToken staticText(String text) {
302            return new TemplateToken(true, text, null, null, false);
303        }
304
305        /**
306         * 创建动态表达式 token
307         */
308        static TemplateToken dynamic(ParseResult parseResult, String rawExpression, boolean explicitEmptyFallback) {
309            return new TemplateToken(false, null, parseResult, rawExpression, explicitEmptyFallback);
310        }
311    }
312
313    /**
314     * 表达式解析结果。
315     * 支持嵌套的默认值链,如:user.name ?? user.nick ?? "匿名"
316     */
317    private static class ParseResult {
318        final String expression;         // 当前表达式内容(可能是 JSONPath 或字符串字面量)
319        final ParseResult defaultResult; // 默认值链的下一个节点
320        final boolean isLiteral;         // 是否为字面量字符串('xxx' 或 "xxx")
321
322        ParseResult(String expression, ParseResult defaultResult) {
323            this.expression = expression;
324            this.defaultResult = defaultResult;
325            this.isLiteral = isLiteralExpression(expression);
326        }
327
328        /**
329         * 判断是否是字符串字面量
330         */
331        private static boolean isLiteralExpression(String expr) {
332            if (expr == null || expr.length() < 2) return false;
333            char first = expr.charAt(0);
334            char last = expr.charAt(expr.length() - 1);
335            return (first == '\'' && last == '\'') || (first == '"' && last == '"');
336        }
337
338        /**
339         * 返回去除引号后的字符串字面量值
340         */
341        String getUnquotedLiteral() {
342            if (!isLiteral) throw new IllegalStateException("Not a literal: " + expression);
343            return unquote(expression);
344        }
345    }
346
347    /**
348     * 模板解析的最终结果,包含:
349     * - 解析后的表达式树(ParseResult)
350     * - 是否显式声明空兜底
351     */
352    private static class TemplateParseResult {
353        final ParseResult parseResult;
354        final boolean explicitEmptyFallback;
355
356        TemplateParseResult(ParseResult parseResult, boolean explicitEmptyFallback) {
357            this.parseResult = parseResult;
358            this.explicitEmptyFallback = explicitEmptyFallback;
359        }
360    }
361}