getTableResults() {
- return tableResults;
- }
-
- public long getDurationSeconds() {
- if (startTime != null && endTime != null) {
- LocalDateTime start = LocalDateTime.parse(startTime, FORMATTER);
- LocalDateTime end = LocalDateTime.parse(endTime, FORMATTER);
- return java.time.Duration.between(start, end).getSeconds();
- }
- return 0;
- }
- }
-
- /**
- * 表结果内部类
- */
- public static class TableResult {
- private final String status;
- private final long rowsCleaned;
- private final String message;
-
- public TableResult(String status, long rowsCleaned, String message) {
- this.status = status;
- this.rowsCleaned = rowsCleaned;
- this.message = message;
- }
-
- // Getters
- public String getStatus() {
- return status;
- }
-
- public long getRowsCleaned() {
- return rowsCleaned;
- }
-
- public String getMessage() {
- return message;
- }
- }
-}
+/**
+ * Copyright (c) 2023 - present TinyEngine Authors. Copyright (c) 2023 - present Huawei Cloud
+ * Computing Technologies Co., Ltd.
+ *
+ * Use of this source code is governed by an MIT-style license.
+ *
+ *
THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
+ * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A
+ * PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
+ */
+package com.tinyengine.it.task;
+
+import jakarta.annotation.PostConstruct;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+@SuppressWarnings("PMD.TooManyMethods")
+@Service
+public class DatabaseCleanupService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseCleanupService.class);
+ private static final DateTimeFormatter FORMATTER =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+ private static final int EXEC_ID_LENGTH = 8;
+
+ @Autowired private JdbcTemplate jdbcTemplate;
+
+ @Autowired private CleanupProperties cleanupProperties;
+
+ private final Map executionStats = new ConcurrentHashMap<>();
+
+ private final AtomicInteger totalExecutions = new AtomicInteger(0);
+
+ // 默认白名单表(如果配置文件未设置)
+ private static final List DEFAULT_TABLES =
+ Arrays.asList(
+ "t_resource",
+ "t_resource_group",
+ "r_resource_group_resource",
+ "t_app_extension",
+ "t_block",
+ "t_block_carriers_relation",
+ "t_block_group",
+ "t_block_history",
+ "r_material_block",
+ "r_material_history_block",
+ "r_block_group_block",
+ "t_datasource",
+ "t_i18n_entry",
+ "t_model",
+ "t_page",
+ "t_page_history",
+ "t_page_template");
+
+ /** 每天24:00自动执行清空操作 */
+ @Scheduled(cron = "${cleanup.cron-expression:0 0 0 * * ?}")
+ public void autoCleanupAtMidnight() {
+ if (!cleanupProperties.isEnabled()) {
+ logInfo("⏸️ Clearing tasks is disabled, skipping execution");
+ return;
+ }
+
+ final String executionId = createExecutionId();
+ final String startTime = currentTime();
+
+ logInfo("======= Start executing the database clearing task [{}] =======", executionId);
+ logInfo("⏰ Time: {}", startTime);
+ logInfo("📋 Tables: {}", getWhitelistTables());
+
+ final ExecutionStats stats = new ExecutionStats(executionId, startTime);
+ executionStats.put(executionId, stats);
+ totalExecutions.incrementAndGet();
+
+ final CleanupSummary cleanupSummary = new CleanupSummary();
+
+ for (final String tableName : getWhitelistTables()) {
+ cleanTable(tableName, stats, cleanupSummary);
+ }
+
+ final String endTime = currentTime();
+ stats.setEndTime(endTime);
+ stats.setTotalRowsCleaned(cleanupSummary.getTotalRowsCleaned());
+
+ logInfo("📊 ======= Task Completion Statistics [{}] =======", executionId);
+ logInfo("✅ Successful table count: {}", cleanupSummary.getSuccessCount());
+ logInfo("❌ Failure count: {}", cleanupSummary.getFailedCount());
+ logInfo("📈 Total deleted records: {}", cleanupSummary.getTotalRowsCleaned());
+ logInfo("⏰ Time-consuming: {} second", stats.getDurationSeconds());
+ logInfo("🕐 Start: {}, End: {}", startTime, endTime);
+ logInfo("🎉 ======= Task execution completed =======\n");
+ }
+
+ /** 每天23:55发送预警通知 */
+ @Scheduled(cron = "0 55 23 * * ?")
+ public void sendCleanupWarning() {
+ if (!cleanupProperties.isEnabled() || !cleanupProperties.isSendWarning()) {
+ return;
+ }
+
+ logWarn(
+ "⚠️ ⚠️ ⚠️ Important Notice: The database table will be automatically cleared in 5"
+ + " minutes!");
+ logWarn("📋 Target table: {}", getWhitelistTables());
+ logWarn("⏰ Execution Time: 00:00:00");
+ logWarn("💡 If you need to cancel, please change the settings: cleanup.enabled=false");
+ logWarn("==========================================");
+ }
+
+ /** 应用启动时初始化 */
+ @PostConstruct
+ public void init() {
+ logInfo("🚀 Database auto-clear service initialization completed");
+ logInfo("📋 Configuration table: {}", getWhitelistTables());
+ logInfo("⏰ Execution time: {}", cleanupProperties.getCronExpression());
+ logInfo(
+ "🔧 Mode in use: {}", cleanupProperties.isUseTruncate() ? "TRUNCATE" : "DELETE");
+ logInfo("✅ Service status: {}", cleanupProperties.isEnabled() ? "Enabled" : "Disabled");
+ logInfo("==========================================");
+ }
+
+ /**
+ * 获取白名单表列表.
+ *
+ * @return whitelist table names
+ */
+ @SuppressWarnings("PMD.LawOfDemeter")
+ public List getWhitelistTables() {
+ final List tables = cleanupProperties.getWhitelistTables();
+ return tables != null && !tables.isEmpty() ? tables : DEFAULT_TABLES;
+ }
+
+ private static String createExecutionId() {
+ final String fullUuid = UUID.randomUUID().toString();
+ return truncateUuid(fullUuid);
+ }
+
+ private static String truncateUuid(String uuid) {
+ return uuid.substring(0, EXEC_ID_LENGTH);
+ }
+
+ private static String currentTime() {
+ final ZoneId systemZone = ZoneId.systemDefault();
+ final LocalDateTime currentDateTime = LocalDateTime.now(systemZone);
+ return FORMATTER.format(currentDateTime); // 调用静态常量,传入参数
+ }
+
+ private void cleanTable(
+ final String tableName,
+ final ExecutionStats stats,
+ final CleanupSummary cleanupSummary) {
+ try {
+ validateTableName(tableName);
+ if (!tableExists(tableName)) {
+ logWarn("⚠️ Table {} does not exist, skip", tableName);
+ stats.recordSkipped(tableName, "Table does not exist");
+ return;
+ }
+
+ final long rowsCleaned = clearTable(tableName);
+ cleanupSummary.recordSuccess(rowsCleaned);
+ logInfo("✅ Table {} cleared: {} records deleted", tableName, rowsCleaned);
+ stats.recordSuccess(tableName, rowsCleaned);
+ } catch (DataAccessException | IllegalArgumentException exception) {
+ cleanupSummary.recordFailure();
+ logError(
+ "❌ Failed to clear table {}: {}",
+ tableName,
+ exception.getMessage(),
+ exception);
+ stats.recordFailure(tableName, exception.getMessage());
+ }
+ }
+
+ private long clearTable(final String tableName) {
+ long result; // 存储最终返回值
+ if (!cleanupProperties.isUseTruncate()) {
+ result = clearTableData(tableName);
+ } else {
+ final long recordCount = getTableRecordCount(tableName);
+ truncateTable(tableName);
+ result = recordCount;
+ }
+ return result; // 唯一的返回语句
+ }
+
+ /**
+ * 清空表数据(DELETE方式).
+ *
+ * @return number of deleted rows
+ */
+ private long clearTableData(final String tableName) {
+ validateTableName(tableName);
+ final String sql = "DELETE FROM " + tableName;
+ return jdbcTemplate.update(sql);
+ }
+
+ /** 清空表数据(TRUNCATE方式) */
+ private void truncateTable(final String tableName) {
+ validateTableName(tableName);
+ final String sql = "TRUNCATE TABLE " + tableName;
+ jdbcTemplate.execute(sql);
+ }
+
+ /**
+ * 检查表是否存在.
+ *
+ * @return whether the table exists
+ */
+ public boolean tableExists(final String tableName) {
+ boolean exists = false;
+ try {
+ final String sql =
+ "SELECT COUNT(*) FROM information_schema.tables "
+ + "WHERE table_schema = DATABASE() AND table_name = ?";
+ final Integer count =
+ jdbcTemplate.queryForObject(sql, Integer.class, tableName.toUpperCase(Locale.ROOT));
+ exists = count != null && count > 0;
+ } catch (DataAccessException | IllegalArgumentException exception) {
+ logWarn("The checklist has failed: {}", exception.getMessage());
+ }
+ return exists;
+ }
+
+ /**
+ * 获取表记录数量.
+ *
+ * @return record count in the table
+ */
+ public long getTableRecordCount(final String tableName) {
+ long result; // 存储返回值
+ try {
+ validateTableName(tableName);
+ final String sql = "SELECT COUNT(*) FROM " + tableName;
+ final Long count = jdbcTemplate.queryForObject(sql, Long.class);
+ result = (count != null) ? count : 0;
+ } catch (DataAccessException | IllegalArgumentException exception) {
+ logError("获取表记录数失败: {}", exception.getMessage());
+ result = -1; // 异常时返回 -1
+ }
+ return result; // 唯一的退出点
+ }
+
+ /** 验证表名安全性 */
+ private void validateTableName(final String tableName) {
+ if (tableName == null || tableName.isBlank()) {
+ throw new IllegalArgumentException("Table name cannot be empty");
+ }
+ if (!tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
+ throw new IllegalArgumentException("Invalid table name format: " + tableName);
+ }
+ }
+
+ /**
+ * 获取执行统计.
+ *
+ * @return execution statistics
+ */
+ public Map getExecutionStats() {
+ return new LinkedHashMap<>(executionStats);
+ }
+
+ public int getTotalExecutions() {
+ return totalExecutions.get();
+ }
+
+ private static void logInfo(final String message, final Object... arguments) {
+ if (LOGGER.isInfoEnabled()) {
+ LOGGER.info(message, arguments);
+ }
+ }
+
+ private static void logWarn(final String message, final Object... arguments) {
+ if (LOGGER.isWarnEnabled()) {
+ LOGGER.warn(message, arguments);
+ }
+ }
+
+ private static void logError(final String message, final Object... arguments) {
+ if (LOGGER.isErrorEnabled()) {
+ LOGGER.error(message, arguments);
+ }
+ }
+
+ private static final class CleanupSummary {
+ private int successCount;
+ private int failedCount;
+ private long totalRowsCleaned;
+
+ private void recordSuccess(final long rowsCleaned) {
+ successCount++;
+ totalRowsCleaned += rowsCleaned;
+ }
+
+ private void recordFailure() {
+ failedCount++;
+ }
+
+ private int getSuccessCount() {
+ return successCount;
+ }
+
+ private int getFailedCount() {
+ return failedCount;
+ }
+
+ private long getTotalRowsCleaned() {
+ return totalRowsCleaned;
+ }
+ }
+
+ /** 执行统计内部类 */
+ public static class ExecutionStats {
+ private final String executionId;
+ private final String startTime;
+ private String endTime;
+ private long totalRowsCleaned;
+ private final Map tableResults = new LinkedHashMap<>();
+
+ public ExecutionStats(final String executionId, final String startTime) {
+ this.executionId = executionId;
+ this.startTime = startTime;
+ }
+
+ public void recordSuccess(final String tableName, final long rowsCleaned) {
+ tableResults.put(tableName, new TableResult("SUCCESS", rowsCleaned, null));
+ }
+
+ public void recordFailure(final String tableName, final String errorMessage) {
+ tableResults.put(tableName, new TableResult("FAILED", 0, errorMessage));
+ }
+
+ public void recordSkipped(final String tableName, final String reason) {
+ tableResults.put(tableName, new TableResult("SKIPPED", 0, reason));
+ }
+
+ // Getters and setters
+ public String getExecutionId() {
+ return executionId;
+ }
+
+ public String getStartTime() {
+ return startTime;
+ }
+
+ public String getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(final String endTime) {
+ this.endTime = endTime;
+ }
+
+ public long getTotalRowsCleaned() {
+ return totalRowsCleaned;
+ }
+
+ public void setTotalRowsCleaned(final long totalRowsCleaned) {
+ this.totalRowsCleaned = totalRowsCleaned;
+ }
+
+ public Map getTableResults() {
+ return tableResults;
+ }
+
+ public long getDurationSeconds() {
+ long result = 0; // 默认值对应 startTime 或 endTime 为空的情况
+ if (startTime != null && endTime != null) {
+ final LocalDateTime start = LocalDateTime.parse(startTime, FORMATTER);
+ final LocalDateTime end = LocalDateTime.parse(endTime, FORMATTER);
+ result = java.time.Duration.between(start, end).getSeconds();
+ }
+ return result; // 唯一的退出点
+ }
+ }
+
+ /** 表结果内部类 */
+ public static class TableResult {
+ private final String status;
+ private final long rowsCleaned;
+ private final String message;
+
+ public TableResult(final String status, final long rowsCleaned, final String message) {
+ this.status = status;
+ this.rowsCleaned = rowsCleaned;
+ this.message = message;
+ }
+
+ // Getters
+ public String getStatus() {
+ return status;
+ }
+
+ public long getRowsCleaned() {
+ return rowsCleaned;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+ }
+}
diff --git a/base/src/main/java/com/tinyengine/it/common/exception/ServiceException.java b/base/src/main/java/com/tinyengine/it/common/exception/ServiceException.java
index 987c8578..683134f8 100644
--- a/base/src/main/java/com/tinyengine/it/common/exception/ServiceException.java
+++ b/base/src/main/java/com/tinyengine/it/common/exception/ServiceException.java
@@ -37,4 +37,10 @@ public ServiceException(String code, String message) {
this.code = code;
this.message = message;
}
+
+ public ServiceException(String code, String message, Throwable cause) {
+ super(message, cause);
+ this.code = code;
+ this.message = message;
+ }
}
diff --git a/base/src/main/java/com/tinyengine/it/common/utils/SM4Utils.java b/base/src/main/java/com/tinyengine/it/common/utils/SM4Utils.java
index 8c613b64..c8b382c8 100644
--- a/base/src/main/java/com/tinyengine/it/common/utils/SM4Utils.java
+++ b/base/src/main/java/com/tinyengine/it/common/utils/SM4Utils.java
@@ -1,71 +1,116 @@
-package com.tinyengine.it.common.utils;
-
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import javax.crypto.Cipher;
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-import java.security.SecureRandom;
-import java.security.Security;
-import java.util.Base64;
-
-public class SM4Utils {
-
- static {
- Security.addProvider(new BouncyCastleProvider());
- }
-
- private static final String ALGORITHM = "SM4";
- private static final String TRANSFORMATION_ECB = "SM4/ECB/PKCS5Padding";
- private static final int KEY_SIZE = 128;
-
- /**
- * 生成 SM4 密钥
- */
- public static String generateKeyBase64() throws Exception {
- byte[] key = generateKey();
- return Base64.getEncoder().encodeToString(key);
- }
-
- public static byte[] generateKey() throws Exception {
- KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM, "BC");
- kg.init(KEY_SIZE, new SecureRandom());
- SecretKey secretKey = kg.generateKey();
- return secretKey.getEncoded();
- }
-
- /**
- * ECB 模式加密 - 只加密API密钥值 (Base64 结果)
- */
- public static String encryptECB(String apiKey, String base64Key) throws Exception {
- byte[] key = Base64.getDecoder().decode(base64Key);
- byte[] encrypted = encryptECB(apiKey.getBytes("UTF-8"), key);
- return Base64.getEncoder().encodeToString(encrypted);
- }
-
- /**
- * ECB 模式解密 - 直接返回API密钥
- */
- public static String decryptECB(String encryptedBase64, String base64Key) throws Exception {
- byte[] key = Base64.getDecoder().decode(base64Key);
- byte[] encrypted = Base64.getDecoder().decode(encryptedBase64);
- byte[] decrypted = decryptECB(encrypted, key);
- return new String(decrypted, "UTF-8");
- }
-
- // ECB 模式的底层方法保持不变
- private static byte[] encryptECB(byte[] data, byte[] key) throws Exception {
- SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGORITHM);
- Cipher cipher = Cipher.getInstance(TRANSFORMATION_ECB, "BC");
- cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
- return cipher.doFinal(data);
- }
-
- private static byte[] decryptECB(byte[] encryptedData, byte[] key) throws Exception {
- SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGORITHM);
- Cipher cipher = Cipher.getInstance(TRANSFORMATION_ECB, "BC");
- cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
- return cipher.doFinal(encryptedData);
- }
-
-}
+package com.tinyengine.it.common.utils;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public final class SM4Utils {
+
+ private static final String ALGORITHM = "SM4";
+ private static final String TRANSFORMATION = "SM4/GCM/NoPadding";
+ private static final int KEY_SIZE = 128;
+ private static final int KEY_LENGTH_BYTES = KEY_SIZE / Byte.SIZE;
+ private static final int IV_LENGTH_BYTES = 12;
+ private static final int GCM_TAG_BITS = 128;
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private SM4Utils() {
+ // Utility class.
+ }
+
+ /**
+ * 生成 SM4 密钥.
+ *
+ * @return generated SM4 key encoded as Base64
+ */
+ public static String generateKeyBase64() throws GeneralSecurityException {
+ final byte[] key = generateKey();
+ final Base64.Encoder encoder = Base64.getEncoder();
+ return encoder.encodeToString(key);
+ }
+
+ public static byte[] generateKey() throws GeneralSecurityException {
+ final KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM, "BC");
+ keyGenerator.init(KEY_SIZE, SECURE_RANDOM);
+ final SecretKey secretKey = keyGenerator.generateKey();
+ return secretKey.getEncoded();
+ }
+
+ public static String encrypt(final String apiKey, final String base64Key)
+ throws GeneralSecurityException {
+ final byte[] key = decodeKey(base64Key);
+ final byte[] nonce = new byte[IV_LENGTH_BYTES];
+ SECURE_RANDOM.nextBytes(nonce);
+
+ final byte[] encrypted =
+ doCipher(
+ Cipher.ENCRYPT_MODE,
+ apiKey.getBytes(StandardCharsets.UTF_8),
+ key,
+ nonce);
+ final ByteBuffer outputBuffer =
+ ByteBuffer.allocate(nonce.length + encrypted.length);
+ outputBuffer.put(nonce);
+ outputBuffer.put(encrypted);
+ final Base64.Encoder encoder = Base64.getEncoder();
+ return encoder.encodeToString(outputBuffer.array());
+ }
+
+ public static String decrypt(final String encryptedBase64, final String base64Key)
+ throws GeneralSecurityException {
+ final byte[] key = decodeKey(base64Key);
+ final Base64.Decoder decoder = Base64.getDecoder();
+ final byte[] encryptedWithIv = decoder.decode(encryptedBase64);
+ if (encryptedWithIv.length <= IV_LENGTH_BYTES) {
+ throw new IllegalArgumentException("Invalid encrypted payload");
+ }
+
+ final ByteBuffer buffer = ByteBuffer.wrap(encryptedWithIv);
+ final byte[] nonce = new byte[IV_LENGTH_BYTES];
+ buffer.get(nonce);
+ final byte[] encrypted = new byte[buffer.remaining()];
+ buffer.get(encrypted);
+
+ final byte[] decrypted =
+ doCipher(Cipher.DECRYPT_MODE, encrypted, key, nonce);
+ return new String(decrypted, StandardCharsets.UTF_8);
+ }
+
+ private static byte[] doCipher(
+ final int mode,
+ final byte[] data,
+ final byte[] key,
+ final byte[] nonce)
+ throws GeneralSecurityException {
+ final SecretKeySpec secretKeySpec = new SecretKeySpec(key, ALGORITHM);
+ final GCMParameterSpec parameterSpec =
+ new GCMParameterSpec(GCM_TAG_BITS, nonce);
+ final Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC");
+ cipher.init(mode, secretKeySpec, parameterSpec);
+ return cipher.doFinal(data);
+ }
+
+ private static byte[] decodeKey(final String base64Key) {
+ final Base64.Decoder decoder = Base64.getDecoder();
+ final byte[] key = decoder.decode(base64Key);
+ if (key.length != KEY_LENGTH_BYTES) {
+ throw new IllegalArgumentException("SM4 key must be 128 bits");
+ }
+ return key;
+ }
+}
diff --git a/base/src/main/java/com/tinyengine/it/common/utils/SqlIdentifierValidator.java b/base/src/main/java/com/tinyengine/it/common/utils/SqlIdentifierValidator.java
index a9b5738d..96fa4279 100644
--- a/base/src/main/java/com/tinyengine/it/common/utils/SqlIdentifierValidator.java
+++ b/base/src/main/java/com/tinyengine/it/common/utils/SqlIdentifierValidator.java
@@ -1,35 +1,74 @@
package com.tinyengine.it.common.utils;
import java.util.List;
-import java.util.regex.Pattern;
-public class SqlIdentifierValidator {
-
- private static final Pattern IDENTIFIER_PATTERN =
- Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");
-
- private static final Pattern ORDER_TYPE_PATTERN =
- Pattern.compile("^(ASC|DESC)$", Pattern.CASE_INSENSITIVE);
+public final class SqlIdentifierValidator {
private SqlIdentifierValidator() {
+ // Utility class.
}
- public static void validate(String identifier) {
- if (identifier == null || !IDENTIFIER_PATTERN.matcher(identifier).matches()) {
+ public static void validate(final String identifier) {
+ if (!isValidIdentifier(identifier)) {
throw new IllegalArgumentException("Invalid SQL identifier: " + identifier);
}
}
- public static void validateAll(List identifiers) {
+ public static String requireValidIdentifier(final String identifier) {
+ validate(identifier);
+ return identifier;
+ }
+
+ public static void validateAll(final List identifiers) {
if (identifiers == null) {
return;
}
identifiers.forEach(SqlIdentifierValidator::validate);
}
- public static void validateOrderType(String orderType) {
- if (orderType == null || !ORDER_TYPE_PATTERN.matcher(orderType).matches()) {
+ public static void validateOrderType(final String orderType) {
+ if (!isValidOrderType(orderType)) {
throw new IllegalArgumentException("Invalid order type: " + orderType);
}
}
+
+ public static String requireValidOrderType(final String orderType) {
+ validateOrderType(orderType);
+ return orderType.toUpperCase(java.util.Locale.ROOT);
+ }
+
+ public static boolean isValidIdentifier(final String identifier) {
+ boolean valid = identifier != null && !identifier.isEmpty();
+ if (valid) {
+ valid = isIdentifierStart(identifier.charAt(0));
+ }
+
+ for (int index = 1; valid && index < identifier.length(); index++) {
+ if (!isIdentifierPart(identifier.charAt(index))) {
+ valid = false;
+ }
+ }
+ return valid;
+ }
+
+ public static boolean isValidOrderType(final String orderType) {
+ return "ASC".equalsIgnoreCase(orderType) || "DESC".equalsIgnoreCase(orderType);
+ }
+
+ public static String escapeSqlLiteral(final Object value) {
+ final String stringValue = value == null ? null : value.toString();
+ return stringValue == null
+ ? null
+ : stringValue.replace("\\", "\\\\").replace("'", "''");
+ }
+
+ private static boolean isIdentifierStart(final char character) {
+ return character == '_'
+ || character >= 'A' && character <= 'Z'
+ || character >= 'a' && character <= 'z';
+ }
+
+ private static boolean isIdentifierPart(final char character) {
+ return isIdentifierStart(character) || character >= '0' && character <= '9';
+ }
}
diff --git a/base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java b/base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java
index fba8eac1..3c63bef5 100644
--- a/base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java
+++ b/base/src/main/java/com/tinyengine/it/dynamic/dao/DynamicSqlProvider.java
@@ -1,107 +1,226 @@
package com.tinyengine.it.dynamic.dao;
+import com.tinyengine.it.common.utils.SqlIdentifierValidator;
+
import org.apache.ibatis.jdbc.SQL;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
-
+@SuppressWarnings("PMD.TooManyMethods")
public class DynamicSqlProvider {
- public String select(Map params) {
- String tableName = (String) params.get("tableName");
- List fields = (List) params.get("fields");
- Map conditions = (Map) params.get("conditions");
- Integer pageNum = (Integer) params.get("pageNum");
- Integer pageSize = (Integer) params.get("pageSize");
- String orderBy = (String) params.get("orderBy");
- String orderType = (String) params.get("orderType");
-
- SQL sql = new SQL();
-
- // 选择字段
- if (fields != null && !fields.isEmpty()) {
- for (String field : fields) {
- sql.SELECT(field);
- }
- } else {
- sql.SELECT("*");
- }
-
- sql.FROM(tableName);
-
- // 条件
- if (conditions != null && !conditions.isEmpty()) {
- for (Map.Entry entry : conditions.entrySet()) {
- if (entry.getValue() != null) {
- sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");
- }
- }
- }
- // 排序
- if (orderBy != null && !orderBy.isEmpty()) {
- sql.ORDER_BY(orderBy + " " + orderType);
- }
-
- // 分页
- if (pageNum != null && pageSize != null) {
- return sql.toString() + " LIMIT " + (pageNum - 1) * pageSize + ", " + pageSize;
- }
-
- return sql.toString();
- }
-
- public String insert(Map params) {
- String tableName = (String) params.get("tableName");
- Map data = (Map) params.get("data");
-
- SQL sql = new SQL();
- sql.INSERT_INTO(tableName);
-
- if (data != null && !data.isEmpty()) {
- for (Map.Entry entry : data.entrySet()) {
- sql.VALUES(entry.getKey(), "#{data." + entry.getKey() + "}");
- }
- }
-
- return sql.toString();
- }
-
- public String update(Map params) {
- String tableName = (String) params.get("tableName");
- Map data = (Map) params.get("data");
- Map conditions = (Map) params.get("conditions");
-
- SQL sql = new SQL();
- sql.UPDATE(tableName);
-
- if (data != null && !data.isEmpty()) {
- for (Map.Entry entry : data.entrySet()) {
- sql.SET(entry.getKey() + " = #{data." + entry.getKey() + "}");
- }
- }
-
- if (conditions != null && !conditions.isEmpty()) {
- for (Map.Entry entry : conditions.entrySet()) {
- sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");
- }
- }
-
- return sql.toString();
- }
-
- public String delete(Map params) {
- String tableName = (String) params.get("tableName");
- Map conditions = (Map) params.get("conditions");
-
- SQL sql = new SQL();
- sql.DELETE_FROM(tableName);
-
- if (conditions != null && !conditions.isEmpty()) {
- for (Map.Entry entry : conditions.entrySet()) {
- sql.WHERE(entry.getKey() + " = #{conditions." + entry.getKey() + "}");
- }
- }
-
- return sql.toString();
- }
+ private static final String COUNT_SELECT = "COUNT(*) AS count";
+ private static final String LEGACY_COUNT = "COUNT(*) as count";
+ private static final String TABLE_NAME_PARAM = "tableName";
+ private static final String CONDITIONS_PARAM = "conditions";
+ private static final String DATA_PARAM = "data";
+
+ @SuppressWarnings("PMD.UnnecessaryConstructor")
+ public DynamicSqlProvider() {
+ // MyBatis instantiates providers through the default constructor.
+ }
+
+ public String select(final Map params) {
+ final String tableName = requireIdentifier(params.get(TABLE_NAME_PARAM), TABLE_NAME_PARAM);
+ final List> fields = getList(params.get("fields"));
+ final Map, ?> conditions = getMap(params.get(CONDITIONS_PARAM));
+ final Integer pageNum = (Integer) params.get("pageNum");
+ final Integer pageSize = (Integer) params.get("pageSize");
+ final String orderBy = getOptionalIdentifier(params.get("orderBy"), "orderBy");
+ final String orderType = getOrderType(params.get("orderType"));
+
+ final SQL sql = new SQL();
+
+ if (fields != null && !fields.isEmpty()) {
+ for (final Object field : fields) {
+ sql.SELECT(getSelectField(field));
+ }
+ } else {
+ sql.SELECT("*");
+ }
+
+ sql.FROM(tableName);
+
+ if (conditions != null && !conditions.isEmpty()) {
+ final List