mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
@@ -732,7 +732,17 @@ public final class DbxJdbcPlugin {
|
||||
throw new SQLException("JDBC logical sessions require the same selected driver");
|
||||
}
|
||||
if (connectionState() != DEFAULT_CONNECTION_STATE) logicalDriverKey = driverKey;
|
||||
String driverClass = optionalText(connection, "jdbc_driver_class");
|
||||
if (driverKey.equals(registeredDriverKey) && registeredDriver != null) {
|
||||
if (driverClass != null) {
|
||||
LegacyJdbcDriverClass.load(
|
||||
driverClass,
|
||||
optionalText(connection, "connection_string"),
|
||||
registeredDriverClassLoader != null
|
||||
? registeredDriverClassLoader
|
||||
: Thread.currentThread().getContextClassLoader()
|
||||
);
|
||||
}
|
||||
if (registeredDriverClassLoader != null) {
|
||||
Thread.currentThread().setContextClassLoader(registeredDriverClassLoader);
|
||||
}
|
||||
@@ -757,9 +767,10 @@ public final class DbxJdbcPlugin {
|
||||
Thread.currentThread().setContextClassLoader(loader);
|
||||
registeredDriverClassLoader = loader;
|
||||
|
||||
String driverClass = optionalText(connection, "jdbc_driver_class");
|
||||
if (driverClass != null) {
|
||||
Constructor<?> constructor = Class.forName(driverClass, true, loader).getDeclaredConstructor();
|
||||
Constructor<?> constructor = LegacyJdbcDriverClass.load(
|
||||
driverClass, optionalText(connection, "connection_string"), loader
|
||||
).getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Driver driver = (Driver) constructor.newInstance();
|
||||
registeredDriver = new DriverShim(driver);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package app.dbx.jdbc;
|
||||
|
||||
final class LegacyJdbcDriverClass {
|
||||
private LegacyJdbcDriverClass() {}
|
||||
|
||||
static Class<?> load(String driverClass, String jdbcUrl, ClassLoader loader) throws ClassNotFoundException {
|
||||
try {
|
||||
return Class.forName(driverClass, true, loader);
|
||||
} catch (ClassNotFoundException error) {
|
||||
if (!driverClass.equals(error.getMessage()) || error.getCause() != null || jdbcUrl == null) {
|
||||
throw error;
|
||||
}
|
||||
String resolved = switch (driverClass) {
|
||||
case "teradata" -> hasUrlPrefix(jdbcUrl, "jdbc:teradata://") ? "com.teradata.jdbc.TeraDriver" : null;
|
||||
case "db2" -> hasUrlPrefix(jdbcUrl, "jdbc:db2:") ? "com.ibm.db2.jcc.DB2Driver" : null;
|
||||
case "h2_embedded", "h2_embedded_v2" -> hasUrlPrefix(jdbcUrl, "jdbc:h2:") ? "org.h2.Driver" : null;
|
||||
default -> null;
|
||||
};
|
||||
if (resolved == null) {
|
||||
throw error;
|
||||
}
|
||||
return Class.forName(resolved, true, loader);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasUrlPrefix(String url, String prefix) {
|
||||
return url.startsWith(prefix) && url.length() > prefix.length();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package app.dbx.jdbc;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import javax.tools.ToolProvider;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Driver;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class LegacyJdbcDriverClassTest {
|
||||
private static final String H2_URL = "jdbc:h2:mem:legacy_class_resolution";
|
||||
private static final String TERADATA_URL = "jdbc:teradata://127.0.0.1/DATABASE=fixture,DBS_PORT=1025";
|
||||
private static final String DB2_URL = "jdbc:db2://127.0.0.1:50000/FIXTURE";
|
||||
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void resolvesHistoricalH2AliasesUsingTheSelectedJar() throws Exception {
|
||||
try (URLClassLoader loader = h2Loader()) {
|
||||
for (String alias : List.of("h2_embedded", "h2_embedded_v2")) {
|
||||
assertThrows(ClassNotFoundException.class, () -> Class.forName(alias, true, loader));
|
||||
Class<?> resolved = LegacyJdbcDriverClass.load(alias, H2_URL, loader);
|
||||
assertEquals("org.h2.Driver", resolved.getName());
|
||||
assertSame(loader, resolved.getClassLoader());
|
||||
assertInstanceOf(Driver.class, resolved.getDeclaredConstructor().newInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesHistoricalTeradataAndDb2Identities() throws Exception {
|
||||
for (String[] fixture : List.of(
|
||||
new String[] { "teradata", TERADATA_URL, "com.teradata.jdbc.TeraDriver" },
|
||||
new String[] { "db2", DB2_URL, "com.ibm.db2.jcc.DB2Driver" }
|
||||
)) {
|
||||
try (URLClassLoader loader = fixtureLoader(fixture[2], "")) {
|
||||
assertThrows(ClassNotFoundException.class, () -> Class.forName(fixture[0], true, loader));
|
||||
Class<?> resolved = LegacyJdbcDriverClass.load(fixture[0], fixture[1], loader);
|
||||
assertEquals(fixture[2], resolved.getName());
|
||||
assertSame(loader, resolved.getClassLoader());
|
||||
assertInstanceOf(Driver.class, resolved.getDeclaredConstructor().newInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesExplicitClassesRegardlessOfUrl() throws Exception {
|
||||
try (URLClassLoader loader = h2Loader()) {
|
||||
Class<?> explicit = Class.forName("org.h2.Driver", true, loader);
|
||||
assertSame(explicit, LegacyJdbcDriverClass.load("org.h2.Driver", TERADATA_URL, loader));
|
||||
assertSame(explicit, LegacyJdbcDriverClass.load("org.h2.Driver", null, loader));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesLoadableUnqualifiedCustomClassesIncludingAliases() throws Exception {
|
||||
for (String name : List.of("teradata", "db2", "h2_embedded", "h2_embedded_v2", "CustomDriver")) {
|
||||
try (URLClassLoader loader = fixtureLoader(name, "")) {
|
||||
Class<?> explicit = Class.forName(name, true, loader);
|
||||
assertSame(explicit, LegacyJdbcDriverClass.load(name, TERADATA_URL, loader));
|
||||
assertSame(explicit, LegacyJdbcDriverClass.load(name, H2_URL, loader));
|
||||
assertInstanceOf(Driver.class, explicit.getDeclaredConstructor().newInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownAndMalformedAliases() throws Exception {
|
||||
try (URLClassLoader loader = h2Loader()) {
|
||||
for (String name : List.of("custom.MissingDriver", "unknown", "h2_embedded_v3", "H2_embedded",
|
||||
" h2_embedded", "h2_embedded ", "", "com.example.teradata")) {
|
||||
ClassNotFoundException error = assertThrows(ClassNotFoundException.class,
|
||||
() -> LegacyJdbcDriverClass.load(name, H2_URL, loader));
|
||||
assertEquals(name, error.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingMalformedAndMismatchedUrls() throws Exception {
|
||||
try (URLClassLoader loader = h2Loader()) {
|
||||
for (String[] fixture : List.of(
|
||||
new String[] { "teradata", H2_URL },
|
||||
new String[] { "db2", TERADATA_URL },
|
||||
new String[] { "h2_embedded", DB2_URL },
|
||||
new String[] { "h2_embedded_v2", "jdbc:h2other:mem:test" },
|
||||
new String[] { "teradata", "jdbc:teradatax://localhost" },
|
||||
new String[] { "db2", "jdbc:db20://localhost/TEST" },
|
||||
new String[] { "h2_embedded", "prefix:jdbc:h2:mem:test" },
|
||||
new String[] { "h2_embedded", "jdbc:h2:" },
|
||||
new String[] { "teradata", "jdbc:teradata://" },
|
||||
new String[] { "db2", "jdbc:db2:" },
|
||||
new String[] { "h2_embedded", "" },
|
||||
new String[] { "teradata", null }
|
||||
)) {
|
||||
ClassNotFoundException error = assertThrows(ClassNotFoundException.class,
|
||||
() -> LegacyJdbcDriverClass.load(fixture[0], fixture[1], loader));
|
||||
assertEquals(fixture[0], error.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotReplaceMissingSelectedJarsWithContextDrivers() throws Exception {
|
||||
ClassLoader previous = Thread.currentThread().getContextClassLoader();
|
||||
try (URLClassLoader context = h2Loader();
|
||||
URLClassLoader selected = new URLClassLoader(
|
||||
new URL[] { temporaryDirectory.resolve("missing.jar").toUri().toURL() },
|
||||
ClassLoader.getPlatformClassLoader())) {
|
||||
Thread.currentThread().setContextClassLoader(context);
|
||||
ClassNotFoundException error = assertThrows(ClassNotFoundException.class,
|
||||
() -> LegacyJdbcDriverClass.load("h2_embedded_v2", H2_URL, selected));
|
||||
assertEquals("org.h2.Driver", error.getMessage());
|
||||
assertSame(context, Thread.currentThread().getContextClassLoader());
|
||||
} finally {
|
||||
Thread.currentThread().setContextClassLoader(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesTheSuppliedContextLoaderWhenNoPathsAreSelected() throws Exception {
|
||||
ClassLoader previous = Thread.currentThread().getContextClassLoader();
|
||||
try (URLClassLoader context = h2Loader()) {
|
||||
Thread.currentThread().setContextClassLoader(context);
|
||||
Class<?> resolved = LegacyJdbcDriverClass.load("h2_embedded", H2_URL,
|
||||
Thread.currentThread().getContextClassLoader());
|
||||
assertSame(context, resolved.getClassLoader());
|
||||
assertSame(context, Thread.currentThread().getContextClassLoader());
|
||||
} finally {
|
||||
Thread.currentThread().setContextClassLoader(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesTheSuppliedLoadersParentDelegation() throws Exception {
|
||||
try (URLClassLoader parent = h2Loader();
|
||||
URLClassLoader child = new URLClassLoader(new URL[0], parent)) {
|
||||
Class<?> resolved = LegacyJdbcDriverClass.load("h2_embedded", H2_URL, child);
|
||||
assertSame(parent, resolved.getClassLoader());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesClassNotFoundForADifferentClass() {
|
||||
ClassNotFoundException failure = new ClassNotFoundException("vendor.RequiredDependency");
|
||||
ClassLoader loader = new ClassLoader(ClassLoader.getPlatformClassLoader()) {
|
||||
@Override
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
throw failure;
|
||||
}
|
||||
};
|
||||
assertSame(failure, assertThrows(ClassNotFoundException.class,
|
||||
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesClassNotFoundThatWrapsALoadingFailure() {
|
||||
ClassNotFoundException failure = new ClassNotFoundException("teradata",
|
||||
new NoClassDefFoundError("vendor/RequiredDependency"));
|
||||
ClassLoader loader = new ClassLoader(ClassLoader.getPlatformClassLoader()) {
|
||||
@Override
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
throw failure;
|
||||
}
|
||||
};
|
||||
assertSame(failure, assertThrows(ClassNotFoundException.class,
|
||||
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void propagatesInitializerFailuresWithoutAliasFallback() throws Exception {
|
||||
try (URLClassLoader loader = fixtureLoader("teradata",
|
||||
"static { if (true) throw new IllegalStateException(\"initializer failed\"); }")) {
|
||||
ExceptionInInitializerError error = assertThrows(ExceptionInInitializerError.class,
|
||||
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader));
|
||||
assertEquals("initializer failed", error.getCause().getMessage());
|
||||
assertThrows(NoClassDefFoundError.class,
|
||||
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void propagatesLinkageErrorsWithoutAliasFallback() {
|
||||
LinkageError failure = new UnsupportedClassVersionError("driver requires a newer Java version");
|
||||
ClassLoader loader = new ClassLoader(ClassLoader.getPlatformClassLoader()) {
|
||||
@Override
|
||||
protected Class<?> findClass(String name) {
|
||||
throw failure;
|
||||
}
|
||||
};
|
||||
assertSame(failure, assertThrows(LinkageError.class,
|
||||
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void propagatesFailuresInTheResolvedVendorClass() throws Exception {
|
||||
try (URLClassLoader loader = fixtureLoader("com.teradata.jdbc.TeraDriver",
|
||||
"static { if (true) throw new UnsatisfiedLinkError(\"vendor library missing\"); }")) {
|
||||
UnsatisfiedLinkError error = assertThrows(UnsatisfiedLinkError.class,
|
||||
() -> LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader));
|
||||
assertEquals("vendor library missing", error.getMessage());
|
||||
}
|
||||
try (URLClassLoader loader = fixtureLoader("com.ibm.db2.jcc.DB2Driver",
|
||||
"static { if (true) throw new IllegalStateException(\"vendor initialization failed\"); }")) {
|
||||
ExceptionInInitializerError error = assertThrows(ExceptionInInitializerError.class,
|
||||
() -> LegacyJdbcDriverClass.load("db2", DB2_URL, loader));
|
||||
assertEquals("vendor initialization failed", error.getCause().getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rechecksTheUrlOnRepeatedResolution() throws Exception {
|
||||
try (URLClassLoader loader = h2Loader()) {
|
||||
Class<?> expected = LegacyJdbcDriverClass.load("h2_embedded", H2_URL, loader);
|
||||
ClassNotFoundException error = assertThrows(ClassNotFoundException.class,
|
||||
() -> LegacyJdbcDriverClass.load("h2_embedded", DB2_URL, loader));
|
||||
assertEquals("h2_embedded", error.getMessage());
|
||||
assertSame(expected, LegacyJdbcDriverClass.load("h2_embedded", H2_URL, loader));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void leavesConstructionFailuresAndDriverTypeValidationToTheCaller() throws Exception {
|
||||
try (URLClassLoader loader = fixtureLoader("teradata",
|
||||
"public teradata() { throw new IllegalStateException(\"constructor failed\"); }")) {
|
||||
Class<?> resolved = LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader);
|
||||
InvocationTargetException error = assertThrows(InvocationTargetException.class,
|
||||
() -> resolved.getDeclaredConstructor().newInstance());
|
||||
assertEquals("constructor failed", error.getCause().getMessage());
|
||||
}
|
||||
try (URLClassLoader loader = compile("teradata", "public class teradata {}")) {
|
||||
Class<?> resolved = LegacyJdbcDriverClass.load("teradata", TERADATA_URL, loader);
|
||||
assertThrows(ClassCastException.class, () -> Driver.class.cast(resolved.getDeclaredConstructor().newInstance()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void isolatesConcurrentResolutionsByLoader() throws Exception {
|
||||
try (URLClassLoader first = h2Loader(); URLClassLoader second = h2Loader();
|
||||
var executor = Executors.newFixedThreadPool(4)) {
|
||||
List<Callable<Class<?>>> tasks = new ArrayList<>();
|
||||
for (int index = 0; index < 20; index++) {
|
||||
ClassLoader selected = index % 2 == 0 ? first : second;
|
||||
tasks.add(() -> LegacyJdbcDriverClass.load("h2_embedded_v2", H2_URL, selected));
|
||||
}
|
||||
var results = executor.invokeAll(tasks);
|
||||
assertNotSame(results.get(0).get(), results.get(1).get());
|
||||
for (int index = 0; index < results.size(); index++) {
|
||||
assertSame(index % 2 == 0 ? first : second, results.get(index).get().getClassLoader());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static URLClassLoader h2Loader() {
|
||||
return new URLClassLoader(new URL[] { org.h2.Driver.class.getProtectionDomain().getCodeSource().getLocation() },
|
||||
ClassLoader.getPlatformClassLoader());
|
||||
}
|
||||
|
||||
private URLClassLoader fixtureLoader(String name, String body) throws Exception {
|
||||
int separator = name.lastIndexOf('.');
|
||||
String packageDeclaration = separator < 0 ? "" : "package " + name.substring(0, separator) + ";";
|
||||
String simpleName = name.substring(separator + 1);
|
||||
return compile(name, packageDeclaration + "public class " + simpleName + " implements java.sql.Driver {"
|
||||
+ body + """
|
||||
public java.sql.Connection connect(String url, java.util.Properties properties) {
|
||||
throw new AssertionError("Registration must not connect");
|
||||
}
|
||||
public boolean acceptsURL(String url) { return false; }
|
||||
public java.sql.DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties properties) {
|
||||
return new java.sql.DriverPropertyInfo[0];
|
||||
}
|
||||
public int getMajorVersion() { return 1; }
|
||||
public int getMinorVersion() { return 0; }
|
||||
public boolean jdbcCompliant() { return false; }
|
||||
public java.util.logging.Logger getParentLogger() { return java.util.logging.Logger.getGlobal(); }
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
private URLClassLoader compile(String name, String source) throws Exception {
|
||||
Path root = Files.createTempDirectory(temporaryDirectory, "driver-");
|
||||
Path file = root.resolve(name.replace('.', '/') + ".java");
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, source);
|
||||
var compiler = ToolProvider.getSystemJavaCompiler();
|
||||
assertNotNull(compiler, "Driver classloading fixtures require the Java 21 JDK");
|
||||
assertEquals(0, compiler.run(null, null, null, "-proc:none", "-d", root.toString(), file.toString()));
|
||||
return new URLClassLoader(new URL[] { root.toUri().toURL() }, ClassLoader.getPlatformClassLoader());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package app.dbx.jdbc;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class LegacyJdbcRegistrationTest {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@AfterEach
|
||||
void closeConnection() throws Exception {
|
||||
request("close", configuration("org.h2.Driver"), null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyAliasesAndExplicitOrDefaultClassesReachTheProductionQueryPath() throws Exception {
|
||||
for (String driverClass : new String[] {"h2_embedded", "h2_embedded_v2", "org.h2.Driver", ""}) {
|
||||
ObjectNode connection = configuration(driverClass);
|
||||
JsonNode connected = request("connect", connection, null);
|
||||
assertFalse(connected.has("error"), connected.toString());
|
||||
JsonNode result = request("executeQuery", connection, "SELECT 1");
|
||||
assertFalse(result.has("error"), result.toString());
|
||||
assertEquals(1, result.path("result").path("rows").path(0).path(0).asInt());
|
||||
request("close", connection, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cachedLegacyAliasCannotBypassUrlValidationAndRecoversAfterAnError() throws Exception {
|
||||
ObjectNode connection = configuration("h2_embedded_v2");
|
||||
JsonNode connected = request("connect", connection, null);
|
||||
assertFalse(connected.has("error"), connected.toString());
|
||||
|
||||
ObjectNode mismatched = connection.deepCopy();
|
||||
mismatched.put("connection_string", "jdbc:postgresql://127.0.0.1/unused");
|
||||
JsonNode rejected = request("connect", mismatched, null);
|
||||
assertTrue(rejected.has("error"), rejected.toString());
|
||||
assertTrue(rejected.path("error").toString().contains("Missing Java class h2_embedded_v2."), rejected.toString());
|
||||
|
||||
JsonNode recovered = request("executeQuery", connection, "SELECT 2");
|
||||
assertFalse(recovered.has("error"), recovered.toString());
|
||||
assertEquals(2, recovered.path("result").path("rows").path(0).path(0).asInt());
|
||||
}
|
||||
|
||||
private static ObjectNode configuration(String driverClass) {
|
||||
ObjectNode connection = MAPPER.createObjectNode();
|
||||
connection.put("connection_string", "jdbc:h2:mem:legacy_registration_test");
|
||||
connection.put("username", "sa");
|
||||
connection.put("password", "");
|
||||
connection.put("jdbc_driver_class", driverClass);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static JsonNode request(String method, ObjectNode connection, String sql) throws Exception {
|
||||
ObjectNode message = MAPPER.createObjectNode();
|
||||
message.put("id", 1);
|
||||
message.put("method", method);
|
||||
ObjectNode params = message.putObject("params");
|
||||
params.set("connection", connection);
|
||||
if (sql != null) params.put("sql", sql);
|
||||
Method handler = DbxJdbcPlugin.class.getDeclaredMethod("handleLine", String.class);
|
||||
handler.setAccessible(true);
|
||||
return (JsonNode) handler.invoke(null, MAPPER.writeValueAsString(message));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user