diff --git a/.gitignore b/.gitignore index be3c74e..bd2fe4d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /.metadata /.idea/ .factorypath +/.claude/ diff --git a/codegen/src/main/java/org/seasar/doma/gradle/codegen/extension/CodeGenConfig.java b/codegen/src/main/java/org/seasar/doma/gradle/codegen/extension/CodeGenConfig.java index 5ef9fb6..6d22be4 100644 --- a/codegen/src/main/java/org/seasar/doma/gradle/codegen/extension/CodeGenConfig.java +++ b/codegen/src/main/java/org/seasar/doma/gradle/codegen/extension/CodeGenConfig.java @@ -29,6 +29,7 @@ import org.seasar.doma.gradle.codegen.dialect.CodeGenDialectRegistry; import org.seasar.doma.gradle.codegen.exception.CodeGenException; import org.seasar.doma.gradle.codegen.generator.Generator; +import org.seasar.doma.gradle.codegen.jdbc.DriverWrapper; import org.seasar.doma.gradle.codegen.message.Message; import org.seasar.doma.gradle.codegen.util.ClassUtil; import org.seasar.doma.gradle.codegen.util.JdbcUtil; @@ -159,9 +160,10 @@ private Provider dataSourceProvider() { ClassLoader classLoader = createClassLoader(); Driver driver = ClassUtil.newInstance(Driver.class, driverClassName, "driverClassName", classLoader); + DriverWrapper driverWrapper = new DriverWrapper(driver); return globalFactory .get() - .createDataSource(driver, user.getOrNull(), password.getOrNull(), url.get()); + .createDataSource(driverWrapper, user.getOrNull(), password.getOrNull(), url.get()); }); } diff --git a/codegen/src/main/java/org/seasar/doma/gradle/codegen/jdbc/DriverWrapper.java b/codegen/src/main/java/org/seasar/doma/gradle/codegen/jdbc/DriverWrapper.java new file mode 100644 index 0000000..029de04 --- /dev/null +++ b/codegen/src/main/java/org/seasar/doma/gradle/codegen/jdbc/DriverWrapper.java @@ -0,0 +1,143 @@ +package org.seasar.doma.gradle.codegen.jdbc; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Objects; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * A wrapper for JDBC Driver that manages class loader context switching. + * + *

