From f09837e2465a4b426fb88e5a326b1e5858973e4f Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Wed, 19 Jun 2024 11:45:14 +0200 Subject: [PATCH 01/16] Update the MappingStructure constant in the docs to match the enum values --- .../runtime/HibernateSearchStandaloneBuildTimeConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/hibernate-search-standalone-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/standalone/elasticsearch/runtime/HibernateSearchStandaloneBuildTimeConfig.java b/extensions/hibernate-search-standalone-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/standalone/elasticsearch/runtime/HibernateSearchStandaloneBuildTimeConfig.java index 8ef6845e7aa049..fc3de18ff9b914 100644 --- a/extensions/hibernate-search-standalone-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/standalone/elasticsearch/runtime/HibernateSearchStandaloneBuildTimeConfig.java +++ b/extensions/hibernate-search-standalone-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/standalone/elasticsearch/runtime/HibernateSearchStandaloneBuildTimeConfig.java @@ -269,7 +269,7 @@ interface MappingConfig { * Associations between entities must be bi-directional: * specifying the inverse side of associations through `@AssociationInverseSide` *is required*, * unless reindexing is disabled for that association through `@IndexingDependency(reindexOnUpdate = ...)`. - * `tree`:: + * `document`:: * Entities indexed through Hibernate Search are the root of a document, * i.e. an indexed entity "owns" other entities it references through associations, * which *cannot* be updated independently of the indexed entity. From a0c7c69c609356ced161b96aab9c10b6197f8e11 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 4 Jul 2024 10:28:36 +0300 Subject: [PATCH 02/16] Don't always set the user parameter when starting container in tests Fixes: #41659 --- .../quarkus/test/common/DefaultDockerContainerLauncher.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java index 2d01ef727fd4a9..533048d7959349 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/DefaultDockerContainerLauncher.java @@ -118,7 +118,9 @@ public void start() throws IOException { args.add("-i"); // Interactive, write logs to stdout args.add("--rm"); - args.addAll(NativeImageBuildLocalContainerRunner.getVolumeAccessArguments(containerRuntime)); + if (!volumeMounts.isEmpty()) { + args.addAll(NativeImageBuildLocalContainerRunner.getVolumeAccessArguments(containerRuntime)); + } args.add("-p"); args.add(httpPort + ":" + httpPort); From b97f08b1d8642f184095cb704bc08eb0d96a6c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Mon, 8 Jul 2024 16:27:03 +0200 Subject: [PATCH 03/16] Clarify that quarkus.hibernate-orm.dialect allows simpler names --- docs/src/main/asciidoc/_attributes.adoc | 1 + docs/src/main/asciidoc/hibernate-orm.adoc | 11 +++++++++-- .../HibernateOrmConfigPersistenceUnit.java | 12 ++++++++---- .../src/main/resources/application.properties | 4 ++-- .../src/main/resources/application.properties | 4 ++-- .../test/resources/property-warnings-test.properties | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/src/main/asciidoc/_attributes.adoc index 4eed987d1446ac..5729f47a657a93 100644 --- a/docs/src/main/asciidoc/_attributes.adoc +++ b/docs/src/main/asciidoc/_attributes.adoc @@ -52,6 +52,7 @@ :quickstarts-tree-url: ${quickstarts-base-url}/tree/main // . :hibernate-orm-docs-url: https://docs.jboss.org/hibernate/orm/{hibernate-orm-version-major-minor}/userguide/html_single/Hibernate_User_Guide.html +:hibernate-orm-dialect-docs-url: https://docs.jboss.org/hibernate/orm/{hibernate-orm-version-major-minor}/dialect/dialect.html :hibernate-search-docs-url: https://docs.jboss.org/hibernate/search/{hibernate-search-version-major-minor}/reference/en-US/html_single/ // . :amazon-services-guide: https://quarkiverse.github.io/quarkiverse-docs/quarkus-amazon-services/dev/index.html diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index f41dd334d1faa6..5b2a0d63c3db85 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -227,9 +227,16 @@ quarkus.datasource.username = hibernate quarkus.datasource.password = hibernate quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:26257/hibernate_db -quarkus.hibernate-orm.dialect=org.hibernate.dialect.CockroachDialect <1> +quarkus.hibernate-orm.dialect=Cockroach <1> ---- <1> Set the Hibernate ORM dialect. ++ +For built-in dialects, the expected value is one of the names +in the link:{hibernate-orm-dialect-docs-url}[official list of dialects], *without* the `Dialect` suffix, +for example `Cockroach` for `CockroachDialect`. ++ +For third-party dialects, the expected value is the fully-qualified class name, +for example `com.acme.hibernate.AcmeDbDialect`. [WARNING] ==== @@ -249,7 +256,7 @@ quarkus.datasource.username = hibernate quarkus.datasource.password = hibernate quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:26257/hibernate_db -quarkus.hibernate-orm.dialect=org.hibernate.dialect.CockroachDialect <2> +quarkus.hibernate-orm.dialect=Cockroach <2> ---- <1> Set the database version. The Hibernate ORM dialect will target that version. Since we're targeting CockroachDB here, we're passing the CockroachDB version, not the PostgreSQL version. diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java index 94fe4d3b64c94b..e6c30584166c6f 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java @@ -276,11 +276,15 @@ default boolean isAnyPropertySet() { interface HibernateOrmConfigPersistenceUnitDialect { /** - * Class name of the Hibernate ORM dialect. + * Name of the Hibernate ORM dialect. * - * The complete list of bundled dialects is available in the - * https://docs.jboss.org/hibernate/stable/orm/javadocs/org/hibernate/dialect/package-summary.html[Hibernate ORM - * JavaDoc]. + * For built-in dialects, the expected value is one of the names + * in the link:{hibernate-orm-dialect-docs-url}[official list of dialects], + * *without* the `Dialect` suffix, + * for example `Cockroach` for `CockroachDialect`. + * + * For third-party dialects, the expected value is the fully-qualified class name, + * for example `com.acme.hibernate.AcmeDbDialect`. * * Setting the dialect directly is only recommended as a last resort: * most popular databases have a corresponding Quarkus extension, diff --git a/integration-tests/hibernate-orm-tenancy/connection-resolver-legacy-qualifiers/src/main/resources/application.properties b/integration-tests/hibernate-orm-tenancy/connection-resolver-legacy-qualifiers/src/main/resources/application.properties index 86da8eec686676..b943eb6cc10f0c 100644 --- a/integration-tests/hibernate-orm-tenancy/connection-resolver-legacy-qualifiers/src/main/resources/application.properties +++ b/integration-tests/hibernate-orm-tenancy/connection-resolver-legacy-qualifiers/src/main/resources/application.properties @@ -3,7 +3,7 @@ quarkus.hibernate-orm.database.generation=none quarkus.hibernate-orm.multitenant=database # Necessary because we're creating datasources dynamically, # which means the extension can't rely on a static datasource to guess the dialect -quarkus.hibernate-orm.dialect=org.hibernate.dialect.MariaDBDialect +quarkus.hibernate-orm.dialect=MariaDB quarkus.hibernate-orm.packages=io.quarkus.it.hibernate.multitenancy.fruit # We create datasources manually, so a lack of configuration doesn't mean Quarkus should step in with defaults. @@ -12,7 +12,7 @@ quarkus.datasource.devservices.enabled=false # Inventory persistence unit quarkus.hibernate-orm."inventory".database.generation=none quarkus.hibernate-orm."inventory".multitenant=database -quarkus.hibernate-orm."inventory".dialect=org.hibernate.dialect.MariaDBDialect +quarkus.hibernate-orm."inventory".dialect=MariaDB quarkus.hibernate-orm."inventory".packages=io.quarkus.it.hibernate.multitenancy.inventory #mariadb.base_url is set through Maven config diff --git a/integration-tests/hibernate-orm-tenancy/connection-resolver/src/main/resources/application.properties b/integration-tests/hibernate-orm-tenancy/connection-resolver/src/main/resources/application.properties index 86da8eec686676..b943eb6cc10f0c 100644 --- a/integration-tests/hibernate-orm-tenancy/connection-resolver/src/main/resources/application.properties +++ b/integration-tests/hibernate-orm-tenancy/connection-resolver/src/main/resources/application.properties @@ -3,7 +3,7 @@ quarkus.hibernate-orm.database.generation=none quarkus.hibernate-orm.multitenant=database # Necessary because we're creating datasources dynamically, # which means the extension can't rely on a static datasource to guess the dialect -quarkus.hibernate-orm.dialect=org.hibernate.dialect.MariaDBDialect +quarkus.hibernate-orm.dialect=MariaDB quarkus.hibernate-orm.packages=io.quarkus.it.hibernate.multitenancy.fruit # We create datasources manually, so a lack of configuration doesn't mean Quarkus should step in with defaults. @@ -12,7 +12,7 @@ quarkus.datasource.devservices.enabled=false # Inventory persistence unit quarkus.hibernate-orm."inventory".database.generation=none quarkus.hibernate-orm."inventory".multitenant=database -quarkus.hibernate-orm."inventory".dialect=org.hibernate.dialect.MariaDBDialect +quarkus.hibernate-orm."inventory".dialect=MariaDB quarkus.hibernate-orm."inventory".packages=io.quarkus.it.hibernate.multitenancy.inventory #mariadb.base_url is set through Maven config diff --git a/integration-tests/spring-data-jpa/src/test/resources/property-warnings-test.properties b/integration-tests/spring-data-jpa/src/test/resources/property-warnings-test.properties index e73c7a6fe14692..318540bf444261 100644 --- a/integration-tests/spring-data-jpa/src/test/resources/property-warnings-test.properties +++ b/integration-tests/spring-data-jpa/src/test/resources/property-warnings-test.properties @@ -1,5 +1,5 @@ spring.jpa.show-sql=true -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect +spring.jpa.properties.hibernate.dialect=MySQL5 spring.jpa.open-in-view=false spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy spring.jpa.hibernate.naming.implicit-strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy From 485f45d9ce7b2a687ae5b81f8a58b158e7b3bafb Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Sat, 6 Jul 2024 16:01:41 +0200 Subject: [PATCH 04/16] Avoid making IsolatedDevModeMain and allegates static for no good reason Also avoid a reference to IsolatedDevModeMain from ApplicationLifecycleManager. --- .../quarkus/deployment/dev/DevModeMain.java | 2 +- .../deployment/dev/IsolatedDevModeMain.java | 97 +++++++++++-------- .../dev/IsolatedRemoteDevModeMain.java | 13 ++- .../deployment/dev/IsolatedTestModeMain.java | 3 +- .../dev/RuntimeUpdatesProcessor.java | 9 +- .../runtime/ApplicationLifecycleManager.java | 2 + 6 files changed, 76 insertions(+), 50 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java index 02bfdcd3405e51..6377b9c3b908a5 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java @@ -40,7 +40,7 @@ public class DevModeMain implements Closeable { private final DevModeContext context; - private static volatile CuratedApplication curatedApplication; + private volatile CuratedApplication curatedApplication; private Closeable realCloseable; public DevModeMain(DevModeContext context) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java index b68f3b9bbc6760..7fb810f807708b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java @@ -15,6 +15,7 @@ import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -61,16 +62,16 @@ public class IsolatedDevModeMain implements BiConsumer hotReplacementSetups = new ArrayList<>(); - private static volatile RunningQuarkusApplication runner; - static volatile Throwable deploymentProblem; - private static volatile CuratedApplication curatedApplication; - private static volatile AugmentAction augmentAction; - private static volatile boolean restarting; - private static volatile boolean firstStartCompleted; - private static final CountDownLatch shutdownLatch = new CountDownLatch(1); + private volatile RunningQuarkusApplication runner; + final AtomicReference deploymentProblem = new AtomicReference<>(); + private volatile CuratedApplication curatedApplication; + private volatile AugmentAction augmentAction; + private volatile boolean restarting; + private volatile boolean firstStartCompleted; + private final CountDownLatch shutdownLatch = new CountDownLatch(1); private Thread shutdownThread; private CodeGenWatcher codeGenWatcher; - private static volatile ConsoleStateManager.ConsoleContext consoleContext; + private volatile ConsoleStateManager.ConsoleContext consoleContext; private final List listeners = new ArrayList<>(); private synchronized void firstStart() { @@ -85,30 +86,7 @@ private synchronized void firstStart() { //TODO: look at implementing a common core classloader, that removes the need for this sort of crappy hack curatedApplication.getOrCreateBaseRuntimeClassLoader().loadClass(ApplicationLifecycleManager.class.getName()) .getMethod("setDefaultExitCodeHandler", Consumer.class) - .invoke(null, new Consumer() { - @Override - public void accept(Integer integer) { - if (restarting || ApplicationLifecycleManager.isVmShuttingDown() - || context.isAbortOnFailedStart() || - context.isTest()) { - return; - } - if (consoleContext == null) { - consoleContext = ConsoleStateManager.INSTANCE - .createContext("Completed Application"); - } - //this sucks, but when we get here logging is gone - //so we just setup basic console logging - InitialConfigurator.DELAYED_HANDLER.addHandler(new ConsoleHandler( - ConsoleHandler.Target.SYSTEM_OUT, - new ColorPatternFormatter("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n"))); - consoleContext.reset(new ConsoleCommand(' ', "Restarts the application", "to restart", 0, null, - () -> { - consoleContext.reset(); - RuntimeUpdatesProcessor.INSTANCE.doScan(true, true); - })); - } - }); + .invoke(null, getExitCodeHandler()); StartupAction start = augmentAction.createInitialRuntimeApplication(); @@ -127,7 +105,7 @@ public void accept(Integer integer) { rootCause = rootCause.getCause(); } if (!(rootCause instanceof BindException)) { - deploymentProblem = t; + deploymentProblem.set(t); if (!context.isAbortOnFailedStart()) { //we need to set this here, while we still have the correct TCCL //this is so the config is still valid, and we can read HTTP config from application.properties @@ -174,6 +152,35 @@ public void accept(Integer integer) { } } + private Consumer getExitCodeHandler() { + if (context.isTest() || context.isAbortOnFailedStart()) { + return TestExitCodeHandler.INSTANCE; + } + + return new Consumer() { + @Override + public void accept(Integer integer) { + if (restarting || ApplicationLifecycleManager.isVmShuttingDown()) { + return; + } + if (consoleContext == null) { + consoleContext = ConsoleStateManager.INSTANCE + .createContext("Completed Application"); + } + //this sucks, but when we get here logging is gone + //so we just setup basic console logging + InitialConfigurator.DELAYED_HANDLER.addHandler(new ConsoleHandler( + ConsoleHandler.Target.SYSTEM_OUT, + new ColorPatternFormatter("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n"))); + consoleContext.reset(new ConsoleCommand(' ', "Restarts the application", "to restart", 0, null, + () -> { + consoleContext.reset(); + RuntimeUpdatesProcessor.INSTANCE.doScan(true, true); + })); + } + }; + } + public void restartCallback(Set changedResources, ClassScanResult result) { restartApp(changedResources, new ClassChangeInformation(result.changedClassNames, result.deletedClassNames, result.addedClassNames)); @@ -186,7 +193,7 @@ public synchronized void restartApp(Set changedResources, ClassChangeInf } stop(); Timing.restart(curatedApplication.getOrCreateAugmentClassLoader()); - deploymentProblem = null; + deploymentProblem.set(null); ClassLoader old = Thread.currentThread().getContextClassLoader(); try { @@ -200,7 +207,7 @@ public synchronized void restartApp(Set changedResources, ClassChangeInf firstStartCompleted = true; } } catch (Throwable t) { - deploymentProblem = t; + deploymentProblem.set(t); Throwable rootCause = t; while (rootCause.getCause() != null) { rootCause = rootCause.getCause(); @@ -253,7 +260,7 @@ private RuntimeUpdatesProcessor setupRuntimeCompilation(DevModeContext context, public byte[] apply(String s, byte[] bytes) { return ClassTransformingBuildStep.transform(s, bytes); } - }, testSupport); + }, testSupport, deploymentProblem); for (HotReplacementSetup service : ServiceLoader.load(HotReplacementSetup.class, curatedApplication.getOrCreateBaseRuntimeClassLoader())) { @@ -350,6 +357,7 @@ public void close() { curatedApplication.close(); curatedApplication = null; augmentAction = null; + deploymentProblem.set(null); } finally { if (shutdownThread != null) { try { @@ -422,10 +430,11 @@ public void run() { firstStart(); // doStart(false, Collections.emptySet()); - if (deploymentProblem != null || RuntimeUpdatesProcessor.INSTANCE.getCompileProblem() != null) { + if (deploymentProblem.get() != null || RuntimeUpdatesProcessor.INSTANCE.getCompileProblem() != null) { if (context.isAbortOnFailedStart()) { - Throwable throwable = deploymentProblem == null ? RuntimeUpdatesProcessor.INSTANCE.getCompileProblem() - : deploymentProblem; + Throwable throwable = deploymentProblem.get() == null + ? RuntimeUpdatesProcessor.INSTANCE.getCompileProblem() + : deploymentProblem.get(); throw (throwable instanceof RuntimeException ? (RuntimeException) throwable : new RuntimeException(throwable)); @@ -483,4 +492,14 @@ public boolean test(String s) { }).produces(ApplicationClassPredicateBuildItem.class).build(); } } + + private static class TestExitCodeHandler implements Consumer { + + private static final TestExitCodeHandler INSTANCE = new TestExitCodeHandler(); + + @Override + public void accept(Integer exitCode) { + // do nothing + } + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java index a1e691f0748ed0..17d4b5e547fb81 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; @@ -57,7 +58,7 @@ public class IsolatedRemoteDevModeMain implements BiConsumer hotReplacementSetups = new ArrayList<>(); - static volatile Throwable deploymentProblem; + private AtomicReference deploymentProblem = new AtomicReference<>(); static volatile RemoteDevClient remoteDevClient; static volatile Closeable remoteDevClientSession; private static volatile CuratedApplication curatedApplication; @@ -99,7 +100,7 @@ private synchronized JarResult generateApplication() { curatedApplication.getApplicationModel(), null); return start.getJar(); } catch (Throwable t) { - deploymentProblem = t; + deploymentProblem.set(t); log.error("Failed to generate Quarkus application", t); return null; } @@ -137,7 +138,7 @@ public void accept(DevModeContext.ModuleInfo moduleInfo, String s) { public byte[] apply(String s, byte[] bytes) { return ClassTransformingBuildStep.transform(s, bytes); } - }, null); + }, null, deploymentProblem); for (HotReplacementSetup service : ServiceLoader.load(HotReplacementSetup.class, curatedApplication.getOrCreateBaseRuntimeClassLoader())) { @@ -189,6 +190,7 @@ public void close() { } } } finally { + deploymentProblem.set(null); curatedApplication.close(); } @@ -248,7 +250,7 @@ public void run() { } private Closeable doConnect() { - return remoteDevClient.sendConnectRequest(new RemoteDevState(currentHashes, deploymentProblem), + return remoteDevClient.sendConnectRequest(new RemoteDevState(currentHashes, deploymentProblem.get()), new Function, Map>() { @Override public Map apply(Set fileNames) { @@ -283,6 +285,7 @@ private RemoteDevClient.SyncResult runSync() { Set removed = new HashSet<>(); Map changed = new HashMap<>(); try { + deploymentProblem.set(null); boolean scanResult = RuntimeUpdatesProcessor.INSTANCE.doScan(true); if (!scanResult && !copiedStaticResources.isEmpty()) { scanResult = true; @@ -305,7 +308,7 @@ private RemoteDevClient.SyncResult runSync() { currentHashes = newHashes; } } catch (IOException e) { - deploymentProblem = e; + deploymentProblem.set(e); } return new RemoteDevClient.SyncResult() { @Override diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedTestModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedTestModeMain.java index 1f5b37aa311f0c..9fa94f1ff6aecd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedTestModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedTestModeMain.java @@ -36,7 +36,6 @@ public class IsolatedTestModeMain extends IsolatedDevModeMain { private volatile DevModeContext context; private final List hotReplacementSetups = new ArrayList<>(); - static volatile Throwable deploymentProblem; private static volatile CuratedApplication curatedApplication; private static volatile AugmentAction augmentAction; @@ -68,7 +67,7 @@ public void accept(DevModeContext.ModuleInfo moduleInfo, String s) { public byte[] apply(String s, byte[] bytes) { return ClassTransformingBuildStep.transform(s, bytes); } - }, testSupport); + }, testSupport, deploymentProblem); for (HotReplacementSetup service : ServiceLoader.load(HotReplacementSetup.class, curatedApplication.getOrCreateBaseRuntimeClassLoader())) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index f32d052a25aec4..ca8f49634d88ad 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -40,6 +40,7 @@ import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; @@ -91,6 +92,7 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable volatile Throwable compileProblem; volatile Throwable testCompileProblem; volatile Throwable hotReloadProblem; + private final AtomicReference deploymentProblem; private volatile Predicate disableInstrumentationForClassPredicate = new AlwaysFalsePredicate<>(); private volatile Predicate disableInstrumentationForIndexPredicate = new AlwaysFalsePredicate<>(); @@ -141,7 +143,7 @@ public RuntimeUpdatesProcessor(Path applicationRoot, DevModeContext context, Qua DevModeType devModeType, BiConsumer, ClassScanResult> restartCallback, BiConsumer copyResourceNotification, BiFunction classTransformers, - TestSupport testSupport) { + TestSupport testSupport, AtomicReference deploymentProblem) { this.applicationRoot = applicationRoot; this.context = context; this.compiler = compiler; @@ -180,6 +182,7 @@ public void testsDisabled() { } }); } + this.deploymentProblem = deploymentProblem; } public TestSupport getTestSupport() { @@ -392,7 +395,7 @@ public List getResourcesDir() { public Throwable getDeploymentProblem() { //we differentiate between these internally, however for the error reporting they are the same return compileProblem != null ? compileProblem - : IsolatedDevModeMain.deploymentProblem != null ? IsolatedDevModeMain.deploymentProblem + : deploymentProblem.get() != null ? deploymentProblem.get() : hotReloadProblem; } @@ -535,7 +538,7 @@ public boolean doScan(boolean userInitiated, boolean forceRestart) { //all broken we just assume the reason that they have refreshed is because they have fixed something //trying to watch all resource files is complex and this is likely a good enough solution for what is already an edge case boolean restartNeeded = !instrumentationChange && (changedClassResults.isChanged() - || (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || fileRestartNeeded); + || (deploymentProblem.get() != null && userInitiated) || fileRestartNeeded); if (restartNeeded) { String changeString = changedFilesForRestart.stream().map(Path::getFileName).map(Object::toString) .collect(Collectors.joining(", ")); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java index fa2d8b32c4255f..48c18f868b7b21 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java @@ -225,6 +225,7 @@ public static void run(Application application, Class Date: Mon, 8 Jul 2024 17:11:08 +0200 Subject: [PATCH 05/16] Clean up configuration examples in Hibernate ORM extension guide 1. Use callouts, not comments. 2. Use links to other guides. 3. Explain the "why" where necessary. 4. Fix weird mix of default and `base` datasource in database multitenancy example. --- docs/src/main/asciidoc/hibernate-orm.adoc | 85 ++++++++++++----------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index 5b2a0d63c3db85..0b90403a4d70a7 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -84,15 +84,15 @@ then add the relevant configuration properties in `{config-file}`. [source,properties] .Example `{config-file}` ---- -# datasource configuration -quarkus.datasource.db-kind = postgresql +quarkus.datasource.db-kind = postgresql <1> quarkus.datasource.username = hibernate quarkus.datasource.password = hibernate quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/hibernate_db -# drop and create the database at startup (use `update` to only update the schema) -quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.database.generation=drop-and-create <2> ---- +<1> xref:datasource.adoc[Configure the datasource]. +<2> Drop and create the database at startup (use `update` to only update the schema). Note that these configuration properties are not the same ones as in your typical Hibernate ORM configuration file. They will often map to Hibernate ORM configuration properties but could have different names and don't necessarily map 1:1 to each other. @@ -1127,32 +1127,37 @@ In general, it is not possible to use the Hibernate ORM database generation feat Therefore, you have to disable it, and you need to make sure that the tables are created per schema. The following setup will use the xref:flyway.adoc[Flyway] extension to achieve this goal. +[[schema-approach]] ==== SCHEMA approach The same data source will be used for all tenants and a schema has to be created for every tenant inside that data source. -CAUTION: Some databases like MariaDB/MySQL do not support database schemas. In these cases you have to use the DATABASE approach below. + +CAUTION: Some databases like MariaDB/MySQL do not support database schemas. In these cases you have to use the <>. [source,properties] ---- -# Disable generation -quarkus.hibernate-orm.database.generation=none +quarkus.hibernate-orm.database.generation=none <1> -# Enable SCHEMA approach and use default datasource -quarkus.hibernate-orm.multitenant=SCHEMA -# You could use a non-default datasource by using the following setting -# quarkus.hibernate-orm.datasource=other +quarkus.hibernate-orm.multitenant=SCHEMA <2> -# The default data source used for all tenant schemas -quarkus.datasource.db-kind=postgresql +quarkus.datasource.db-kind=postgresql <3> quarkus.datasource.username=quarkus_test quarkus.datasource.password=quarkus_test quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_test -# Enable Flyway configuration to create schemas -quarkus.flyway.schemas=base,mycompany +quarkus.flyway.schemas=base,mycompany <4> quarkus.flyway.locations=classpath:schema quarkus.flyway.migrate-at-start=true ---- +<1> Disable schema generation, because it is not supported by Hibernate ORM for schema multi-tenancy. +We'll use Flyway instead, see further down. +<2> Enable schema multi-tenancy. ++ +We use the default datasource here, but could use a named datasource if we wanted to, +by following instructions <>. +<3> xref:datasource.adoc[Configure the datasource]. +<4> Configure xref:flyway.adoc[Flyway] for database initialization, +because schema generation by Hibernate ORM is not supported in this case. Here is an example of the Flyway SQL (`V1.0.0__create_fruits.sql`) to be created in the configured folder `src/main/resources/schema`. @@ -1181,44 +1186,48 @@ INSERT INTO mycompany.known_fruits(id, name) VALUES (2, 'Apricots'); INSERT INTO mycompany.known_fruits(id, name) VALUES (3, 'Blackberries'); ---- - - +[[database-approach]] ==== DATABASE approach For every tenant you need to create a named data source with the same identifier that is returned by the `TenantResolver`. [source,properties] ---- -# Disable generation -quarkus.hibernate-orm.database.generation=none +quarkus.hibernate-orm.database.generation=none <1> -# Enable DATABASE approach -quarkus.hibernate-orm.multitenant=DATABASE +quarkus.hibernate-orm.multitenant=DATABASE <2> # Default tenant 'base' -quarkus.datasource.base.db-kind=postgresql +quarkus.datasource.base.db-kind=postgresql <3> quarkus.datasource.base.username=quarkus_test quarkus.datasource.base.password=quarkus_test quarkus.datasource.base.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_test +quarkus.flyway.base.locations=classpath:database/base <4> +quarkus.flyway.base.migrate-at-start=true # Tenant 'mycompany' -quarkus.datasource.mycompany.db-kind=postgresql +quarkus.datasource.mycompany.db-kind=postgresql <5> quarkus.datasource.mycompany.username=mycompany quarkus.datasource.mycompany.password=mycompany quarkus.datasource.mycompany.jdbc.url=jdbc:postgresql://localhost:5433/mycompany - -# Flyway configuration for the default datasource -quarkus.flyway.locations=classpath:database/default -quarkus.flyway.migrate-at-start=true - -# Flyway configuration for the mycompany datasource -quarkus.flyway.mycompany.locations=classpath:database/mycompany +quarkus.flyway.mycompany.locations=classpath:database/mycompany <6> quarkus.flyway.mycompany.migrate-at-start=true ---- +<1> Disable schema generation, because it is not supported by Hibernate ORM for database multi-tenancy. +We'll use Flyway instead, see further down. +<2> Enable database multi-tenancy. +<3> xref:datasource.adoc[Configure the datasource] for one tenant, `base`. +<4> Configure xref:flyway.adoc[Flyway] for database initialization for tenant `base`, +because schema generation by Hibernate ORM is not supported in this case. +<5> xref:datasource.adoc[Configure the datasource] for another tenant. ++ +There could be more tenants, but here we're stopping at two. +<6> Configure xref:flyway.adoc[Flyway] for database initialization for tenant `mycompany`, +because schema generation by Hibernate ORM is not supported in this case. Following are examples of the Flyway SQL files to be created in the configured folder `src/main/resources/database`. -Default schema (`src/main/resources/database/default/V1.0.0__create_fruits.sql`): +Schema for tenant `base` (`src/main/resources/database/base/V1.0.0__create_fruits.sql`): [source,sql] ---- @@ -1234,7 +1243,7 @@ INSERT INTO known_fruits(id, name) VALUES (2, 'Apple'); INSERT INTO known_fruits(id, name) VALUES (3, 'Banana'); ---- -Mycompany schema (`src/main/resources/database/mycompany/V1.0.0__create_fruits.sql`): +Schema for tenant `mycompany` (`src/main/resources/database/mycompany/V1.0.0__create_fruits.sql`): [source,sql] ---- @@ -1250,8 +1259,7 @@ INSERT INTO known_fruits(id, name) VALUES (2, 'Apricots'); INSERT INTO known_fruits(id, name) VALUES (3, 'Blackberries'); ---- - - +[[discriminator-approach]] ==== DISCRIMINATOR approach The default data source will be used for all tenants. All entities defining a field annotated with `@TenantId` will have that field populated automatically, and will get filtered automatically in queries. @@ -1259,16 +1267,15 @@ The default data source will be used for all tenants. All entities defining a fi [source,properties] ---- -# Enable DISCRIMINATOR approach -quarkus.hibernate-orm.multitenant=DISCRIMINATOR +quarkus.hibernate-orm.multitenant=DISCRIMINATOR <1> -# The default data source used for all tenant schemas -quarkus.datasource.db-kind=postgresql +quarkus.datasource.db-kind=postgresql <2> quarkus.datasource.username=quarkus_test quarkus.datasource.password=quarkus_test quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_test ---- - +<1> Enable discriminator multi-tenancy. +<2> xref:datasource.adoc[Configure the datasource]. === Programmatically Resolving Tenants Connections From 81ecc46938397a63625d3b727675de8317b10846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Mon, 8 Jul 2024 17:37:09 +0200 Subject: [PATCH 06/16] Clarify documentation of Hibernate ORM dialects wrt database multi-tenancy --- docs/src/main/asciidoc/hibernate-orm.adoc | 47 +++++++++++++++---- .../HibernateOrmConfigPersistenceUnit.java | 26 +++++----- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index 0b90403a4d70a7..a323321d9dc2e0 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -262,6 +262,23 @@ quarkus.hibernate-orm.dialect=Cockroach <2> Since we're targeting CockroachDB here, we're passing the CockroachDB version, not the PostgreSQL version. <2> Set the Hibernate ORM dialect. +[[hibernate-dialect-varying-database]] +== Varying database + +When enabling <>, +Hibernate ORM will use multiple datasources at runtime for the same persistence unit, +and by default Quarkus cannot tell which datasource is going to be used, +so it will not be able to detect a dialect to use in Hibernate ORM. + +For that reason, when enabling <>, +it is recommended to explicitly point the Hibernate ORM configuration to one datasource +among those that will be used at runtime, e.g. with `quarkus.hibernate-orm.datasource=base` +(`base` being the name of a datasource). + +When doing so, Quarkus will infer the database version and (if possible) dialect from that datasource. +For unsupported databases, you may still need to set the Hibernate ORM dialect explicitly, +as explained in <>. + [[hibernate-configuration-properties]] === Hibernate ORM configuration properties @@ -1191,38 +1208,52 @@ INSERT INTO mycompany.known_fruits(id, name) VALUES (3, 'Blackberries'); For every tenant you need to create a named data source with the same identifier that is returned by the `TenantResolver`. +[CAUTION] +==== +// Related to https://github.com/quarkusio/quarkus/issues/11861 +With this approach, all datasources used by the same persistence unit +are assumed to point to a database of the same vendor (same `db-kind`) and version. + +Mismatches will not be detected, and may result in unpredictable behavior. +==== + [source,properties] ---- quarkus.hibernate-orm.database.generation=none <1> quarkus.hibernate-orm.multitenant=DATABASE <2> +quarkus.hibernate-orm.datasource=base <3> # Default tenant 'base' -quarkus.datasource.base.db-kind=postgresql <3> +quarkus.datasource.base.db-kind=postgresql <4> quarkus.datasource.base.username=quarkus_test quarkus.datasource.base.password=quarkus_test quarkus.datasource.base.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_test -quarkus.flyway.base.locations=classpath:database/base <4> +quarkus.flyway.base.locations=classpath:database/base <5> quarkus.flyway.base.migrate-at-start=true # Tenant 'mycompany' -quarkus.datasource.mycompany.db-kind=postgresql <5> +quarkus.datasource.mycompany.db-kind=postgresql <6> quarkus.datasource.mycompany.username=mycompany quarkus.datasource.mycompany.password=mycompany quarkus.datasource.mycompany.jdbc.url=jdbc:postgresql://localhost:5433/mycompany -quarkus.flyway.mycompany.locations=classpath:database/mycompany <6> +quarkus.flyway.mycompany.locations=classpath:database/mycompany <7> quarkus.flyway.mycompany.migrate-at-start=true ---- <1> Disable schema generation, because it is not supported by Hibernate ORM for database multi-tenancy. We'll use Flyway instead, see further down. <2> Enable database multi-tenancy. -<3> xref:datasource.adoc[Configure the datasource] for one tenant, `base`. -<4> Configure xref:flyway.adoc[Flyway] for database initialization for tenant `base`, +<3> Select a datasource for the persistence unit. ++ +This is only to allow Quarkus to determine the Hibernate ORM dialect to use; +see <> for details. +<4> xref:datasource.adoc[Configure the datasource] for one tenant, `base`. +<5> Configure xref:flyway.adoc[Flyway] for database initialization for tenant `base`, because schema generation by Hibernate ORM is not supported in this case. -<5> xref:datasource.adoc[Configure the datasource] for another tenant. +<6> xref:datasource.adoc[Configure the datasource] for another tenant. + There could be more tenants, but here we're stopping at two. -<6> Configure xref:flyway.adoc[Flyway] for database initialization for tenant `mycompany`, +<7> Configure xref:flyway.adoc[Flyway] for database initialization for tenant `mycompany`, because schema generation by Hibernate ORM is not supported in this case. Following are examples of the Flyway SQL files to be created in the configured folder `src/main/resources/database`. diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java index e6c30584166c6f..dd91316bffb0eb 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java @@ -278,6 +278,18 @@ interface HibernateOrmConfigPersistenceUnitDialect { /** * Name of the Hibernate ORM dialect. * + * For xref:datasource.adoc#extensions-and-database-drivers-reference[supported databases], + * this property does not need to be set explicitly: + * it is selected automatically based on the datasource, + * and configured using the xref:datasource.adoc#quarkus-datasource_quarkus.datasource.db-version[DB version set on the + * datasource] + * to benefit from the best performance and latest features. + * + * If your database does not have a corresponding Quarkus extension, + * you *will* need to set this property explicitly. + * In that case, keep in mind that the JDBC driver and Hibernate ORM dialect + * may not work properly in GraalVM native executables. + * * For built-in dialects, the expected value is one of the names * in the link:{hibernate-orm-dialect-docs-url}[official list of dialects], * *without* the `Dialect` suffix, @@ -286,20 +298,6 @@ interface HibernateOrmConfigPersistenceUnitDialect { * For third-party dialects, the expected value is the fully-qualified class name, * for example `com.acme.hibernate.AcmeDbDialect`. * - * Setting the dialect directly is only recommended as a last resort: - * most popular databases have a corresponding Quarkus extension, - * allowing Quarkus to select the dialect automatically, - * in which case you do not need to set the dialect at all, - * though you may want to set - * xref:datasource.adoc#quarkus-datasource_quarkus.datasource.db-version[`quarkus.datasource.db-version`] as - * high as possible - * to benefit from the best performance and latest features. - * - * If your database does not have a corresponding Quarkus extension, - * you will need to set the dialect directly. - * In that case, keep in mind that the JDBC driver and Hibernate ORM dialect - * may not work properly in GraalVM native executables. - * * @asciidoclet */ @WithParentName From c9060fef45d353fba05c35178ba6e5e0aaf2fb50 Mon Sep 17 00:00:00 2001 From: Peter Palaga Date: Tue, 9 Jul 2024 16:08:24 +0200 Subject: [PATCH 07/16] Add class level JavaDoc to BytecodeTransformerBuildItem --- .../builditem/BytecodeTransformerBuildItem.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java index ec968f494b8b09..ea79ca227b8bf7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java @@ -8,6 +8,15 @@ import io.quarkus.builder.item.MultiBuildItem; +/** + * Transform a class using ASM {@link ClassVisitor}. Note that the transformation is performed after assembling the + * index and thus the changes won't be visible to any processor steps relying on the index. + *

+ * You may consider using {@code io.quarkus.arc.deployment.AnnotationsTransformerBuildItem} if your transformation + * should be visible for Arc. See also + * I Need To + * Transform Annotation Metadata section of Quarkus CDI integration guide. + */ public final class BytecodeTransformerBuildItem extends MultiBuildItem { final String classToTransform; From 30af1a6bffe303d275c6f03acb958109c5436bd2 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 9 Jul 2024 11:30:59 +0200 Subject: [PATCH 08/16] Scheduler: fix Trigger#getNextFireTime() for cron-based jobs - fixes #41717 --- .../ScheduledMethodTimeZoneTest.java | 2 +- .../timezone/TriggerNextFireTimeZoneTest.java | 74 +++++++++++++++ .../timezone/TriggerPrevFireTimeZoneTest.java | 93 +++++++++++++++++++ .../quarkus/scheduler/ScheduledExecution.java | 9 ++ .../java/io/quarkus/scheduler/Trigger.java | 12 +++ .../ScheduledMethodTimeZoneTest.java | 3 +- .../timezone/TriggerNextFireTimeZoneTest.java | 74 +++++++++++++++ .../timezone/TriggerPrevFireTimeZoneTest.java | 93 +++++++++++++++++++ .../scheduler/runtime/SimpleScheduler.java | 23 +++-- 9 files changed, 371 insertions(+), 12 deletions(-) rename extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/{ => timezone}/ScheduledMethodTimeZoneTest.java (98%) create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java rename extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/{ => timezone}/ScheduledMethodTimeZoneTest.java (97%) create mode 100644 extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java create mode 100644 extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java similarity index 98% rename from extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java rename to extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java index 95d4d47724c53b..38b42d218c2a67 100644 --- a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java @@ -1,4 +1,4 @@ -package io.quarkus.quartz.test; +package io.quarkus.quartz.test.timezone; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java new file mode 100644 index 00000000000000..4e588223254a33 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java @@ -0,0 +1,74 @@ +package io.quarkus.quartz.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerNextFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Jobs.class); + }); + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger boston = scheduler.getScheduledJob("boston"); + Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar"); + assertNotNull(prague); + assertNotNull(boston); + assertNotNull(ulaanbaatar); + Instant pragueNext = prague.getNextFireTime(); + Instant bostonNext = boston.getNextFireTime(); + Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime(); + assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague"))); + assertTime(bostonNext.atZone(ZoneId.of("America/New_York"))); + assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + private static void assertTime(ZonedDateTime time) { + assertEquals(20, time.getHour()); + assertEquals(30, time.getMinute()); + assertEquals(0, time.getSecond()); + } + + static class Jobs { + + @Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague") + void withPragueTimezone(ScheduledExecution execution) { + assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime()); + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague"))); + } + + @Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York") + void withBostonTimezone() { + } + + @Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar") + void withIstanbulTimezone(ScheduledExecution execution) { + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + } + +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java new file mode 100644 index 00000000000000..4da6a548d7e439 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java @@ -0,0 +1,93 @@ +package io.quarkus.quartz.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerPrevFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime prague = now.withZoneSameInstant(ZoneId.of("Europe/Prague")); + ZonedDateTime istanbul = now.withZoneSameInstant(ZoneId.of("Europe/Istanbul")); + // For example, the current date-time is 2024-07-09 10:08:00; + // the default time zone is Europe/London + // then the config should look like: + // simpleJobs1.cron=0/1 * 11 * * ? + // simpleJobs2.cron=0/1 * 12 * * ? + String properties = String.format( + "simpleJobs1.cron=0/1 * %s * * ?\n" + + "simpleJobs1.hour=%s\n" + + "simpleJobs2.cron=0/1 * %s * * ?\n" + + "simpleJobs2.hour=%s", + prague.getHour(), prague.getHour(), istanbul.getHour(), istanbul.getHour()); + root.addClasses(Jobs.class) + .addAsResource( + new StringAsset(properties), + "application.properties"); + }); + + @ConfigProperty(name = "simpleJobs1.hour") + int pragueHour; + + @ConfigProperty(name = "simpleJobs2.hour") + int istanbulHour; + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + assertTrue(Jobs.PRAGUE_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.ISTANBUL_LATCH.await(5, TimeUnit.SECONDS)); + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger istanbul = scheduler.getScheduledJob("istanbul"); + assertNotNull(prague); + assertNotNull(istanbul); + Instant praguePrev = prague.getPreviousFireTime(); + Instant istanbulPrev = istanbul.getPreviousFireTime(); + assertNotNull(praguePrev); + assertNotNull(istanbulPrev); + assertEquals(praguePrev, istanbulPrev); + assertEquals(pragueHour, praguePrev.atZone(ZoneId.of("Europe/Prague")).getHour()); + assertEquals(istanbulHour, istanbulPrev.atZone(ZoneId.of("Europe/Istanbul")).getHour()); + } + + static class Jobs { + + static final CountDownLatch PRAGUE_LATCH = new CountDownLatch(1); + static final CountDownLatch ISTANBUL_LATCH = new CountDownLatch(1); + + @Scheduled(identity = "prague", cron = "{simpleJobs1.cron}", timeZone = "Europe/Prague") + void withPragueTimezone() { + PRAGUE_LATCH.countDown(); + } + + @Scheduled(identity = "istanbul", cron = "{simpleJobs2.cron}", timeZone = "Europe/Istanbul") + void withIstanbulTimezone() { + ISTANBUL_LATCH.countDown(); + } + + } + +} diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java index 5f69240af667cf..fda1ac26fec881 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java @@ -14,6 +14,9 @@ public interface ScheduledExecution { Trigger getTrigger(); /** + * The returned {@code Instant} is converted from the date-time in the default timezone. A timezone of a cron-based job + * is not taken into account. + *

* Unlike {@link Trigger#getPreviousFireTime()} this method always returns the same value. * * @return the time the associated trigger was fired @@ -21,6 +24,12 @@ public interface ScheduledExecution { Instant getFireTime(); /** + * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into + * account. + *

+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, + * then the return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for + * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}. * * @return the time the action was scheduled for */ diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java index c076e5712bc0eb..0a5f94d48ffb30 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java @@ -21,12 +21,24 @@ public interface Trigger { String getId(); /** + * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into + * account. + *

+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, then the + * return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for + * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}. * * @return the next time at which the trigger is scheduled to fire, or {@code null} if it will not fire again */ Instant getNextFireTime(); /** + * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into + * account. + *

+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, then the + * return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for + * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}. * * @return the previous time at which the trigger fired, or {@code null} if it has not fired yet */ diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java similarity index 97% rename from extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java rename to extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java index aba645216812ac..9db547b46e7a06 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java @@ -1,4 +1,4 @@ -package io.quarkus.scheduler.test; +package io.quarkus.scheduler.test.timezone; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -41,7 +41,6 @@ public class ScheduledMethodTimeZoneTest { + "simpleJobs2.cron=0/1 * %s * * ?\n" + "simpleJobs2.timeZone=%s", now.getHour(), timeZone, job2Hour, timeZone); - // System.out.println(properties); jar.addClasses(Jobs.class) .addAsResource( new StringAsset(properties), diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java new file mode 100644 index 00000000000000..d301a0e1cff5da --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java @@ -0,0 +1,74 @@ +package io.quarkus.scheduler.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerNextFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Jobs.class); + }); + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger boston = scheduler.getScheduledJob("boston"); + Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar"); + assertNotNull(prague); + assertNotNull(boston); + assertNotNull(ulaanbaatar); + Instant pragueNext = prague.getNextFireTime(); + Instant bostonNext = boston.getNextFireTime(); + Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime(); + assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague"))); + assertTime(bostonNext.atZone(ZoneId.of("America/New_York"))); + assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + private static void assertTime(ZonedDateTime time) { + assertEquals(20, time.getHour()); + assertEquals(30, time.getMinute()); + assertEquals(0, time.getSecond()); + } + + static class Jobs { + + @Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague") + void withPragueTimezone(ScheduledExecution execution) { + assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime()); + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague"))); + } + + @Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York") + void withBostonTimezone() { + } + + @Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar") + void withIstanbulTimezone(ScheduledExecution execution) { + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + } + +} diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java new file mode 100644 index 00000000000000..ed1ef873b77b47 --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java @@ -0,0 +1,93 @@ +package io.quarkus.scheduler.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerPrevFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime prague = now.withZoneSameInstant(ZoneId.of("Europe/Prague")); + ZonedDateTime istanbul = now.withZoneSameInstant(ZoneId.of("Europe/Istanbul")); + // For example, the current date-time is 2024-07-09 10:08:00; + // the default time zone is Europe/London + // then the config should look like: + // simpleJobs1.cron=0/1 * 11 * * ? + // simpleJobs2.cron=0/1 * 12 * * ? + String properties = String.format( + "simpleJobs1.cron=0/1 * %s * * ?\n" + + "simpleJobs1.hour=%s\n" + + "simpleJobs2.cron=0/1 * %s * * ?\n" + + "simpleJobs2.hour=%s", + prague.getHour(), prague.getHour(), istanbul.getHour(), istanbul.getHour()); + root.addClasses(Jobs.class) + .addAsResource( + new StringAsset(properties), + "application.properties"); + }); + + @ConfigProperty(name = "simpleJobs1.hour") + int pragueHour; + + @ConfigProperty(name = "simpleJobs2.hour") + int istanbulHour; + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + assertTrue(Jobs.PRAGUE_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.ISTANBUL_LATCH.await(5, TimeUnit.SECONDS)); + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger istanbul = scheduler.getScheduledJob("istanbul"); + assertNotNull(prague); + assertNotNull(istanbul); + Instant praguePrev = prague.getPreviousFireTime(); + Instant istanbulPrev = istanbul.getPreviousFireTime(); + assertNotNull(praguePrev); + assertNotNull(istanbulPrev); + assertEquals(praguePrev, istanbulPrev); + assertEquals(pragueHour, praguePrev.atZone(ZoneId.of("Europe/Prague")).getHour()); + assertEquals(istanbulHour, istanbulPrev.atZone(ZoneId.of("Europe/Istanbul")).getHour()); + } + + static class Jobs { + + static final CountDownLatch PRAGUE_LATCH = new CountDownLatch(1); + static final CountDownLatch ISTANBUL_LATCH = new CountDownLatch(1); + + @Scheduled(identity = "prague", cron = "{simpleJobs1.cron}", timeZone = "Europe/Prague") + void withPragueTimezone() { + PRAGUE_LATCH.countDown(); + } + + @Scheduled(identity = "istanbul", cron = "{simpleJobs2.cron}", timeZone = "Europe/Istanbul") + void withIstanbulTimezone() { + ISTANBUL_LATCH.countDown(); + } + + } + +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index 02e2b46e25b142..8a3d77797146e8 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -589,28 +589,29 @@ static class CronTrigger extends SimpleTrigger { super(id, start, description); this.cron = cron; this.executionTime = ExecutionTime.forCron(cron); - this.lastFireTime = start; this.gracePeriod = gracePeriod; this.timeZone = timeZone; + // The last fire time stores the zoned time + this.lastFireTime = zoned(start); } @Override public Instant getNextFireTime() { - Optional nextFireTime = executionTime.nextExecution(lastFireTime); - return nextFireTime.isPresent() ? nextFireTime.get().toInstant() : null; + return executionTime.nextExecution(lastFireTime).map(ZonedDateTime::toInstant).orElse(null); } + @Override ZonedDateTime evaluate(ZonedDateTime now) { if (now.isBefore(start)) { return null; } - ZonedDateTime zonedNow = timeZone == null ? now : now.withZoneSameInstant(timeZone); - Optional lastExecution = executionTime.lastExecution(zonedNow); + now = zoned(now); + Optional lastExecution = executionTime.lastExecution(now); if (lastExecution.isPresent()) { ZonedDateTime lastTruncated = lastExecution.get().truncatedTo(ChronoUnit.SECONDS); - if (zonedNow.isAfter(lastTruncated) && lastFireTime.isBefore(lastTruncated)) { + if (now.isAfter(lastTruncated) && lastFireTime.isBefore(lastTruncated)) { LOG.tracef("%s fired, last=%s", this, lastTruncated); - lastFireTime = zonedNow; + lastFireTime = now; return lastTruncated; } } @@ -623,9 +624,9 @@ public boolean isOverdue() { if (now.isBefore(start)) { return false; } - ZonedDateTime zonedNow = timeZone == null ? now : now.withZoneSameInstant(timeZone); + now = zoned(now); Optional nextFireTime = executionTime.nextExecution(lastFireTime); - return nextFireTime.isEmpty() || nextFireTime.get().plus(gracePeriod).isBefore(zonedNow); + return nextFireTime.isEmpty() || nextFireTime.get().plus(gracePeriod).isBefore(now); } @Override @@ -634,6 +635,10 @@ public String toString() { + timeZone + "]"; } + private ZonedDateTime zoned(ZonedDateTime time) { + return timeZone == null ? time : time.withZoneSameInstant(timeZone); + } + } static class SimpleScheduledExecution implements ScheduledExecution { From c58af08ae0ce59a469f3f23991977e31800f1415 Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Tue, 9 Jul 2024 10:23:08 -0500 Subject: [PATCH 09/16] Use `SecureDirectoryStream` to avoid FS problems and fix other minor issues in `IoUtils` Possible fix for #41767. --- .../io/quarkus/bootstrap/util/IoUtils.java | 154 ++++++++++++------ 1 file changed, 102 insertions(+), 52 deletions(-) diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java index 0c4006fc3dc74c..18513222b7384b 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java @@ -1,21 +1,23 @@ package io.quarkus.bootstrap.util; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.StringWriter; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.SecureDirectoryStream; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; import java.util.Objects; @@ -29,8 +31,6 @@ */ public class IoUtils { - private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; - private static final Path TMP_DIR = Paths.get(PropertyUtils.getProperty("java.io.tmpdir")); private static final Logger log = Logger.getLogger(IoUtils.class); @@ -60,40 +60,36 @@ public static Path mkdirs(Path dir) { return dir; } + /** + * Recursively delete the file or directory given by {@code root}. + * The implementation will attempt to do so in a secure manner. + * Any problems encountered will be logged at {@code DEBUG} level. + * + * @param root the root path (must not be {@code null}) + */ public static void recursiveDelete(Path root) { - log.debugf("Recursively delete directory %s", root); + log.debugf("Recursively delete path %s", root); if (root == null || !Files.exists(root)) { return; } try { - Files.walkFileTree(root, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - try { - Files.delete(file); - } catch (IOException ex) { - log.debugf(ex, "Unable to delete file " + file); - } - return FileVisitResult.CONTINUE; + if (Files.isDirectory(root)) { + try (DirectoryStream ds = Files.newDirectoryStream(root)) { + recursiveDelete(ds); } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) - throws IOException { - if (e == null) { - try { - Files.delete(dir); - } catch (IOException ex) { - log.debugf(ex, "Unable to delete directory " + dir); - } - return FileVisitResult.CONTINUE; - } else { - // directory iteration failed - throw e; - } + try { + Files.delete(root); + } catch (IOException e) { + log.debugf(e, "Unable to delete directory %s", root); } - }); + } else { + log.debugf("Delete file %s", root); + try { + Files.delete(root); + } catch (IOException e) { + log.debugf(e, "Unable to delete file %s", root); + } + } } catch (IOException e) { log.debugf(e, "Error recursively deleting directory"); } @@ -101,9 +97,10 @@ public FileVisitResult postVisitDirectory(Path dir, IOException e) /** * Creates a new empty directory or empties an existing one. + * Any problems encountered while emptying the directory will be logged at {@code DEBUG} level. * * @param dir directory - * @throws IOException in case of a failure + * @throws IOException if creating or accessing the directory itself fails */ public static void createOrEmptyDir(Path dir) throws IOException { log.debugf("Create or empty directory %s", dir); @@ -113,17 +110,51 @@ public static void createOrEmptyDir(Path dir) throws IOException { Files.createDirectories(dir); return; } - if (!Files.isDirectory(dir)) { - throw new IllegalArgumentException(dir + " is not a directory"); + // recursively delete the *contents* of the directory, if any (keep the directory itself) + try (DirectoryStream ds = Files.newDirectoryStream(dir)) { + recursiveDelete(ds); + } + } + + private static void recursiveDelete(DirectoryStream ds) { + if (ds instanceof SecureDirectoryStream sds) { + // best, fastest, and most likely path for most OSes + recursiveDeleteSecure(sds); + } else { + // this may not work well on e.g. NFS, so we avoid this path if possible + for (Path p : ds) { + recursiveDelete(p); + } } - log.debugf("Iterate over contents of %s to delete its contents", dir); - try (DirectoryStream stream = Files.newDirectoryStream(dir)) { - for (Path p : stream) { - if (Files.isDirectory(p)) { - recursiveDelete(p); - } else { - log.debugf("Delete file %s", p); - Files.delete(p); + } + + private static void recursiveDeleteSecure(SecureDirectoryStream sds) { + for (Path p : sds) { + Path file = p.getFileName(); + BasicFileAttributes attrs; + try { + attrs = sds.getFileAttributeView(file, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS) + .readAttributes(); + } catch (IOException e) { + log.debugf(e, "Unable to query file type of %s", p); + continue; + } + if (attrs.isDirectory()) { + try { + try (SecureDirectoryStream nested = sds.newDirectoryStream(file)) { + recursiveDeleteSecure(nested); + } + sds.deleteDirectory(file); + } catch (IOException e) { + log.debugf(e, "Unable to delete directory %s", p); + } + } else { + // log the whole path, not the file name + log.debugf("Delete file %s", p); + try { + sds.deleteFile(file); + } catch (IOException e) { + log.debugf(e, "Unable to delete file %s", p); } } } @@ -163,24 +194,43 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) return target; } + /** + * Read the contents of a file as a string. + * + * @param file the file to read (must not be {@code null}) + * @return the file content, as a string (not {@code null}) + * @throws IOException if an error occurs when reading the file + * @deprecated Use {@link Files#readString(Path, Charset)} instead. + */ + @Deprecated(forRemoval = true) public static String readFile(Path file) throws IOException { - final char[] charBuffer = new char[DEFAULT_BUFFER_SIZE]; - int n = 0; - final StringWriter output = new StringWriter(); - try (BufferedReader input = Files.newBufferedReader(file)) { - while ((n = input.read(charBuffer)) != -1) { - output.write(charBuffer, 0, n); - } - } - return output.getBuffer().toString(); + return Files.readString(file, StandardCharsets.UTF_8); } + /** + * Copy the input stream to the given output stream. + * Calling this method is identical to calling {@code in.transferTo(out)}. + * + * @param out the output stream (must not be {@code null}) + * @param in the input stream (must not be {@code null}) + * @throws IOException if an error occurs during the copy + * @see InputStream#transferTo(OutputStream) + */ public static void copy(OutputStream out, InputStream in) throws IOException { in.transferTo(out); } + /** + * Write a string to a file using UTF-8 encoding. + * The file will be created if it does not exist, and truncated if it is not empty. + * + * @param file the file to write (must not be {@code null}) + * @param content the string to write to the file (must not be {@code null}) + * @throws IOException if an error occurs when writing the file + */ public static void writeFile(Path file, String content) throws IOException { - Files.write(file, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); + Files.writeString(file, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); } } From 8dfb870c6e89ca1ff1c5e526db4f0824bd3e57d0 Mon Sep 17 00:00:00 2001 From: Bruno Alves Date: Tue, 9 Jul 2024 16:47:30 -0300 Subject: [PATCH 10/16] Fix #41789 Add git option to enable longpaths on Windows --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6e1625f9e3c9a..e042ea1b5973a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -233,6 +233,7 @@ If you have not done so on this machine, you need to: * macOS: Use the `Disk Utility.app` to check. It also allows you to create a case-sensitive volume to store your code projects. See this [blog entry](https://karnsonline.com/case-sensitive-apfs/) for more. * Windows: [Enable case sensitive file names per directory](https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity) * Install Git and configure your GitHub access + * Windows: enable longpaths: `git config --global core.longpaths true` * Install Java SDK 17+ (OpenJDK recommended) * Install [GraalVM](https://quarkus.io/guides/building-native-image) * Install platform C developer tools: From 7d874019a21416327cdeaf253470af37ed7352b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:57:32 +0000 Subject: [PATCH 11/16] Bump org.assertj:assertj-core from 3.26.0 to 3.26.3 in /devtools/gradle Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.26.0 to 3.26.3. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.26.0...assertj-build-3.26.3) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- devtools/gradle/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml index e613c297de53e6..4ac165ca0f938c 100644 --- a/devtools/gradle/gradle/libs.versions.toml +++ b/devtools/gradle/gradle/libs.versions.toml @@ -6,7 +6,7 @@ kotlin = "2.0.0" smallrye-config = "3.8.3" junit5 = "5.10.3" -assertj = "3.26.0" +assertj = "3.26.3" [plugins] plugin-publish = { id = "com.gradle.plugin-publish", version.ref = "plugin-publish" } From b5f55088173bdd4ba6c7773c7a350c51ecc7bc8b Mon Sep 17 00:00:00 2001 From: Roberto Balarezo Date: Tue, 9 Jul 2024 14:14:51 -0500 Subject: [PATCH 12/16] Fix NPE when calling a ClientResponseFilter --- .../client/handlers/ClientSetResponseEntityRestHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSetResponseEntityRestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSetResponseEntityRestHandler.java index cb811043af91bc..8e0232603448e0 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSetResponseEntityRestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSetResponseEntityRestHandler.java @@ -24,7 +24,7 @@ public class ClientSetResponseEntityRestHandler implements ClientRestHandler { @Override public void handle(RestClientRequestContext context) throws Exception { - ClientRequestContextImpl requestContext = context.getClientRequestContext(); + ClientRequestContextImpl requestContext = context.getOrCreateClientRequestContext(); if (context.isCheckSuccessfulFamily()) { StatusType effectiveResponseStatus = determineEffectiveResponseStatus(context, requestContext); if (Response.Status.Family.familyOf(effectiveResponseStatus.getStatusCode()) != Response.Status.Family.SUCCESSFUL) { From b5890b5e007aace0a4e4188699ec6e0c962f7686 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 22:03:07 +0000 Subject: [PATCH 13/16] Bump org.assertj:assertj-core from 3.26.0 to 3.26.3 Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.26.0 to 3.26.3. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.26.0...assertj-build-3.26.3) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build-parent/pom.xml | 2 +- independent-projects/arc/pom.xml | 2 +- independent-projects/bootstrap/pom.xml | 2 +- independent-projects/junit5-virtual-threads/pom.xml | 2 +- independent-projects/qute/pom.xml | 2 +- independent-projects/resteasy-reactive/pom.xml | 2 +- independent-projects/tools/pom.xml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 371a946a9aea2f..053deff0e84c41 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -101,7 +101,7 @@ 7.0.1 - 3.26.0 + 3.26.3 3.8.0 7.3.0 diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index dc4ae1b543f150..f305342687fefe 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -50,7 +50,7 @@ 2.6.1 1.6.Final - 3.26.0 + 3.26.3 5.10.3 2.0.0 1.8.1 diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 9947110925a774..56cf070bcae3f1 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -38,7 +38,7 @@ 1.37 - 3.26.0 + 3.26.3 0.9.5 3.6.0.Final 5.10.3 diff --git a/independent-projects/junit5-virtual-threads/pom.xml b/independent-projects/junit5-virtual-threads/pom.xml index d3c57b6cd6c798..875e37d03ee88f 100644 --- a/independent-projects/junit5-virtual-threads/pom.xml +++ b/independent-projects/junit5-virtual-threads/pom.xml @@ -46,7 +46,7 @@ 1.11.0 5.10.3 - 3.26.0 + 3.26.3 diff --git a/independent-projects/qute/pom.xml b/independent-projects/qute/pom.xml index e5bc2a45674c32..dbb58c57a40eeb 100644 --- a/independent-projects/qute/pom.xml +++ b/independent-projects/qute/pom.xml @@ -39,7 +39,7 @@ UTF-8 5.10.3 - 3.26.0 + 3.26.3 3.2.0 1.8.0 3.6.0.Final diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 4f2d4c0744c21b..9f47d79ec57759 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -49,7 +49,7 @@ 1.14.11 5.10.3 3.9.8 - 3.26.0 + 3.26.3 3.6.0.Final 3.0.6.Final 3.0.0 diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index 28abaed4649c61..6fd63c9de17350 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -48,7 +48,7 @@ 4.4.0 - 3.26.0 + 3.26.3 2.17.2 4.1.0 5.10.3 From c34b6420eca614969666b98a3af01d2f2566e765 Mon Sep 17 00:00:00 2001 From: Bruno Alves Date: Tue, 9 Jul 2024 22:15:40 -0300 Subject: [PATCH 14/16] Add git option to avoid CRLF breaks on Windows --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e042ea1b5973a0..8133b71a9f14ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -233,7 +233,9 @@ If you have not done so on this machine, you need to: * macOS: Use the `Disk Utility.app` to check. It also allows you to create a case-sensitive volume to store your code projects. See this [blog entry](https://karnsonline.com/case-sensitive-apfs/) for more. * Windows: [Enable case sensitive file names per directory](https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity) * Install Git and configure your GitHub access - * Windows: enable longpaths: `git config --global core.longpaths true` + * Windows: + * enable longpaths: `git config --global core.longpaths true` + * avoid CRLF breaks: `git config --global core.autocrlf false` * Install Java SDK 17+ (OpenJDK recommended) * Install [GraalVM](https://quarkus.io/guides/building-native-image) * Install platform C developer tools: From f34fa1a59fa464a0ca545ce0537f1b99e0ab9041 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 8 Jul 2024 11:28:51 +0200 Subject: [PATCH 15/16] QuarkusComponentTest: register default and support custom converters - fixes #41709 --- .../test/component/QuarkusComponentTest.java | 8 +++ .../QuarkusComponentTestConfiguration.java | 49 +++++++++++++++-- .../QuarkusComponentTestExtension.java | 4 +- .../QuarkusComponentTestExtensionBuilder.java | 28 +++++++++- .../config/ConfigConverterExtensionTest.java | 55 +++++++++++++++++++ .../component/config/ConfigConverterTest.java | 52 ++++++++++++++++++ 6 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterExtensionTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterTest.java diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java index 3d66a0e200756d..3af12981b584e6 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java @@ -6,6 +6,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import org.eclipse.microprofile.config.spi.Converter; import org.junit.jupiter.api.extension.ExtendWith; import io.quarkus.arc.processor.AnnotationsTransformer; @@ -72,4 +73,11 @@ */ Class[] annotationsTransformers() default {}; + /** + * The additional config converters. By default, the Quarkus-specific converters are registered. + * + * @see QuarkusComponentTestExtensionBuilder#addConverter(Converter) + */ + Class>[] configConverters() default {}; + } diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java index 8279bbf99c84b4..8bde3f42823140 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java @@ -17,16 +17,42 @@ import jakarta.inject.Inject; import jakarta.inject.Provider; +import org.eclipse.microprofile.config.spi.Converter; import org.jboss.logging.Logger; import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.runtime.configuration.CharsetConverter; +import io.quarkus.runtime.configuration.CidrAddressConverter; +import io.quarkus.runtime.configuration.DurationConverter; +import io.quarkus.runtime.configuration.InetAddressConverter; +import io.quarkus.runtime.configuration.InetSocketAddressConverter; +import io.quarkus.runtime.configuration.LocaleConverter; +import io.quarkus.runtime.configuration.MemorySizeConverter; +import io.quarkus.runtime.configuration.PathConverter; +import io.quarkus.runtime.configuration.RegexConverter; +import io.quarkus.runtime.configuration.ZoneIdConverter; +import io.quarkus.runtime.logging.LevelConverter; import io.quarkus.test.InjectMock; class QuarkusComponentTestConfiguration { + // As defined in /quarkus/core/runtime/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter + static final List> DEFAULT_CONVERTERS = List.of(new InetSocketAddressConverter(), + new CharsetConverter(), + new CidrAddressConverter(), + new InetAddressConverter(), + new RegexConverter(), + new PathConverter(), + new DurationConverter(), + new MemorySizeConverter(), + new LocaleConverter(), + new ZoneIdConverter(), + new LevelConverter()); + static final QuarkusComponentTestConfiguration DEFAULT = new QuarkusComponentTestConfiguration(Map.of(), List.of(), - List.of(), false, true, QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL, List.of()); + List.of(), false, true, QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL, List.of(), + DEFAULT_CONVERTERS); private static final Logger LOG = Logger.getLogger(QuarkusComponentTestConfiguration.class); @@ -37,11 +63,12 @@ class QuarkusComponentTestConfiguration { final boolean addNestedClassesAsComponents; final int configSourceOrdinal; final List annotationsTransformers; + final List> configConverters; QuarkusComponentTestConfiguration(Map configProperties, List> componentClasses, List> mockConfigurators, boolean useDefaultConfigProperties, boolean addNestedClassesAsComponents, int configSourceOrdinal, - List annotationsTransformers) { + List annotationsTransformers, List> configConverters) { this.configProperties = configProperties; this.componentClasses = componentClasses; this.mockConfigurators = mockConfigurators; @@ -49,6 +76,7 @@ class QuarkusComponentTestConfiguration { this.addNestedClassesAsComponents = addNestedClassesAsComponents; this.configSourceOrdinal = configSourceOrdinal; this.annotationsTransformers = annotationsTransformers; + this.configConverters = configConverters; } QuarkusComponentTestConfiguration update(Class testClass) { @@ -58,6 +86,7 @@ QuarkusComponentTestConfiguration update(Class testClass) { boolean addNestedClassesAsComponents = this.addNestedClassesAsComponents; int configSourceOrdinal = this.configSourceOrdinal; List annotationsTransformers = new ArrayList<>(this.annotationsTransformers); + List> configConverters = new ArrayList<>(this.configConverters); QuarkusComponentTest testAnnotation = testClass.getAnnotation(QuarkusComponentTest.class); if (testAnnotation != null) { @@ -71,7 +100,17 @@ QuarkusComponentTestConfiguration update(Class testClass) { try { annotationsTransformers.add(transformerClass.getDeclaredConstructor().newInstance()); } catch (Exception e) { - LOG.errorf("Unable to instantiate %s", transformerClass); + LOG.errorf(e, "Unable to instantiate %s", transformerClass); + } + } + } + Class>[] converters = testAnnotation.configConverters(); + if (converters.length > 0) { + for (Class> converterClass : converters) { + try { + configConverters.add(converterClass.getDeclaredConstructor().newInstance()); + } catch (Exception e) { + LOG.errorf(e, "Unable to instantiate %s", converterClass); } } } @@ -120,7 +159,7 @@ QuarkusComponentTestConfiguration update(Class testClass) { return new QuarkusComponentTestConfiguration(Map.copyOf(configProperties), List.copyOf(componentClasses), this.mockConfigurators, useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal, - List.copyOf(annotationsTransformers)); + List.copyOf(annotationsTransformers), List.copyOf(configConverters)); } QuarkusComponentTestConfiguration update(Method testMethod) { @@ -132,7 +171,7 @@ QuarkusComponentTestConfiguration update(Method testMethod) { } return new QuarkusComponentTestConfiguration(configProperties, componentClasses, mockConfigurators, useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal, - annotationsTransformers); + annotationsTransformers, configConverters); } private static boolean resolvesToBuiltinBean(Class rawType) { diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java index 2656adaa4fcb11..e8204f6d3f4eca 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java @@ -57,6 +57,7 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.Converter; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; @@ -203,7 +204,7 @@ public QuarkusComponentTestExtension() { public QuarkusComponentTestExtension(Class... additionalComponentClasses) { this(new QuarkusComponentTestConfiguration(Map.of(), List.of(additionalComponentClasses), List.of(), false, true, QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL, - List.of())); + List.of(), List.of())); } QuarkusComponentTestExtension(QuarkusComponentTestConfiguration baseConfiguration) { @@ -421,6 +422,7 @@ private void startContainer(ExtensionContext context, Lifecycle testInstanceLife ClassLoader tccl = Thread.currentThread().getContextClassLoader(); SmallRyeConfigBuilder configBuilder = new SmallRyeConfigBuilder().forClassLoader(tccl) .addDefaultInterceptors() + .withConverters(configuration.configConverters.toArray(new Converter[] {})) .addDefaultSources() .withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem()) .withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath()) diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java index e0c267e5070650..cdf13d9c25c3a8 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java @@ -7,6 +7,8 @@ import java.util.Map; import java.util.function.Function; +import org.eclipse.microprofile.config.spi.Converter; + import io.quarkus.arc.processor.AnnotationsTransformer; /** @@ -26,6 +28,7 @@ public class QuarkusComponentTestExtensionBuilder { private final List> componentClasses = new ArrayList<>(); private final List> mockConfigurators = new ArrayList<>(); private final List annotationsTransformers = new ArrayList<>(); + private final List> configConverters = new ArrayList<>(); private boolean useDefaultConfigProperties = false; private boolean addNestedClassesAsComponents = true; private int configSourceOrdinal = QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL; @@ -105,6 +108,17 @@ public QuarkusComponentTestExtensionBuilder addAnnotationsTransformer(Annotation return this; } + /** + * Add an additional {@link Converter}. By default, the Quarkus-specific converters are registered. + * + * @param transformer + * @return self + */ + public QuarkusComponentTestExtensionBuilder addConverter(Converter converter) { + configConverters.add(converter); + return this; + } + /** * Configure a new mock of a bean. *

@@ -124,10 +138,18 @@ public MockBeanConfigurator mock(Class beanClass) { * @return a new extension instance */ public QuarkusComponentTestExtension build() { + List> converters; + if (configConverters.isEmpty()) { + converters = QuarkusComponentTestConfiguration.DEFAULT_CONVERTERS; + } else { + converters = new ArrayList<>(QuarkusComponentTestConfiguration.DEFAULT_CONVERTERS); + converters.addAll(configConverters); + converters = List.copyOf(converters); + } return new QuarkusComponentTestExtension(new QuarkusComponentTestConfiguration(Map.copyOf(configProperties), - List.copyOf(componentClasses), - List.copyOf(mockConfigurators), useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal, - List.copyOf(annotationsTransformers))); + List.copyOf(componentClasses), List.copyOf(mockConfigurators), useDefaultConfigProperties, + addNestedClassesAsComponents, configSourceOrdinal, + List.copyOf(annotationsTransformers), converters)); } void registerMockBean(MockBeanConfiguratorImpl mock) { diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterExtensionTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterExtensionTest.java new file mode 100644 index 00000000000000..2714e2ab0a1357 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterExtensionTest.java @@ -0,0 +1,55 @@ +package io.quarkus.test.component.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.config.spi.Converter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.component.QuarkusComponentTestExtension; +import io.quarkus.test.component.TestConfigProperty; + +public class ConfigConverterExtensionTest { + + @RegisterExtension + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() + .addConverter(new CustomBooleanConverter()).build(); + + @TestConfigProperty(key = "my.boolean", value = "jo") + @TestConfigProperty(key = "my.duration", value = "5s") + @Test + public void testQuarkusDurationConverter(Foo foo) { + assertEquals(TimeUnit.SECONDS.toMillis(5), foo.durationVal.toMillis()); + assertTrue(foo.boolVal); + } + + @Singleton + public static class Foo { + + @ConfigProperty(name = "my.duration", defaultValue = "60s") + Duration durationVal; + + @ConfigProperty(name = "my.boolean") + boolean boolVal; + + } + + @SuppressWarnings("serial") + @Priority(300) + public static class CustomBooleanConverter implements Converter { + + @Override + public Boolean convert(String value) throws IllegalArgumentException, NullPointerException { + return "jo".equals(value) ? true : false; + } + + } +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterTest.java new file mode 100644 index 00000000000000..28c498b785beb1 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterTest.java @@ -0,0 +1,52 @@ +package io.quarkus.test.component.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.config.spi.Converter; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.TestConfigProperty; +import io.quarkus.test.component.config.ConfigConverterTest.CustomBooleanConverter; + +@QuarkusComponentTest(configConverters = CustomBooleanConverter.class) +public class ConfigConverterTest { + + @TestConfigProperty(key = "my.boolean", value = "jo") + @TestConfigProperty(key = "my.duration", value = "5s") + @Test + public void testQuarkusDurationConverter(Foo foo) { + assertEquals(TimeUnit.SECONDS.toMillis(5), foo.durationVal.toMillis()); + assertTrue(foo.boolVal); + } + + @Singleton + public static class Foo { + + @ConfigProperty(name = "my.duration", defaultValue = "60s") + Duration durationVal; + + @ConfigProperty(name = "my.boolean") + boolean boolVal; + + } + + @SuppressWarnings("serial") + @Priority(300) + public static class CustomBooleanConverter implements Converter { + + @Override + public Boolean convert(String value) throws IllegalArgumentException, NullPointerException { + return "jo".equals(value) ? true : false; + } + + } +} From 947bbdb6c0eeabbf3202fd2dd0e79f7c4573dc45 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 8 Jul 2024 16:17:11 +0200 Subject: [PATCH 16/16] Add QuarkusComponentTestExtensionBuilder.setConfigBuilderCustomizer() --- .../QuarkusComponentTestConfiguration.java | 13 +++-- .../QuarkusComponentTestExtension.java | 56 ++++++++++--------- .../QuarkusComponentTestExtensionBuilder.java | 21 ++++++- .../config/ConfigBuilderCustomizerTest.java | 52 +++++++++++++++++ .../config/ConfigConverterExtensionTest.java | 2 +- .../component/config/ConfigConverterTest.java | 2 +- 6 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigBuilderCustomizerTest.java diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java index 8bde3f42823140..1d6f226a713dbc 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import jakarta.enterprise.event.Event; import jakarta.enterprise.inject.Instance; @@ -34,6 +35,7 @@ import io.quarkus.runtime.configuration.ZoneIdConverter; import io.quarkus.runtime.logging.LevelConverter; import io.quarkus.test.InjectMock; +import io.smallrye.config.SmallRyeConfigBuilder; class QuarkusComponentTestConfiguration { @@ -52,7 +54,7 @@ class QuarkusComponentTestConfiguration { static final QuarkusComponentTestConfiguration DEFAULT = new QuarkusComponentTestConfiguration(Map.of(), List.of(), List.of(), false, true, QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL, List.of(), - DEFAULT_CONVERTERS); + DEFAULT_CONVERTERS, null); private static final Logger LOG = Logger.getLogger(QuarkusComponentTestConfiguration.class); @@ -64,11 +66,13 @@ class QuarkusComponentTestConfiguration { final int configSourceOrdinal; final List annotationsTransformers; final List> configConverters; + final Consumer configBuilderCustomizer; QuarkusComponentTestConfiguration(Map configProperties, List> componentClasses, List> mockConfigurators, boolean useDefaultConfigProperties, boolean addNestedClassesAsComponents, int configSourceOrdinal, - List annotationsTransformers, List> configConverters) { + List annotationsTransformers, List> configConverters, + Consumer configBuilderCustomizer) { this.configProperties = configProperties; this.componentClasses = componentClasses; this.mockConfigurators = mockConfigurators; @@ -77,6 +81,7 @@ class QuarkusComponentTestConfiguration { this.configSourceOrdinal = configSourceOrdinal; this.annotationsTransformers = annotationsTransformers; this.configConverters = configConverters; + this.configBuilderCustomizer = configBuilderCustomizer; } QuarkusComponentTestConfiguration update(Class testClass) { @@ -159,7 +164,7 @@ QuarkusComponentTestConfiguration update(Class testClass) { return new QuarkusComponentTestConfiguration(Map.copyOf(configProperties), List.copyOf(componentClasses), this.mockConfigurators, useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal, - List.copyOf(annotationsTransformers), List.copyOf(configConverters)); + List.copyOf(annotationsTransformers), List.copyOf(configConverters), configBuilderCustomizer); } QuarkusComponentTestConfiguration update(Method testMethod) { @@ -171,7 +176,7 @@ QuarkusComponentTestConfiguration update(Method testMethod) { } return new QuarkusComponentTestConfiguration(configProperties, componentClasses, mockConfigurators, useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal, - annotationsTransformers, configConverters); + annotationsTransformers, configConverters, configBuilderCustomizer); } private static boolean resolvesToBuiltinBean(Class rawType) { diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java index e8204f6d3f4eca..5896ee4f04115d 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java @@ -79,6 +79,7 @@ import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; @@ -204,7 +205,7 @@ public QuarkusComponentTestExtension() { public QuarkusComponentTestExtension(Class... additionalComponentClasses) { this(new QuarkusComponentTestConfiguration(Map.of(), List.of(additionalComponentClasses), List.of(), false, true, QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL, - List.of(), List.of())); + List.of(), List.of(), null)); } QuarkusComponentTestExtension(QuarkusComponentTestConfiguration baseConfiguration) { @@ -252,7 +253,7 @@ public void afterEach(ExtensionContext context) throws Exception { @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { long start = System.nanoTime(); - context.getRoot().getStore(NAMESPACE).put(KEY_TEST_INSTANCE, testInstance); + store(context).put(KEY_TEST_INSTANCE, testInstance); LOG.debugf("postProcessTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } @@ -322,7 +323,7 @@ && isTestMethod(parameterContext.getDeclaringExecutable()) public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context) throws ParameterResolutionException { @SuppressWarnings("unchecked") - List> injectedParams = context.getRoot().getStore(NAMESPACE).get(KEY_INJECTED_PARAMS, List.class); + List> injectedParams = store(context).get(KEY_INJECTED_PARAMS, List.class); ArcContainer container = Arc.container(); BeanManager beanManager = container.beanManager(); java.lang.reflect.Type requiredType = parameterContext.getParameter().getParameterizedType(); @@ -339,7 +340,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte private void destroyDependentTestMethodParams(ExtensionContext context) { @SuppressWarnings("unchecked") - List> injectedParams = context.getRoot().getStore(NAMESPACE).get(KEY_INJECTED_PARAMS, List.class); + List> injectedParams = store(context).get(KEY_INJECTED_PARAMS, List.class); for (InstanceHandle handle : injectedParams) { if (handle.getBean() != null && handle.getBean().getScope().equals(Dependent.class)) { try { @@ -355,17 +356,17 @@ private void destroyDependentTestMethodParams(ExtensionContext context) { private void buildContainer(ExtensionContext context) { QuarkusComponentTestConfiguration testClassConfiguration = baseConfiguration .update(context.getRequiredTestClass()); - context.getRoot().getStore(NAMESPACE).put(KEY_TEST_CLASS_CONFIG, testClassConfiguration); + store(context).put(KEY_TEST_CLASS_CONFIG, testClassConfiguration); ClassLoader oldTccl = initArcContainer(context, testClassConfiguration); - context.getRoot().getStore(NAMESPACE).put(KEY_OLD_TCCL, oldTccl); + store(context).put(KEY_OLD_TCCL, oldTccl); } @SuppressWarnings("unchecked") private void cleanup(ExtensionContext context) { - ClassLoader oldTccl = context.getRoot().getStore(NAMESPACE).get(KEY_OLD_TCCL, ClassLoader.class); + ClassLoader oldTccl = store(context).get(KEY_OLD_TCCL, ClassLoader.class); Thread.currentThread().setContextClassLoader(oldTccl); - context.getRoot().getStore(NAMESPACE).remove(KEY_CONFIG_MAPPINGS); - Set generatedResources = context.getRoot().getStore(NAMESPACE).get(KEY_GENERATED_RESOURCES, Set.class); + store(context).remove(KEY_CONFIG_MAPPINGS); + Set generatedResources = store(context).get(KEY_GENERATED_RESOURCES, Set.class); for (Path path : generatedResources) { try { LOG.debugf("Delete generated %s", path); @@ -379,7 +380,7 @@ private void cleanup(ExtensionContext context) { @SuppressWarnings("unchecked") private void stopContainer(ExtensionContext context, Lifecycle testInstanceLifecycle) throws Exception { if (testInstanceLifecycle.equals(context.getTestInstanceLifecycle().orElse(Lifecycle.PER_METHOD))) { - for (FieldInjector fieldInjector : (List) context.getRoot().getStore(NAMESPACE) + for (FieldInjector fieldInjector : (List) store(context) .get(KEY_INJECTED_FIELDS, List.class)) { fieldInjector.unset(context.getRequiredTestInstance()); } @@ -392,10 +393,10 @@ private void stopContainer(ExtensionContext context, Lifecycle testInstanceLifec ConfigBeanCreator.clear(); InterceptorMethodCreator.clear(); - SmallRyeConfig config = context.getRoot().getStore(NAMESPACE).get(KEY_CONFIG, SmallRyeConfig.class); + SmallRyeConfig config = store(context).get(KEY_CONFIG, SmallRyeConfig.class); ConfigProviderResolver.instance().releaseConfig(config); ConfigProviderResolver - .setInstance(context.getRoot().getStore(NAMESPACE).get(KEY_OLD_CONFIG_PROVIDER_RESOLVER, + .setInstance(store(context).get(KEY_OLD_CONFIG_PROVIDER_RESOLVER, ConfigProviderResolver.class)); } } @@ -405,15 +406,15 @@ private void startContainer(ExtensionContext context, Lifecycle testInstanceLife // Init ArC Arc.initialize(); - QuarkusComponentTestConfiguration configuration = context.getRoot().getStore(NAMESPACE) - .get(KEY_TEST_CLASS_CONFIG, QuarkusComponentTestConfiguration.class); + QuarkusComponentTestConfiguration configuration = store(context).get(KEY_TEST_CLASS_CONFIG, + QuarkusComponentTestConfiguration.class); Optional testMethod = context.getTestMethod(); if (testMethod.isPresent()) { configuration = configuration.update(testMethod.get()); } ConfigProviderResolver oldConfigProviderResolver = ConfigProviderResolver.instance(); - context.getRoot().getStore(NAMESPACE).put(KEY_OLD_CONFIG_PROVIDER_RESOLVER, oldConfigProviderResolver); + store(context).put(KEY_OLD_CONFIG_PROVIDER_RESOLVER, oldConfigProviderResolver); SmallRyeConfigProviderResolver smallRyeConfigProviderResolver = new SmallRyeConfigProviderResolver(); ConfigProviderResolver.setInstance(smallRyeConfigProviderResolver); @@ -430,28 +431,33 @@ private void startContainer(ExtensionContext context, Lifecycle testInstanceLife new QuarkusComponentTestConfigSource(configuration.configProperties, configuration.configSourceOrdinal)); @SuppressWarnings("unchecked") - Set configMappings = context.getRoot().getStore(NAMESPACE).get(KEY_CONFIG_MAPPINGS, - Set.class); + Set configMappings = store(context).get(KEY_CONFIG_MAPPINGS, Set.class); if (configMappings != null) { // Register the mappings found during bean discovery for (ConfigClassWithPrefix mapping : configMappings) { configBuilder.withMapping(mapping.getKlass(), mapping.getPrefix()); } } + if (configuration.configBuilderCustomizer != null) { + configuration.configBuilderCustomizer.accept(configBuilder); + } SmallRyeConfig config = configBuilder.build(); smallRyeConfigProviderResolver.registerConfig(config, tccl); - context.getRoot().getStore(NAMESPACE).put(KEY_CONFIG, config); + store(context).put(KEY_CONFIG, config); ConfigBeanCreator.setClassLoader(tccl); // Inject fields declated on the test class Object testInstance = context.getRequiredTestInstance(); - context.getRoot().getStore(NAMESPACE).put(KEY_INJECTED_FIELDS, - injectFields(context.getRequiredTestClass(), testInstance)); + store(context).put(KEY_INJECTED_FIELDS, injectFields(context.getRequiredTestClass(), testInstance)); // Injected test method parameters - context.getRoot().getStore(NAMESPACE).put(KEY_INJECTED_PARAMS, new CopyOnWriteArrayList<>()); + store(context).put(KEY_INJECTED_PARAMS, new CopyOnWriteArrayList<>()); } } + private Store store(ExtensionContext context) { + return context.getRoot().getStore(NAMESPACE); + } + private BeanRegistrar registrarForMock(MockBeanConfiguratorImpl mock) { return new BeanRegistrar() { @@ -633,7 +639,7 @@ public void writeResource(Resource resource) throws IOException { }); } - extensionContext.getRoot().getStore(NAMESPACE).put(KEY_GENERATED_RESOURCES, generatedResources); + store(extensionContext).put(KEY_GENERATED_RESOURCES, generatedResources); builder.addAnnotationTransformation(AnnotationsTransformer.appliedToField().whenContainsAny(qualifiers) .whenContainsNone(DotName.createSimple(Inject.class)).thenTransform(t -> t.add(Inject.class))); @@ -783,7 +789,7 @@ public void register(RegistrationContext registrationContext) { .configClassWithPrefix(ConfigMappingBeanCreator.tryLoad(mapping), e.getKey())); } } - extensionContext.getRoot().getStore(NAMESPACE).put(KEY_CONFIG_MAPPINGS, configMappings); + store(extensionContext).put(KEY_CONFIG_MAPPINGS, configMappings); } LOG.debugf("Test injection points analyzed in %s ms [found: %s, mocked: %s]", @@ -850,7 +856,7 @@ public void accept(BytecodeTransformer transformer) { return oldTccl; } - private void processTestInterceptorMethods(Class testClass, ExtensionContext extensionContext, + private void processTestInterceptorMethods(Class testClass, ExtensionContext context, BeanRegistrar.RegistrationContext registrationContext, Set interceptorBindings) { List> annotations = List.of(AroundInvoke.class, PostConstruct.class, PreDestroy.class, AroundConstruct.class); @@ -873,7 +879,7 @@ private void processTestInterceptorMethods(Class testClass, ExtensionContext return ic -> { Object instance = null; if (!Modifier.isStatic(method.getModifiers())) { - Object testInstance = extensionContext.getRoot().getStore(NAMESPACE).get(KEY_TEST_INSTANCE); + Object testInstance = store(context).get(KEY_TEST_INSTANCE); if (testInstance == null) { throw new IllegalStateException("Test instance not available"); } diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java index cdf13d9c25c3a8..b1fdccb0d1aeb7 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java @@ -5,11 +5,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import org.eclipse.microprofile.config.spi.Converter; import io.quarkus.arc.processor.AnnotationsTransformer; +import io.smallrye.config.SmallRyeConfigBuilder; /** * Convenient builder for {@link QuarkusComponentTestExtension}. @@ -32,6 +34,7 @@ public class QuarkusComponentTestExtensionBuilder { private boolean useDefaultConfigProperties = false; private boolean addNestedClassesAsComponents = true; private int configSourceOrdinal = QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL; + private Consumer configBuilderCustomizer; /** * The initial set of components under test is derived from the test class. The types of all fields annotated with @@ -111,14 +114,28 @@ public QuarkusComponentTestExtensionBuilder addAnnotationsTransformer(Annotation /** * Add an additional {@link Converter}. By default, the Quarkus-specific converters are registered. * - * @param transformer + * @param converter * @return self + * @see #setConfigBuilderCustomizer(Consumer) */ public QuarkusComponentTestExtensionBuilder addConverter(Converter converter) { configConverters.add(converter); return this; } + /** + * Set the {@link SmallRyeConfigBuilder} customizer. + *

+ * The customizer can affect the configuration of a test method and should be used with caution. + * + * @param customizer + * @return self + */ + public QuarkusComponentTestExtensionBuilder setConfigBuilderCustomizer(Consumer customizer) { + this.configBuilderCustomizer = customizer; + return this; + } + /** * Configure a new mock of a bean. *

@@ -149,7 +166,7 @@ public QuarkusComponentTestExtension build() { return new QuarkusComponentTestExtension(new QuarkusComponentTestConfiguration(Map.copyOf(configProperties), List.copyOf(componentClasses), List.copyOf(mockConfigurators), useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal, - List.copyOf(annotationsTransformers), converters)); + List.copyOf(annotationsTransformers), converters, configBuilderCustomizer)); } void registerMockBean(MockBeanConfiguratorImpl mock) { diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigBuilderCustomizerTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigBuilderCustomizerTest.java new file mode 100644 index 00000000000000..6150ea7aff01a0 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigBuilderCustomizerTest.java @@ -0,0 +1,52 @@ +package io.quarkus.test.component.config; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.function.Consumer; + +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.config.spi.Converter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.component.QuarkusComponentTestExtension; +import io.quarkus.test.component.TestConfigProperty; +import io.smallrye.config.SmallRyeConfigBuilder; + +public class ConfigBuilderCustomizerTest { + + @RegisterExtension + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() + .setConfigBuilderCustomizer(new Consumer() { + @Override + public void accept(SmallRyeConfigBuilder builder) { + builder.withConverter(Boolean.class, 300, new CustomBooleanConverter()); + } + }).build(); + + @TestConfigProperty(key = "my.boolean", value = "jo") + @Test + public void testBuilderCustomizer(Foo foo) { + assertTrue(foo.boolVal); + } + + @Singleton + public static class Foo { + + @ConfigProperty(name = "my.boolean") + boolean boolVal; + + } + + @SuppressWarnings("serial") + public static class CustomBooleanConverter implements Converter { + + @Override + public Boolean convert(String value) throws IllegalArgumentException, NullPointerException { + return "jo".equals(value) ? true : false; + } + + } +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterExtensionTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterExtensionTest.java index 2714e2ab0a1357..3b4eb785f45f51 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterExtensionTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterExtensionTest.java @@ -26,7 +26,7 @@ public class ConfigConverterExtensionTest { @TestConfigProperty(key = "my.boolean", value = "jo") @TestConfigProperty(key = "my.duration", value = "5s") @Test - public void testQuarkusDurationConverter(Foo foo) { + public void testConverters(Foo foo) { assertEquals(TimeUnit.SECONDS.toMillis(5), foo.durationVal.toMillis()); assertTrue(foo.boolVal); } diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterTest.java index 28c498b785beb1..423df0e2e41ea8 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/config/ConfigConverterTest.java @@ -23,7 +23,7 @@ public class ConfigConverterTest { @TestConfigProperty(key = "my.boolean", value = "jo") @TestConfigProperty(key = "my.duration", value = "5s") @Test - public void testQuarkusDurationConverter(Foo foo) { + public void testConverters(Foo foo) { assertEquals(TimeUnit.SECONDS.toMillis(5), foo.durationVal.toMillis()); assertTrue(foo.boolVal); }