Skip to content

Commit c3151a4

Browse files
committed
fear(mcp):Add regular expression test and modify comments
1 parent fe519b7 commit c3151a4

File tree

2 files changed

+62
-26
lines changed

2 files changed

+62
-26
lines changed

spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/main/java/com/alibaba/cloud/ai/mcp/gateway/core/jsontemplate/ResponseTemplateParser.java

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,85 +33,77 @@ public class ResponseTemplateParser {
3333

3434
private static final Handlebars handlebars = new Handlebars();
3535

36-
// 支持 {{.}} {{.xxx}} {{.xxx.yyy}} 等多层级变量
36+
// Supports {{.}} or {{.xxx}} or {{.xxx.yyy}} multi-level variables
3737
private static final Pattern TEMPLATE_PATTERN = Pattern.compile("\\{\\{\\s*\\.([\\w\\$\\[\\]\\.]*)\\s*}}",
3838
Pattern.DOTALL);
3939

40-
// 检测是否包含多层级路径访问(如 {{.xxx.yyy}})
40+
// Detects multi-level path access patterns like {{.xxx.yyy}}
41+
// This regex is fully covered by unit tests in ResponseTemplateParserTest.java
4142
private static final Pattern MULTI_LEVEL_PATTERN = Pattern.compile("\\{\\{\\s*\\.\\w+\\.[\\w\\.]+\\s*}}");
4243

4344
/**
44-
* 处理响应模板
45-
* @param rawResponse 原始响应(JSON或文本)
46-
* @param responseTemplate 模板字符串(可为jsonPath、模板、null/空)
47-
* @return 处理后的字符串
45+
* Process response template
46+
* @param rawResponse raw response (JSON or text)
47+
* @param responseTemplate template string (can be jsonPath, template, null/empty)
48+
* @return processed string
4849
*/
4950
public static String parse(String rawResponse, String responseTemplate) {
5051
if (!StringUtils.hasText(responseTemplate) || "{{.}}".equals(responseTemplate.trim())) {
51-
// 原样输出
52+
// Return raw output
5253
return rawResponse;
5354
}
5455

55-
// jsonPath 提取
56+
// JsonPath extraction
5657
if (responseTemplate.trim().startsWith("$.") || responseTemplate.trim().startsWith("$[")) {
5758
try {
5859
Object result = JsonPath.read(rawResponse, responseTemplate.trim());
5960
return result != null ? result.toString() : "";
6061
}
6162
catch (Exception e) {
62-
// jsonPath 失败,降级为模板处理
63+
// JsonPath failed, fallback to template processing
6364
}
6465
}
6566

66-
// 检测是否包含多层级路径访问,如果包含则使用 Handlebars 引擎
67+
// Detect multi-level path access
6768
if (MULTI_LEVEL_PATTERN.matcher(responseTemplate).find()) {
6869
return parseWithHandlebars(rawResponse, responseTemplate);
6970
}
7071

71-
// 简单模板变量替换(保持原有逻辑以确保向后兼容)
72+
// Simple template variable replacement (maintain backward compatibility)
7273
return parseWithSimpleTemplate(rawResponse, responseTemplate);
7374
}
7475

75-
/**
76-
* 使用 Handlebars 引擎处理多层级模板 兼容 higress.cn/ai/mcp-server 的模板语法 {{ .xxx.yyy }}
77-
*/
7876
private static String parseWithHandlebars(String rawResponse, String responseTemplate) {
7977
try {
80-
// 1. 预处理模板:转换语法以兼容 Handlebars
78+
// 1. Preprocess template: convert syntax to be compatible with Handlebars
8179
String handlebarsTemplateStr = responseTemplate
82-
// 移除点号前缀:{{ .xxx.yyy }} -> {{xxx.yyy}}
80+
// Remove dot prefix: {{ .xxx.yyy }} -> {{xxx.yyy}}
8381
.replaceAll("\\{\\{\\s*\\.", "{{")
84-
// 转换数组访问语法:{{users.[0].name}} -> {{users.0.name}}
82+
// Convert array access syntax: {{users.[0].name}} -> {{users.0.name}}
8583
.replaceAll("\\[([0-9]+)\\]", "$1");
8684

87-
// 2. 编译模板
85+
// 2. Compile template
8886
Template template = handlebars.compileInline(handlebarsTemplateStr);
8987

90-
// 3. 准备数据上下文:将JSON字符串解析为 Map
9188
Map<String, Object> dataContext;
9289
boolean isJson = rawResponse.trim().startsWith("{") || rawResponse.trim().startsWith("[");
9390
if (isJson) {
9491
dataContext = objectMapper.readValue(rawResponse, new TypeReference<Map<String, Object>>() {
9592
});
9693
}
9794
else {
98-
// 非JSON数据,创建一个包含原始响应的上下文
95+
// Non-JSON data, create a context containing the raw response
9996
dataContext = Map.of("_raw", rawResponse);
10097
}
10198

102-
// 4. 应用模板并返回结果
10399
return template.apply(dataContext);
104100

105101
}
106102
catch (Exception e) {
107-
// Handlebars 处理失败,降级为简单模板处理
108103
return parseWithSimpleTemplate(rawResponse, responseTemplate);
109104
}
110105
}
111106

112-
/**
113-
* 简单模板变量替换(原有逻辑)
114-
*/
115107
private static String parseWithSimpleTemplate(String rawResponse, String responseTemplate) {
116108
try {
117109
Map<String, Object> context = null;

spring-ai-alibaba-mcp/spring-ai-alibaba-mcp-router/src/test/java/com/alibaba/cloud/ai/mcp/gateway/core/jsontemplate/ResponseTemplateParserTest.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818

1919
import org.junit.jupiter.api.Test;
2020
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
import static org.junit.jupiter.api.Assertions.assertFalse;
23+
24+
import java.util.regex.Pattern;
2125

2226
class ResponseTemplateParserTest {
2327

@@ -168,8 +172,48 @@ void shouldHandleEmptyObjectsAndNulls() {
168172

169173
String result = ResponseTemplateParser.parse(rawResponse, template);
170174

171-
// Handlebars 将 null 值渲染为空字符串,这是符合模板引擎标准行为的
175+
// Handlebars renders null values as empty strings, which is standard template
176+
// engine behavior
172177
assertEquals("Data: , Info: test", result);
173178
}
174179

180+
// Regex pattern tests to validate MULTI_LEVEL_PATTERN detection
181+
@Test
182+
void shouldDetectMultiLevelPatternsCorrectly() {
183+
Pattern MULTI_LEVEL_PATTERN = Pattern.compile("\\{\\{\\s*\\.\\w+\\.[\\w\\.]+\\s*}}");
184+
185+
// Should match multi-level patterns
186+
assertTrue(MULTI_LEVEL_PATTERN.matcher("{{.user.name}}").find());
187+
assertTrue(MULTI_LEVEL_PATTERN.matcher("{{ .location.city }}").find());
188+
assertTrue(MULTI_LEVEL_PATTERN.matcher("{{.data.user.profile.name}}").find());
189+
assertTrue(MULTI_LEVEL_PATTERN.matcher("{{.api.response.status.code}}").find());
190+
191+
// Should NOT match single-level patterns
192+
assertFalse(MULTI_LEVEL_PATTERN.matcher("{{.}}").find());
193+
assertFalse(MULTI_LEVEL_PATTERN.matcher("{{.status}}").find());
194+
assertFalse(MULTI_LEVEL_PATTERN.matcher("{{.message}}").find());
195+
196+
// Should NOT match non-template strings
197+
assertFalse(MULTI_LEVEL_PATTERN.matcher("plain text").find());
198+
assertFalse(MULTI_LEVEL_PATTERN.matcher("$.location.city").find());
199+
}
200+
201+
@Test
202+
void shouldMatchTemplatePatternCorrectly() {
203+
Pattern TEMPLATE_PATTERN = Pattern.compile("\\{\\{\\s*\\.([\\w\\$\\[\\]\\.]*)\\s*}}", Pattern.DOTALL);
204+
205+
// Should match all template patterns
206+
assertTrue(TEMPLATE_PATTERN.matcher("{{.}}").find());
207+
assertTrue(TEMPLATE_PATTERN.matcher("{{.status}}").find());
208+
assertTrue(TEMPLATE_PATTERN.matcher("{{.user.name}}").find());
209+
assertTrue(TEMPLATE_PATTERN.matcher("{{ .location.city }}").find());
210+
assertTrue(TEMPLATE_PATTERN.matcher("{{.users.[0].name}}").find());
211+
assertTrue(TEMPLATE_PATTERN.matcher("{{.data$}}").find());
212+
213+
// Should NOT match non-template patterns
214+
assertFalse(TEMPLATE_PATTERN.matcher("$.location.city").find());
215+
assertFalse(TEMPLATE_PATTERN.matcher("plain text").find());
216+
assertFalse(TEMPLATE_PATTERN.matcher("{single brace}").find());
217+
}
218+
175219
}

0 commit comments

Comments
 (0)