refactor(core): 删除存在sql注入的几个废弃功能

This commit is contained in:
Join
2026-06-25 13:12:45 +08:00
parent 9cad388599
commit a33fd95814
21 changed files with 368 additions and 1316 deletions
@@ -142,10 +142,4 @@ public interface ISysMenuService {
*/
public String checkMenuNameUnique(SysMenu menu);
/**
* 执行菜单脚本
*
* @param sql 菜单脚本
*/
void execMenuSql(String sql);
}
@@ -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);
}
}
@@ -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();
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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("引号"));
}
}
@@ -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());
}
}
@@ -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"), "第二个占位符应该被替换");
}
}
@@ -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);
@@ -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());
}
}
}
@@ -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();
}
}
}
@@ -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());
}
}
}
@@ -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"))) {
@@ -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') &lt;= 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') &lt;= 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') &lt;= 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') &lt;= 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>