mirror of
https://gitee.com/NexIoT/Universal-IoT-Java.git
synced 2026-10-30 22:38:44 +08:00
refactor(core): 删除存在sql注入的几个废弃功能
This commit is contained in:
-6
@@ -142,10 +142,4 @@ public interface ISysMenuService {
|
||||
*/
|
||||
public String checkMenuNameUnique(SysMenu menu);
|
||||
|
||||
/**
|
||||
* 执行菜单脚本
|
||||
*
|
||||
* @param sql 菜单脚本
|
||||
*/
|
||||
void execMenuSql(String sql);
|
||||
}
|
||||
|
||||
-9
@@ -453,13 +453,4 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
return getChildList(list, t).size() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行菜单脚本
|
||||
*
|
||||
* @param sql 菜单脚本
|
||||
*/
|
||||
@Override
|
||||
public void execMenuSql(String sql) {
|
||||
menuMapper.execMenuSql(sql);
|
||||
}
|
||||
}
|
||||
|
||||
+101
-42
@@ -2,53 +2,107 @@ package cn.universal.databridge.engine;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 参数化模板引擎:支持 #{path} 值占位
|
||||
* 所有值均以 '?' 参数占位绑定,避免引号与转义问题
|
||||
*/
|
||||
/** 参数化模板引擎:支持 #{path} 值占位 所有值均以 '?' 参数占位绑定,避免引号与转义问题 */
|
||||
public class ParamTemplateEngine {
|
||||
|
||||
private static final Pattern VAR_PATTERN = Pattern.compile("#\\{([a-zA-Z0-9_.]+)\\}");
|
||||
private static final Pattern TEMPLATE_PATTERN =
|
||||
Pattern.compile("#\\{([a-zA-Z0-9_.]+)}|\\$\\{(json|json_text)\\((.*?)\\)}", Pattern.DOTALL);
|
||||
|
||||
public ParamSql process(String template, Map<String, Object> variables, SqlDialectAdapter adapter) {
|
||||
public ParamSql process(
|
||||
String template, Map<String, Object> variables, SqlDialectAdapter adapter) {
|
||||
if (StrUtil.isBlank(template)) {
|
||||
return new ParamSql("", List.of());
|
||||
}
|
||||
String sql = template;
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
// 处理 #{var.path} 占位符
|
||||
Matcher m = VAR_PATTERN.matcher(sql);
|
||||
Matcher m = TEMPLATE_PATTERN.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (m.find()) {
|
||||
String path = m.group(1);
|
||||
Object value = getNestedValue(variables, path);
|
||||
|
||||
// 将值转换为适合数据库的格式
|
||||
Object paramValue = convertValueForDatabase(value, path);
|
||||
params.add(paramValue);
|
||||
|
||||
// properties 和 data 字段如果是 Map 序列化的 JSON 字符串,需要特殊处理
|
||||
String placeholder = "?";
|
||||
if (("properties".equals(path) || "data".equals(path)) && paramValue instanceof String) {
|
||||
String strValue = (String) paramValue;
|
||||
// 检查是否是 JSON 对象格式(以 { 开头且以 } 结尾)
|
||||
if (strValue.trim().startsWith("{") && strValue.trim().endsWith("}")) {
|
||||
placeholder = adapter.wrapJsonParameter();
|
||||
}
|
||||
String varPath = m.group(1);
|
||||
String macroName = m.group(2);
|
||||
String replacement;
|
||||
if (varPath != null) {
|
||||
Object paramValue = convertValueForDatabase(getNestedValue(variables, varPath), varPath);
|
||||
params.add(paramValue);
|
||||
replacement = jsonParameterPlaceholder(varPath, paramValue, adapter);
|
||||
} else {
|
||||
replacement = processJsonMacro(macroName, m.group(3), variables, adapter, params);
|
||||
}
|
||||
m.appendReplacement(sb, Matcher.quoteReplacement(placeholder));
|
||||
m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
|
||||
}
|
||||
m.appendTail(sb);
|
||||
sql = sb.toString();
|
||||
return new ParamSql(sb.toString(), params);
|
||||
}
|
||||
|
||||
return new ParamSql(sql, params);
|
||||
private String processJsonMacro(
|
||||
String macroName,
|
||||
String argumentsText,
|
||||
Map<String, Object> variables,
|
||||
SqlDialectAdapter adapter,
|
||||
List<Object> params) {
|
||||
List<String> arguments = splitArguments(argumentsText);
|
||||
if (arguments.isEmpty() || arguments.size() % 2 != 0) {
|
||||
throw new IllegalArgumentException("invalid " + macroName + " macro arguments: " + argumentsText);
|
||||
}
|
||||
List<String> keys = new ArrayList<>(arguments.size() / 2);
|
||||
for (int index = 0; index < arguments.size(); index += 2) {
|
||||
keys.add(unquote(arguments.get(index)));
|
||||
String valuePath = arguments.get(index + 1).trim();
|
||||
params.add(convertValueForDatabase(getNestedValue(variables, valuePath), valuePath));
|
||||
}
|
||||
if ("json_text".equals(macroName)) {
|
||||
return adapter.jsonTextFragment(keys, keys.size());
|
||||
}
|
||||
return adapter.jsonFragment(keys, keys.size());
|
||||
}
|
||||
|
||||
private List<String> splitArguments(String text) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (text == null || text.isBlank()) {
|
||||
return result;
|
||||
}
|
||||
StringBuilder current = new StringBuilder();
|
||||
boolean inSingleQuote = false;
|
||||
boolean inDoubleQuote = false;
|
||||
for (int index = 0; index < text.length(); index++) {
|
||||
char ch = text.charAt(index);
|
||||
if (ch == '\'' && !inDoubleQuote) {
|
||||
inSingleQuote = !inSingleQuote;
|
||||
current.append(ch);
|
||||
continue;
|
||||
}
|
||||
if (ch == '"' && !inSingleQuote) {
|
||||
inDoubleQuote = !inDoubleQuote;
|
||||
current.append(ch);
|
||||
continue;
|
||||
}
|
||||
if (ch == ',' && !inSingleQuote && !inDoubleQuote) {
|
||||
result.add(current.toString().trim());
|
||||
current.setLength(0);
|
||||
continue;
|
||||
}
|
||||
current.append(ch);
|
||||
}
|
||||
if (!current.isEmpty()) {
|
||||
result.add(current.toString().trim());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String unquote(String value) {
|
||||
String trimmed = value == null ? "" : value.trim();
|
||||
if (trimmed.length() >= 2) {
|
||||
char first = trimmed.charAt(0);
|
||||
char last = trimmed.charAt(trimmed.length() - 1);
|
||||
if ((first == '\'' && last == '\'') || (first == '"' && last == '"')) {
|
||||
return trimmed.substring(1, trimmed.length() - 1).replace("\\'", "'").replace("\\\"", "\"");
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -71,8 +125,19 @@ public class ParamTemplateEngine {
|
||||
return curr;
|
||||
}
|
||||
|
||||
private String jsonParameterPlaceholder(String path, Object paramValue, SqlDialectAdapter adapter) {
|
||||
if (("properties".equals(path) || "data".equals(path) || "rawData".equals(path))
|
||||
&& paramValue instanceof String strValue
|
||||
&& strValue.trim().startsWith("{")
|
||||
&& strValue.trim().endsWith("}")) {
|
||||
return adapter.wrapJsonParameter();
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将值转换为适合数据库存储的格式
|
||||
*
|
||||
* @param value 原始值
|
||||
* @param path 变量路径
|
||||
* @return 转换后的值
|
||||
@@ -81,26 +146,20 @@ public class ParamTemplateEngine {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果已经是字符串,直接返回
|
||||
if (value instanceof String) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 其他类型转为字符串,避免类型不匹配问题
|
||||
if (value instanceof Number || value instanceof Boolean) {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
|
||||
if (value instanceof java.util.Date) {
|
||||
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value);
|
||||
}
|
||||
|
||||
|
||||
if (value instanceof Number || value instanceof Boolean || value instanceof String) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Map 类型序列化为 JSON
|
||||
if (value instanceof Map) {
|
||||
return cn.hutool.json.JSONUtil.toJsonStr(value);
|
||||
}
|
||||
|
||||
|
||||
// 其他类型默认转为字符串
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (c) 2025, NexIoT. All Rights Reserved.
|
||||
*
|
||||
* @Description: SQL模板处理器
|
||||
* @Author: gitee.com/NexIoT
|
||||
* @Email: wo8335224@gmail.com
|
||||
* @Wechat: outlookFil
|
||||
*
|
||||
*/
|
||||
|
||||
package cn.universal.databridge.util;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* SQL模板处理器
|
||||
* 智能处理SQL模板中的变量替换,避免引号冲突
|
||||
*
|
||||
* @author gitee.com/NexIoT
|
||||
* @version 1.0
|
||||
* @since 2025/1/15
|
||||
*/
|
||||
@Slf4j
|
||||
public class SqlTemplateProcessor {
|
||||
|
||||
/**
|
||||
* 处理SQL模板,智能处理引号冲突
|
||||
*
|
||||
* @param template SQL模板
|
||||
* @param variables 变量映射
|
||||
* @return 处理后的SQL
|
||||
*/
|
||||
public static String processTemplate(String template, Map<String, Object> variables) {
|
||||
if (template == null || template.trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 使用正则表达式匹配所有 #{...} 占位符
|
||||
Pattern pattern = Pattern.compile("#\\{([a-zA-Z0-9_.]+)\\}");
|
||||
Matcher matcher = pattern.matcher(template);
|
||||
|
||||
StringBuffer result = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String paramPath = matcher.group(1);
|
||||
Object value = getNestedValue(variables, paramPath);
|
||||
String replacement = convertToSqlValue(value, template, matcher.start());
|
||||
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能转换为SQL值,根据上下文选择合适的引号
|
||||
*
|
||||
* @param value 要转换的值
|
||||
* @param template 完整模板
|
||||
* @param position 当前位置
|
||||
* @return SQL安全格式的字符串
|
||||
*/
|
||||
private static String convertToSqlValue(Object value, String template, int position) {
|
||||
if (value == null) {
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
// 检查上下文,判断是否在JSON字符串中
|
||||
boolean inJsonContext = isInJsonContext(template, position);
|
||||
|
||||
if (inJsonContext) {
|
||||
// 在JSON上下文中,使用双引号
|
||||
return convertToJsonValue(value);
|
||||
} else {
|
||||
// 在普通SQL上下文中,使用单引号
|
||||
return SqlValueConverter.convertToSqlValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前位置是否在JSON字符串上下文中
|
||||
*
|
||||
* @param template 模板
|
||||
* @param position 位置
|
||||
* @return 是否在JSON上下文中
|
||||
*/
|
||||
private static boolean isInJsonContext(String template, int position) {
|
||||
// 向前查找最近的引号
|
||||
int singleQuotePos = template.lastIndexOf("'", position);
|
||||
int doubleQuotePos = template.lastIndexOf("\"", position);
|
||||
|
||||
// 如果最近的是双引号,说明在JSON上下文中
|
||||
if (doubleQuotePos > singleQuotePos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否在JSON对象中(包含大括号)
|
||||
String beforePosition = template.substring(0, position);
|
||||
int lastOpenBrace = beforePosition.lastIndexOf("{");
|
||||
int lastCloseBrace = beforePosition.lastIndexOf("}");
|
||||
|
||||
// 如果最近的大括号是开括号,说明在JSON对象中
|
||||
if (lastOpenBrace > lastCloseBrace) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为JSON值
|
||||
*
|
||||
* @param value 要转换的值
|
||||
* @return JSON格式的字符串
|
||||
*/
|
||||
private static String convertToJsonValue(Object value) {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
if (value instanceof String) {
|
||||
// 字符串:转义双引号
|
||||
String strValue = (String) value;
|
||||
return "\"" + strValue.replace("\"", "\\\"") + "\"";
|
||||
} else if (value instanceof Number || value instanceof Boolean) {
|
||||
// 数字和布尔类型直接转换
|
||||
return value.toString();
|
||||
} else {
|
||||
// 其他类型:转为JSON字符串
|
||||
return JSONUtil.toJsonStr(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取嵌套属性值
|
||||
*
|
||||
* @param variables 变量映射
|
||||
* @param path 属性路径
|
||||
* @return 属性值
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Object getNestedValue(Map<String, Object> variables, String path) {
|
||||
if (path == null || path.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] keys = path.split("\\.");
|
||||
Object current = variables;
|
||||
|
||||
for (String key : keys) {
|
||||
if (current == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (current instanceof Map) {
|
||||
current = ((Map<String, Object>) current).get(key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (c) 2025, NexIoT. All Rights Reserved.
|
||||
*
|
||||
* @Description: 本文件由 gitee.com/NexIoT 开发并拥有版权,未经授权严禁擅自商用、复制或传播。
|
||||
* @Author: gitee.com/NexIoT
|
||||
* @Email: wo8335224@gmail.com
|
||||
* @Wechat: outlookFil
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package cn.universal.databridge.util;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* SQL值转换工具类
|
||||
* 提供将Java对象转换为SQL安全格式的通用方法
|
||||
*
|
||||
* @author gitee.com/NexIoT
|
||||
* @version 1.0
|
||||
* @since 2025/1/15
|
||||
*/
|
||||
@Slf4j
|
||||
public class SqlValueConverter {
|
||||
|
||||
/**
|
||||
* 将变量值转换为SQL安全格式
|
||||
* 1. 字符串类型:添加单引号,转义内部单引号
|
||||
* 2. 数字/布尔类型:直接转换为字符串
|
||||
* 3. null:转换为SQL的NULL关键字
|
||||
* 4. 日期类型:转换为SQL标准格式
|
||||
* 5. 其他类型:转为JSON字符串
|
||||
*
|
||||
* @param value 要转换的值
|
||||
* @return SQL安全格式的字符串
|
||||
*/
|
||||
public static String convertToSqlValue(Object value) {
|
||||
if (value == null) {
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
if (value instanceof String) {
|
||||
// 转义单引号(防止SQL注入和语法错误)
|
||||
String strValue = (String) value;
|
||||
return "'" + strValue.replace("'", "''") + "'";
|
||||
} else if (value instanceof Number || value instanceof Boolean) {
|
||||
// 数字和布尔类型直接转换
|
||||
return value.toString();
|
||||
} else if (value instanceof java.util.Date) {
|
||||
// 日期类型转换为SQL标准格式(带单引号)
|
||||
return "'" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value) + "'";
|
||||
} else {
|
||||
// 其他类型:转为JSON字符串(带单引号)
|
||||
String jsonValue = JSONUtil.toJsonStr(value);
|
||||
return "'" + jsonValue.replace("'", "''") + "'";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量转换多个值为SQL安全格式
|
||||
*
|
||||
* @param values 要转换的值数组
|
||||
* @return SQL安全格式的字符串数组
|
||||
*/
|
||||
public static String[] convertToSqlValues(Object... values) {
|
||||
if (values == null) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
String[] result = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
result[i] = convertToSqlValue(values[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将变量值转换为SQL安全格式(JSON字符串专用)
|
||||
* 使用双引号包围JSON字符串,避免单引号冲突
|
||||
*
|
||||
* @param value 要转换的值
|
||||
* @return SQL安全格式的字符串
|
||||
*/
|
||||
public static String convertToSqlJsonValue(Object value) {
|
||||
if (value == null) {
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
if (value instanceof String) {
|
||||
// JSON字符串:使用双引号包围,转义内部双引号
|
||||
String strValue = (String) value;
|
||||
return "\"" + strValue.replace("\"", "\\\"") + "\"";
|
||||
} else if (value instanceof Number || value instanceof Boolean) {
|
||||
// 数字和布尔类型直接转换
|
||||
return value.toString();
|
||||
} else if (value instanceof java.util.Date) {
|
||||
// 日期类型转换为SQL标准格式(带双引号)
|
||||
return "\"" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value) + "\"";
|
||||
} else {
|
||||
// 其他类型:转为JSON字符串(带双引号)
|
||||
String jsonValue = JSONUtil.toJsonStr(value);
|
||||
return "\"" + jsonValue.replace("\"", "\\\"") + "\"";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否需要SQL转义
|
||||
*
|
||||
* @param value 要检查的值
|
||||
* @return 是否需要转义
|
||||
*/
|
||||
public static boolean needsEscaping(Object value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value instanceof String) {
|
||||
String strValue = (String) value;
|
||||
return strValue.contains("'") || strValue.contains("\\") || strValue.contains("\"");
|
||||
}
|
||||
|
||||
return !(value instanceof Number || value instanceof Boolean);
|
||||
}
|
||||
}
|
||||
+140
-37
@@ -1,17 +1,17 @@
|
||||
package cn.universal.databridge.engine;
|
||||
|
||||
import cn.universal.databridge.engine.dialect.MySqlDialectAdapter;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* ParamTemplateEngine 单元测试
|
||||
*/
|
||||
import cn.universal.databridge.engine.dialect.MySqlDialectAdapter;
|
||||
import cn.universal.databridge.engine.dialect.OracleDialectAdapter;
|
||||
import cn.universal.databridge.engine.dialect.PostgresDialectAdapter;
|
||||
import cn.universal.databridge.engine.dialect.SqlServerDialectAdapter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** ParamTemplateEngine 单元测试 */
|
||||
class ParamTemplateEngineTest {
|
||||
|
||||
private ParamTemplateEngine engine;
|
||||
@@ -40,27 +40,98 @@ class ParamTemplateEngineTest {
|
||||
|
||||
@Test
|
||||
void testMysqlTemplate() {
|
||||
SqlDialectAdapter adapter = new MySqlDialectAdapter();
|
||||
SqlDialectAdapter adapter = new MySqlDialectAdapter();
|
||||
|
||||
String template = "INSERT INTO device_data(\n" +
|
||||
" device_id, product_key, device_name, iot_id,\n" +
|
||||
" device_node, message_type, properties, raw_data\n" +
|
||||
") VALUES(\n" +
|
||||
" #{deviceId}, #{productKey}, #{deviceName}, #{iotId},\n" +
|
||||
" #{deviceNode}, #{messageType}, #{properties}, \n" +
|
||||
" ${json('csq', properties.temperature, 'tips', properties.illuminationDesc)}\n" +
|
||||
");";
|
||||
String template =
|
||||
"INSERT INTO device_data(\n"
|
||||
+ " device_id, product_key, device_name, iot_id,\n"
|
||||
+ " device_node, message_type, properties, raw_data\n"
|
||||
+ ") VALUES(\n"
|
||||
+ " #{deviceId}, #{productKey}, #{deviceName}, #{iotId},\n"
|
||||
+ " #{deviceNode}, #{messageType}, #{properties}, \n"
|
||||
+ " ${json('csq', properties.temperature, 'tips', properties.illuminationDesc)}\n"
|
||||
+ ");";
|
||||
|
||||
ParamSql result = engine.process(template, variables, adapter);
|
||||
ParamSql result = engine.process(template, variables, adapter);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getSql().contains("JSON_OBJECT"));
|
||||
assertTrue(result.getSql().contains("?"));
|
||||
assertEquals(9, result.getParams().size());
|
||||
assertEquals("dev123", result.getParams().get(0));
|
||||
assertEquals("pk001", result.getParams().get(1));
|
||||
assertEquals(25.5, result.getParams().get(3)); // csq value
|
||||
assertEquals("明亮", result.getParams().get(4)); // tips value
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getSql().contains("JSON_OBJECT"));
|
||||
assertTrue(result.getSql().contains("?"));
|
||||
assertEquals(9, result.getParams().size());
|
||||
assertEquals("dev123", result.getParams().get(0));
|
||||
assertEquals("pk001", result.getParams().get(1));
|
||||
assertEquals(25.5, result.getParams().get(7)); // csq value
|
||||
assertEquals("明亮", result.getParams().get(8)); // tips value
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPostgreSqlTemplate() {
|
||||
SqlDialectAdapter adapter = new PostgresDialectAdapter();
|
||||
|
||||
String template =
|
||||
"INSERT INTO device_data(\n"
|
||||
+ " device_id, product_key, device_name, iot_id,\n"
|
||||
+ " device_node, message_type, properties, raw_data\n"
|
||||
+ ") VALUES(\n"
|
||||
+ " #{deviceId}, #{productKey}, #{deviceName}, #{iotId},\n"
|
||||
+ " #{deviceNode}, #{messageType}, #{properties}, \n"
|
||||
+ " ${json_text('csq', properties.temperature, 'tips', properties.illuminationDesc)}\n"
|
||||
+ ");";
|
||||
|
||||
ParamSql result = engine.process(template, variables, adapter);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getSql().contains("json_build_object"));
|
||||
assertTrue(result.getSql().contains("::TEXT"));
|
||||
assertTrue(result.getSql().contains("?"));
|
||||
assertEquals(9, result.getParams().size());
|
||||
assertEquals("dev123", result.getParams().get(0));
|
||||
assertEquals("明亮", result.getParams().get(8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOracleTemplate() {
|
||||
SqlDialectAdapter adapter = new OracleDialectAdapter();
|
||||
|
||||
String template =
|
||||
"INSERT INTO device_data(\n"
|
||||
+ " device_id, product_key, device_name, iot_id,\n"
|
||||
+ " device_node, message_type, properties, raw_data\n"
|
||||
+ ") VALUES(\n"
|
||||
+ " #{deviceId}, #{productKey}, #{deviceName}, #{iotId},\n"
|
||||
+ " #{deviceNode}, #{messageType}, #{properties}, \n"
|
||||
+ " ${json('csq', properties.temperature, 'tips', properties.illuminationDesc)}\n"
|
||||
+ ");";
|
||||
|
||||
ParamSql result = engine.process(template, variables, adapter);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getSql().contains("JSON_OBJECT"));
|
||||
assertTrue(result.getSql().contains("VALUE"));
|
||||
assertTrue(result.getSql().contains("?"));
|
||||
assertEquals(9, result.getParams().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSqlServerTemplate() {
|
||||
SqlDialectAdapter adapter = new SqlServerDialectAdapter();
|
||||
|
||||
String template =
|
||||
"INSERT INTO device_data(\n"
|
||||
+ " device_id, product_key, device_name, iot_id,\n"
|
||||
+ " device_node, message_type, properties, raw_data\n"
|
||||
+ ") VALUES(\n"
|
||||
+ " #{deviceId}, #{productKey}, #{deviceName}, #{iotId},\n"
|
||||
+ " #{deviceNode}, #{messageType}, #{properties}, \n"
|
||||
+ " ${json('csq', properties.temperature, 'tips', properties.illuminationDesc)}\n"
|
||||
+ ");";
|
||||
|
||||
ParamSql result = engine.process(template, variables, adapter);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getSql().contains("CONCAT"));
|
||||
assertTrue(result.getSql().contains("?"));
|
||||
assertEquals(9, result.getParams().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -140,18 +211,50 @@ class ParamTemplateEngineTest {
|
||||
assertEquals(template, result.getSql());
|
||||
assertTrue(result.getParams().isEmpty());
|
||||
}
|
||||
@Test
|
||||
void testMixedJsonAndJsonText() {
|
||||
SqlDialectAdapter adapter = new PostgresDialectAdapter();
|
||||
|
||||
String template =
|
||||
"INSERT INTO test(col1, col2) VALUES("
|
||||
+ "${json('temp', properties.temperature)}, "
|
||||
+ "${json_text('desc', properties.illuminationDesc)}"
|
||||
+ ")";
|
||||
|
||||
@Test
|
||||
void testChineseCharacters() {
|
||||
SqlDialectAdapter adapter = new MySqlDialectAdapter();
|
||||
ParamSql result = engine.process(template, variables, adapter);
|
||||
|
||||
variables.put("chineseValue", "测试中文");
|
||||
String template = "INSERT INTO test(name) VALUES(#{chineseValue})";
|
||||
assertTrue(result.getSql().contains("json_build_object"));
|
||||
assertTrue(result.getSql().contains("::TEXT"));
|
||||
assertEquals(2, result.getParams().size());
|
||||
assertEquals(25.5, result.getParams().get(0));
|
||||
assertEquals("明亮", result.getParams().get(1));
|
||||
}
|
||||
|
||||
ParamSql result = engine.process(template, variables, adapter);
|
||||
@Test
|
||||
void testChineseCharacters() {
|
||||
SqlDialectAdapter adapter = new MySqlDialectAdapter();
|
||||
|
||||
assertEquals("INSERT INTO test(name) VALUES(?)", result.getSql());
|
||||
assertEquals("测试中文", result.getParams().get(0));
|
||||
}
|
||||
variables.put("chineseValue", "测试中文");
|
||||
String template = "INSERT INTO test(name) VALUES(#{chineseValue})";
|
||||
|
||||
ParamSql result = engine.process(template, variables, adapter);
|
||||
|
||||
assertEquals("INSERT INTO test(name) VALUES(?)", result.getSql());
|
||||
assertEquals("测试中文", result.getParams().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSpecialCharactersInJson() {
|
||||
SqlDialectAdapter adapter = new PostgresDialectAdapter();
|
||||
|
||||
Map<String, Object> props = (Map<String, Object>) variables.get("properties");
|
||||
props.put("specialDesc", "包含\"引号\"和'单引号'");
|
||||
|
||||
String template = "SELECT ${json_text('data', properties.specialDesc)}";
|
||||
|
||||
ParamSql result = engine.process(template, variables, adapter);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getParams().get(0).toString().contains("引号"));
|
||||
}
|
||||
}
|
||||
|
||||
+54
-136
@@ -1,12 +1,15 @@
|
||||
package cn.universal.databridge.plugin.jdbc;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.universal.databridge.engine.ParamSql;
|
||||
import cn.universal.databridge.engine.ParamTemplateEngine;
|
||||
import cn.universal.databridge.entity.DataBridgeConfig;
|
||||
import cn.universal.databridge.entity.PluginInfo;
|
||||
import cn.universal.databridge.entity.ResourceConnection;
|
||||
import cn.universal.databridge.plugin.AbstractDataOutputPlugin;
|
||||
import cn.universal.databridge.plugin.SourceScope;
|
||||
import cn.universal.databridge.util.DataBridgeConnectionManager;
|
||||
import cn.universal.databridge.util.SqlValueConverter;
|
||||
import cn.universal.persistence.base.BaseUPRequest;
|
||||
import java.sql.Connection;
|
||||
import java.util.ArrayList;
|
||||
@@ -76,6 +79,25 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
|
||||
SourceScope.ALL_PRODUCTS, SourceScope.SPECIFIC_PRODUCTS, SourceScope.APPLICATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processWithDefaultTemplate(
|
||||
List<BaseUPRequest> requests,
|
||||
DataBridgeConfig config,
|
||||
ResourceConnection connection,
|
||||
JSONObject configJson) {
|
||||
try {
|
||||
DataSource dataSource = connectionManager.getOrCreateDataSource(connection);
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
|
||||
for (BaseUPRequest request : requests) {
|
||||
Map<String, Object> variables = buildTemplateVariables(request, configJson);
|
||||
executeParamSql(jdbcTemplate, config, variables, connection);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
connectionManager.removeDataSource(connection);
|
||||
throw new RuntimeException("SQL执行失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processProcessedData(
|
||||
Object processedData,
|
||||
@@ -86,12 +108,9 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
|
||||
DataSource dataSource = connectionManager.getOrCreateDataSource(connection);
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
|
||||
|
||||
List<String> sqlList = generateExecutableSqlList(processedData, config);
|
||||
|
||||
if (sqlList.size() > 1) {
|
||||
jdbcTemplate.batchUpdate(sqlList.toArray(new String[0]));
|
||||
} else if (sqlList.size() == 1) {
|
||||
jdbcTemplate.update(sqlList.get(0));
|
||||
List<ParamSql> sqlList = generateExecutableSqlList(processedData, config, connection);
|
||||
for (ParamSql sql : sqlList) {
|
||||
executeParamSql(jdbcTemplate, sql);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -108,15 +127,16 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
|
||||
ResourceConnection connection) {
|
||||
try {
|
||||
javax.sql.DataSource dataSource = connectionManager.getOrCreateDataSource(connection);
|
||||
org.springframework.jdbc.core.JdbcTemplate jdbcTemplate = new org.springframework.jdbc.core.JdbcTemplate(dataSource);
|
||||
org.springframework.jdbc.core.JdbcTemplate jdbcTemplate =
|
||||
new org.springframework.jdbc.core.JdbcTemplate(dataSource);
|
||||
|
||||
// 构建变量并进行参数化模板解析(避免引号问题,兼容多方言)
|
||||
java.util.Map<String, Object> variables = buildTemplateVariables(request, parseConfig(config));
|
||||
cn.universal.databridge.engine.ParamTemplateEngine engine = new cn.universal.databridge.engine.ParamTemplateEngine();
|
||||
ParamTemplateEngine engine = new ParamTemplateEngine();
|
||||
cn.universal.databridge.engine.SqlDialectAdapter adapter = getAdapter(connection.getType());
|
||||
cn.universal.databridge.engine.ParamSql ps = engine.process(config.getTemplate(), variables, adapter);
|
||||
ParamSql ps = engine.process(config.getTemplate(), variables, adapter);
|
||||
|
||||
jdbcTemplate.update(ps.getSql(), ps.getParams().toArray());
|
||||
executeParamSql(jdbcTemplate, ps);
|
||||
} catch (Exception e) {
|
||||
log.error("执行SQL失败: {} - {}", templateResult, e.getMessage());
|
||||
connectionManager.removeDataSource(connection);
|
||||
@@ -124,7 +144,8 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private cn.universal.databridge.engine.SqlDialectAdapter getAdapter(cn.universal.databridge.entity.ResourceConnection.ResourceType type) {
|
||||
private cn.universal.databridge.engine.SqlDialectAdapter getAdapter(
|
||||
cn.universal.databridge.entity.ResourceConnection.ResourceType type) {
|
||||
switch (type) {
|
||||
case MYSQL:
|
||||
case H2:
|
||||
@@ -140,28 +161,29 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> generateExecutableSqlList(Object processedData, DataBridgeConfig config) {
|
||||
List<String> sqlList = new ArrayList<>();
|
||||
private List<ParamSql> generateExecutableSqlList(
|
||||
Object processedData, DataBridgeConfig config, ResourceConnection connection) {
|
||||
List<ParamSql> sqlList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
if (processedData instanceof String) {
|
||||
sqlList.add((String) processedData);
|
||||
throw new IllegalArgumentException("JDBC输出Magic脚本不允许直接返回SQL字符串");
|
||||
} else if (processedData instanceof List) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> dataList = (List<Object>) processedData;
|
||||
for (Object item : dataList) {
|
||||
if (item instanceof String) {
|
||||
sqlList.add((String) item);
|
||||
throw new IllegalArgumentException("JDBC输出Magic脚本不允许直接返回SQL字符串");
|
||||
} else if (item instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dataMap = (Map<String, Object>) item;
|
||||
sqlList.add(processTemplate(config.getTemplate(), dataMap));
|
||||
sqlList.add(buildParamSql(config, dataMap, connection));
|
||||
}
|
||||
}
|
||||
} else if (processedData instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dataMap = (Map<String, Object>) processedData;
|
||||
sqlList.add(processTemplate(config.getTemplate(), dataMap));
|
||||
sqlList.add(buildParamSql(config, dataMap, connection));
|
||||
} else {
|
||||
log.warn(
|
||||
"Magic脚本返回的数据类型不支持: {}",
|
||||
@@ -176,128 +198,24 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
|
||||
return sqlList;
|
||||
}
|
||||
|
||||
// 使用正则表达式匹配所有 #{...} 占位符,支持嵌套属性
|
||||
// 智能处理引号,兼容MySQL和PostgreSQL
|
||||
protected String processTemplate(String template, Map<String, Object> variables) {
|
||||
if (template == null || template.trim().isEmpty()) {
|
||||
log.warn("模板为空,无法处理");
|
||||
return "";
|
||||
}
|
||||
String result = template;
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("#\\{([a-zA-Z0-9_.]+)\\}");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(template);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String paramPath = matcher.group(1); // 获取变量路径,如 "properties.csq"
|
||||
Object value = getNestedValue(variables, paramPath);
|
||||
|
||||
// 检测占位符在模板中的上下文,判断是否已经在引号内
|
||||
int start = matcher.start();
|
||||
int end = matcher.end();
|
||||
boolean inSingleQuotes = isInQuotes(template, start, end, '\'');
|
||||
boolean inDoubleQuotes = isInQuotes(template, start, end, '"');
|
||||
|
||||
String sqlValue;
|
||||
if (inSingleQuotes) {
|
||||
// 如果已经在单引号内,只替换值,不添加引号,但需要转义单引号
|
||||
sqlValue = convertValueForQuotedContext(value, true);
|
||||
} else if (inDoubleQuotes) {
|
||||
// 如果已经在双引号内,只替换值,不添加引号,但需要转义双引号
|
||||
sqlValue = convertValueForQuotedContext(value, false);
|
||||
} else {
|
||||
// 如果不在引号内,使用标准的SQL值转换(添加引号)
|
||||
sqlValue = SqlValueConverter.convertToSqlValue(value);
|
||||
}
|
||||
|
||||
matcher.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(sqlValue));
|
||||
}
|
||||
matcher.appendTail(sb);
|
||||
result = sb.toString();
|
||||
|
||||
return result;
|
||||
private ParamSql buildParamSql(
|
||||
DataBridgeConfig config, Map<String, Object> variables, ResourceConnection connection) {
|
||||
ParamTemplateEngine engine = new ParamTemplateEngine();
|
||||
return engine.process(config.getTemplate(), variables, getAdapter(connection.getType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测占位符是否在引号内
|
||||
* @param template 模板字符串
|
||||
* @param start 占位符开始位置
|
||||
* @param end 占位符结束位置
|
||||
* @param quoteChar 引号字符(' 或 ")
|
||||
* @return 是否在引号内
|
||||
*/
|
||||
private boolean isInQuotes(String template, int start, int end, char quoteChar) {
|
||||
// 向前查找最近的未转义引号
|
||||
int quoteBefore = -1;
|
||||
for (int i = start - 1; i >= 0; i--) {
|
||||
if (template.charAt(i) == quoteChar) {
|
||||
// 检查是否被转义(考虑连续的反斜杠)
|
||||
int backslashCount = 0;
|
||||
for (int j = i - 1; j >= 0 && template.charAt(j) == '\\'; j--) {
|
||||
backslashCount++;
|
||||
}
|
||||
// 如果反斜杠数量是偶数,说明引号未被转义
|
||||
if (backslashCount % 2 == 0) {
|
||||
quoteBefore = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (quoteBefore == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 向后查找匹配的未转义引号
|
||||
int quoteAfter = -1;
|
||||
for (int i = end; i < template.length(); i++) {
|
||||
if (template.charAt(i) == quoteChar) {
|
||||
// 检查是否被转义
|
||||
int backslashCount = 0;
|
||||
for (int j = i - 1; j >= 0 && template.charAt(j) == '\\'; j--) {
|
||||
backslashCount++;
|
||||
}
|
||||
// 如果反斜杠数量是偶数,说明引号未被转义
|
||||
if (backslashCount % 2 == 0) {
|
||||
quoteAfter = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了匹配的引号对,说明在引号内
|
||||
return quoteAfter != -1 && quoteBefore < start && quoteAfter >= end;
|
||||
private void executeParamSql(
|
||||
JdbcTemplate jdbcTemplate,
|
||||
DataBridgeConfig config,
|
||||
Map<String, Object> variables,
|
||||
ResourceConnection connection) {
|
||||
executeParamSql(jdbcTemplate, buildParamSql(config, variables, connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* 为已在引号内的上下文转换值
|
||||
* @param value 要转换的值
|
||||
* @param isSingleQuote 是否是单引号上下文
|
||||
* @return 转换后的值(不包含引号,但已转义)
|
||||
*/
|
||||
private String convertValueForQuotedContext(Object value, boolean isSingleQuote) {
|
||||
if (value == null) {
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
String quoteChar = isSingleQuote ? "'" : "\"";
|
||||
String escapeChar = isSingleQuote ? "''" : "\\\"";
|
||||
|
||||
if (value instanceof String) {
|
||||
String strValue = (String) value;
|
||||
// 转义引号
|
||||
return strValue.replace(quoteChar, escapeChar);
|
||||
} else if (value instanceof Number || value instanceof Boolean) {
|
||||
// 数字和布尔类型直接转换
|
||||
return value.toString();
|
||||
} else if (value instanceof java.util.Date) {
|
||||
// 日期类型转换为SQL标准格式
|
||||
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value);
|
||||
} else {
|
||||
// 其他类型:转为JSON字符串并转义引号
|
||||
String jsonValue = cn.hutool.json.JSONUtil.toJsonStr(value);
|
||||
return jsonValue.replace(quoteChar, escapeChar);
|
||||
private void executeParamSql(JdbcTemplate jdbcTemplate, ParamSql sql) {
|
||||
if (sql == null || StrUtil.isBlank(sql.getSql())) {
|
||||
return;
|
||||
}
|
||||
jdbcTemplate.update(sql.getSql(), sql.getParams().toArray());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
-305
@@ -1,305 +0,0 @@
|
||||
package cn.universal.databridge.plugin.jdbc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* DefaultJdbcOutPlugin 智能引号处理测试
|
||||
* 测试兼容MySQL和PostgreSQL的引号处理功能
|
||||
*
|
||||
* @author gitee.com/NexIoT
|
||||
* @version 1.0
|
||||
* @since 2025/1/15
|
||||
*/
|
||||
@DisplayName("JDBC插件智能引号处理测试")
|
||||
public class DefaultJdbcOutPluginQuoteTest {
|
||||
|
||||
private DefaultJdbcOutPlugin plugin;
|
||||
private Map<String, Object> variables;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
plugin = new DefaultJdbcOutPlugin();
|
||||
|
||||
// 准备测试数据
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("temperature", 14.3);
|
||||
properties.put("illuminationDesc", "bright");
|
||||
properties.put("csq", 25);
|
||||
|
||||
variables = new HashMap<>();
|
||||
variables.put("deviceId", "nex123321");
|
||||
variables.put("productKey", "Ru871cfJjhoM");
|
||||
variables.put("deviceName", "期望值验证");
|
||||
variables.put("iotId", "Ru871cfJjhoMnex123321");
|
||||
variables.put("deviceNode", "DEVICE");
|
||||
variables.put("messageType", "PROPERTIES");
|
||||
variables.put("properties", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用反射调用受保护的processTemplate方法
|
||||
*/
|
||||
private String processTemplate(String template, Map<String, Object> variables) {
|
||||
try {
|
||||
java.lang.reflect.Method method = DefaultJdbcOutPlugin.class.getDeclaredMethod(
|
||||
"processTemplate", String.class, Map.class);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(plugin, template, variables);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("调用processTemplate方法失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试占位符在单引号内的情况(MySQL/PostgreSQL标准)")
|
||||
void testPlaceholderInSingleQuotes() {
|
||||
// 测试场景:'#{deviceId}' 应该替换为 'nex123321',不产生双重引号
|
||||
String template = "INSERT INTO device_data(device_id) VALUES('#{deviceId}');";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 应该只包含一对单引号,不应该有双重引号
|
||||
assertEquals("INSERT INTO device_data(device_id) VALUES('nex123321');", result);
|
||||
assertFalse(result.contains("''"), "不应该包含双重单引号");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试PostgreSQL json_build_object场景")
|
||||
void testPostgreSQLJsonBuildObject() {
|
||||
// 用户提供的实际场景
|
||||
String template =
|
||||
"INSERT INTO device_data(device_id, product_key, device_name, iot_id, " +
|
||||
"device_node, message_type, properties, raw_data) VALUES(" +
|
||||
"'#{deviceId}', '#{productKey}', '#{deviceName}', '#{iotId}', " +
|
||||
"'#{deviceNode}', '#{messageType}', '#{properties}', " +
|
||||
"json_build_object(" +
|
||||
"'csq', #{properties.temperature}, " +
|
||||
"'tips', #{properties.illuminationDesc}" +
|
||||
")::TEXT);";
|
||||
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 验证单引号内的占位符被正确替换,不产生双重引号
|
||||
assertTrue(result.contains("'nex123321'"), "deviceId应该被正确替换");
|
||||
assertTrue(result.contains("'Ru871cfJjhoM'"), "productKey应该被正确替换");
|
||||
assertTrue(result.contains("'期望值验证'"), "deviceName应该被正确替换");
|
||||
|
||||
// 验证json_build_object中的占位符(不在引号内)被正确替换
|
||||
assertTrue(result.contains("14.3"), "temperature应该被替换为数字");
|
||||
assertTrue(result.contains("bright"), "illuminationDesc应该被替换");
|
||||
|
||||
// 不应该有双重引号
|
||||
assertFalse(result.contains("''nex123321''"), "不应该有双重单引号");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试占位符在双引号内的情况(PostgreSQL JSON)")
|
||||
void testPlaceholderInDoubleQuotes() {
|
||||
String template = "SELECT \"#{deviceId}\" as id;";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 应该只包含一对双引号,不应该有双重引号
|
||||
assertEquals("SELECT \"nex123321\" as id;", result);
|
||||
assertFalse(result.contains("\"\""), "不应该包含双重双引号");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试占位符不在引号内的情况")
|
||||
void testPlaceholderNotInQuotes() {
|
||||
String template = "SELECT #{deviceId} as id;";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 不在引号内时,应该添加单引号
|
||||
assertEquals("SELECT 'nex123321' as id;", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试字符串值中包含单引号的情况")
|
||||
void testStringWithSingleQuote() {
|
||||
variables.put("deviceName", "O'Brien");
|
||||
String template = "INSERT INTO device_data(device_name) VALUES('#{deviceName}');";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 单引号应该被转义为两个单引号
|
||||
assertTrue(result.contains("O''Brien"), "单引号应该被转义");
|
||||
assertEquals("INSERT INTO device_data(device_name) VALUES('O''Brien');", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试字符串值中包含双引号的情况")
|
||||
void testStringWithDoubleQuote() {
|
||||
variables.put("description", "He said \"Hello\"");
|
||||
String template = "SELECT \"#{description}\" as desc;";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 双引号应该被转义
|
||||
assertTrue(result.contains("\\\""), "双引号应该被转义");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试NULL值处理")
|
||||
void testNullValue() {
|
||||
variables.put("optionalField", null);
|
||||
String template = "INSERT INTO device_data(optional_field) VALUES('#{optionalField}');";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// NULL值应该被替换为NULL关键字
|
||||
assertEquals("INSERT INTO device_data(optional_field) VALUES('NULL');", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试数字值处理")
|
||||
void testNumberValue() {
|
||||
String template = "INSERT INTO device_data(temperature) VALUES(#{properties.temperature});";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 数字值不在引号内时,应该直接使用,不添加引号
|
||||
assertTrue(result.contains("14.3"), "数字值应该被正确替换");
|
||||
assertFalse(result.contains("'14.3'"), "数字值不应该被引号包围");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试数字值在引号内的情况")
|
||||
void testNumberInQuotes() {
|
||||
String template = "INSERT INTO device_data(temperature) VALUES('#{properties.temperature}');";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 数字值在引号内时,应该直接替换,不添加引号
|
||||
assertEquals("INSERT INTO device_data(temperature) VALUES('14.3');", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试嵌套属性访问")
|
||||
void testNestedPropertyAccess() {
|
||||
String template =
|
||||
"INSERT INTO device_data(csq, temp) VALUES(" +
|
||||
"'#{properties.csq}', #{properties.temperature});";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
assertTrue(result.contains("'25'"), "嵌套属性csq应该被正确替换");
|
||||
assertTrue(result.contains("14.3"), "嵌套属性temperature应该被正确替换");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试混合场景:部分在引号内,部分不在")
|
||||
void testMixedQuotes() {
|
||||
String template =
|
||||
"INSERT INTO device_data(device_id, product_key, temperature) VALUES(" +
|
||||
"'#{deviceId}', '#{productKey}', #{properties.temperature});";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 在引号内的应该只替换值
|
||||
assertTrue(result.contains("'nex123321'"), "引号内的deviceId应该被正确替换");
|
||||
assertTrue(result.contains("'Ru871cfJjhoM'"), "引号内的productKey应该被正确替换");
|
||||
|
||||
// 不在引号内的应该添加引号
|
||||
assertTrue(result.contains("'14.3'"), "不在引号内的temperature应该被添加引号");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试复杂的PostgreSQL JSON场景")
|
||||
void testComplexPostgreSQLJson() {
|
||||
String template =
|
||||
"INSERT INTO device_data(raw_data) VALUES(" +
|
||||
"json_build_object(" +
|
||||
"'deviceId', '#{deviceId}', " +
|
||||
"'temperature', #{properties.temperature}, " +
|
||||
"'description', '#{properties.illuminationDesc}'" +
|
||||
")::TEXT);";
|
||||
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 验证JSON字符串中的占位符被正确替换
|
||||
assertTrue(result.contains("'nex123321'"), "JSON中的deviceId应该被正确替换");
|
||||
assertTrue(result.contains("14.3"), "JSON中的temperature应该被正确替换");
|
||||
assertTrue(result.contains("'bright'"), "JSON中的description应该被正确替换");
|
||||
|
||||
// 不应该有双重引号
|
||||
assertFalse(result.contains("''nex123321''"), "不应该有双重单引号");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试布尔值处理")
|
||||
void testBooleanValue() {
|
||||
variables.put("isActive", true);
|
||||
String template = "INSERT INTO device_data(is_active) VALUES(#{isActive});";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 布尔值不在引号内时,应该直接使用
|
||||
assertTrue(result.contains("true"), "布尔值应该被正确替换");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试空字符串处理")
|
||||
void testEmptyString() {
|
||||
variables.put("emptyField", "");
|
||||
String template = "INSERT INTO device_data(empty_field) VALUES('#{emptyField}');";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 空字符串应该被正确处理
|
||||
assertEquals("INSERT INTO device_data(empty_field) VALUES('');", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试多个占位符在同一引号内")
|
||||
void testMultiplePlaceholdersInQuotes() {
|
||||
// 注意:这种情况在实际SQL中不常见,但测试边界情况
|
||||
String template = "SELECT '#{deviceId}_#{productKey}' as combined;";
|
||||
String result = processTemplate(template, variables);
|
||||
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 两个占位符都在同一个引号内,应该都被正确替换
|
||||
assertTrue(result.contains("nex123321"), "第一个占位符应该被替换");
|
||||
assertTrue(result.contains("Ru871cfJjhoM"), "第二个占位符应该被替换");
|
||||
}
|
||||
}
|
||||
|
||||
+60
-54
@@ -2,6 +2,9 @@ package cn.universal.databridge.plugin.jdbc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import cn.universal.databridge.engine.ParamSql;
|
||||
import cn.universal.databridge.engine.ParamTemplateEngine;
|
||||
import cn.universal.databridge.engine.dialect.MySqlDialectAdapter;
|
||||
import cn.universal.databridge.entity.DataBridgeConfig;
|
||||
import cn.universal.databridge.entity.PluginInfo;
|
||||
import cn.universal.databridge.entity.ResourceConnection;
|
||||
@@ -16,8 +19,7 @@ import org.junit.jupiter.api.Test;
|
||||
public class QuickTemplateTest {
|
||||
|
||||
@Test
|
||||
public void testGenericVsJdbcProcessing() {
|
||||
// 创建测试数据
|
||||
public void testGenericTemplateProcessing() {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("csq", "25");
|
||||
properties.put("code", "200");
|
||||
@@ -29,68 +31,70 @@ public class QuickTemplateTest {
|
||||
|
||||
String template = "Device: #{deviceId}, CSQ: #{properties.csq}, Code: #{properties.code}";
|
||||
|
||||
System.out.println("=== 通用 vs JDBC 模板处理对比 ===");
|
||||
System.out.println("=== 通用模板处理测试 ===");
|
||||
System.out.println("原始模板: " + template);
|
||||
|
||||
try {
|
||||
// 测试通用实现
|
||||
AbstractDataBridgePlugin genericPlugin = new AbstractDataBridgePlugin() {
|
||||
@Override
|
||||
public PluginInfo getPluginInfo() {
|
||||
AbstractDataBridgePlugin genericPlugin =
|
||||
new AbstractDataBridgePlugin() {
|
||||
@Override
|
||||
public PluginInfo getPluginInfo() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean testConnection(ResourceConnection connection) {
|
||||
@Override
|
||||
public Boolean testConnection(ResourceConnection connection) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean validateConfig(DataBridgeConfig config) {
|
||||
@Override
|
||||
public Boolean validateConfig(DataBridgeConfig config) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SourceScope> getSupportedSourceScopes() {
|
||||
@Override
|
||||
public List<SourceScope> getSupportedSourceScopes() {
|
||||
return List.of();
|
||||
}
|
||||
};
|
||||
java.lang.reflect.Method genericMethod =
|
||||
AbstractDataBridgePlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
}
|
||||
};
|
||||
java.lang.reflect.Method genericMethod =
|
||||
AbstractDataBridgePlugin.class.getDeclaredMethod(
|
||||
"processTemplate", String.class, Map.class);
|
||||
genericMethod.setAccessible(true);
|
||||
String genericResult = (String) genericMethod.invoke(genericPlugin, template, variables);
|
||||
|
||||
System.out.println("通用处理结果: " + genericResult);
|
||||
|
||||
// 测试JDBC实现
|
||||
DefaultJdbcOutPlugin jdbcPlugin = new DefaultJdbcOutPlugin();
|
||||
java.lang.reflect.Method jdbcMethod =
|
||||
DefaultJdbcOutPlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
jdbcMethod.setAccessible(true);
|
||||
String jdbcResult = (String) jdbcMethod.invoke(jdbcPlugin, template, variables);
|
||||
|
||||
System.out.println("JDBC处理结果: " + jdbcResult);
|
||||
|
||||
// 验证通用实现 - 直接字符串转换,无单引号
|
||||
assertTrue(genericResult.contains("device001"));
|
||||
assertTrue(genericResult.contains("25"));
|
||||
assertTrue(genericResult.contains("200"));
|
||||
assertFalse(genericResult.contains("'device001'")); // 不应该有单引号
|
||||
|
||||
// 验证JDBC实现 - SQL安全转换,有单引号
|
||||
assertTrue(jdbcResult.contains("'device001'")); // 应该有单引号
|
||||
assertTrue(jdbcResult.contains("'25'")); // 应该有单引号
|
||||
assertTrue(jdbcResult.contains("'200'")); // 应该有单引号
|
||||
|
||||
System.out.println("✅ 通用 vs JDBC 处理对比测试通过!");
|
||||
System.out.println("通用实现: 直接字符串转换,适合JSON等非SQL场景");
|
||||
System.out.println("JDBC实现: SQL安全转换,适合SQL场景");
|
||||
System.out.println("✅ 通用模板处理测试通过!");
|
||||
|
||||
} catch (Exception e) {
|
||||
fail("测试失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJdbcParamTemplateDoesNotInlineSqlValues() {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("remark", "x'); drop table device_data; --");
|
||||
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("deviceId", "device001");
|
||||
variables.put("properties", properties);
|
||||
|
||||
String template =
|
||||
"insert into device_data(device_id, remark) values(#{deviceId}, #{properties.remark})";
|
||||
ParamSql result = new ParamTemplateEngine().process(template, variables, new MySqlDialectAdapter());
|
||||
|
||||
assertEquals("insert into device_data(device_id, remark) values(?, ?)", result.getSql());
|
||||
assertEquals(List.of("device001", "x'); drop table device_data; --"), result.getParams());
|
||||
assertFalse(result.getSql().contains("drop table"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedPropertyAccess() {
|
||||
Map<String, Object> level2 = new HashMap<>();
|
||||
@@ -104,31 +108,33 @@ public class QuickTemplateTest {
|
||||
|
||||
String template = "Nested: #{root.level2.value}";
|
||||
|
||||
AbstractDataBridgePlugin plugin = new AbstractDataBridgePlugin() {
|
||||
@Override
|
||||
public PluginInfo getPluginInfo() {
|
||||
AbstractDataBridgePlugin plugin =
|
||||
new AbstractDataBridgePlugin() {
|
||||
@Override
|
||||
public PluginInfo getPluginInfo() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean testConnection(ResourceConnection connection) {
|
||||
@Override
|
||||
public Boolean testConnection(ResourceConnection connection) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean validateConfig(DataBridgeConfig config) {
|
||||
@Override
|
||||
public Boolean validateConfig(DataBridgeConfig config) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SourceScope> getSupportedSourceScopes() {
|
||||
@Override
|
||||
public List<SourceScope> getSupportedSourceScopes() {
|
||||
return List.of();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method method =
|
||||
AbstractDataBridgePlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
java.lang.reflect.Method method =
|
||||
AbstractDataBridgePlugin.class.getDeclaredMethod(
|
||||
"processTemplate", String.class, Map.class);
|
||||
method.setAccessible(true);
|
||||
String result = (String) method.invoke(plugin, template, variables);
|
||||
|
||||
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
package cn.universal.databridge.plugin.jdbc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** 简单的模板语法测试 */
|
||||
public class SimpleTemplateTest {
|
||||
|
||||
@Test
|
||||
public void testSQLTemplate() {
|
||||
// 创建测试数据
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("csq", "25");
|
||||
properties.put("code", "200");
|
||||
properties.put("temperature", "23.5");
|
||||
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("deviceId", "device001");
|
||||
variables.put("productKey", "product001");
|
||||
variables.put("deviceName", "测试设备");
|
||||
variables.put("iotId", "iot001");
|
||||
variables.put("deviceNode", "DEVICE");
|
||||
variables.put("messageType", "PROPERTIES");
|
||||
variables.put("properties", properties);
|
||||
variables.put("data", "raw_data_content");
|
||||
|
||||
// 测试SQL模板
|
||||
String template =
|
||||
"INSERT INTO device_data(device_id,product_key,device_name,iot_id,device_node,message_type,csq,code,temperature,properties,raw_data) "
|
||||
+ "VALUES(#{deviceId},#{productKey},#{deviceName},#{iotId},#{deviceNode},#{messageType},#{properties.csq},#{properties.code},#{properties.temperature},#{properties},#{data});";
|
||||
|
||||
DefaultJdbcOutPlugin plugin = new DefaultJdbcOutPlugin();
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method method =
|
||||
DefaultJdbcOutPlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
method.setAccessible(true);
|
||||
String result = (String) method.invoke(plugin, template, variables);
|
||||
|
||||
System.out.println("SQL模板测试:");
|
||||
System.out.println("原始模板: " + template);
|
||||
System.out.println("处理结果: " + result);
|
||||
|
||||
// 验证SQL结果
|
||||
assertTrue(result.contains("'device001'"));
|
||||
assertTrue(result.contains("'product001'"));
|
||||
assertTrue(result.contains("'测试设备'"));
|
||||
assertTrue(result.contains("'iot001'"));
|
||||
assertTrue(result.contains("'DEVICE'"));
|
||||
assertTrue(result.contains("'PROPERTIES'"));
|
||||
assertTrue(result.contains("'25'")); // properties.csq
|
||||
assertTrue(result.contains("'200'")); // properties.code
|
||||
assertTrue(result.contains("'23.5'")); // properties.temperature
|
||||
|
||||
System.out.println("✅ SQL模板处理测试通过!");
|
||||
|
||||
} catch (Exception e) {
|
||||
fail("测试失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
package cn.universal.databridge.plugin.jdbc;
|
||||
|
||||
import cn.universal.databridge.entity.DataBridgeConfig;
|
||||
import cn.universal.databridge.entity.PluginInfo;
|
||||
import cn.universal.databridge.entity.ResourceConnection;
|
||||
import cn.universal.databridge.plugin.AbstractDataBridgePlugin;
|
||||
import cn.universal.databridge.plugin.SourceScope;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** 模板处理演示程序 */
|
||||
public class TemplateDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== 模板处理功能演示 ===\n");
|
||||
|
||||
// 创建测试数据
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("csq", "25");
|
||||
properties.put("code", "200");
|
||||
properties.put("temperature", "23.5");
|
||||
properties.put("location", Map.of("city", "Beijing", "country", "China"));
|
||||
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("deviceId", "device001");
|
||||
variables.put("productKey", "product001");
|
||||
variables.put("deviceName", "测试设备");
|
||||
variables.put("properties", properties);
|
||||
|
||||
// 测试模板
|
||||
String template =
|
||||
"Device: #{deviceId}, Product: #{productKey}, Name: #{deviceName}, " +
|
||||
"CSQ: #{properties.csq}, Code: #{properties.code}, " +
|
||||
"City: #{properties.location.city}, Country: #{properties.location.country}";
|
||||
|
||||
System.out.println("原始模板:");
|
||||
System.out.println(template);
|
||||
System.out.println();
|
||||
|
||||
try {
|
||||
// 1. 通用模板处理
|
||||
System.out.println("1. 通用模板处理 (AbstractDataBridgePlugin):");
|
||||
AbstractDataBridgePlugin genericPlugin = new AbstractDataBridgePlugin() {
|
||||
@Override
|
||||
public PluginInfo getPluginInfo() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean testConnection(ResourceConnection connection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean validateConfig(DataBridgeConfig config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SourceScope> getSupportedSourceScopes() {
|
||||
return List.of();
|
||||
}
|
||||
};
|
||||
java.lang.reflect.Method genericMethod =
|
||||
AbstractDataBridgePlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
genericMethod.setAccessible(true);
|
||||
String genericResult = (String) genericMethod.invoke(genericPlugin, template, variables);
|
||||
System.out.println("结果: " + genericResult);
|
||||
System.out.println("特点: 直接字符串转换,无单引号,适合JSON等非SQL场景");
|
||||
System.out.println();
|
||||
|
||||
// 2. JDBC专用模板处理
|
||||
System.out.println("2. JDBC专用模板处理 (DefaultJdbcOutPlugin):");
|
||||
DefaultJdbcOutPlugin jdbcPlugin = new DefaultJdbcOutPlugin();
|
||||
java.lang.reflect.Method jdbcMethod =
|
||||
DefaultJdbcOutPlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
jdbcMethod.setAccessible(true);
|
||||
String jdbcResult = (String) jdbcMethod.invoke(jdbcPlugin, template, variables);
|
||||
System.out.println("结果: " + jdbcResult);
|
||||
System.out.println("特点: SQL安全转换,有单引号,防止SQL注入");
|
||||
System.out.println();
|
||||
|
||||
// 3. 对比分析
|
||||
System.out.println("3. 对比分析:");
|
||||
System.out.println("通用实现 vs JDBC实现:");
|
||||
System.out.println("- 通用: device001 → device001 (无引号)");
|
||||
System.out.println("- JDBC: device001 → 'device001' (有引号)");
|
||||
System.out.println();
|
||||
System.out.println("- 通用: 25 → 25 (数字无引号)");
|
||||
System.out.println("- JDBC: 25 → '25' (数字有引号)");
|
||||
System.out.println();
|
||||
System.out.println("- 通用: Beijing → Beijing (字符串无引号)");
|
||||
System.out.println("- JDBC: Beijing → 'Beijing' (字符串有引号)");
|
||||
System.out.println();
|
||||
|
||||
// 4. 使用场景建议
|
||||
System.out.println("4. 使用场景建议:");
|
||||
System.out.println("通用实现适用于:");
|
||||
System.out.println("- JSON模板: {\"deviceId\": \"#{deviceId}\"}");
|
||||
System.out.println("- XML模板: <device id=\"#{deviceId}\">");
|
||||
System.out.println("- 文本模板: Device #{deviceId} is online");
|
||||
System.out.println();
|
||||
System.out.println("JDBC实现适用于:");
|
||||
System.out.println("- SQL模板: INSERT INTO devices(id, name) VALUES(#{deviceId}, #{deviceName})");
|
||||
System.out.println("- 需要SQL注入防护的场景");
|
||||
System.out.println();
|
||||
|
||||
System.out.println("✅ 演示完成!");
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("演示失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
package cn.universal.databridge.plugin.jdbc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import cn.universal.common.constant.IoTConstant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** 模板处理测试类 测试嵌套属性访问功能 */
|
||||
public class TemplateProcessingTest {
|
||||
@Test
|
||||
public void testNestedPropertyJSON() {
|
||||
// 创建测试数据
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("csq", "25");
|
||||
properties.put("code", "200");
|
||||
properties.put("temperature", "23.5");
|
||||
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("deviceId", "device001");
|
||||
variables.put("productKey", "product001");
|
||||
variables.put("deviceName", "测试设备");
|
||||
variables.put("iotId", "iot001");
|
||||
variables.put("deviceNode", IoTConstant.DeviceNode.DEVICE.toString());
|
||||
variables.put("messageType", "PROPERTIES");
|
||||
variables.put("properties", properties);
|
||||
variables.put("data", "raw_data_content");
|
||||
|
||||
// 测试模板
|
||||
String template =
|
||||
"""
|
||||
{
|
||||
"deviceId": "#{deviceId}",
|
||||
"location": {
|
||||
"city": "#{properties.code}",
|
||||
"deviceNode": "#{deviceNode}",
|
||||
"country": "#{properties.temperature}"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// 创建插件实例进行测试
|
||||
DefaultJdbcOutPlugin plugin = new DefaultJdbcOutPlugin();
|
||||
|
||||
// 使用反射调用受保护方法进行测试
|
||||
try {
|
||||
java.lang.reflect.Method method =
|
||||
DefaultJdbcOutPlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
method.setAccessible(true);
|
||||
String result = (String) method.invoke(plugin, template, variables);
|
||||
|
||||
System.out.println("原始模板:");
|
||||
System.out.println(template);
|
||||
System.out.println("\n处理结果:");
|
||||
System.out.println(result);
|
||||
|
||||
// 验证结果
|
||||
assertTrue(result.contains("'device001'"));
|
||||
assertTrue(result.contains("'product001'"));
|
||||
assertTrue(result.contains("'测试设备'"));
|
||||
assertTrue(result.contains("'iot001'"));
|
||||
assertTrue(result.contains("'DEVICE'"));
|
||||
assertTrue(result.contains("'PROPERTIES'"));
|
||||
assertTrue(result.contains("'25'")); // properties.csq
|
||||
assertTrue(result.contains("'200'")); // properties.code
|
||||
assertTrue(result.contains("'23.5'")); // properties.temperature
|
||||
|
||||
System.out.println("\n✅ 嵌套属性访问测试通过!");
|
||||
|
||||
} catch (Exception e) {
|
||||
fail("测试失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedPropertyAccess() {
|
||||
// 创建测试数据
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("csq", "25");
|
||||
properties.put("code", "200");
|
||||
properties.put("temperature", "23.5");
|
||||
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("deviceId", "device001");
|
||||
variables.put("productKey", "product001");
|
||||
variables.put("deviceName", "测试设备");
|
||||
variables.put("iotId", "iot001");
|
||||
variables.put("deviceNode", "DEVICE");
|
||||
variables.put("messageType", "PROPERTIES");
|
||||
variables.put("properties", properties);
|
||||
variables.put("data", "raw_data_content");
|
||||
|
||||
// 测试模板
|
||||
String template =
|
||||
"INSERT INTO device_data(device_id,product_key,device_name,iot_id,device_node,message_type,csq,code,temperature,properties,raw_data) "
|
||||
+ "VALUES(#{deviceId},#{productKey},#{deviceName},#{iotId},#{deviceNode},#{messageType},#{properties.csq},#{properties.code},#{properties.temperature},#{properties},#{data});";
|
||||
|
||||
// 创建插件实例进行测试
|
||||
DefaultJdbcOutPlugin plugin = new DefaultJdbcOutPlugin();
|
||||
|
||||
// 使用反射调用受保护方法进行测试
|
||||
try {
|
||||
java.lang.reflect.Method method =
|
||||
DefaultJdbcOutPlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
method.setAccessible(true);
|
||||
String result = (String) method.invoke(plugin, template, variables);
|
||||
|
||||
System.out.println("原始模板:");
|
||||
System.out.println(template);
|
||||
System.out.println("\n处理结果:");
|
||||
System.out.println(result);
|
||||
|
||||
// 验证结果
|
||||
assertTrue(result.contains("'device001'"));
|
||||
assertTrue(result.contains("'product001'"));
|
||||
assertTrue(result.contains("'测试设备'"));
|
||||
assertTrue(result.contains("'iot001'"));
|
||||
assertTrue(result.contains("'DEVICE'"));
|
||||
assertTrue(result.contains("'PROPERTIES'"));
|
||||
assertTrue(result.contains("'25'")); // properties.csq
|
||||
assertTrue(result.contains("'200'")); // properties.code
|
||||
assertTrue(result.contains("'23.5'")); // properties.temperature
|
||||
|
||||
System.out.println("\n✅ 嵌套属性访问测试通过!");
|
||||
|
||||
} catch (Exception e) {
|
||||
fail("测试失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedPropertyWithNull() {
|
||||
// 测试嵌套属性为null的情况
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("csq", "25");
|
||||
properties.put("code", null); // 故意设置为null
|
||||
properties.put("temperature", "23.5");
|
||||
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("deviceId", "device001");
|
||||
variables.put("properties", properties);
|
||||
|
||||
String template =
|
||||
"INSERT INTO test_table(csq,code,temperature) VALUES(#{properties.csq},#{properties.code},#{properties.temperature});";
|
||||
|
||||
DefaultJdbcOutPlugin plugin = new DefaultJdbcOutPlugin();
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method method =
|
||||
DefaultJdbcOutPlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
method.setAccessible(true);
|
||||
String result = (String) method.invoke(plugin, template, variables);
|
||||
|
||||
System.out.println("测试null值处理:");
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 验证null值被转换为SQL的NULL
|
||||
assertTrue(result.contains("'25'"));
|
||||
assertTrue(result.contains("NULL")); // properties.code为null
|
||||
assertTrue(result.contains("'23.5'"));
|
||||
|
||||
System.out.println("✅ null值处理测试通过!");
|
||||
|
||||
} catch (Exception e) {
|
||||
fail("测试失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonExistentNestedProperty() {
|
||||
// 测试不存在的嵌套属性
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("csq", "25");
|
||||
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("deviceId", "device001");
|
||||
variables.put("properties", properties);
|
||||
|
||||
String template =
|
||||
"INSERT INTO test_table(csq,non_existent) VALUES(#{properties.csq},#{properties.nonExistent});";
|
||||
|
||||
DefaultJdbcOutPlugin plugin = new DefaultJdbcOutPlugin();
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method method =
|
||||
DefaultJdbcOutPlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
|
||||
method.setAccessible(true);
|
||||
String result = (String) method.invoke(plugin, template, variables);
|
||||
|
||||
System.out.println("测试不存在的嵌套属性:");
|
||||
System.out.println("模板: " + template);
|
||||
System.out.println("结果: " + result);
|
||||
|
||||
// 验证不存在的属性被转换为SQL的NULL
|
||||
assertTrue(result.contains("'25'"));
|
||||
assertTrue(result.contains("NULL")); // 不存在的属性
|
||||
|
||||
System.out.println("✅ 不存在属性处理测试通过!");
|
||||
|
||||
} catch (Exception e) {
|
||||
fail("测试失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -410,8 +410,8 @@ public class CkDeviceLogService extends AbstractIoTDeviceLogService {
|
||||
}
|
||||
if (MapUtil.isNotEmpty(logQuery.getParams())
|
||||
&& ObjectUtil.isNotNull(logQuery.getParams().get("properties"))) {
|
||||
sql.append(
|
||||
" AND JSONHas(content,'properties','" + logQuery.getParams().get("properties") + "')=1");
|
||||
sql.append(" AND JSONHas(content,'properties',?)=1");
|
||||
params.add(logQuery.getParams().get("properties"));
|
||||
}
|
||||
if (MapUtil.isNotEmpty(logQuery.getParams())
|
||||
&& ObjectUtil.isNotNull(logQuery.getParams().get("event"))) {
|
||||
|
||||
-7
@@ -72,11 +72,4 @@ public interface SysMenuMapper extends BaseMapper<SysMenu> {
|
||||
public List<Long> selectMenuListByRoleId(
|
||||
@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly);
|
||||
|
||||
/**
|
||||
* 执行菜单脚本
|
||||
*
|
||||
* @param sql 菜单脚本
|
||||
* @return 返回null
|
||||
*/
|
||||
Integer execMenuSql(@Param("sql") String sql);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@
|
||||
AND p.product_key = #{productKey}
|
||||
</if>
|
||||
<if test="companyNo!=null and companyNo!=''">
|
||||
AND p.company_no like '%${companyNo}%'
|
||||
AND p.company_no like concat('%', #{companyNo}, '%')
|
||||
</if>
|
||||
<if test="iotUnionId!=null and iotUnionId!=''">
|
||||
AND a.creator_id = #{iotUnionId}
|
||||
@@ -347,7 +347,7 @@
|
||||
AND p.product_id = #{productId}
|
||||
</if>
|
||||
<if test="companyNo!=null and companyNo!=''">
|
||||
AND p.company_no like '%${companyNo}%'
|
||||
AND p.company_no like concat('%', #{companyNo}, '%')
|
||||
</if>
|
||||
<if test="iotUnionId!=null and iotUnionId!=''">
|
||||
AND a.creator_id = #{iotUnionId}
|
||||
@@ -475,9 +475,6 @@
|
||||
<if test="unionId != null and unionId != ''">
|
||||
and d.creator_id=#{unionId}
|
||||
</if>
|
||||
<!-- <if test="params.scope != null and params.scope != ''">-->
|
||||
<!-- AND ( ${params.scope} )-->
|
||||
<!-- </if>-->
|
||||
</where>
|
||||
ORDER BY d.create_time DESC
|
||||
</select>
|
||||
@@ -556,9 +553,6 @@
|
||||
<if test="unionId != null and unionId != ''">
|
||||
and d.creator_id=#{unionId}
|
||||
</if>
|
||||
<!-- <if test="params.scope != null and params.scope != ''">-->
|
||||
<!-- AND ( ${params.scope} )-->
|
||||
<!-- </if>-->
|
||||
</where>
|
||||
ORDER BY d.create_time DESC
|
||||
</select>
|
||||
@@ -832,9 +826,6 @@
|
||||
<if test="dev.registryTime != null ">and registry_time = #{dev.registryTime}</if>
|
||||
<if test="dev.onlineTime != null ">and online_time = #{dev.onlineTime}</if>
|
||||
<if test="dev.areasId != null and dev.areasId != ''">and areas_id = #{dev.areasId}</if>
|
||||
<if test="dev.params.scope != null and dev.params.scope != ''">
|
||||
AND ( ${dev.params.scope} )
|
||||
</if>
|
||||
<if test="unionId != null and unionId != ''">
|
||||
and creator_id=#{unionId}
|
||||
</if>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
AND device_node = #{deviceNode}
|
||||
</if>
|
||||
<if test="productName!=null and productName!=''">
|
||||
AND `name` like '%${productName}%'
|
||||
AND `name` like concat('%', #{productName}, '%')
|
||||
</if>
|
||||
<if test="transportProtocol!=null and transportProtocol!=''">
|
||||
AND a.transport_protocol = #{transportProtocol}
|
||||
@@ -59,7 +59,7 @@
|
||||
AND product_id = #{productId}
|
||||
</if>
|
||||
<if test="companyNo!=null and companyNo!=''">
|
||||
AND company_no like '%${companyNo}%'
|
||||
AND company_no like concat('%', #{companyNo}, '%')
|
||||
</if>
|
||||
</where>
|
||||
|
||||
@@ -670,4 +670,4 @@
|
||||
WHERE product_key = #{productKey}
|
||||
AND is_deleted = 0
|
||||
</select>
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -69,9 +69,6 @@
|
||||
<if test="params!=null and params.endTime!=null and params.endTime!=''">
|
||||
and date_format(create_date,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
|
||||
</if>
|
||||
<!-- <if test="params!=null and params.scope != null and params.scope != ''">-->
|
||||
<!-- AND ( ${params.scope} )-->
|
||||
<!-- </if>-->
|
||||
</where>
|
||||
</select>
|
||||
|
||||
@@ -126,4 +123,4 @@
|
||||
or (login_date is not null and DATEDIFF(NOW(), login_date) >= 90))
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -47,10 +47,7 @@
|
||||
<if test="params!=null and params.endTime!=null and params.endTime!=''">
|
||||
and date_format(login_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
|
||||
</if>
|
||||
<!-- <if test="params!=null and params.scope != null and params.scope != ''">-->
|
||||
<!-- AND ( ${params.scope} )-->
|
||||
<!-- </if>-->
|
||||
</where>
|
||||
order by login_time desc
|
||||
</select>
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -45,10 +45,7 @@
|
||||
<if test="params!=null and params.endTime!=null and params.endTime!=''">
|
||||
and date_format(oper_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
|
||||
</if>
|
||||
<!-- <if test="params!=null and params.scope != null and params.scope != ''">-->
|
||||
<!-- AND ( ${params.scope} )-->
|
||||
<!-- </if>-->
|
||||
</where>
|
||||
order by oper_time desc
|
||||
</select>
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -41,10 +41,6 @@
|
||||
create_time
|
||||
from sys_menu
|
||||
</sql>
|
||||
<select id="execMenuSql" resultType="integer">
|
||||
${sql}
|
||||
</select>
|
||||
|
||||
<select id="selectMenuList" parameterType="cn.universal.persistence.entity.admin.SysMenu"
|
||||
resultMap="SysMenuResult">
|
||||
select distinct m.menu_id,
|
||||
@@ -54,7 +50,7 @@
|
||||
m.component,
|
||||
m.visible,
|
||||
m.status,
|
||||
ifnull(m.perms, '') as perms,
|
||||
ifnull(m.perms, '') as perms,
|
||||
m.is_frame,
|
||||
m.is_cache,
|
||||
m.menu_type,
|
||||
@@ -64,7 +60,7 @@
|
||||
from sys_menu m
|
||||
<where>
|
||||
<if test="menuName!=null and menuName!=''">
|
||||
and menu_name like '%${menuName}%'
|
||||
and menu_name like concat('%', #{menuName}, '%')
|
||||
</if>
|
||||
<if test="visible!=null and visible!=''">
|
||||
and visible = #{visible}
|
||||
@@ -188,4 +184,4 @@
|
||||
and ur.union_id = #{unionId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -80,9 +80,6 @@
|
||||
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
|
||||
and date_format(r.create_time,'%y%m%d') <= date_format(#{params.endTime},'%y%m%d')
|
||||
</if>
|
||||
<!-- <if test="params.scope != null and params.scope != ''">-->
|
||||
<!-- AND ( ${params.scope} )-->
|
||||
<!-- </if>-->
|
||||
<if test="createBy !=null and createBy != ''">
|
||||
and create_by=#{createBy}
|
||||
</if>
|
||||
|
||||
Reference in New Issue
Block a user