From 29c5bfba6dc5253dbd428cef361ed5f1b77498be Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Wed, 23 Oct 2024 17:45:02 +0200 Subject: [PATCH 1/2] Quartz - add configuration option for custom JDBC delegate option --- .../quartz/deployment/QuartzProcessor.java | 69 +++++++++++++++---- .../quartz/runtime/QuartzBuildTimeConfig.java | 13 ++++ 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java index 127d583a8bd3c..81e2739284704 100644 --- a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java +++ b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/QuartzProcessor.java @@ -8,11 +8,14 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import jakarta.inject.Singleton; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; import org.quartz.Job; import org.quartz.JobDataMap; import org.quartz.JobListener; @@ -46,6 +49,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; @@ -70,6 +74,11 @@ public class QuartzProcessor { private static final DotName JOB = DotName.createSimple(Job.class.getName()); + private static final DotName DELEGATE_POSTGRESQL = DotName.createSimple(QuarkusPostgreSQLDelegate.class.getName()); + private static final DotName DELEGATE_DB2V8 = DotName.createSimple(QuarkusDBv8Delegate.class.getName()); + private static final DotName DELEGATE_HSQLDB = DotName.createSimple(QuarkusHSQLDBDelegate.class.getName()); + private static final DotName DELEGATE_MSSQL = DotName.createSimple(QuarkusMSSQLDelegate.class.getName()); + private static final DotName DELEGATE_STDJDBC = DotName.createSimple(QuarkusStdJDBCDelegate.class.getName()); @BuildStep FeatureBuildItem feature() { @@ -103,8 +112,7 @@ NativeImageProxyDefinitionBuildItem connectionProxy(QuartzBuildTimeConfig config @BuildStep QuartzJDBCDriverDialectBuildItem driver(List jdbcDataSourceBuildItems, - QuartzBuildTimeConfig config, - Capabilities capabilities) { + QuartzBuildTimeConfig config, Capabilities capabilities, CombinedIndexBuildItem indexBuildItem) { if (!config.storeType.isDbStore()) { if (config.clustered) { throw new ConfigurationException("Clustered jobs configured with unsupported job store option"); @@ -118,19 +126,52 @@ QuartzJDBCDriverDialectBuildItem driver(List jdbcDataSo "The Agroal extension is missing and it is required when a Quartz JDBC store is used."); } - Optional selectedJdbcDataSourceBuildItem = jdbcDataSourceBuildItems.stream() - .filter(i -> config.dataSourceName.isPresent() ? config.dataSourceName.get().equals(i.getName()) - : i.isDefault()) - .findFirst(); - - if (!selectedJdbcDataSourceBuildItem.isPresent()) { - String message = String.format( - "JDBC Store configured but the '%s' datasource is not configured properly. You can configure your datasource by following the guide available at: https://quarkus.io/guides/datasource", - config.dataSourceName.isPresent() ? config.dataSourceName.get() : "default"); - throw new ConfigurationException(message); + Optional driverDelegate = config.driverDelegate; + if (driverDelegate.isPresent()) { + // user-specified custom delegate + IndexView indexView = indexBuildItem.getIndex(); + ClassInfo customDelegate = indexView.getClassByName(driverDelegate.get()); + if (customDelegate == null) { + String message = String.format( + "Custom JDBC delegate implementation class '%s' was not found in Jandex index. " + + "Make sure the dependency containing this class has proper marker file enabling discovery. " + + "Alternatively, you can index a dependency using IndexDependencyBuildItem.", + driverDelegate.get()); + throw new ConfigurationException(message); + } else { + // any custom implementation needs to be a subclass of known Quarkus delegate + boolean implementsKnownDelegate = false; + for (DotName knownImplementation : Set.of(DELEGATE_MSSQL, DELEGATE_POSTGRESQL, DELEGATE_DB2V8, DELEGATE_STDJDBC, + DELEGATE_HSQLDB)) { + for (ClassInfo classInfo : indexView.getAllKnownSubclasses(knownImplementation)) { + if (classInfo.name().equals(customDelegate.name())) { + implementsKnownDelegate = true; + break; + } + } + } + if (!implementsKnownDelegate) { + String message = String.format( + "Custom JDBC delegate implementation with name '%s' needs to be a subclass of one of the existing Quarkus delegates such as io.quarkus.quartz.runtime.jdbc.QuarkusPostgreSQLDelegate.", + driverDelegate.get()); + throw new ConfigurationException(message); + } + } + } else { + Optional selectedJdbcDataSourceBuildItem = jdbcDataSourceBuildItems.stream() + .filter(i -> config.dataSourceName.isPresent() ? config.dataSourceName.get().equals(i.getName()) + : i.isDefault()) + .findFirst(); + + if (!selectedJdbcDataSourceBuildItem.isPresent()) { + String message = String.format( + "JDBC Store configured but the '%s' datasource is not configured properly. You can configure your datasource by following the guide available at: https://quarkus.io/guides/datasource", + config.dataSourceName.isPresent() ? config.dataSourceName.get() : "default"); + throw new ConfigurationException(message); + } + driverDelegate = Optional.of(guessDriver(selectedJdbcDataSourceBuildItem)); } - - return new QuartzJDBCDriverDialectBuildItem(Optional.of(guessDriver(selectedJdbcDataSourceBuildItem))); + return new QuartzJDBCDriverDialectBuildItem(driverDelegate); } private String guessDriver(Optional jdbcDataSource) { diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java index 2f17bea7f22ef..0126024e34cb9 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzBuildTimeConfig.java @@ -74,6 +74,19 @@ public class QuartzBuildTimeConfig { @ConfigItem public Optional selectWithLockSql; + /** + * Allows users to specify fully qualified class name for a custom JDBC driver delegate. + *

+ * This property is optional and leaving it empty will result in Quarkus automatically choosing appropriate default + * driver delegate implementation. + *

+ * Note that any custom implementation has to be a subclass of existing Quarkus implementation such as + * {@link io.quarkus.quartz.runtime.jdbc.QuarkusPostgreSQLDelegate} or + * {@link io.quarkus.quartz.runtime.jdbc.QuarkusMSSQLDelegate} + */ + @ConfigItem + public Optional driverDelegate; + /** * Instructs JDBCJobStore to serialize JobDataMaps in the BLOB column. *

From 1730f704173b55981b012654bfb2cd4c194d15f7 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Fri, 25 Oct 2024 10:06:57 +0200 Subject: [PATCH 2/2] Quartz - add validation tests for custom JDBC delegate --- .../DelegateNotASubclassTest.java | 53 +++++++++++++++++++ .../DelegateNotIndexedTest.java | 53 +++++++++++++++++++ .../test/customDelegate/InvalidDelegate.java | 5 ++ 3 files changed, 111 insertions(+) create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/DelegateNotASubclassTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/DelegateNotIndexedTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/InvalidDelegate.java diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/DelegateNotASubclassTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/DelegateNotASubclassTest.java new file mode 100644 index 0000000000000..b6200573a038c --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/DelegateNotASubclassTest.java @@ -0,0 +1,53 @@ +package io.quarkus.quartz.test.customDelegate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.function.Consumer; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.deployment.Capability; +import io.quarkus.deployment.builditem.CapabilityBuildItem; +import io.quarkus.quartz.test.SimpleJobs; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class DelegateNotASubclassTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + // add a mock pretending to provide Agroal Capability to pass our validation + .addBuildChainCustomizer(new Consumer<>() { + @Override + public void accept(BuildChainBuilder buildChainBuilder) { + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce( + new CapabilityBuildItem(Capability.AGROAL, "fakeProvider")); + } + }).produces(CapabilityBuildItem.class).build(); + } + }) + .assertException(t -> { + assertEquals(ConfigurationException.class, t.getClass()); + Assertions.assertTrue(t.getMessage().contains( + "Custom JDBC delegate implementation with name 'io.quarkus.quartz.test.customDelegate.InvalidDelegate' needs to be a subclass")); + }) + .withApplicationRoot((jar) -> jar + .addClasses(SimpleJobs.class, InvalidDelegate.class) + .addAsResource(new StringAsset( + "quarkus.quartz.driver-delegate=io.quarkus.quartz.test.customDelegate.InvalidDelegate\nquarkus.quartz.store-type=jdbc-cmt"), + "application.properties")); + + @Test + public void shouldFailIfNotASubclass() { + Assertions.fail(); + } +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/DelegateNotIndexedTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/DelegateNotIndexedTest.java new file mode 100644 index 0000000000000..248f1584ade99 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/DelegateNotIndexedTest.java @@ -0,0 +1,53 @@ +package io.quarkus.quartz.test.customDelegate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.function.Consumer; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.deployment.Capability; +import io.quarkus.deployment.builditem.CapabilityBuildItem; +import io.quarkus.quartz.test.SimpleJobs; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class DelegateNotIndexedTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + // add a mock pretending to provide Agroal Capability to pass our validation + .addBuildChainCustomizer(new Consumer<>() { + @Override + public void accept(BuildChainBuilder buildChainBuilder) { + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce( + new CapabilityBuildItem(Capability.AGROAL, "fakeProvider")); + } + }).produces(CapabilityBuildItem.class).build(); + } + }) + .assertException(t -> { + assertEquals(ConfigurationException.class, t.getClass()); + Assertions.assertTrue(t.getMessage().contains( + "Custom JDBC delegate implementation class 'org.acme.DoesNotExist' was not found in Jandex index")); + }) + .withApplicationRoot((jar) -> jar + .addClasses(SimpleJobs.class) + .addAsResource(new StringAsset( + "quarkus.quartz.driver-delegate=org.acme.DoesNotExist\nquarkus.quartz.store-type=jdbc-cmt"), + "application.properties")); + + @Test + public void shouldFailWhenNotIndexed() { + Assertions.fail(); + } +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/InvalidDelegate.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/InvalidDelegate.java new file mode 100644 index 0000000000000..a337418403997 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/customDelegate/InvalidDelegate.java @@ -0,0 +1,5 @@ +package io.quarkus.quartz.test.customDelegate; + +// dummy class representing an invalid JDBC delegate by not subclassing a known one +public class InvalidDelegate { +}