diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/text/CodePointIter.java b/hutool-core/src/main/java/cn/hutool/v7/core/text/CodePointIter.java index 95c40d99e..e8405bd63 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/text/CodePointIter.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/text/CodePointIter.java @@ -23,7 +23,7 @@ import java.util.Iterator; * 参考:http://stackoverflow.com/a/21791059/6030888 * * @author Looly - * @param str + * @param str 字符串 */ public record CodePointIter(String str) implements Iterable { diff --git a/hutool-db/src/main/java/cn/hutool/v7/db/config/TomlConfigParser.java b/hutool-db/src/main/java/cn/hutool/v7/db/config/TomlConfigParser.java new file mode 100644 index 000000000..7c6a18faf --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/v7/db/config/TomlConfigParser.java @@ -0,0 +1,237 @@ +package cn.hutool.v7.db.config; + +import cn.hutool.v7.core.array.ArrayUtil; +import cn.hutool.v7.core.convert.ConvertUtil; +import cn.hutool.v7.core.io.resource.NoResourceException; +import cn.hutool.v7.core.io.resource.ResourceUtil; +import cn.hutool.v7.core.text.StrUtil; +import cn.hutool.v7.core.util.ObjUtil; +import cn.hutool.v7.db.DbException; +import cn.hutool.v7.db.driver.DriverUtil; +import cn.hutool.v7.db.sql.SqlLog; +import cn.hutool.v7.db.sql.filter.SqlLogFilter; +import cn.hutool.v7.log.level.Level; +import cn.hutool.v7.setting.toml.TomlReader; + +import java.util.HashMap; +import java.util.Map; + +/** + * 基于TOML类型的数据库配置解析器 + * + * @author Looly + * @since 7.0.0 + */ +public class TomlConfigParser implements ConfigParser { + + private static final String CONNECTION_PREFIX = "connection."; + + /** + * 默认TOML配置文件路径 + */ + private static final String[] DEFAULT_DB_TOML_PATHS = {"config/db.toml", "db.toml"}; + + /** + * 创建默认配置解析器 + * + * @return TomlConfigParser + */ + public static TomlConfigParser of() { + return of(null); + } + + /** + * 创建配置解析器 + * + * @param tomlPath TOML配置文件路径 + * @return TomlConfigParser + */ + public static TomlConfigParser of(final String tomlPath) { + return new TomlConfigParser(tomlPath); + } + + private final Map tomlData; + + /** + * 构造函数 + * + * @param tomlPath 自定义TOML配置文件路径,{@code null}表示使用默认路径 + */ + public TomlConfigParser(final String tomlPath) { + String tomlContent = null; + if (null != tomlPath) { + // 读取指定TOML文件 + tomlContent = ResourceUtil.readUtf8Str(tomlPath); + } else { + // 读取默认TOML文件 + for (String defaultDbTomlPath : DEFAULT_DB_TOML_PATHS) { + try { + tomlContent = ResourceUtil.readUtf8Str(defaultDbTomlPath); + } catch (NoResourceException e) { + // ignore + } + } + } + + if (null == tomlContent) { + throw new NoResourceException("Default db toml [{}] in classpath not found !", ArrayUtil.join(DEFAULT_DB_TOML_PATHS, ",")); + } + + this.tomlData = new TomlReader(tomlContent, false).read(); + } + + @SuppressWarnings("unchecked") + @Override + public DbConfig parse(final String group) { + Map groupData; + + if (StrUtil.isEmpty(group)) { + groupData = this.tomlData; + } else { + Object groupObj = this.tomlData.get(group); + if (groupObj instanceof Map) { + // 新建一个Map,避免继承属性影响原数据 + groupData = new HashMap<>((Map) groupObj); + } else { + throw new DbException("No config for group: [{}]", group); + } + } + + // 继承属性 + copyPropertyIfAbsent(groupData, DSKeys.KEY_SHOW_SQL); + copyPropertyIfAbsent(groupData, DSKeys.KEY_FORMAT_SQL); + copyPropertyIfAbsent(groupData, DSKeys.KEY_SHOW_PARAMS); + copyPropertyIfAbsent(groupData, DSKeys.KEY_SQL_LEVEL); + + return toDbConfig(groupData); + } + + /** + * 如果目标map中没有指定键,则从全局配置复制 + */ + private void copyPropertyIfAbsent(Map target, String key) { + if (!target.containsKey(key) && this.tomlData.containsKey(key)) { + target.put(key, this.tomlData.get(key)); + } + } + + /** + * 将TOML数据转换为DbConfig对象 + */ + private DbConfig toDbConfig(Map data) { + // 基本信息 + String url = getAndRemoveString(data, DSKeys.KEY_ALIAS_URL); + if (StrUtil.isBlank(url)) { + throw new DbException("No JDBC URL!"); + } + + // 自动识别Driver + String driver = getAndRemoveString(data, DSKeys.KEY_ALIAS_DRIVER); + if (StrUtil.isBlank(driver)) { + driver = DriverUtil.identifyDriver(url); + } + + DbConfig dbConfig = DbConfig.of() + .setUrl(url) + .setDriver(driver) + .setUser(getAndRemoveString(data, DSKeys.KEY_ALIAS_USER)) + .setPass(getAndRemoveString(data, DSKeys.KEY_ALIAS_PASSWORD)); + + // SQL日志 + SqlLogFilter sqlLogFilter = getSqlLogFilter(data); + if (sqlLogFilter != null) { + dbConfig.addSqlFilter(sqlLogFilter); + } + + // 大小写等配置 + Boolean caseInsensitive = getAndRemoveBoolean(data, DSKeys.KEY_CASE_INSENSITIVE); + if (null != caseInsensitive) { + dbConfig.setCaseInsensitive(caseInsensitive); + } + + // 连接配置 + for (String key : DSKeys.KEY_CONN_PROPS) { + String connValue = getAndRemoveString(data, key); + if (StrUtil.isNotBlank(connValue)) { + dbConfig.addConnProps(key, connValue); + } + } + + // 自定义连接属性 + data.forEach((key, value) -> { + if(key.startsWith(CONNECTION_PREFIX) && !(value instanceof Map)){ + dbConfig.addConnProps(key.substring(CONNECTION_PREFIX.length()), value.toString()); + } + }); + + // 池属性 - 移除已处理的属性后剩余的作为池属性 + data.forEach((key, value) -> { + // 非Map的属性作为池属性 + if(!(value instanceof Map)){ + dbConfig.addPoolProps(key, StrUtil.toStringOrNull(value)); + } + }); + + return dbConfig; + } + + /** + * 获取SQL日志过滤器 + */ + private SqlLogFilter getSqlLogFilter(Map data) { + final Boolean isShowSql = getAndRemoveBoolean(data, DSKeys.KEY_SHOW_SQL); + if (isShowSql == null || !isShowSql) { + return null; + } + + final boolean isFormatSql = ObjUtil.defaultIfNull(getAndRemoveBoolean(data, DSKeys.KEY_FORMAT_SQL), false); + final boolean isShowParams = ObjUtil.defaultIfNull(getAndRemoveBoolean(data, DSKeys.KEY_SHOW_PARAMS), false); + + String sqlLevelStr = getAndRemoveString(data, DSKeys.KEY_SQL_LEVEL); + if (sqlLevelStr != null) { + sqlLevelStr = sqlLevelStr.toUpperCase(); + } + Level level = ConvertUtil.toEnum(Level.class, sqlLevelStr, Level.DEBUG); + + SqlLog sqlLog = new SqlLog(); + sqlLog.init(true, isFormatSql, isShowParams, level); + + return new SqlLogFilter(sqlLog); + } + + /** + * 从map中获取字符串值 + * + * @param map 源map + * @param keys 多个键 + * @return 字符串值 + */ + private String getAndRemoveString(Map map, String... keys) { + Object value = null; + for (String key : keys) { + value = map.remove(key); + if (null != value) { + break; + } + } + return StrUtil.toStringOrNull(value); + } + + /** + * 从map中获取布尔值 + * + * @param map 源map + * @param keys 多个键 + * @return 布尔值 + */ + private Boolean getAndRemoveBoolean(Map map, String... keys) { + Object value = null; + for (String key : keys) { + value = map.remove(key); + if (null != value) { + break; + } + } + return ConvertUtil.toBoolean(value); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/v7/db/config/TomlConfigParserTest.java b/hutool-db/src/test/java/cn/hutool/v7/db/config/TomlConfigParserTest.java new file mode 100644 index 000000000..9528b9460 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/v7/db/config/TomlConfigParserTest.java @@ -0,0 +1,37 @@ +package cn.hutool.v7.db.config; + +import cn.hutool.v7.core.lang.Console; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TomlConfigParserTest { + @Test + public void parseTest() { + DbConfig dbConfig = TomlConfigParser.of().parse(""); + assertEquals("org.sqlite.JDBC", dbConfig.getDriver()); + assertEquals("jdbc:sqlite:test.db", dbConfig.getUrl()); + assertNull(dbConfig.getUser()); + assertNull(dbConfig.getPass()); + + assertEquals(1, dbConfig.getConnProps().size()); + assertEquals("true", dbConfig.getConnProps().getProperty("remarks")); + assertNull(dbConfig.getPoolProps()); + } + + @Test + void parseOrclTest(){ + DbConfig dbConfig = TomlConfigParser.of().parse("orcl"); + Console.log(dbConfig); + + assertEquals("oracle.jdbc.OracleDriver", dbConfig.getDriver()); + assertEquals("jdbc:oracle:thin:@//localhost:1521/XEPDB1", dbConfig.getUrl()); + assertEquals("system", dbConfig.getUser()); + assertEquals("123456", dbConfig.getPass()); + + assertEquals(1, dbConfig.getConnProps().size()); + assertEquals("true", dbConfig.getConnProps().getProperty("remarks")); + assertNull(dbConfig.getPoolProps()); + } +} diff --git a/hutool-db/src/test/resources/config/db.toml b/hutool-db/src/test/resources/config/db.toml new file mode 100644 index 000000000..52d995ae2 --- /dev/null +++ b/hutool-db/src/test/resources/config/db.toml @@ -0,0 +1,115 @@ +# +# Copyright (c) 2013-2025 Hutool Team and hutool.cn +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# suppress inspection "Annotator" for whole file +#=================================================================== +# 数据库配置文件样例 +# DsFactory默认读取的配置文件是config/db.setting +# db.setting的配置包括两部分:基本连接信息和连接池配置信息。 +# 基本连接信息所有连接池都支持,连接池配置信息根据不同的连接池,连接池配置是根据连接池相应的配置项移植而来 +#=================================================================== + +## 打印SQL的配置 +# 是否在日志中显示执行的SQL,默认false +showSql = true +# 是否格式化显示的SQL,默认false +formatSql = true +# 是否显示SQL参数,默认false +showParams = true +# 打印SQL的日志等级,默认debug +sqlLevel = "debug" + +# 默认数据源 +url = "jdbc:sqlite:test.db" +remarks = true + +# 测试数据源 +[test] +url = "jdbc:sqlite:test.db" +remarks = true +driver = "org.sqlite.JDBC" + +# 测试用HSQLDB数据库 +[hsqldb] +url = "jdbc:hsqldb:mem:mem_hutool" +user = "SA" +pass = "" +remarks = true + +# 测试用HSQLDB数据库 +[h2] +url = "jdbc:h2:mem:h2_hutool" +user = "sa" +pass = "" +remarks = true + +# 测试用HSQLDB数据库 +[derby] +url = "jdbc:derby:.derby/test_db;create=true" +remarks = true + +# 测试用Oracle数据库 +[orcl] +url = "jdbc:oracle:thin:@//localhost:1521/XEPDB1" +user = "system" +pass = "123456" +remarks = true + +[mysql] +url = "jdbc:mysql://Looly.centos8:3306/hutool_test?useSSL=false" +user = "root" +pass = "123456" +remarks = true + +[mariadb_local] +url = "jdbc:mysql://localhost:3306/test?useSSL=false" +user = "root" +pass = "123456" +remarks = true + +[postgre] +url = "jdbc:postgresql://localhost:5432/test_hutool" +user = "postgres" +pass = "123456" +remarks = true + +[sqlserver] +url = "jdbc:sqlserver://Looly.database.chinacloudapi.cn:1433;database=test;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.chinacloudapi.cn;loginTimeout=30;" +user = "Looly@Looly" +pass = "123" +remarks = true + +# 测试用达梦8数据库 +# 测试环境使用docker启动,见:https://eco.dameng.com/document/dm/zh-cn/start/dm-install-docker.html +[dm] +url = "jdbc:dm://localhost:5236" +user = "SYSDBA" +pass = "SYSDBA001" +remarks = true + +# OceanBase +# 测试环境使用docker启动,见:https://www.oceanbase.com/docs/common-oceanbase-database-cn-1000000000217958 +[ob] +url = "jdbc:oceanbase://localhost:2881/test" +user = "root" +pass = "123456" +remarks = true + +[hana] +url = "jdbc:sap://localhost:30015/HAP_CONN?autoReconnect=true" +user = "DB" +pass = "123456" +remarks = "true"