diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c6e1625f9e3c9..8133b71a9f14e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -233,6 +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`
+ * 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:
diff --git a/build-parent/pom.xml b/build-parent/pom.xml
index 371a946a9aea2..053deff0e84c4 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/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/BytecodeTransformerBuildItem.java
index ec968f494b8b0..ea79ca227b8bf 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;
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 02bfdcd3405e5..6377b9c3b908a 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 b68f3b9bbc676..7fb810f807708 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 a1e691f0748ed..17d4b5e547fb8 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 1f5b37aa311f0..9fa94f1ff6aec 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 f32d052a25aec..ca8f49634d88a 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 fa2d8b32c4255..48c18f868b7b2 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 extends QuarkusApplicati
if (!alreadyStarted) {
application.stop(); //this could have already been called
}
+ currentApplication = null;
(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler).accept(getExitCode(), null); //this may not be called if shutdown was initiated by a signal
}
@@ -435,6 +436,7 @@ public void run() {
currentApplication.stop();
}
currentApplication.awaitShutdown();
+ currentApplication = null;
System.out.flush();
System.err.flush();
}
diff --git a/devtools/gradle/gradle/libs.versions.toml b/devtools/gradle/gradle/libs.versions.toml
index e613c297de53e..4ac165ca0f938 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" }
diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/src/main/asciidoc/_attributes.adoc
index 4eed987d1446a..5729f47a657a9 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 f41dd334d1faa..a323321d9dc2e 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.
@@ -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,12 +256,29 @@ 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.
<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
@@ -1120,32 +1144,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`.
@@ -1174,44 +1203,62 @@ 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`.
+[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]
----
-# 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>
+quarkus.hibernate-orm.datasource=base <3>
# Default tenant 'base'
-quarkus.datasource.base.db-kind=postgresql
+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 <5>
+quarkus.flyway.base.migrate-at-start=true
# Tenant 'mycompany'
-quarkus.datasource.mycompany.db-kind=postgresql
+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
-
-# 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 <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> 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.
+<6> xref:datasource.adoc[Configure the datasource] for another tenant.
++
+There could be more tenants, but here we're stopping at two.
+<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`.
-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]
----
@@ -1227,7 +1274,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]
----
@@ -1243,8 +1290,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.
@@ -1252,16 +1298,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
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 94fe4d3b64c94..dd91316bffb0e 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,26 +276,28 @@ 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].
- *
- * 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
+ * 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 the dialect directly.
+ * 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,
+ * for example `Cockroach` for `CockroachDialect`.
+ *
+ * For third-party dialects, the expected value is the fully-qualified class name,
+ * for example `com.acme.hibernate.AcmeDbDialect`.
+ *
* @asciidoclet
*/
@WithParentName
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 8ef6845e7aa04..fc3de18ff9b91 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.
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 95d4d47724c53..38b42d218c2a6 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 0000000000000..4e588223254a3
--- /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 0000000000000..4da6a548d7e43
--- /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 5f69240af667c..fda1ac26fec88 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 c076e5712bc0e..0a5f94d48ffb3 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 aba645216812a..9db547b46e7a0 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 0000000000000..d301a0e1cff5d
--- /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 0000000000000..ed1ef873b77b4
--- /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 02e2b46e25b14..8a3d77797146e 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 {
diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml
index dc4ae1b543f15..f305342687fef 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/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 0c4006fc3dc74..18513222b7384 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);
}
}
diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml
index 9947110925a77..56cf070bcae3f 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 d3c57b6cd6c79..875e37d03ee88 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 e5bc2a45674c3..dbb58c57a40ee 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/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 cb811043af91b..8e0232603448e 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) {
diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml
index 4f2d4c0744c21..9f47d79ec5775 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 28abaed4649c6..6fd63c9de1735 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
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 86da8eec68667..b943eb6cc10f0 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 86da8eec68667..b943eb6cc10f0 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 e73c7a6fe1469..318540bf44426 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
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 2d01ef727fd4a..533048d795934 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);
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 3d66a0e200756..3af12981b584e 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 extends AnnotationsTransformer>[] annotationsTransformers() default {};
+ /**
+ * The additional config converters. By default, the Quarkus-specific converters are registered.
+ *
+ * @see QuarkusComponentTestExtensionBuilder#addConverter(Converter)
+ */
+ Class extends Converter>>[] 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 8279bbf99c84b..1d6f226a713db 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;
@@ -17,16 +18,43 @@
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;
+import io.smallrye.config.SmallRyeConfigBuilder;
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, null);
private static final Logger LOG = Logger.getLogger(QuarkusComponentTestConfiguration.class);
@@ -37,11 +65,14 @@ class QuarkusComponentTestConfiguration {
final boolean addNestedClassesAsComponents;
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 annotationsTransformers, List> configConverters,
+ Consumer configBuilderCustomizer) {
this.configProperties = configProperties;
this.componentClasses = componentClasses;
this.mockConfigurators = mockConfigurators;
@@ -49,6 +80,8 @@ class QuarkusComponentTestConfiguration {
this.addNestedClassesAsComponents = addNestedClassesAsComponents;
this.configSourceOrdinal = configSourceOrdinal;
this.annotationsTransformers = annotationsTransformers;
+ this.configConverters = configConverters;
+ this.configBuilderCustomizer = configBuilderCustomizer;
}
QuarkusComponentTestConfiguration update(Class> testClass) {
@@ -58,6 +91,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 +105,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 extends Converter>>[] converters = testAnnotation.configConverters();
+ if (converters.length > 0) {
+ for (Class extends Converter>> converterClass : converters) {
+ try {
+ configConverters.add(converterClass.getDeclaredConstructor().newInstance());
+ } catch (Exception e) {
+ LOG.errorf(e, "Unable to instantiate %s", converterClass);
}
}
}
@@ -120,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(annotationsTransformers), List.copyOf(configConverters), configBuilderCustomizer);
}
QuarkusComponentTestConfiguration update(Method testMethod) {
@@ -132,7 +176,7 @@ QuarkusComponentTestConfiguration update(Method testMethod) {
}
return new QuarkusComponentTestConfiguration(configProperties, componentClasses,
mockConfigurators, useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal,
- annotationsTransformers);
+ 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 2656adaa4fcb1..5896ee4f04115 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;
@@ -78,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;
@@ -203,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(), null));
}
QuarkusComponentTestExtension(QuarkusComponentTestConfiguration baseConfiguration) {
@@ -251,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));
}
@@ -321,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();
@@ -338,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 {
@@ -354,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);
@@ -378,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());
}
@@ -391,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));
}
}
@@ -404,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);
@@ -421,6 +423,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())
@@ -428,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() {
@@ -631,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)));
@@ -781,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]",
@@ -848,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);
@@ -871,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 e0c267e507065..b1fdccb0d1aeb 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,9 +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}.
@@ -26,9 +30,11 @@ 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;
+ private Consumer configBuilderCustomizer;
/**
* The initial set of components under test is derived from the test class. The types of all fields annotated with
@@ -105,6 +111,31 @@ public QuarkusComponentTestExtensionBuilder addAnnotationsTransformer(Annotation
return this;
}
+ /**
+ * Add an additional {@link Converter}. By default, the Quarkus-specific converters are registered.
+ *
+ * @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.
*
@@ -124,10 +155,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, 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 0000000000000..6150ea7aff01a
--- /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
new file mode 100644
index 0000000000000..3b4eb785f45f5
--- /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 testConverters(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 0000000000000..423df0e2e41ea
--- /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 testConverters(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;
+ }
+
+ }
+}