This wrapper ensures that all driver operations are executed with the correct class loader + * context, preventing class loading issues when the driver and calling code are loaded by different + * class loaders. + * + * @see java.sql.Driver + */ +public class DriverWrapper implements Driver { + + private final Driver driver; + + /** + * Constructs a new DriverWrapper with the specified driver. + * + * @param driver the driver to wrap, must not be null + * @throws NullPointerException if driver is null + */ + public DriverWrapper(Driver driver) { + this.driver = Objects.requireNonNull(driver); + } + + /** + * {@inheritDoc} + * + *

Executes with the driver's class loader context. + */ + @Override + public Connection connect(String url, Properties info) throws SQLException { + return execute(() -> driver.connect(url, info)); + } + + /** + * {@inheritDoc} + * + *

Executes with the driver's class loader context. + */ + @Override + public boolean acceptsURL(String url) throws SQLException { + return execute(() -> driver.acceptsURL(url)); + } + + /** + * {@inheritDoc} + * + *

Executes with the driver's class loader context. + */ + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return execute(() -> driver.getPropertyInfo(url, info)); + } + + /** + * {@inheritDoc} + * + *

Executes with the driver's class loader context. + */ + @Override + public int getMajorVersion() { + return execute(driver::getMajorVersion); + } + + /** + * {@inheritDoc} + * + *

Executes with the driver's class loader context. + */ + @Override + public int getMinorVersion() { + return execute(driver::getMinorVersion); + } + + /** + * {@inheritDoc} + * + *

Executes with the driver's class loader context. + */ + @Override + public boolean jdbcCompliant() { + return execute(driver::jdbcCompliant); + } + + /** + * {@inheritDoc} + * + *

Executes with the driver's class loader context. + */ + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return execute(driver::getParentLogger); + } + + /** + * Executes an operation with the driver's class loader as the context class loader. + * + *

This method temporarily switches the thread's context class loader to the driver's class + * loader, executes the operation, and then restores the original class loader. This ensures that + * the driver can properly load its internal classes and resources. + * + * @param the return type of the executable + * @param the type of exception that may be thrown + * @param executable the operation to execute + * @return the result of the executable operation + * @throws TH if the executable operation throws an exception + */ + private R execute(Executable executable) throws TH { + var classLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(driver.getClass().getClassLoader()); + try { + return executable.execute(); + } finally { + Thread.currentThread().setContextClassLoader(classLoader); + } + } + + /** + * Represents an executable operation that can return a result and throw an exception. + * + * @param the return type + * @param the type of exception that may be thrown + */ + private interface Executable { + /** + * Executes the operation. + * + * @return the result of the operation + * @throws TH if an error occurs during execution + */ + R execute() throws TH; + } +} diff --git a/codegen/src/main/java/org/seasar/doma/gradle/codegen/util/JdbcUtil.java b/codegen/src/main/java/org/seasar/doma/gradle/codegen/util/JdbcUtil.java index 1d8d03c..797a5ab 100644 --- a/codegen/src/main/java/org/seasar/doma/gradle/codegen/util/JdbcUtil.java +++ b/codegen/src/main/java/org/seasar/doma/gradle/codegen/util/JdbcUtil.java @@ -19,7 +19,7 @@ public final class JdbcUtil { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JdbcUtil.class); protected static final Pattern jdbcUrlPattern = - Pattern.compile("jdbc:(?:tc:)?([^:]+):(([^:]+)?:)?"); + Pattern.compile("jdbc:(tc:)?([^:]+):(([^:]+)?:)?"); public static Connection getConnection(DataSource dataSource) { try { @@ -79,7 +79,7 @@ public static String inferDialectName(String url) { return match( url, names -> { - switch (names.getFirst()) { + switch (names.second()) { case "h2": return "h2"; case "hsqldb": @@ -107,7 +107,10 @@ public static String inferDriverClassName(String url) { return match( url, names -> { - switch (names.getFirst()) { + if ("tc:".equals(names.first())) { + return "org.testcontainers.jdbc.ContainerDatabaseDriver"; + } + switch (names.second()) { case "h2": return "org.h2.Driver"; case "hsqldb": @@ -115,7 +118,7 @@ public static String inferDriverClassName(String url) { case "sqlite": return "org.sqlite.JDBC"; case "mysql": - if ("aws".equals(names.getSecond())) { + if ("aws".equals(names.third())) { return "software.aws.rds.jdbc.mysql.Driver"; } return "com.mysql.cj.jdbc.Driver"; @@ -135,16 +138,19 @@ public static String inferDriverClassName(String url) { }); } - protected static R match(String url, Function, R> mapper) { + protected static R match(String url, Function mapper) { if (url == null) { throw new CodeGenNullPointerException("url"); } Matcher matcher = jdbcUrlPattern.matcher(url); if (matcher.lookingAt()) { String first = matcher.group(1); - String second = matcher.group(3); - return mapper.apply(new Pair<>(first, second)); + String second = matcher.group(2); + String third = matcher.group(4); + return mapper.apply(new Triple(first, second, third)); } return null; } + + protected record Triple(String first, String second, String third) {} } diff --git a/codegen/src/test/java/org/seasar/doma/gradle/codegen/util/JdbcUtilTest.java b/codegen/src/test/java/org/seasar/doma/gradle/codegen/util/JdbcUtilTest.java index f68c14e..4f35b3c 100644 --- a/codegen/src/test/java/org/seasar/doma/gradle/codegen/util/JdbcUtilTest.java +++ b/codegen/src/test/java/org/seasar/doma/gradle/codegen/util/JdbcUtilTest.java @@ -116,36 +116,36 @@ public void testInferDialectName_testcontainers_db2() throws Exception { @Test public void testInferDriverClassName_testcontainers_postgresql() throws Exception { String driverClassName = JdbcUtil.inferDriverClassName("jdbc:tc:postgresql:13:///test"); - assertEquals("org.postgresql.Driver", driverClassName); + assertEquals("org.testcontainers.jdbc.ContainerDatabaseDriver", driverClassName); } @Test public void testInferDriverClassName_testcontainers_mysql() throws Exception { String driverClassName = JdbcUtil.inferDriverClassName("jdbc:tc:mysql:8:///test"); - assertEquals("com.mysql.cj.jdbc.Driver", driverClassName); + assertEquals("org.testcontainers.jdbc.ContainerDatabaseDriver", driverClassName); } @Test public void testInferDriverClassName_testcontainers_mariadb() throws Exception { String driverClassName = JdbcUtil.inferDriverClassName("jdbc:tc:mariadb:10.5:///test"); - assertEquals("org.mariadb.jdbc.Driver", driverClassName); + assertEquals("org.testcontainers.jdbc.ContainerDatabaseDriver", driverClassName); } @Test public void testInferDriverClassName_testcontainers_oracle() throws Exception { String driverClassName = JdbcUtil.inferDriverClassName("jdbc:tc:oracle:21c:///test"); - assertEquals("oracle.jdbc.driver.OracleDriver", driverClassName); + assertEquals("org.testcontainers.jdbc.ContainerDatabaseDriver", driverClassName); } @Test public void testInferDriverClassName_testcontainers_sqlserver() throws Exception { String driverClassName = JdbcUtil.inferDriverClassName("jdbc:tc:sqlserver:2019:///test"); - assertEquals("com.microsoft.sqlserver.jdbc.SQLServerDriver", driverClassName); + assertEquals("org.testcontainers.jdbc.ContainerDatabaseDriver", driverClassName); } @Test public void testInferDriverClassName_testcontainers_db2() throws Exception { String driverClassName = JdbcUtil.inferDriverClassName("jdbc:tc:db2:11.5:///test"); - assertEquals("com.ibm.db2.jcc.DB2Driver", driverClassName); + assertEquals("org.testcontainers.jdbc.ContainerDatabaseDriver", driverClassName); } }