diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 086334df6de6d..e82fed04711a2 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -148,7 +148,7 @@ jobs: env: CAPTURE_BUILD_SCAN: true run: | - ./mvnw -T1C $COMMON_MAVEN_ARGS -DskipTests -DskipITs -DskipDocs -Dinvoker.skip -Dno-format -Dtcks -Prelocations clean install + ./mvnw -T1C $COMMON_MAVEN_ARGS -DskipTests -DskipITs -DskipDocs -Dinvoker.skip -Dskip.gradle.tests -Djbang.skip -Dtruststore.skip -Dno-format -Dtcks -Prelocations clean install - name: Verify extension dependencies run: ./update-extension-dependencies.sh $COMMON_MAVEN_ARGS - name: Get GIB arguments diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 059718fa95e2c..729acf449a32b 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -24,7 +24,7 @@ 1.3.2 1 1.1.5 - 2.1.4.Final + 2.1.5.Final 3.1.0.Final 6.2.7.Final 0.33.0 @@ -100,7 +100,7 @@ bytebuddy.version (just below), hibernate-orm.version-for-documentation (in docs/pom.xml) and both hibernate-orm.version and antlr.version in build-parent/pom.xml WARNING again for diffs that don't provide enough context: when updating, see above --> - 6.4.1.Final + 6.4.2.Final 1.14.7 6.0.6.Final 2.2.1.Final @@ -144,11 +144,11 @@ 14.0.21.Final 4.6.5.Final 3.1.5 - 4.1.103.Final + 4.1.106.Final 1.12.0 1.0.4 3.5.3.Final - 2.5.3 + 2.5.4 3.6.1 1.8.0 1.1.10.5 @@ -188,8 +188,8 @@ 5.8.0 5.8.0 4.13.0 - 2.0.2.Final - 23.0.3 + 2.0.3.Final + 23.0.4 1.15.1 3.42.0 2.24.0 @@ -221,7 +221,7 @@ 9.37.3 0.0.6 0.1.3 - 2.10.0 + 2.12.0 0.8.9 1.0.0 3.0.0 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 13cb7562806c8..8e65e4e5d157f 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -104,7 +104,7 @@ - 23.0.3 + 23.0.4 19.0.3 quay.io/keycloak/keycloak:${keycloak.version} quay.io/keycloak/keycloak:${keycloak.wildfly.version}-legacy diff --git a/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleCommand.java b/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleCommand.java index 73eaf0a311231..8c0d56ba3036c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleCommand.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleCommand.java @@ -39,6 +39,16 @@ public ConsoleCommand(char key, String description, HelpState helpState, Runnabl this(key, description, null, -1, helpState, runnable); } + public static ConsoleCommand duplicateCommandWithNewPromptString(ConsoleCommand commandToDuplicate, + String newPromptString) { + return new ConsoleCommand(commandToDuplicate.getKey(), + commandToDuplicate.getDescription(), + newPromptString, + commandToDuplicate.getPromptPriority(), + commandToDuplicate.getHelpState(), + commandToDuplicate.getReadLineHandler()); + } + public char getKey() { return key; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleStateManager.java b/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleStateManager.java index a344770dc0d83..6b891851e5ddc 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleStateManager.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/console/ConsoleStateManager.java @@ -100,15 +100,20 @@ public static void init(QuarkusConsole console, DevModeType devModeType) { } void installBuiltins(DevModeType devModeType) { + final String editPromptFormat = "to edit command line args (currently '" + + MessageFormat.GREEN + + "%s" + + MessageFormat.RESET + + "')"; + ConsoleContext context = createContext("System"); List commands = new ArrayList<>(); if (devModeType != DevModeType.TEST_ONLY) { commands.add(new ConsoleCommand('s', "Force restart", null, () -> { forceRestart(); })); commands.add(new ConsoleCommand('e', "Edits the command line parameters and restarts", - "to edit command line args (currently '" + MessageFormat.GREEN - + String.join(" ", RuntimeUpdatesProcessor.INSTANCE.getCommandLineArgs()) + MessageFormat.RESET - + "')", + editPromptFormat.formatted(String.join(" ", + RuntimeUpdatesProcessor.INSTANCE.getCommandLineArgs())), 100, new ConsoleCommand.HelpState(() -> BLUE, () -> String.join(" ", RuntimeUpdatesProcessor.INSTANCE.getCommandLineArgs())), new Consumer() { @@ -121,6 +126,10 @@ public void accept(String args) { Logger.getLogger(ConsoleStateManager.class).errorf(e, "Failed to parse command line %s", args); return; } + // Reload command prompt string + context.reset(ConsoleCommand.duplicateCommandWithNewPromptString(context.getCommandByKey('e'), + editPromptFormat.formatted(String.join(" ", + RuntimeUpdatesProcessor.INSTANCE.getCommandLineArgs())))); RuntimeUpdatesProcessor.INSTANCE.doScan(true, true); } })); @@ -145,8 +154,6 @@ public void accept(String args) { })); } - ConsoleContext context = createContext("System"); - commands.add(new ConsoleCommand('j', "Toggle log levels", new ConsoleCommand.HelpState(() -> currentLevel == null ? BLUE : RED, () -> (currentLevel == null @@ -280,8 +287,9 @@ public ConsoleContext createContext(String name) { void redraw() { List sorted = commands.values().stream().map(s -> s.consoleCommand) - .filter(s -> s.getPromptString() != null).sorted(Comparator.comparingInt(ConsoleCommand::getPromptPriority)) - .collect(Collectors.toList()); + .filter(s -> s.getPromptString() != null) + .sorted(Comparator.comparingInt(ConsoleCommand::getPromptPriority)) + .toList(); if (sorted.isEmpty()) { QuarkusConsole.INSTANCE.setPromptMessage(null); oldPrompt = null; @@ -338,6 +346,12 @@ public void addCommandInternal(ConsoleCommand consoleCommand) { } } + public ConsoleCommand getCommandByKey(Character key) { + synchronized (commands) { + return commands.get(key).consoleCommand; + } + } + public void reset(ConsoleCommand... command) { synchronized (commands) { internal.clear(); diff --git a/devtools/cli/src/main/java/io/quarkus/cli/build/BuildSystemRunner.java b/devtools/cli/src/main/java/io/quarkus/cli/build/BuildSystemRunner.java index 86fb31863deab..5a6fd63209125 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/build/BuildSystemRunner.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/build/BuildSystemRunner.java @@ -71,10 +71,16 @@ default BuildCommandArgs prependExecutable(ArrayDeque args) { default void paramsToQuarkusArgs(List params, ArrayDeque args) { if (!params.isEmpty()) { - args.add("-Dquarkus.args='" + String.join(" ", params) + "'"); + args.add("-Dquarkus.args=" + String.join(" ", wrapWithDoubleQuotes(params))); } } + default List wrapWithDoubleQuotes(List stringsToWrap) { + return stringsToWrap.stream() + .map("\"%s\""::formatted) + .toList(); + } + default List flattenMappedProperties(Map props) { List result = new ArrayList<>(); props.entrySet().forEach(x -> { diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java index 354ed4055f511..eb11e76ff4b91 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectGradleTest.java @@ -348,8 +348,8 @@ public void testDevOptions() throws Exception { Assertions.assertFalse(result.stdout.contains("-Dsuspend"), "gradle command should not specify '-Dsuspend'\n" + result); - Assertions.assertTrue(result.stdout.contains("-Dquarkus.args='arg1 arg2'"), - "gradle command should not specify -Dquarkus.args='arg1 arg2'\n" + result); + Assertions.assertTrue(result.stdout.contains("-Dquarkus.args=\"arg1\" \"arg2\""), + "gradle command should not specify -Dquarkus.args=\"arg1\" \"arg2\"\n" + result); // 4 TEST MODE: test --clean --debug --suspend --offline result = CliDriver.execute(project, "test", "-e", "--dry-run", @@ -366,6 +366,13 @@ public void testDevOptions() throws Exception { "Expected OK return code. Result:\n" + result); Assertions.assertTrue(result.stdout.contains("Run current project in test mode"), result.toString()); Assertions.assertTrue(result.stdout.contains("--tests FooTest"), result.toString()); + + // 6 TEST MODE: Two word argument + result = CliDriver.execute(project, "dev", "-e", "--dry-run", + "--no-suspend", "--debug-host=0.0.0.0", "--debug-port=8008", "--debug-mode=connect", "--", "arg1 arg2"); + + Assertions.assertTrue(result.stdout.contains("-Dquarkus.args=\"arg1 arg2\""), + "mvn command should not specify -Dquarkus.args=\"arg1 arg2\"\n" + result); } @Test diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java index 180795f1ae0e3..1374448c5ab51 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java @@ -273,8 +273,8 @@ public void testDevTestOptions() throws Exception { Assertions.assertFalse(result.stdout.contains("-Dsuspend"), "mvn command should not specify '-Dsuspend'\n" + result); - Assertions.assertTrue(result.stdout.contains("-Dquarkus.args='arg1 arg2'"), - "mvn command should not specify -Dquarkus.args='arg1 arg2'\n" + result); + Assertions.assertTrue(result.stdout.contains("-Dquarkus.args=\"arg1\" \"arg2\""), + "mvn command should not specify -Dquarkus.args=\"arg1\" \"arg2\"\n" + result); // 4 TEST MODE: test --clean --debug --suspend --offline result = CliDriver.execute(project, "test", "-e", "--dry-run", @@ -291,6 +291,13 @@ public void testDevTestOptions() throws Exception { "Expected OK return code. Result:\n" + result); Assertions.assertTrue(result.stdout.contains("Run current project in test mode"), result.toString()); Assertions.assertTrue(result.stdout.contains("-Dtest=FooTest"), result.toString()); + + // 6 TEST MODE: Two word argument + result = CliDriver.execute(project, "dev", "-e", "--dry-run", + "--no-suspend", "--debug-host=0.0.0.0", "--debug-port=8008", "--debug-mode=connect", "--", "arg1 arg2"); + + Assertions.assertTrue(result.stdout.contains("-Dquarkus.args=\"arg1 arg2\""), + "mvn command should not specify -Dquarkus.args=\"arg1 arg2\"\n" + result); } @Test diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index 16a7742557386..942515adbefc8 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -136,6 +136,7 @@ public void apply(Project project) { final QuarkusPluginExtension quarkusExt = project.getExtensions().create(EXTENSION_NAME, QuarkusPluginExtension.class, project); + createSourceSets(project); createConfigurations(project); registerTasks(project, quarkusExt); } @@ -323,7 +324,7 @@ public boolean isSatisfiedBy(Task t) { quarkusGenerateCodeDev.configure(task -> task.setSourcesDirectories(getSourcesParents(mainSourceSet))); quarkusGenerateCodeTests.configure(task -> task.setSourcesDirectories(getSourcesParents(testSourceSet))); - SourceSet intTestSourceSet = sourceSets.create(INTEGRATION_TEST_SOURCE_SET_NAME); + SourceSet intTestSourceSet = sourceSets.getByName(INTEGRATION_TEST_SOURCE_SET_NAME); intTestSourceSet.setCompileClasspath( intTestSourceSet.getCompileClasspath() .plus(mainSourceSet.getOutput()) @@ -345,7 +346,7 @@ public boolean isSatisfiedBy(Task t) { intTestTask.setTestClassesDirs(intTestSourceOutputClasses); }); - SourceSet nativeTestSourceSet = sourceSets.create(NATIVE_TEST_SOURCE_SET_NAME); + SourceSet nativeTestSourceSet = sourceSets.getByName(NATIVE_TEST_SOURCE_SET_NAME); nativeTestSourceSet.setCompileClasspath( nativeTestSourceSet.getCompileClasspath() .plus(mainSourceSet.getOutput()) @@ -391,8 +392,8 @@ public void execute(Task task) { // quarkusBuild is expected to run after the project has passed the tests quarkusBuildCacheableAppParts.configure(task -> task.shouldRunAfter(tasks.withType(Test.class))); - SourceSet generatedSourceSet = sourceSets.create(QuarkusGenerateCode.QUARKUS_GENERATED_SOURCES); - SourceSet generatedTestSourceSet = sourceSets.create(QuarkusGenerateCode.QUARKUS_TEST_GENERATED_SOURCES); + SourceSet generatedSourceSet = sourceSets.getByName(QuarkusGenerateCode.QUARKUS_GENERATED_SOURCES); + SourceSet generatedTestSourceSet = sourceSets.getByName(QuarkusGenerateCode.QUARKUS_TEST_GENERATED_SOURCES); // Register the quarkus-generated-code for (String provider : QuarkusGenerateCode.CODE_GENERATION_PROVIDER) { @@ -423,14 +424,22 @@ private static void configureGenerateCodeTask(QuarkusGenerateCode task, String g task.getGeneratedOutputDirectory().set(generatedSources.getJava().getClassesDirectory().get().getAsFile()); } + private void createSourceSets(Project project) { + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + sourceSets.create(INTEGRATION_TEST_SOURCE_SET_NAME); + sourceSets.create(NATIVE_TEST_SOURCE_SET_NAME); + sourceSets.create(QuarkusGenerateCode.QUARKUS_GENERATED_SOURCES); + sourceSets.create(QuarkusGenerateCode.QUARKUS_TEST_GENERATED_SOURCES); + } + private void createConfigurations(Project project) { final ConfigurationContainer configContainer = project.getConfigurations(); // Custom configuration to be used for the dependencies of the testNative task - configContainer.maybeCreate(NATIVE_TEST_IMPLEMENTATION_CONFIGURATION_NAME) + configContainer.getByName(NATIVE_TEST_IMPLEMENTATION_CONFIGURATION_NAME) .extendsFrom(configContainer.findByName(JavaPlugin.TEST_IMPLEMENTATION_CONFIGURATION_NAME)); - configContainer.maybeCreate(NATIVE_TEST_RUNTIME_ONLY_CONFIGURATION_NAME) + configContainer.getByName(NATIVE_TEST_RUNTIME_ONLY_CONFIGURATION_NAME) .extendsFrom(configContainer.findByName(JavaPlugin.TEST_RUNTIME_ONLY_CONFIGURATION_NAME)); // create a custom configuration to be used for the dependencies of the quarkusIntTest task diff --git a/docs/src/main/asciidoc/cdi.adoc b/docs/src/main/asciidoc/cdi.adoc index 7d393c12f7b38..0cdc9da9d2fb3 100644 --- a/docs/src/main/asciidoc/cdi.adoc +++ b/docs/src/main/asciidoc/cdi.adoc @@ -253,7 +253,7 @@ Client proxies allow for: * Circular dependencies in the dependency graph. Having circular dependencies is often an indication that a redesign should be considered, but sometimes it's inevitable. * In rare cases it's practical to destroy the beans manually. A direct injected reference would lead to a stale bean instance. - +[[ok-you-said-that-there-are-several-kinds-of-beans]] == OK. You said that there are several kinds of beans? Yes. In general, we distinguish: diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index 8d68f8462abdf..286d6f5601177 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -329,6 +329,7 @@ By default, Quarkus provides three profiles, that activate automatically in cert * *test* - Activated when running tests * *prod* - The default profile when not running in development or test mode +[[custom-profiles]] === Custom Profiles It is also possible to create additional profiles and activate them with the `quarkus.profile` configuration property. A diff --git a/docs/src/main/asciidoc/container-image.adoc b/docs/src/main/asciidoc/container-image.adoc index eb5e7c3ebff19..2fd411e028c52 100644 --- a/docs/src/main/asciidoc/container-image.adoc +++ b/docs/src/main/asciidoc/container-image.adoc @@ -47,14 +47,14 @@ For example, the presence of `src/main/jib/foo/bar` would result in `/foo/bar` There are cases where the built container image may need to have Java debugging conditionally enabled at runtime. -When the base image has not been changed (and therefore `ubi8/openjdk-11-runtime`, `ubi8/openjdk-17-runtime`, or `ubi8/openjdk-21-runtime` is used), then the `quarkus.jib.jvm-arguments` configuration property can be used in order to +When the base image has not been changed (and therefore `ubi8/openjdk-11-runtime`, `ubi8/openjdk-17-runtime`, or `ubi8/openjdk-21-runtime` is used), then the `quarkus.jib.jvm-additional-arguments` configuration property can be used in order to make the JVM listen on the debug port at startup. The exact configuration is: [source,properties] ---- -quarkus.jib.jvm-arguments=-agentlib:jdwp=transport=dt_socket\\,server=y\\,suspend=n\\,address=*:5005 +quarkus.jib.jvm-additional-arguments=-agentlib:jdwp=transport=dt_socket\\,server=y\\,suspend=n\\,address=*:5005 ---- Other base images might provide launch scripts that enable debugging when an environment variable is set, in which case you would set than environment variable when launching the container. diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index 81fbb90de0eba..74f7fcfcfd121 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -253,7 +253,12 @@ Without an extension, the driver will work correctly in any Quarkus app running However, the driver is unlikely to work when compiling your application to a native executable. If you plan to make a native executable, use the existing JDBC Quarkus extensions, or contribute one for your driver. -.An example with the OpenTracing driver: +[WARNING] +==== +OpenTracing has been deprecated in favor of OpenTelemetry. For tracing information, please check the related section about <>, bellow. +==== + +.A custom driver definition example with the legacy OpenTracing driver: [source, properties] ---- @@ -405,6 +410,99 @@ AgroalDataSource usersDataSource; AgroalDataSource inventoryDataSource; ---- +[[datasource-active]] +=== Activate/deactivate datasources + +If a datasource is configured at build time, +by default it is active at runtime, +that is Quarkus will start the corresponding JDBC connection pool or reactive client on application startup. + +To deactivate a datasource at runtime, set `quarkus.datasource[.optional name].active` to `false`. +Then Quarkus will not start the corresponding JDBC connection pool or reactive client on application startup. +Any attempt to use the corresponding datasource at runtime will fail with a clear error message. + +This is in particular useful when you want an application to be able +to use one of a pre-determined set of datasources at runtime. + +[WARNING] +==== +If another Quarkus extension relies on an inactive datasource, +that extension might fail to start. + +In such case, you will need to deactivate that other extension too. +For example see xref:hibernate-orm.adoc#persistence-unit-active[here for Hibernate ORM]. +==== + +For example, with the following configuration: + +[source,properties] +---- +quarkus.datasource."pg".db-kind=postgres +quarkus.datasource."pg".active=false +quarkus.datasource."pg".jdbc.url=jdbc:postgresql:///your_database + +quarkus.datasource."oracle".db-kind=oracle +quarkus.datasource."oracle".active=false +quarkus.datasource."oracle".jdbc.url=jdbc:oracle:///your_database +---- + +Setting `quarkus.datasource."pg".active=true` xref:config-reference.adoc#configuration-sources[at runtime] +will make only the PostgreSQL datasource available, +and setting `quarkus.datasource."oracle".active=true` at runtime +will make only the Oracle datasource available. + +[TIP] +==== +xref:config-reference.adoc#custom-profiles[Custom configuration profiles] can help simplify such a setup. +By appending the following profile-specific configuration to the one above, +you can select a persistence unit/datasource at runtime simply by +xref:config-reference.adoc#multiple-profiles[setting `quarkus.profile`]: +`quarkus.profile=prod,pg` or `quarkus.profile=prod,oracle`. + +[source,properties] +---- +%pg.quarkus.hibernate-orm."pg".active=true +%pg.quarkus.datasource."pg".active=true +# Add any pg-related runtime configuration here, prefixed with "%pg." + +%oracle.quarkus.hibernate-orm."oracle".active=true +%oracle.quarkus.datasource."oracle".active=true +# Add any pg-related runtime configuration here, prefixed with "%pg." +---- +==== + +[TIP] +==== +It can also be useful to define a xref:cdi.adoc#ok-you-said-that-there-are-several-kinds-of-beans[CDI bean producer] redirecting to the currently active datasource, +like this: + +[source,java,indent=0] +---- +public class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @DataSource("pg") + AgroalDataSource pgDataSourceBean; + + @Inject + @DataSource("oracle") + AgroalDataSource oracleDataSourceBean; + + @Produces + @ApplicationScoped + public AgroalDataSource dataSource() { + if (dataSourceSupport.getInactiveNames().contains("pg")) { + return oracleDataSourceBean; + } else { + return pgDataSourceBean; + } + } +} +---- +==== + == Datasource integrations === Datasource health check @@ -441,6 +539,20 @@ They are available after calling `dataSource.getMetrics()` on an injected `Agroa If the metrics collection for this datasource is disabled, all values result in zero. +[[datasource-tracing]] +=== Datasource tracing + +To use tracing with a datasource, you need to add the xref:opentelemetry.adoc[`quarkus-opentelemetry`] extension to your project. + +You don't need to declare a different driver because you need tracing. If you use a JDBC driver, you need to follow the instructions in the OpenTelemetry extension xref:opentelemetry.adoc#jdbc[here]. + +Even with all the tracing infrastructure in place the datasource tracing is not enabled by default, and you need to enable it by setting this property: +[source, properties] +---- +# enable tracing +quarkus.datasource.jdbc.telemetry=true +---- + === Narayana transaction manager integration Integration is automatic if the Narayana JTA extension is also available. diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index 3439cbae1e469..ff704928e292f 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -464,6 +464,98 @@ You can inject the `EntityManagerFactory` of a named persistence unit using the EntityManagerFactory entityManagerFactory; ---- +[[persistence-unit-active]] +=== Activate/deactivate persistence units + +If a persistence unit is configured at build time, +by default it is active at runtime, +that is Quarkus will start the corresponding Hibernate ORM `SessionFactory` on application startup. + +To deactivate a persistence unit at runtime, set `quarkus.hibernate-orm[.optional name].active` to `false`. +Then Quarkus will not start the corresponding Hibernate ORM `SessionFactory` on application startup. +Any attempt to use the corresponding persistence unit at runtime will fail with a clear error message. + +This is in particular useful when you want an application to be able +to xref:datasource.adoc#datasource-active[use one of a pre-determined set of datasources at runtime]. + +For example, with the following configuration: + +[source,properties] +---- +quarkus.hibernate-orm."pg".packages=org.acme.model.shared +quarkus.hibernate-orm."pg".datasource=pg +quarkus.hibernate-orm."pg".database.generation=drop-and-create +quarkus.hibernate-orm."pg".active=false +quarkus.datasource."pg".db-kind=h2 +quarkus.datasource."pg".active=false +quarkus.datasource."pg".jdbc.url=jdbc:postgresql:///your_database + +quarkus.hibernate-orm."oracle".packages=org.acme.model.shared +quarkus.hibernate-orm."oracle".datasource=oracle +quarkus.hibernate-orm."oracle".database.generation=drop-and-create +quarkus.hibernate-orm."oracle".active=false +quarkus.datasource."oracle".db-kind=oracle +quarkus.datasource."oracle".active=false +quarkus.datasource."oracle".jdbc.url=jdbc:oracle:///your_database +---- + +xref:config-reference.adoc#configuration-sources[Setting] `quarkus.hibernate-orm."pg".active=true` and `quarkus.datasource."pg".active=true` at runtime +will make only the PostgreSQL persistence unit and datasource available, +and setting `quarkus.hibernate-orm."oracle".active=true` and `quarkus.datasource."oracle".active=true` at runtime +will make only the Oracle persistence unit and datasource available. + +[TIP] +==== +xref:config-reference.adoc#custom-profiles[Custom configuration profiles] can help simplify such a setup. +By appending the following profile-specific configuration to the one above, +you can select a persistence unit/datasource at runtime simply by +xref:config-reference.adoc#multiple-profiles[setting `quarkus.profile`]: +`quarkus.profile=prod,pg` or `quarkus.profile=prod,oracle`. + +[source,properties] +---- +%pg.quarkus.hibernate-orm."pg".active=true +%pg.quarkus.datasource."pg".active=true +# Add any pg-related runtime configuration here, prefixed with "%pg." + +%oracle.quarkus.hibernate-orm."oracle".active=true +%oracle.quarkus.datasource."oracle".active=true +# Add any pg-related runtime configuration here, prefixed with "%pg." +---- +==== + +[TIP] +==== +It can also be useful to define a xref:cdi.adoc#ok-you-said-that-there-are-several-kinds-of-beans[CDI bean producer] redirecting to the currently active persistence unit, +like this: + +[source,java,indent=0] +---- +public class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @PersistenceUnit("pg") + Session pgSessionBean; + + @Inject + @PersistenceUnit("oracle") + Session oracleSessionBean; + + @Produces + @ApplicationScoped + public Session session() { + if (dataSourceSupport.getInactiveNames().contains("pg")) { + return oracleSessionBean; + } else { + return pgSessionBean; + } + } +} +---- +==== + [[persistence-xml]] == Setting up and configuring Hibernate ORM with a `persistence.xml` diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 8e4480d6b64ba..ba56d9283048c 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -223,7 +223,7 @@ Likewise, a line that contains an _expression_ or a _non-whitespace character_ i <3> {/for} <4> - + ---- <1> This is a standalone line and will be removed. @@ -240,7 +240,7 @@ Likewise, a line that contains an _expression_ or a _non-whitespace character_ i
  • Foo 100
  • - + ---- @@ -258,7 +258,7 @@ In this case, all whitespace characters from a standalone line will be printed t - + ---- @@ -273,7 +273,7 @@ In the `object.property` (dot notation) syntax, the `property` must be a <> value. An expression can start with an optional namespace followed by a colon (`:`). -A valid namespace consist of alphanumeric characters and underscores. +A valid namespace consists of alphanumeric characters and underscores. Namespace expressions are resolved differently - see also <>. .Property Accessor Examples @@ -336,7 +336,7 @@ You can learn more about virtual methods in the <>. -If no result is found for the first part it's resolved against the parent context object (if available). +If no result is found for the first part, it's resolved against the parent context object (if available). For an expression that starts with a namespace the current context object is found using all the available ``NamespaceResolver``s. For an expression that does not start with a namespace the current context object is *derived from the position* of the tag. All other parts of an expression are resolved using all ``ValueResolver``s against the result of the previous resolution. @@ -1152,18 +1152,26 @@ In this case, just add `_isolated=false` or `_unisolated` argument to the call s ===== Arguments -Named arguments can be accessed directly in a tag template. -The first argument does not have to define a name but can be accessed using the `it` alias. -Furthermore, arguments metadata are accessible in a tag using the `_args` alias. +Named arguments can be accessed directly in the tag template. +However, the first argument does not need to define a name and it can be accessed using the `it` alias. +Furthermore, if an argument does not have a name defined and the value is a single identifier, such as `foo`, then the name is defaulted to the value identifier, e.g. `{#myTag foo /}` becomes `{#myTag foo=foo /}`. +In other words, the argument value `foo` is resolved and can be accessed using `{foo}` in the tag template. + +NOTE: If an argument does not have a name and the value is a single word string literal , such as `"foo"`, then the name is defaulted and quotation marks are removed, e.g. `{#myTag "foo" /}` becomes `{#myTag foo="foo" /}`. + +`io.quarkus.qute.UserTagSectionHelper.Arguments` metadata are accessible in a tag using the `_args` alias. * `_args.size` - returns the actual number of arguments passed to a tag -* `_args.empty` - returns `true` if no arguments are passed -* `_args.get(String name)` - returns the argument value of the given name +* `_args.empty`/`_args.isEmpty` - returns `true` if no arguments are passed +* `_args.get(String name)` - returns the argument value of the given name or `null` * `_args.filter(String...)` - returns the arguments matching the given names +* `_args.filterIdenticalKeyValue` - returns the arguments with the name equal to the value; typically `foo` from `{#test foo="foo" bar=true}` or `{#test "foo" bar=true /}` * `_args.skip(String...)` - returns only the arguments that do not match the given names -* `_args.asHtmlAttributes` - renders the arguments as HTML attributes; e.g. `foo="true" bar="false"` (the arguments are sorted by name in alphabetical order) +* `_args.skipIdenticalKeyValue` - returns only the arguments with the name not equal to the value; typically `bar` from `{#test foo="foo" bar=true /}` +* `_args.skipIt` - returns all arguments except for the first unnamed argument; typically `bar` from `{#test foo bar=true /}` +* `_args.asHtmlAttributes` - renders the arguments as HTML attributes; e.g. `foo="true" readonly="readonly"`; the arguments are sorted by name in alphabetical order and the `'`, `"`, `<`, `>`, `&` characters are escaped -`_args` is also iterable: `{#each _args}{it.key}={it.value}{/each}`. +`_args` is also iterable of `java.util.Map.Entry`: `{#each _args}{it.key}={it.value}{/each}`. For example, we can call the user tag defined below with `{#test 'Martin' readonly=true /}`. @@ -1426,7 +1434,7 @@ template.data(foo).createUni().subscribe().with(System.out::println); `TemplateInstance.createMulti()` returns a new `Multi` object. Each item represents a part/chunk of the rendered template. Again, `createMulti()` does not trigger rendering. -Instead, every time a computation is triggered by a subscriber the template is rendered again. +Instead, every time a computation is triggered by a subscriber, the template is rendered again. .`TemplateInstance.createMulti()` Example [source,java] diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index bd78ec95b0ad1..8b23992d27078 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1643,7 +1643,8 @@ Assuming a `Record` looks like: ---- public class Record { - // the class must contain/inherit either and `id` field or an `@Id` annotated field + // The class must contain/inherit either and `id` field, an `@Id` or `@RestLinkId` annotated field. + // When resolving the id the order of preference is: `@RestLinkId` > `@Id` > `id` field. private int id; public Record() { diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index 658fa2007436e..edc525b969e50 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -302,6 +302,47 @@ class SecurityIdentitySupplier implements Supplier { } ---- +The CDI request context activation shown in the example above does not help you to access the `RoutingContext` when the proactive authentication is enabled. +The following example illustrates how you can access the `RoutingContext` from the `SecurityIdentityAugmentor`: + +[source,java] +---- +package org.acme.security; + +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + @Override + public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context, + Map attributes) { + RoutingContext routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes); + if (routingContext != null) { + // Augment SecurityIdentity using RoutingContext + } else { + return augment(identity, context); <1> + } + } + + ... +} +---- +<1> The `RoutingContext` is not be available when the `SecurityIdentity` is augmented after HTTP request has completed. + +NOTE: If you implemented a custom `HttpAuthenticationMechanism`, then you need to add the `RoutingContext` to the authentication +request attributes with the `io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.setRoutingContextAttribute` method call. +Otherwise, the `RoutingContext` will not be available during augmentation. + [[jaxrs-security-context]] == Custom Jakarta REST SecurityContext diff --git a/docs/src/main/asciidoc/security-keycloak-admin-client.adoc b/docs/src/main/asciidoc/security-keycloak-admin-client.adoc index cb774ef010718..a62d239d0a28c 100644 --- a/docs/src/main/asciidoc/security-keycloak-admin-client.adoc +++ b/docs/src/main/asciidoc/security-keycloak-admin-client.adoc @@ -195,6 +195,23 @@ quarkus.keycloak.admin-client.grant-type=CLIENT_CREDENTIALS <1> NOTE: Note that the xref:security-openid-connect-client.adoc[OidcClient] can also be used to acquire tokens. +== Testing + +The preferred approach for testing Keycloak Admin Client against Keycloak is xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak]. +`Dev Services for Keycloak` will start and initialize a test container. +Then, it will create a `quarkus` realm and a `quarkus-app` client (`secret` secret) and add `alice` (`admin` and `user` roles) and `bob` (`user` role) users, where all of these properties can be customized. + +For example, by default, a test container will be available at a randomly allocated port but you can make both Keycloak Admin Client and the container use the same port as follows: + +[source,properties] +---- +%test.quarkus.keycloak.devservices.port=${kc.admin.port.test:45180} <1> +%test.quarkus.keycloak.admin-client.server-url=http://localhost:${kc.admin.port.test:45180}/ <2> +---- + +<1> Configure the Keycloak container to listen on the `45180` port by default +<2> Configure the Keycloak Admin Client to use the same port + [[keycloak-admin-client-configuration-reference]] == Quarkus Keycloak Admin Client Configuration Reference diff --git a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc index 948257f55fede..5645acfd4a8c7 100644 --- a/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-dev-services.adoc @@ -258,7 +258,7 @@ For more information, see xref:security-oidc-bearer-token-authentication.adoc#in [[keycloak-initialization]] === Keycloak initialization -The `quay.io/keycloak/keycloak:23.0.3` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. +The `quay.io/keycloak/keycloak:23.0.4` image which contains a Keycloak distribution powered by Quarkus is used to start a container by default. `quarkus.keycloak.devservices.image-name` can be used to change the Keycloak image name. For example, set it to `quay.io/keycloak/keycloak:19.0.3-legacy` to use a Keycloak distribution powered by WildFly. Be aware that a Quarkus-based Keycloak distribution is only available starting from Keycloak `20.0.0`. diff --git a/docs/src/main/asciidoc/security-webauthn.adoc b/docs/src/main/asciidoc/security-webauthn.adoc index 878b2398069ff..e8eff73e60fa7 100644 --- a/docs/src/main/asciidoc/security-webauthn.adoc +++ b/docs/src/main/asciidoc/security-webauthn.adoc @@ -90,7 +90,7 @@ The solution is located in the `security-webauthn-quickstart` link:{quickstarts- First, we need a new project. Create a new project with the following command: :create-app-artifact-id: security-webauthn-quickstart -:create-app-extensions: security-webauthn,reactive-pg-client,resteasy-reactive,hibernate-reactive-panache +:create-app-extensions: security-webauthn,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache include::{includes}/devtools/create-app.adoc[] [NOTE] @@ -229,8 +229,7 @@ import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import io.quarkus.hibernate.reactive.panache.PanacheEntity; -import io.smallrye.mutiny.Uni; +import io.quarkus.hibernate.orm.panache.PanacheEntity; import io.vertx.ext.auth.webauthn.Authenticator; import io.vertx.ext.auth.webauthn.PublicKeyCredential; @@ -319,17 +318,13 @@ public class WebAuthnCredential extends PanacheEntity { user.webAuthnCredential = this; } - public static Uni> findByUserName(String userName) { + public static List findByUserName(String userName) { return list("userName", userName); } - public static Uni> findByCredID(String credID) { + public static List findByCredID(String credID) { return list("credID", credID); } - - public Uni fetch(T association) { - return getSession().flatMap(session -> session.fetch(association)); - } } ---- @@ -339,11 +334,10 @@ We also need a second entity for the credentials: ---- package org.acme.security.webauthn; +import io.quarkus.hibernate.orm.panache.PanacheEntity; import jakarta.persistence.Entity; import jakarta.persistence.ManyToOne; -import io.quarkus.hibernate.reactive.panache.PanacheEntity; - @Entity public class WebAuthnCertificate extends PanacheEntity { @@ -364,14 +358,12 @@ And last but not least, our user entity: ---- package org.acme.security.webauthn; +import io.quarkus.hibernate.orm.panache.PanacheEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; -import io.quarkus.hibernate.reactive.panache.PanacheEntity; -import io.smallrye.mutiny.Uni; - @Table(name = "user_table") @Entity public class User extends PanacheEntity { @@ -383,8 +375,8 @@ public class User extends PanacheEntity { @OneToOne(mappedBy = "user") public WebAuthnCredential webAuthnCredential; - public static Uni findByUserName(String userName) { - return find("userName", userName).firstResult(); + public static User findByUserName(String userName) { + return User.find("userName", userName).firstResult(); } } ---- @@ -412,98 +404,83 @@ WebAuthn security model: ---- package org.acme.security.webauthn; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import io.smallrye.common.annotation.Blocking; import jakarta.enterprise.context.ApplicationScoped; -import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional; import io.quarkus.security.webauthn.WebAuthnUserProvider; import io.smallrye.mutiny.Uni; import io.vertx.ext.auth.webauthn.AttestationCertificates; import io.vertx.ext.auth.webauthn.Authenticator; +import jakarta.transaction.Transactional; + +import static org.acme.security.webauthn.WebAuthnCredential.findByCredID; +import static org.acme.security.webauthn.WebAuthnCredential.findByUserName; +@Blocking @ApplicationScoped public class MyWebAuthnSetup implements WebAuthnUserProvider { - @ReactiveTransactional + @Transactional @Override public Uni> findWebAuthnCredentialsByUserName(String userName) { - return WebAuthnCredential.findByUserName(userName) - .flatMap(MyWebAuthnSetup::toAuthenticators); + return Uni.createFrom().item(toAuthenticators(findByUserName(userName))); } - @ReactiveTransactional + @Transactional @Override public Uni> findWebAuthnCredentialsByCredID(String credID) { - return WebAuthnCredential.findByCredID(credID) - .flatMap(MyWebAuthnSetup::toAuthenticators); + return Uni.createFrom().item(toAuthenticators(findByCredID(credID))); } - @ReactiveTransactional + @Transactional @Override public Uni updateOrStoreWebAuthnCredentials(Authenticator authenticator) { - return User.findByUserName(authenticator.getUserName()) - .flatMap(user -> { + // leave the scooby user to the manual endpoint, because if we do it here it will be created/updated twice + if(!authenticator.getUserName().equals("scooby")) { + User user = User.findByUserName(authenticator.getUserName()); + if(user == null) { // new user - if(user == null) { - User newUser = new User(); - newUser.userName = authenticator.getUserName(); - WebAuthnCredential credential = new WebAuthnCredential(authenticator, newUser); - return credential.persist() - .flatMap(c -> newUser.persist()) - .onItem().ignore().andContinueWithNull(); - } else { - // existing user - user.webAuthnCredential.counter = authenticator.getCounter(); - return Uni.createFrom().nullItem(); - } - }); + User newUser = new User(); + newUser.userName = authenticator.getUserName(); + WebAuthnCredential credential = new WebAuthnCredential(authenticator, newUser); + credential.persist(); + newUser.persist(); + } else { + // existing user + user.webAuthnCredential.counter = authenticator.getCounter(); + } + } + return Uni.createFrom().nullItem(); } - private static Uni> toAuthenticators(List dbs) { - // can't call combine/uni on empty list - if(dbs.isEmpty()) - return Uni.createFrom().item(Collections.emptyList()); - List> ret = new ArrayList<>(dbs.size()); - for (WebAuthnCredential db : dbs) { - ret.add(toAuthenticator(db)); - } - return Uni.combine().all().unis(ret).combinedWith(f -> (List)f); + private static List toAuthenticators(List dbs) { + return dbs.stream().map(MyWebAuthnSetup::toAuthenticator).collect(Collectors.toList()); } - private static Uni toAuthenticator(WebAuthnCredential credential) { - return credential.fetch(credential.x5c) - .map(x5c -> { - Authenticator ret = new Authenticator(); - ret.setAaguid(credential.aaguid); - AttestationCertificates attestationCertificates = new AttestationCertificates(); - attestationCertificates.setAlg(credential.alg); - List x5cs = new ArrayList<>(x5c.size()); - for (WebAuthnCertificate webAuthnCertificate : x5c) { - x5cs.add(webAuthnCertificate.x5c); - } - ret.setAttestationCertificates(attestationCertificates); - ret.setCounter(credential.counter); - ret.setCredID(credential.credID); - ret.setFmt(credential.fmt); - ret.setPublicKey(credential.publicKey); - ret.setType(credential.type); - ret.setUserName(credential.userName); - return ret; - }); + private static Authenticator toAuthenticator(WebAuthnCredential credential) { + Authenticator ret = new Authenticator(); + ret.setAaguid(credential.aaguid); + AttestationCertificates attestationCertificates = new AttestationCertificates(); + attestationCertificates.setAlg(credential.alg); + ret.setAttestationCertificates(attestationCertificates); + ret.setCounter(credential.counter); + ret.setCredID(credential.credID); + ret.setFmt(credential.fmt); + ret.setPublicKey(credential.publicKey); + ret.setType(credential.type); + ret.setUserName(credential.userName); + return ret; } @Override public Set getRoles(String userId) { if(userId.equals("admin")) { - Set ret = new HashSet<>(); - ret.add("user"); - ret.add("admin"); - return ret; + return Set.of("user", "admin"); } return Collections.singleton("user"); } @@ -934,23 +911,19 @@ and `WebAuthnSecurity.register` methods. For example, here's how you can handle ---- package org.acme.security.webauthn; -import java.net.URI; - import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import org.jboss.resteasy.reactive.RestForm; -import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional; import io.quarkus.security.webauthn.WebAuthnLoginResponse; import io.quarkus.security.webauthn.WebAuthnRegisterResponse; import io.quarkus.security.webauthn.WebAuthnSecurity; -import io.smallrye.mutiny.Uni; import io.vertx.ext.auth.webauthn.Authenticator; import io.vertx.ext.web.RoutingContext; @@ -962,85 +935,65 @@ public class LoginResource { @Path("/login") @POST - @ReactiveTransactional - public Uni login(@RestForm String userName, - @BeanParam WebAuthnLoginResponse webAuthnResponse, - RoutingContext ctx) { + @Transactional + public Response login(@RestForm String userName, + @BeanParam WebAuthnLoginResponse webAuthnResponse, + RoutingContext ctx) { // Input validation - if(userName == null || userName.isEmpty() - || !webAuthnResponse.isSet() - || !webAuthnResponse.isValid()) { - return Uni.createFrom().item(Response.status(Status.BAD_REQUEST).build()); + if(userName == null || userName.isEmpty() || !webAuthnResponse.isSet() || !webAuthnResponse.isValid()) { + return Response.status(Status.BAD_REQUEST).build(); } - Uni userUni = User.findByUserName(userName); - return userUni.flatMap(user -> { - if(user == null) { - // Invalid user - return Uni.createFrom().item(Response.status(Status.BAD_REQUEST).build()); - } - Uni authenticator = this.webAuthnSecurity.login(webAuthnResponse, ctx); - - return authenticator - // bump the auth counter - .invoke(auth -> user.webAuthnCredential.counter = auth.getCounter()) - .map(auth -> { - // make a login JWT cookie - NewCookie cookie = null; - return Response.seeOther(URI.create("/")).cookie(cookie).build(); - }) - // handle login failure - .onFailure().recoverWithItem(x -> { - // make a proper error response - return Response.status(Status.BAD_REQUEST).build(); - }); - - }); + User user = User.findByUserName(userName); + if(user == null) { + // Invalid user + return Response.status(Status.BAD_REQUEST).build(); + } + try { + Authenticator authenticator = this.webAuthnSecurity.login(webAuthnResponse, ctx).await().indefinitely(); + // bump the auth counter + user.webAuthnCredential.counter = authenticator.getCounter(); + // make a login cookie + this.webAuthnSecurity.rememberUser(authenticator.getUserName(), ctx); + return Response.ok().build(); + } catch (Exception exception) { + // handle login failure - make a proper error response + return Response.status(Status.BAD_REQUEST).build(); + } } @Path("/register") @POST - @ReactiveTransactional - public Uni register(@RestForm String userName, + @Transactional + public Response register(@RestForm String userName, @BeanParam WebAuthnRegisterResponse webAuthnResponse, RoutingContext ctx) { // Input validation - if(userName == null || userName.isEmpty() - || !webAuthnResponse.isSet() - || !webAuthnResponse.isValid()) { - return Uni.createFrom().item(Response.status(Status.BAD_REQUEST).build()); + if(userName == null || userName.isEmpty() || !webAuthnResponse.isSet() || !webAuthnResponse.isValid()) { + return Response.status(Status.BAD_REQUEST).build(); } - Uni userUni = User.findByUserName(userName); - return userUni.flatMap(user -> { - if(user != null) { - // Duplicate user - return Uni.createFrom().item(Response.status(Status.BAD_REQUEST).build()); - } - Uni authenticator = this.webAuthnSecurity.register(webAuthnResponse, ctx); - - return authenticator - // store the user - .flatMap(auth -> { - User newUser = new User(); - newUser.userName = auth.getUserName(); - WebAuthnCredential credential = new WebAuthnCredential(auth, newUser); - return credential.persist() - .flatMap(c -> newUser.persist()); - - }) - .map(newUser -> { - // make a login JWT cookie - NewCookie cookie = null; - return Response.seeOther(URI.create("/")).cookie(cookie).build(); - }) - // handle login failure - .onFailure().recoverWithItem(x -> { - // make a proper error response - return Response.status(Status.BAD_REQUEST).build(); - }); - - }); + User user = User.findByUserName(userName); + if(user != null) { + // Duplicate user + return Response.status(Status.BAD_REQUEST).build(); + } + try { + // store the user + Authenticator authenticator = this.webAuthnSecurity.register(webAuthnResponse, ctx).await().indefinitely(); + User newUser = new User(); + newUser.userName = authenticator.getUserName(); + WebAuthnCredential credential = new WebAuthnCredential(authenticator, newUser); + credential.persist(); + newUser.persist(); + // make a login cookie + this.webAuthnSecurity.rememberUser(newUser.userName, ctx); + return Response.ok().build(); + } catch (Exception ignored) { + // handle login failure + // make a proper error response + return Response.status(Status.BAD_REQUEST).build(); + } } } ---- @@ -1070,13 +1023,14 @@ Testing WebAuthn can be complicated because normally you need a hardware token, io.quarkus quarkus-test-security-webauthn + test ---- [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -implementation("io.quarkus:quarkus-test-security-webauthn") +testImplementation("io.quarkus:quarkus-test-security-webauthn") ---- With this, you can use `WebAuthnHardware` to emulate an authenticator token, as well as the diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java index b52cb438b06dc..740b255164904 100644 --- a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java +++ b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java @@ -24,10 +24,10 @@ import io.agroal.api.AgroalDataSource; import io.agroal.api.AgroalPoolInterceptor; import io.quarkus.agroal.DataSource; +import io.quarkus.agroal.runtime.AgroalDataSourceSupport; import io.quarkus.agroal.runtime.AgroalDataSourcesInitializer; import io.quarkus.agroal.runtime.AgroalRecorder; import io.quarkus.agroal.runtime.DataSourceJdbcBuildTimeConfig; -import io.quarkus.agroal.runtime.DataSourceSupport; import io.quarkus.agroal.runtime.DataSources; import io.quarkus.agroal.runtime.DataSourcesJdbcBuildTimeConfig; import io.quarkus.agroal.runtime.JdbcDriver; @@ -202,20 +202,20 @@ private static void validateBuildTimeConfig(AggregatedDataSourceBuildTimeConfigB } } - private DataSourceSupport getDataSourceSupport( + private AgroalDataSourceSupport getDataSourceSupport( List aggregatedBuildTimeConfigBuildItems, SslNativeConfigBuildItem sslNativeConfig, Capabilities capabilities) { - Map dataSourceSupportEntries = new HashMap<>(); + Map dataSourceSupportEntries = new HashMap<>(); for (AggregatedDataSourceBuildTimeConfigBuildItem aggregatedDataSourceBuildTimeConfig : aggregatedBuildTimeConfigBuildItems) { String dataSourceName = aggregatedDataSourceBuildTimeConfig.getName(); dataSourceSupportEntries.put(dataSourceName, - new DataSourceSupport.Entry(dataSourceName, aggregatedDataSourceBuildTimeConfig.getDbKind(), + new AgroalDataSourceSupport.Entry(dataSourceName, aggregatedDataSourceBuildTimeConfig.getDbKind(), aggregatedDataSourceBuildTimeConfig.getDataSourceConfig().dbVersion(), aggregatedDataSourceBuildTimeConfig.getResolvedDriverClass(), aggregatedDataSourceBuildTimeConfig.isDefault())); } - return new DataSourceSupport(sslNativeConfig.isExplicitlyDisabled(), + return new AgroalDataSourceSupport(sslNativeConfig.isExplicitlyDisabled(), capabilities.isPresent(Capability.METRICS), dataSourceSupportEntries); } @@ -247,10 +247,11 @@ void generateDataSourceSupportBean(AgroalRecorder recorder, unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(AgroalPoolInterceptor.class)); // create the DataSourceSupport bean that DataSourceProducer uses as a dependency - DataSourceSupport dataSourceSupport = getDataSourceSupport(aggregatedBuildTimeConfigBuildItems, sslNativeConfig, + AgroalDataSourceSupport agroalDataSourceSupport = getDataSourceSupport(aggregatedBuildTimeConfigBuildItems, + sslNativeConfig, capabilities); - syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(DataSourceSupport.class) - .supplier(recorder.dataSourceSupportSupplier(dataSourceSupport)) + syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem.configure(AgroalDataSourceSupport.class) + .supplier(recorder.dataSourceSupportSupplier(agroalDataSourceSupport)) .unremovable() .done()); } diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/AgroalMetricsConfigActiveFalseTest.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/AgroalMetricsConfigActiveFalseTest.java new file mode 100644 index 0000000000000..b680914997d6a --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/AgroalMetricsConfigActiveFalseTest.java @@ -0,0 +1,51 @@ +package io.quarkus.agroal.test; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.Gauge; +import org.eclipse.microprofile.metrics.MetricID; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.Tag; +import org.eclipse.microprofile.metrics.annotation.RegistryType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class AgroalMetricsConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-metrics-enabled.properties") + .overrideConfigKey("quarkus.datasource.active", "false") + .overrideConfigKey("quarkus.datasource.ds1.active", "false"); + + @Inject + @RegistryType(type = MetricRegistry.Type.VENDOR) + MetricRegistry registry; + + @Test + public void testMetricsOfDefaultDS() { + Counter acquireCount = registry.getCounters() + .get(new MetricID("agroal.acquire.count", new Tag("datasource", "default"))); + Gauge maxUsed = registry.getGauges() + .get(new MetricID("agroal.max.used.count", new Tag("datasource", "default"))); + + Assertions.assertNull(acquireCount, "Agroal metrics should not be registered for deactivated datasources eagerly"); + Assertions.assertNull(maxUsed, "Agroal metrics should not be registered for deactivated datasources eagerly"); + } + + @Test + public void testMetricsOfDs1() { + Counter acquireCount = registry.getCounters().get(new MetricID("agroal.acquire.count", + new Tag("datasource", "ds1"))); + Gauge maxUsed = registry.getGauges().get(new MetricID("agroal.max.used.count", + new Tag("datasource", "ds1"))); + + Assertions.assertNull(acquireCount, "Agroal metrics should not be registered for deactivated datasources eagerly"); + Assertions.assertNull(maxUsed, "Agroal metrics should not be registered for deactivated datasources eagerly"); + } + +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..4e71a02d8503e --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,91 @@ +package io.quarkus.agroal.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.CreationException; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void dataSource() { + DataSource ds = Arc.container().instance(DataSource.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(ds).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> ds.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void agroalDataSource() { + AgroalDataSource ds = Arc.container().instance(AgroalDataSource.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(ds).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> ds.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.useDatasource()) + .isInstanceOf(CreationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + DataSource ds; + + public void useDatasource() throws SQLException { + ds.getConnection(); + } + } +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseNamedDatasourceTest.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..812aeadd02514 --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,96 @@ +package io.quarkus.agroal.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.CreationException; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.users.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2"); + + @Inject + MyBean myBean; + + @Test + public void dataSource() { + DataSource ds = Arc.container().instance(DataSource.class, + new io.quarkus.agroal.DataSource.DataSourceLiteral("users")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(ds).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> ds.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void agroalDataSource() { + DataSource ds = Arc.container().instance(DataSource.class, + new io.quarkus.agroal.DataSource.DataSourceLiteral("users")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(ds).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> ds.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.useDatasource()) + .isInstanceOf(CreationException.class) + .hasMessageContainingAll("Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @io.quarkus.agroal.DataSource("users") + DataSource ds; + + public void useDatasource() throws SQLException { + ds.getConnection(); + } + } +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..a6a4fe8f5c03e --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.agroal.test; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.brokenDS.jdbc.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS1Test.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS1Test.java new file mode 100644 index 0000000000000..208241cef1d8c --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS1Test.java @@ -0,0 +1,98 @@ +package io.quarkus.agroal.test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.agroal.DataSource; +import io.quarkus.datasource.runtime.DataSourceSupport; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests a use case where multiple datasources are defined at build time, + * but only one is used at runtime. + *

    + * This is mostly useful when each datasource has a distinct db-kind, but in theory that shouldn't matter, + * so we use the h2 db-kind everywhere here to keep test dependencies simpler. + *

    + * See {@link MultipleDataSourcesAsAlternativesWithActiveDS1Test} for the counterpart where PU2 is used at runtime. + */ +public class MultipleDataSourcesAsAlternativesWithActiveDS1Test { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyProducer.class)) + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + .overrideConfigKey("quarkus.datasource.ds-2.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-2.active", "false") + // This is where we select datasource 1 + .overrideRuntimeConfigKey("quarkus.datasource.ds-1.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-1.jdbc.url", "jdbc:h2:mem:testds1"); + + @Inject + @DataSource("ds-1") + AgroalDataSource explicitDatasourceBean; + + @Inject + AgroalDataSource customIndirectDatasourceBean; + + @Inject + @DataSource("ds-2") + AgroalDataSource inactiveDatasourceBean; + + @Test + public void testExplicitDatasourceBeanUsable() { + doTestDatasource(explicitDatasourceBean); + } + + @Test + public void testCustomIndirectDatasourceBeanUsable() { + doTestDatasource(customIndirectDatasourceBean); + } + + @Test + public void testInactiveDatasourceBeanUnusable() { + assertThatThrownBy(() -> inactiveDatasourceBean.getConnection()) + .hasMessageContaining("Datasource 'ds-2' was deactivated through configuration properties."); + } + + private static void doTestDatasource(AgroalDataSource dataSource) { + assertThatCode(() -> { + try (var connection = dataSource.getConnection()) { + } + }) + .doesNotThrowAnyException(); + } + + private static class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @DataSource("ds-1") + AgroalDataSource dataSource1Bean; + + @Inject + @DataSource("ds-2") + AgroalDataSource dataSource2Bean; + + @Produces + @ApplicationScoped + public AgroalDataSource dataSource() { + if (dataSourceSupport.getInactiveNames().contains("ds-1")) { + return dataSource2Bean; + } else { + return dataSource1Bean; + } + } + } +} diff --git a/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS2Test.java b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS2Test.java new file mode 100644 index 0000000000000..7159381c15143 --- /dev/null +++ b/extensions/agroal/deployment/src/test/java/io/quarkus/agroal/test/MultipleDataSourcesAsAlternativesWithActiveDS2Test.java @@ -0,0 +1,98 @@ +package io.quarkus.agroal.test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.agroal.DataSource; +import io.quarkus.datasource.runtime.DataSourceSupport; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests a use case where multiple datasources are defined at build time, + * but only one is used at runtime. + *

    + * This is mostly useful when each datasource has a distinct db-kind, but in theory that shouldn't matter, + * so we use the h2 db-kind everywhere here to keep test dependencies simpler. + *

    + * See {@link MultipleDataSourcesAsAlternativesWithActiveDS1Test} for the counterpart where PU2 is used at runtime. + */ +public class MultipleDataSourcesAsAlternativesWithActiveDS2Test { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyProducer.class)) + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + .overrideConfigKey("quarkus.datasource.ds-2.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-2.active", "false") + // This is where we select datasource 2 + .overrideRuntimeConfigKey("quarkus.datasource.ds-2.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-2.jdbc.url", "jdbc:h2:mem:testds2"); + + @Inject + @DataSource("ds-2") + AgroalDataSource explicitDatasourceBean; + + @Inject + AgroalDataSource customIndirectDatasourceBean; + + @Inject + @DataSource("ds-1") + AgroalDataSource inactiveDatasourceBean; + + @Test + public void testExplicitDatasourceBeanUsable() { + doTestDatasource(explicitDatasourceBean); + } + + @Test + public void testCustomIndirectDatasourceBeanUsable() { + doTestDatasource(customIndirectDatasourceBean); + } + + @Test + public void testInactiveDatasourceBeanUnusable() { + assertThatThrownBy(() -> inactiveDatasourceBean.getConnection()) + .hasMessageContaining("Datasource 'ds-1' was deactivated through configuration properties."); + } + + private static void doTestDatasource(AgroalDataSource dataSource) { + assertThatCode(() -> { + try (var connection = dataSource.getConnection()) { + } + }) + .doesNotThrowAnyException(); + } + + private static class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @DataSource("ds-1") + AgroalDataSource dataSource1Bean; + + @Inject + @DataSource("ds-2") + AgroalDataSource dataSource2Bean; + + @Produces + @ApplicationScoped + public AgroalDataSource dataSource() { + if (dataSourceSupport.getInactiveNames().contains("ds-1")) { + return dataSource2Bean; + } else { + return dataSource1Bean; + } + } + } +} diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceSupport.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalDataSourceSupport.java similarity index 87% rename from extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceSupport.java rename to extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalDataSourceSupport.java index 886085b97f797..fdf6f9c77e793 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSourceSupport.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalDataSourceSupport.java @@ -3,13 +3,13 @@ import java.util.Map; import java.util.Optional; -public class DataSourceSupport { +public class AgroalDataSourceSupport { public final boolean disableSslSupport; public final boolean mpMetricsPresent; public final Map entries; - public DataSourceSupport(boolean disableSslSupport, boolean mpMetricsPresent, Map entries) { + public AgroalDataSourceSupport(boolean disableSslSupport, boolean mpMetricsPresent, Map entries) { this.disableSslSupport = disableSslSupport; this.mpMetricsPresent = mpMetricsPresent; this.entries = entries; diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java index a69ce8f788d32..52b7d1a20907d 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalRecorder.java @@ -11,11 +11,11 @@ @Recorder public class AgroalRecorder { - public Supplier dataSourceSupportSupplier(DataSourceSupport dataSourceSupport) { - return new Supplier() { + public Supplier dataSourceSupportSupplier(AgroalDataSourceSupport agroalDataSourceSupport) { + return new Supplier() { @Override - public DataSourceSupport get() { - return dataSourceSupport; + public AgroalDataSourceSupport get() { + return agroalDataSourceSupport; } }; } diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java index 0e6e827b06a42..3fc83fad2d675 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/DataSources.java @@ -8,6 +8,7 @@ import java.util.Iterator; import java.util.Map; import java.util.ServiceLoader; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; @@ -45,6 +46,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.narayana.jta.runtime.TransactionManagerConfiguration; @@ -76,6 +78,7 @@ public class DataSources { private final XAResourceRecoveryRegistry xaResourceRecoveryRegistry; private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; private final DataSourceSupport dataSourceSupport; + private final AgroalDataSourceSupport agroalDataSourceSupport; private final Instance agroalPoolInterceptors; private final Instance agroalOpenTelemetryWrapper; @@ -89,6 +92,7 @@ public DataSources(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, XAResourceRecoveryRegistry xaResourceRecoveryRegistry, TransactionSynchronizationRegistry transactionSynchronizationRegistry, DataSourceSupport dataSourceSupport, + AgroalDataSourceSupport agroalDataSourceSupport, @Any Instance agroalPoolInterceptors, Instance agroalOpenTelemetryWrapper) { this.dataSourcesBuildTimeConfig = dataSourcesBuildTimeConfig; @@ -100,6 +104,7 @@ public DataSources(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, this.xaResourceRecoveryRegistry = xaResourceRecoveryRegistry; this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; this.dataSourceSupport = dataSourceSupport; + this.agroalDataSourceSupport = agroalDataSourceSupport; this.agroalPoolInterceptors = agroalPoolInterceptors; this.agroalOpenTelemetryWrapper = agroalOpenTelemetryWrapper; } @@ -127,25 +132,36 @@ public boolean isDataSourceCreated(String dataSourceName) { return dataSources.containsKey(dataSourceName); } + public Set getActiveDataSourceNames() { + // Datasources are created on startup, + // and we only create active datasources. + return dataSources.keySet(); + } + public AgroalDataSource getDataSource(String dataSourceName) { return dataSources.computeIfAbsent(dataSourceName, new Function() { @Override public AgroalDataSource apply(String s) { - return doCreateDataSource(s); + return doCreateDataSource(s, true); } }); } @PostConstruct public void start() { - for (String dataSourceName : dataSourceSupport.entries.keySet()) { - getDataSource(dataSourceName); + for (String dataSourceName : agroalDataSourceSupport.entries.keySet()) { + dataSources.computeIfAbsent(dataSourceName, new Function() { + @Override + public AgroalDataSource apply(String s) { + return doCreateDataSource(s, false); + } + }); } } @SuppressWarnings("resource") - public AgroalDataSource doCreateDataSource(String dataSourceName) { - if (!dataSourceSupport.entries.containsKey(dataSourceName)) { + public AgroalDataSource doCreateDataSource(String dataSourceName, boolean failIfInactive) { + if (!agroalDataSourceSupport.entries.containsKey(dataSourceName)) { throw new IllegalArgumentException("No datasource named '" + dataSourceName + "' exists"); } @@ -153,10 +169,18 @@ public AgroalDataSource doCreateDataSource(String dataSourceName) { .dataSources().get(dataSourceName).jdbc(); DataSourceRuntimeConfig dataSourceRuntimeConfig = dataSourcesRuntimeConfig.dataSources().get(dataSourceName); + if (dataSourceSupport.getInactiveNames().contains(dataSourceName)) { + if (failIfInactive) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } else { + // This only happens on startup, and effectively cancels the creation + // so that we only throw an exception on first actual use. + return null; + } + } + DataSourceJdbcRuntimeConfig dataSourceJdbcRuntimeConfig = dataSourcesJdbcRuntimeConfig .getDataSourceJdbcRuntimeConfig(dataSourceName); - - DataSourceSupport.Entry matchingSupportEntry = dataSourceSupport.entries.get(dataSourceName); if (!dataSourceJdbcRuntimeConfig.url().isPresent()) { //this is not an error situation, because we want to allow the situation where a JDBC extension //is installed but has not been configured @@ -167,6 +191,7 @@ public AgroalDataSource doCreateDataSource(String dataSourceName) { // we first make sure that all available JDBC drivers are loaded in the current TCCL loadDriversInTCCL(); + AgroalDataSourceSupport.Entry matchingSupportEntry = agroalDataSourceSupport.entries.get(dataSourceName); String resolvedDriverClass = matchingSupportEntry.resolvedDriverClass; Class driver; try { @@ -233,13 +258,13 @@ public AgroalDataSource doCreateDataSource(String dataSourceName) { AgroalConnectionFactoryConfigurationSupplier connectionFactoryConfiguration = poolConfiguration .connectionFactoryConfiguration(); - boolean mpMetricsPresent = dataSourceSupport.mpMetricsPresent; + boolean mpMetricsPresent = agroalDataSourceSupport.mpMetricsPresent; applyNewConfiguration(dataSourceName, dataSourceConfiguration, poolConfiguration, connectionFactoryConfiguration, driver, jdbcUrl, dataSourceJdbcBuildTimeConfig, dataSourceRuntimeConfig, dataSourceJdbcRuntimeConfig, transactionRuntimeConfig, mpMetricsPresent); - if (dataSourceSupport.disableSslSupport) { + if (agroalDataSourceSupport.disableSslSupport) { agroalConnectionConfigurer.disableSslSupport(resolvedDbKind, dataSourceConfiguration); } //we use a custom cache for two reasons: diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java index 36f7fd7d7a2fd..7a6d03fee425a 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/health/DataSourceHealthCheck.java @@ -9,6 +9,8 @@ import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; @@ -16,28 +18,37 @@ import org.eclipse.microprofile.health.Readiness; import io.agroal.api.AgroalDataSource; -import io.quarkus.agroal.DataSource.DataSourceLiteral; +import io.quarkus.agroal.runtime.DataSources; import io.quarkus.arc.Arc; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; @Readiness @ApplicationScoped public class DataSourceHealthCheck implements HealthCheck { - private final Map dataSources = new HashMap<>(); + + @Inject + Instance dataSources; + + private final Map checkedDataSources = new HashMap<>(); @PostConstruct protected void init() { - DataSourcesHealthSupport support = Arc.container().instance(DataSourcesHealthSupport.class) + if (!dataSources.isResolvable()) { + // No configured Agroal datasource at build time. + return; + } + DataSourceSupport support = Arc.container().instance(DataSourceSupport.class) .get(); Set names = support.getConfiguredNames(); - Set excludedNames = support.getExcludedNames(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (String name : names) { - DataSource ds = DataSourceUtil.isDefault(name) - ? (DataSource) Arc.container().instance(DataSource.class).get() - : (DataSource) Arc.container().instance(DataSource.class, new DataSourceLiteral(name)).get(); - if (!excludedNames.contains(name) && ds != null) { - dataSources.put(name, ds); + if (excludedNames.contains(name)) { + continue; + } + DataSource ds = dataSources.get().getDataSource(name); + if (ds != null) { + checkedDataSources.put(name, ds); } } } @@ -45,7 +56,7 @@ protected void init() { @Override public HealthCheckResponse call() { HealthCheckResponseBuilder builder = HealthCheckResponse.named("Database connections health check").up(); - for (Map.Entry dataSource : dataSources.entrySet()) { + for (Map.Entry dataSource : checkedDataSources.entrySet()) { boolean isDefault = DataSourceUtil.isDefault(dataSource.getKey()); AgroalDataSource ads = (AgroalDataSource) dataSource.getValue(); String dsName = dataSource.getKey(); diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/metrics/AgroalMetricsRecorder.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/metrics/AgroalMetricsRecorder.java index 081ef0c0b6d95..726a0c0512e5a 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/metrics/AgroalMetricsRecorder.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/metrics/AgroalMetricsRecorder.java @@ -5,9 +5,10 @@ import java.util.function.Function; import java.util.function.Supplier; -import io.agroal.api.AgroalDataSource; +import org.jboss.logging.Logger; + import io.agroal.api.AgroalDataSourceMetrics; -import io.quarkus.agroal.DataSource; +import io.quarkus.agroal.runtime.DataSources; import io.quarkus.arc.Arc; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.runtime.annotations.Recorder; @@ -19,6 +20,8 @@ */ @Recorder public class AgroalMetricsRecorder { + private static final Logger log = Logger.getLogger(AgroalMetricsRecorder.class); + static Function, Long> convertToMillis = new Function, Long>() { @Override public Long apply(Supplier durationSupplier) { @@ -31,8 +34,15 @@ public Consumer registerDataSourceMetrics(String dataSourceName) return new Consumer() { @Override public void accept(MetricsFactory metricsFactory) { + DataSources dataSources = Arc.container().instance(DataSources.class).get(); + if (!dataSources.getActiveDataSourceNames().contains(dataSourceName)) { + log.debug("Not registering metrics for datasource '" + dataSourceName + "'" + + " as the datasource has been deactivated in the configuration"); + return; + } + String tagValue = DataSourceUtil.isDefault(dataSourceName) ? "default" : dataSourceName; - AgroalDataSourceMetrics metrics = getDataSource(dataSourceName).getMetrics(); + AgroalDataSourceMetrics metrics = dataSources.getDataSource(dataSourceName).getMetrics(); metricsFactory.builder("agroal.active.count") .description( @@ -114,14 +124,4 @@ public void accept(MetricsFactory metricsFactory) { } }; } - - private AgroalDataSource getDataSource(String dataSourceName) { - if (dataSourceName == null || DataSourceUtil.isDefault(dataSourceName)) { - return Arc.container().instance(AgroalDataSource.class).get(); - } else { - return Arc.container() - .instance(AgroalDataSource.class, new DataSource.DataSourceLiteral(dataSourceName)) - .get(); - } - } } diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 226efb4f0edc6..843f5a0480238 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -365,6 +365,15 @@ private void doWriteOutputFile(OutputTargetBuildItem outputTarget, Path configPa } } + private JibContainerBuilder toJibContainerBuilder(String baseImage, ContainerImageJibConfig jibConfig) + throws InvalidImageReferenceException { + if (baseImage.startsWith(Jib.TAR_IMAGE_PREFIX) || baseImage.startsWith(Jib.DOCKER_DAEMON_IMAGE_PREFIX)) { + return Jib.from(baseImage); + } + return Jib.from(toRegistryImage(ImageReference.parse(baseImage), jibConfig.baseRegistryUsername, + jibConfig.baseRegistryPassword)); + } + private RegistryImage toRegistryImage(ImageReference imageReference, Optional username, Optional password) { CredentialRetrieverFactory credentialRetrieverFactory = CredentialRetrieverFactory.forImage(imageReference, log::info); @@ -519,9 +528,7 @@ private JibContainerBuilder createContainerBuilderFromFastJar(String baseJvmImag Instant now = Instant.now(); Instant modificationTime = jibConfig.useCurrentTimestampFileModification ? now : Instant.EPOCH; - JibContainerBuilder jibContainerBuilder = Jib - .from(toRegistryImage(ImageReference.parse(baseJvmImage), jibConfig.baseRegistryUsername, - jibConfig.baseRegistryPassword)); + JibContainerBuilder jibContainerBuilder = toJibContainerBuilder(baseJvmImage, jibConfig); if (fastChangingLibPaths.isEmpty()) { // just create a layer with the entire lib structure intact addLayer(jibContainerBuilder, Collections.singletonList(componentsPath.resolve(JarResultBuildStep.LIB)), @@ -718,9 +725,17 @@ private JibContainerBuilder createContainerBuilderFromLegacyJar(String baseJvmIm // not ideal since this has been previously zipped - we would like to just reuse it Path classesDir = outputTargetBuildItem.getOutputDirectory().resolve("jib"); ZipUtils.unzip(sourceJarBuildItem.getPath(), classesDir); - JavaContainerBuilder javaContainerBuilder = JavaContainerBuilder - .from(toRegistryImage(ImageReference.parse(baseJvmImage), jibConfig.baseRegistryUsername, - jibConfig.baseRegistryPassword)) + + JavaContainerBuilder javaContainerBuilder; + if (baseJvmImage.startsWith(Jib.TAR_IMAGE_PREFIX) || baseJvmImage.startsWith(Jib.DOCKER_DAEMON_IMAGE_PREFIX)) { + javaContainerBuilder = JavaContainerBuilder.from(baseJvmImage); + } else { + javaContainerBuilder = JavaContainerBuilder + .from(toRegistryImage(ImageReference.parse(baseJvmImage), jibConfig.baseRegistryUsername, + jibConfig.baseRegistryPassword)); + } + + javaContainerBuilder = javaContainerBuilder .addResources(classesDir, IS_CLASS_PREDICATE.negate()) .addClasses(classesDir, IS_CLASS_PREDICATE); @@ -778,9 +793,7 @@ private JibContainerBuilder createContainerBuilderFromNative(ContainerImageJibCo } try { AbsoluteUnixPath workDirInContainer = AbsoluteUnixPath.get("/work"); - JibContainerBuilder jibContainerBuilder = Jib - .from(toRegistryImage(ImageReference.parse(jibConfig.baseNativeImage), jibConfig.baseRegistryUsername, - jibConfig.baseRegistryPassword)) + JibContainerBuilder jibContainerBuilder = toJibContainerBuilder(jibConfig.baseNativeImage, jibConfig) .addFileEntriesLayer(FileEntriesLayer.builder() .addEntry(nativeImageBuildItem.getPath(), workDirInContainer.resolve(BINARY_NAME_IN_CONTAINER), FilePermissions.fromOctalString("775")) diff --git a/extensions/datasource/common/src/main/java/io/quarkus/datasource/common/runtime/DataSourceUtil.java b/extensions/datasource/common/src/main/java/io/quarkus/datasource/common/runtime/DataSourceUtil.java index 7b11b9e4aab7a..8158ad25b7e3c 100644 --- a/extensions/datasource/common/src/main/java/io/quarkus/datasource/common/runtime/DataSourceUtil.java +++ b/extensions/datasource/common/src/main/java/io/quarkus/datasource/common/runtime/DataSourceUtil.java @@ -50,6 +50,16 @@ public static ConfigurationException dataSourceNotConfigured(String dataSourceNa dataSourcePropertyKey(dataSourceName, "jdbc.url"))); } + public static ConfigurationException dataSourceInactive(String dataSourceName) { + return new ConfigurationException(String.format(Locale.ROOT, + "Datasource '%s' was deactivated through configuration properties." + + " To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...)." + + " Alternatively, activate the datasource by setting configuration property '%s' to 'true' and configure datasource '%s'." + + " Refer to https://quarkus.io/guides/datasource for guidance.", + dataSourceName, dataSourcePropertyKey(dataSourceName, "active"), dataSourceName), + Set.of(dataSourcePropertyKey(dataSourceName, "active"))); + } + private DataSourceUtil() { } diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/DataSourcesExcludedFromHealthChecksProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/DataSourcesExcludedFromHealthChecksProcessor.java index 80b43e8033719..22f45aefa2ed9 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/DataSourcesExcludedFromHealthChecksProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/DataSourcesExcludedFromHealthChecksProcessor.java @@ -1,15 +1,15 @@ package io.quarkus.datasource.deployment; -import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import jakarta.inject.Singleton; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.datasource.runtime.DataSourceRecorder; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; -import io.quarkus.datasource.runtime.DataSourcesHealthSupportRecorder; +import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; -import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; @@ -17,18 +17,17 @@ public class DataSourcesExcludedFromHealthChecksProcessor { @BuildStep - @Record(STATIC_INIT) + @Record(RUNTIME_INIT) void produceBean( Capabilities capabilities, - DataSourcesHealthSupportRecorder recorder, - DataSourcesBuildTimeConfig config, + DataSourceRecorder recorder, + DataSourcesBuildTimeConfig buildTimeConfig, DataSourcesRuntimeConfig runtimeConfig, BuildProducer syntheticBeans) { - if (capabilities.isPresent(Capability.SMALLRYE_HEALTH)) { - syntheticBeans.produce(SyntheticBeanBuildItem.configure(DataSourcesHealthSupport.class) - .scope(Singleton.class) - .unremovable() - .runtimeValue(recorder.configureDataSourcesHealthSupport(config)) - .done()); - } + syntheticBeans.produce(SyntheticBeanBuildItem.configure(DataSourceSupport.class) + .scope(Singleton.class) + .unremovable() + .runtimeValue(recorder.createDataSourceSupport(buildTimeConfig, runtimeConfig)) + .setRuntimeInit() + .done()); } } diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index 22b6650110044..559fcbe6fa3d0 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -198,11 +198,17 @@ private RunningDevService startDevDb( DockerStatusBuildItem dockerStatusBuildItem, LaunchMode launchMode, Optional consoleInstalledBuildItem, LoggingSetupBuildItem loggingSetupBuildItem, GlobalDevServicesConfig globalDevServicesConfig) { - boolean explicitlyDisabled = !(dataSourceBuildTimeConfig.devservices().enabled().orElse(true)); String dataSourcePrettyName = DataSourceUtil.isDefault(dbName) ? "default datasource" : "datasource " + dbName; - if (explicitlyDisabled) { - //explicitly disabled + if (!ConfigUtils.getFirstOptionalValue( + DataSourceUtil.dataSourcePropertyKeys(dbName, "active"), Boolean.class) + .orElse(true)) { + log.debug("Not starting Dev Services for " + dataSourcePrettyName + + " as the datasource has been deactivated in the configuration"); + return null; + } + + if (!(dataSourceBuildTimeConfig.devservices().enabled().orElse(true))) { log.debug("Not starting Dev Services for " + dataSourcePrettyName + " as it has been disabled in the configuration"); return null; diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java new file mode 100644 index 0000000000000..87a39c554f6c3 --- /dev/null +++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRecorder.java @@ -0,0 +1,44 @@ +package io.quarkus.datasource.runtime; + +import static java.util.stream.Collectors.toUnmodifiableSet; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class DataSourceRecorder { + + public RuntimeValue createDataSourceSupport( + DataSourcesBuildTimeConfig buildTimeConfig, + DataSourcesRuntimeConfig runtimeConfig) { + Stream.Builder configured = Stream.builder(); + Stream.Builder excludedForHealthChecks = Stream.builder(); + for (Map.Entry dataSource : buildTimeConfig.dataSources().entrySet()) { + // TODO this is wrong, as the default datasource could be configured without db-kind being set: + // it's inferred automatically for the default datasource when possible. + // See https://github.com/quarkusio/quarkus/issues/37779 + if (dataSource.getValue().dbKind().isPresent()) { + configured.add(dataSource.getKey()); + } + if (dataSource.getValue().healthExclude()) { + excludedForHealthChecks.add(dataSource.getKey()); + } + } + Set names = configured.build().collect(toUnmodifiableSet()); + Set excludedNames = excludedForHealthChecks.build().collect(toUnmodifiableSet()); + + Stream.Builder inactive = Stream.builder(); + for (Map.Entry entry : runtimeConfig.dataSources().entrySet()) { + if (!entry.getValue().active()) { + inactive.add(entry.getKey()); + } + } + Set inactiveNames = inactive.build().collect(toUnmodifiableSet()); + + return new RuntimeValue<>(new DataSourceSupport(names, excludedNames, inactiveNames)); + } +} diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRuntimeConfig.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRuntimeConfig.java index dc9cd2b972e99..58fb761d34767 100644 --- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRuntimeConfig.java +++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceRuntimeConfig.java @@ -5,10 +5,26 @@ import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.configuration.TrimmedStringConverter; import io.smallrye.config.WithConverter; +import io.smallrye.config.WithDefault; @ConfigGroup public interface DataSourceRuntimeConfig { + /** + * Whether this datasource should be active at runtime. + * + * See xref:datasource.adoc#datasource-active[this section of the documentation]. + * + * If the datasource is not active, it won't start with the application, + * and accessing the corresponding Datasource CDI bean will fail, + * meaning in particular that consumers of this datasource + * (e.g. Hibernate ORM persistence units) will fail to start unless they are inactive too. + * + * @asciidoclet + */ + @WithDefault("true") + boolean active(); + /** * The datasource username */ diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java new file mode 100644 index 0000000000000..96b4b0f1fa9a9 --- /dev/null +++ b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourceSupport.java @@ -0,0 +1,42 @@ +package io.quarkus.datasource.runtime; + +import java.util.HashSet; +import java.util.Set; + +/** + * Helper class that holds the names of all configured data sources, + * along with the names of those that are inactive or excluded from health checks. + *

    + * This is used by any feature that needs runtime access to data sources, + * e.g. Flyway/Liquibase or health check implementation classes. + */ +public class DataSourceSupport { + + private final Set configuredNames; + private final Set inactiveNames; + private final Set inactiveOrHealthCheckExcludedNames; + + public DataSourceSupport(Set configuredNames, Set healthCheckExcludedNames, + Set inactiveNames) { + this.configuredNames = configuredNames; + this.inactiveOrHealthCheckExcludedNames = new HashSet<>(); + inactiveOrHealthCheckExcludedNames.addAll(inactiveNames); + inactiveOrHealthCheckExcludedNames.addAll(healthCheckExcludedNames); + this.inactiveNames = inactiveNames; + } + + // TODO careful when using this, as it might (incorrectly) not include the default datasource. + // See TODO in code that calls the constructor of this class. + // See https://github.com/quarkusio/quarkus/issues/37779 + public Set getConfiguredNames() { + return configuredNames; + } + + public Set getInactiveNames() { + return inactiveNames; + } + + public Set getInactiveOrHealthCheckExcludedNames() { + return inactiveOrHealthCheckExcludedNames; + } +} diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupport.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupport.java deleted file mode 100644 index 00a3c19a91b90..0000000000000 --- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupport.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.quarkus.datasource.runtime; - -import java.util.Set; - -/** - * Helper class that holds the names of all configured data sources, along with the names of those - * that are excluded from health checks. This is used by health check implementation classes. - */ -public class DataSourcesHealthSupport { - - private final Set configuredNames; - private final Set excludedNames; - - public DataSourcesHealthSupport(Set configuredNames, Set excludedNames) { - this.configuredNames = configuredNames; - this.excludedNames = excludedNames; - } - - public Set getConfiguredNames() { - return configuredNames; - } - - public Set getExcludedNames() { - return excludedNames; - } -} diff --git a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupportRecorder.java b/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupportRecorder.java deleted file mode 100644 index e39d700a6ed64..0000000000000 --- a/extensions/datasource/runtime/src/main/java/io/quarkus/datasource/runtime/DataSourcesHealthSupportRecorder.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.quarkus.datasource.runtime; - -import static java.util.stream.Collectors.toUnmodifiableSet; - -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.annotations.Recorder; - -@Recorder -public class DataSourcesHealthSupportRecorder { - - public RuntimeValue configureDataSourcesHealthSupport( - DataSourcesBuildTimeConfig config) { - Stream.Builder configured = Stream.builder(); - Stream.Builder excluded = Stream.builder(); - for (Map.Entry dataSource : config.dataSources().entrySet()) { - if (dataSource.getValue().dbKind().isPresent()) { - configured.add(dataSource.getKey()); - } - if (dataSource.getValue().healthExclude()) { - excluded.add(dataSource.getKey()); - } - } - Set names = configured.build().collect(toUnmodifiableSet()); - Set excludedNames = excluded.build().collect(toUnmodifiableSet()); - return new RuntimeValue<>(new DataSourcesHealthSupport(names, excludedNames)); - } -} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseDefaultDatasourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..79ff4aad04ff4 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,38 @@ +package io.quarkus.flyway.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + Instance flywayForDefaultDatasource; + + @Test + @DisplayName("If the default datasource is deactivated, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flywayForDefaultDatasource::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseNamedDataSourceTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseNamedDataSourceTest.java new file mode 100644 index 0000000000000..a00dd3f237b13 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionConfigActiveFalseNamedDataSourceTest.java @@ -0,0 +1,49 @@ +package io.quarkus.flyway.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionConfigActiveFalseNamedDataSourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.users.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @FlywayDataSource("users") + Instance flywayForNamedDatasource; + + @Test + @DisplayName("If a named datasource is deactivated, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flywayForNamedDatasource::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..240237ff5d727 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java @@ -0,0 +1,42 @@ +package io.quarkus.flyway.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql")) + .overrideConfigKey("quarkus.datasource.active", "false") + .overrideConfigKey("quarkus.flyway.migrate-at-start", "true"); + + @Inject + Instance flywayForDefaultDatasource; + + @Test + @DisplayName("If the default datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flywayForDefaultDatasource::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Flyway", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..7c2e303c9d4ce --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java @@ -0,0 +1,52 @@ +package io.quarkus.flyway.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.flyway.FlywayDataSource; +import io.quarkus.test.QuarkusUnitTest; + +public class FlywayExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql")) + .overrideConfigKey("quarkus.datasource.users.active", "false") + .overrideConfigKey("quarkus.flyway.users.migrate-at-start", "true") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise the *default* datasource may impact this test + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @FlywayDataSource("users") + Instance flywayForNamedDatasource; + + @Test + @DisplayName("If a named datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Flyway should be deactivated for that datasource") + public void testBootSucceedsButFlywayDeactivated() { + assertThatThrownBy(flywayForNamedDatasource::get) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Flyway", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerUtil.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerUtil.java new file mode 100644 index 0000000000000..34e6a4629f295 --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainerUtil.java @@ -0,0 +1,44 @@ +package io.quarkus.flyway.runtime; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.inject.Default; + +import io.quarkus.agroal.runtime.DataSources; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.flyway.FlywayDataSource; + +public final class FlywayContainerUtil { + private FlywayContainerUtil() { + } + + public static FlywayContainer getFlywayContainer(String dataSourceName) { + return Arc.container().instance(FlywayContainer.class, + getFlywayContainerQualifier(dataSourceName)).get(); + } + + public static List getActiveFlywayContainers() { + List result = new ArrayList<>(); + for (String datasourceName : Arc.container().instance(DataSources.class).get().getActiveDataSourceNames()) { + InstanceHandle handle = Arc.container().instance(FlywayContainer.class, + getFlywayContainerQualifier(datasourceName)); + if (!handle.isAvailable()) { + continue; + } + result.add(handle.get()); + } + return result; + } + + public static Annotation getFlywayContainerQualifier(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return Default.Literal.INSTANCE; + } + + return FlywayDataSource.FlywayDataSourceLiteral.of(dataSourceName); + } +} diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java index 11261c88c30a1..d44d997f01964 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayContainersSupplier.java @@ -2,29 +2,18 @@ import java.util.Collection; import java.util.Comparator; -import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.function.Supplier; -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; import io.quarkus.datasource.common.runtime.DataSourceUtil; public class FlywayContainersSupplier implements Supplier> { @Override public Collection get() { - List> flywayContainerHandles = Arc.container().listAll(FlywayContainer.class); - - if (flywayContainerHandles.isEmpty()) { - return Set.of(); - } - Set containers = new TreeSet<>(FlywayContainerComparator.INSTANCE); - for (InstanceHandle flywayContainerHandle : flywayContainerHandles) { - containers.add(flywayContainerHandle.get()); - } + containers.addAll(FlywayContainerUtil.getActiveFlywayContainers()); return containers; } diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java index 726ada0de89fd..2e9d9aa3dd35e 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayDataSourceRuntimeConfig.java @@ -25,8 +25,8 @@ public static FlywayDataSourceRuntimeConfig defaultConfig() { /** * Flag to activate/deactivate Flyway for a specific datasource at runtime. */ - @ConfigItem(defaultValue = "true") - public boolean active = true; + @ConfigItem(defaultValueDocumentation = "'true' if the datasource is active; 'false' otherwise") + public Optional active = Optional.empty(); /** * The maximum number of retries when attempting to connect to the database. diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java index 4e858dcf71a3b..3af211f682891 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java @@ -1,6 +1,5 @@ package io.quarkus.flyway.runtime; -import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Locale; import java.util.Map; @@ -8,8 +7,6 @@ import javax.sql.DataSource; -import jakarta.enterprise.inject.Default; - import org.flywaydb.core.Flyway; import org.flywaydb.core.FlywayExecutor; import org.flywaydb.core.api.callback.Callback; @@ -29,7 +26,6 @@ import io.quarkus.arc.InstanceHandle; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.flyway.FlywayDataSource.FlywayDataSourceLiteral; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; @@ -73,7 +69,7 @@ public FlywayContainer apply(SyntheticCreationalContext context throw DataSourceUtil.dataSourceNotConfigured(dataSourceName); } } catch (ConfigurationException e) { - // TODO do we really want to enable retrieval of a FlywayContainer for an unconfigured datasource? + // TODO do we really want to enable retrieval of a FlywayContainer for an unconfigured/inactive datasource? // Assigning ApplicationScoped to the FlywayContainer // and throwing UnsatisfiedResolutionException on bean creation (first access) // would probably make more sense. @@ -93,7 +89,7 @@ public Function, Flyway> flywayFunction(Strin @Override public Flyway apply(SyntheticCreationalContext context) { FlywayContainer flywayContainer = context.getInjectedReference(FlywayContainer.class, - getFlywayContainerQualifier(dataSourceName)); + FlywayContainerUtil.getFlywayContainerQualifier(dataSourceName)); return flywayContainer.getFlyway(); } }; @@ -103,12 +99,15 @@ public void doStartActions(String dataSourceName) { FlywayDataSourceRuntimeConfig flywayDataSourceRuntimeConfig = config.getValue() .getConfigForDataSourceName(dataSourceName); - if (!config.getValue().getConfigForDataSourceName(dataSourceName).active) { + if (!flywayDataSourceRuntimeConfig.active + // If not specified explicitly, Flyway is active when the datasource itself is active. + .orElseGet(() -> Arc.container().instance(DataSources.class).get().getActiveDataSourceNames() + .contains(dataSourceName))) { return; } InstanceHandle flywayContainerInstanceHandle = Arc.container().instance(FlywayContainer.class, - getFlywayContainerQualifier(dataSourceName)); + FlywayContainerUtil.getFlywayContainerQualifier(dataSourceName)); if (!flywayContainerInstanceHandle.isAvailable()) { return; @@ -138,14 +137,6 @@ public void doStartActions(String dataSourceName) { } } - private static Annotation getFlywayContainerQualifier(String dataSourceName) { - if (DataSourceUtil.isDefault(dataSourceName)) { - return Default.Literal.INSTANCE; - } - - return FlywayDataSourceLiteral.of(dataSourceName); - } - static class BaselineCommand implements FlywayExecutor.Command { BaselineCommand(Flyway flyway) { this.flyway = flyway; diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java index 57dab412c16d1..529b80a67c33e 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywaySchemaProvider.java @@ -1,23 +1,19 @@ package io.quarkus.flyway.runtime; -import io.quarkus.arc.Arc; import io.quarkus.datasource.runtime.DatabaseSchemaProvider; public class FlywaySchemaProvider implements DatabaseSchemaProvider { @Override public void resetDatabase(String dbName) { - for (FlywayContainer flywayContainer : Arc.container().select(FlywayContainer.class)) { - if (flywayContainer.getDataSourceName().equals(dbName)) { - flywayContainer.getFlyway().clean(); - flywayContainer.getFlyway().migrate(); - } - } + FlywayContainer flywayContainer = FlywayContainerUtil.getFlywayContainer(dbName); + flywayContainer.getFlyway().clean(); + flywayContainer.getFlyway().migrate(); } @Override public void resetAllDatabases() { - for (FlywayContainer flywayContainer : Arc.container().select(FlywayContainer.class)) { + for (FlywayContainer flywayContainer : FlywayContainerUtil.getActiveFlywayContainers()) { flywayContainer.getFlyway().clean(); flywayContainer.getFlyway().migrate(); } diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldAccessFinalFieldTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldAccessFinalFieldTest.java index c0c317ac5212d..77acb088262f6 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldAccessFinalFieldTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldAccessFinalFieldTest.java @@ -1,9 +1,3 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ package io.quarkus.hibernate.orm.applicationfieldaccess; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldWithProxyAndLazyLoadingAndInheritanceTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldWithProxyAndLazyLoadingAndInheritanceTest.java index 0d54300f1837c..881d68994b018 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldWithProxyAndLazyLoadingAndInheritanceTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldWithProxyAndLazyLoadingAndInheritanceTest.java @@ -1,9 +1,3 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ package io.quarkus.hibernate.orm.applicationfieldaccess; import static io.quarkus.hibernate.orm.TransactionTestUtils.inTransaction; diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithExplicitDatasourceConfigActiveFalseTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithExplicitDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..570fdd3e6368f --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithExplicitDatasourceConfigActiveFalseTest.java @@ -0,0 +1,41 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.config.MyEntity; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class EntitiesInDefaultPUWithExplicitDatasourceConfigActiveFalseTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyEntity.class)) + .overrideConfigKey("quarkus.hibernate-orm.datasource", "ds-1") + .overrideConfigKey("quarkus.hibernate-orm.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.datasource.\"ds-1\".active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.\"ds-1\".db-kind", "h2") + .assertException(t -> assertThat(t) + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll( + "Unable to find datasource 'ds-1' for persistence unit ''", + "Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance.")); + + @Test + public void testInvalidConfiguration() { + // deployment exception should happen first + Assertions.fail(); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..04e0448723341 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java @@ -0,0 +1,36 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.config.MyEntity; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyEntity.class)) + .overrideConfigKey("quarkus.datasource.active", "false") + .assertException(t -> assertThat(t) + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll( + "Unable to find datasource '' for persistence unit ''", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance.")); + + @Test + public void testInvalidConfiguration() { + // deployment exception should happen first + Assertions.fail(); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInNamedPUWithExplicitDatasourceConfigActiveFalseTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInNamedPUWithExplicitDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..8f5f8c6904fc5 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/EntitiesInNamedPUWithExplicitDatasourceConfigActiveFalseTest.java @@ -0,0 +1,41 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.config.namedpu.MyEntity; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class EntitiesInNamedPUWithExplicitDatasourceConfigActiveFalseTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addPackage(MyEntity.class.getPackage().getName())) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.datasource", "ds-1") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.datasource.\"ds-1\".active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.\"ds-1\".db-kind", "h2") + .assertException(t -> assertThat(t) + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll( + "Unable to find datasource 'ds-1' for persistence unit 'pu-1'", + "Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance.")); + + @Test + public void testInvalidConfiguration() { + // deployment exception should happen first + Assertions.fail(); + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU1Test.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU1Test.java new file mode 100644 index 0000000000000..298c13516a675 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU1Test.java @@ -0,0 +1,119 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import org.hibernate.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.datasource.runtime.DataSourceSupport; +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.hibernate.orm.config.namedpu.MyEntity; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests a use case where multiple PU/datasources are defined at build time, + * but only one is used at runtime. + *

    + * This is mostly useful when each datasource has a distinct db-kind, but in theory that shouldn't matter, + * so we use the h2 db-kind everywhere here to keep test dependencies simpler. + *

    + * See {@link MultiplePUAsAlternativesWithActivePU2Test} for the counterpart where PU2 is used at runtime. + */ +public class MultiplePUAsAlternativesWithActivePU1Test { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addPackage(MyEntity.class.getPackage().getName()) + .addClass(MyProducer.class)) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.packages", MyEntity.class.getPackageName()) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.datasource", "ds-1") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.active", "false") + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.packages", MyEntity.class.getPackageName()) + .overrideConfigKey("quarkus.hibernate-orm.pu-2.datasource", "ds-2") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.active", "false") + .overrideConfigKey("quarkus.datasource.ds-2.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-2.active", "false") + // This is where we select PU1 / datasource 1 + .overrideRuntimeConfigKey("quarkus.hibernate-orm.pu-1.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-1.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-1.jdbc.url", "jdbc:h2:mem:testds1"); + + @Inject + @PersistenceUnit("pu-1") + Session explicitSessionBean; + + @Inject + Session customIndirectSessionBean; + + @Inject + @PersistenceUnit("pu-2") + Session inactiveSessionBean; + + @Test + public void testExplicitSessionBeanUsable() { + doTestPersistRetrieve(explicitSessionBean, 1L); + } + + @Test + public void testCustomIndirectSessionBeanUsable() { + doTestPersistRetrieve(customIndirectSessionBean, 2L); + } + + @Test + public void testInactiveSessionBeanUnusable() { + QuarkusTransaction.requiringNew().run(() -> { + assertThatThrownBy(() -> inactiveSessionBean.find(MyEntity.class, 3L)) + .hasMessageContainingAll( + "Cannot retrieve the EntityManagerFactory/SessionFactory for persistence unit pu-2", + "Hibernate ORM was deactivated through configuration properties"); + }); + } + + private static void doTestPersistRetrieve(Session session, long id) { + QuarkusTransaction.requiringNew().run(() -> { + MyEntity entity = new MyEntity(); + entity.setId(id); + entity.setName("text" + id); + session.persist(entity); + }); + QuarkusTransaction.requiringNew().run(() -> { + MyEntity entity = session.get(MyEntity.class, id); + assertThat(entity.getName()).isEqualTo("text" + id); + }); + } + + private static class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @PersistenceUnit("pu-1") + Session pu1SessionBean; + + @Inject + @PersistenceUnit("pu-2") + Session pu2SessionBean; + + @Produces + @ApplicationScoped + public Session session() { + if (dataSourceSupport.getInactiveNames().contains("ds-1")) { + return pu2SessionBean; + } else { + return pu1SessionBean; + } + } + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU2Test.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU2Test.java new file mode 100644 index 0000000000000..c9eb9d247962a --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/config/datasource/MultiplePUAsAlternativesWithActivePU2Test.java @@ -0,0 +1,119 @@ +package io.quarkus.hibernate.orm.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + +import org.hibernate.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.datasource.runtime.DataSourceSupport; +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.quarkus.hibernate.orm.config.namedpu.MyEntity; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Tests a use case where multiple PU/datasources are defined at build time, + * but only one is used at runtime. + *

    + * This is mostly useful when each datasource has a distinct db-kind, but in theory that shouldn't matter, + * so we use the h2 db-kind everywhere here to keep test dependencies simpler. + *

    + * See {@link MultiplePUAsAlternativesWithActivePU1Test} for the counterpart where PU1 is used at runtime. + */ +public class MultiplePUAsAlternativesWithActivePU2Test { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addPackage(MyEntity.class.getPackage().getName()) + .addClass(MyProducer.class)) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.packages", MyEntity.class.getPackageName()) + .overrideConfigKey("quarkus.hibernate-orm.pu-1.datasource", "ds-1") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.hibernate-orm.pu-1.active", "false") + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.packages", MyEntity.class.getPackageName()) + .overrideConfigKey("quarkus.hibernate-orm.pu-2.datasource", "ds-2") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.database.generation", "drop-and-create") + .overrideConfigKey("quarkus.hibernate-orm.pu-2.active", "false") + .overrideConfigKey("quarkus.datasource.ds-2.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.ds-2.active", "false") + // This is where we select PU1 / datasource 2 + .overrideRuntimeConfigKey("quarkus.hibernate-orm.pu-2.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-2.active", "true") + .overrideRuntimeConfigKey("quarkus.datasource.ds-2.jdbc.url", "jdbc:h2:mem:testds2"); + + @Inject + @PersistenceUnit("pu-2") + Session explicitSessionBean; + + @Inject + Session customIndirectSessionBean; + + @Inject + @PersistenceUnit("pu-1") + Session inactiveSessionBean; + + @Test + public void testExplicitSessionBeanUsable() { + doTestPersistRetrieve(explicitSessionBean, 1L); + } + + @Test + public void testCustomIndirectSessionBeanUsable() { + doTestPersistRetrieve(customIndirectSessionBean, 2L); + } + + @Test + public void testInactiveSessionBeanUnusable() { + QuarkusTransaction.requiringNew().run(() -> { + assertThatThrownBy(() -> inactiveSessionBean.find(MyEntity.class, 3L)) + .hasMessageContainingAll( + "Cannot retrieve the EntityManagerFactory/SessionFactory for persistence unit pu-1", + "Hibernate ORM was deactivated through configuration properties"); + }); + } + + private static void doTestPersistRetrieve(Session session, long id) { + QuarkusTransaction.requiringNew().run(() -> { + MyEntity entity = new MyEntity(); + entity.setId(id); + entity.setName("text" + id); + session.persist(entity); + }); + QuarkusTransaction.requiringNew().run(() -> { + MyEntity entity = session.get(MyEntity.class, id); + assertThat(entity.getName()).isEqualTo("text" + id); + }); + } + + private static class MyProducer { + @Inject + DataSourceSupport dataSourceSupport; + + @Inject + @PersistenceUnit("pu-1") + Session pu1SessionBean; + + @Inject + @PersistenceUnit("pu-2") + Session pu2SessionBean; + + @Produces + @ApplicationScoped + public Session session() { + if (dataSourceSupport.getInactiveNames().contains("ds-1")) { + return pu2SessionBean; + } else { + return pu1SessionBean; + } + } + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerFinalFieldTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerFinalFieldTest.java index cb75eb3788fa6..15cbcbeba154f 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerFinalFieldTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerFinalFieldTest.java @@ -1,9 +1,3 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ package io.quarkus.hibernate.orm.enhancer; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerMissingEmbeddableAnnotationTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerMissingEmbeddableAnnotationTest.java index 860a8676d83f9..f40dcc94d5666 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerMissingEmbeddableAnnotationTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/enhancer/HibernateEntityEnhancerMissingEmbeddableAnnotationTest.java @@ -1,9 +1,3 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ package io.quarkus.hibernate.orm.enhancer; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java index 7ba8bb8141912..a272afe7f6cac 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java @@ -17,6 +17,8 @@ public class HibernateOrmRuntimeConfigPersistenceUnit { /** * Whether this persistence unit should be active at runtime. * + * See xref:hibernate-orm.adoc#persistence-unit-active[this section of the documentation]. + * * If the persistence unit is not active, it won't start with the application, * and accessing the corresponding EntityManagerFactory/EntityManager or SessionFactory/Session * will not be possible. diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/cdi/QuarkusArcBeanContainer.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/cdi/QuarkusArcBeanContainer.java index 258569ddcb93b..2da69b8be8fbc 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/cdi/QuarkusArcBeanContainer.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/cdi/QuarkusArcBeanContainer.java @@ -1,9 +1,3 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ package io.quarkus.hibernate.orm.runtime.cdi; import jakarta.annotation.PreDestroy; diff --git a/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java b/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..80ed019c91ce2 --- /dev/null +++ b/extensions/hibernate-reactive/deployment/src/test/java/io/quarkus/hibernate/reactive/config/datasource/EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest.java @@ -0,0 +1,37 @@ +package io.quarkus.hibernate.reactive.config.datasource; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.config.MyEntity; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; + +public class EntitiesInDefaultPUWithImplicitDatasourceConfigActiveFalseTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(MyEntity.class)) + .withConfigurationResource("application.properties") + .overrideConfigKey("quarkus.datasource.active", "false") + .assertException(t -> assertThat(t) + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll( + "Unable to find datasource '' for persistence unit 'default-reactive'", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance.")); + + @Test + public void testInvalidConfiguration() { + // deployment exception should happen first + Assertions.fail(); + } + +} diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java index 3e94c94fd1367..5faa419621b96 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java @@ -28,11 +28,14 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.hibernate.orm.runtime.BuildTimeSettings; import io.quarkus.hibernate.orm.runtime.FastBootHibernatePersistenceProvider; import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfig; import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfigPersistenceUnit; import io.quarkus.hibernate.orm.runtime.IntegrationSettings; +import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; import io.quarkus.hibernate.orm.runtime.PersistenceUnitsHolder; import io.quarkus.hibernate.orm.runtime.RuntimeSettings; import io.quarkus.hibernate.orm.runtime.RuntimeSettings.Builder; @@ -76,31 +79,25 @@ public FastBootHibernateReactivePersistenceProvider(HibernateOrmRuntimeConfig hi public EntityManagerFactory createEntityManagerFactory(String emName, Map properties) { if (properties == null) properties = new HashMap(); - try { - // These are pre-parsed during image generation: - final List units = PersistenceUnitsHolder.getPersistenceUnitDescriptors(); - - for (PersistenceUnitDescriptor unit : units) { - //if the provider is not set, don't use it as people might want to use Hibernate ORM - if (IMPLEMENTATION_NAME.equalsIgnoreCase(unit.getProviderClassName()) || - unit.getProviderClassName() == null) { - EntityManagerFactoryBuilder builder = getEntityManagerFactoryBuilderOrNull(emName, properties); - if (builder == null) { - log.trace("Could not obtain matching EntityManagerFactoryBuilder, returning null"); - return null; - } else { - return builder.build(); - } + // These are pre-parsed during image generation: + final List units = PersistenceUnitsHolder.getPersistenceUnitDescriptors(); + + for (PersistenceUnitDescriptor unit : units) { + //if the provider is not set, don't use it as people might want to use Hibernate ORM + if (IMPLEMENTATION_NAME.equalsIgnoreCase(unit.getProviderClassName()) || + unit.getProviderClassName() == null) { + EntityManagerFactoryBuilder builder = getEntityManagerFactoryBuilderOrNull(emName, properties); + if (builder == null) { + log.trace("Could not obtain matching EntityManagerFactoryBuilder, returning null"); + return null; + } else { + return builder.build(); } } - - //not the right provider - return null; - } catch (PersistenceException pe) { - throw pe; - } catch (Exception e) { - throw new PersistenceException("Unable to build EntityManagerFactory", e); } + + //not the right provider + return null; } private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String persistenceUnitName, @@ -286,12 +283,22 @@ private void registerVertxAndPool(String persistenceUnitName, } // for now, we only support one pool but this will change - InstanceHandle poolHandle = Arc.container().instance(Pool.class); - if (!poolHandle.isAvailable()) { - throw new IllegalStateException("No pool has been defined for persistence unit " + persistenceUnitName); + String datasourceName = DataSourceUtil.DEFAULT_DATASOURCE_NAME; + Pool pool; + try { + if (Arc.container().instance(DataSourceSupport.class).get().getInactiveNames().contains(datasourceName)) { + throw DataSourceUtil.dataSourceInactive(datasourceName); + } + InstanceHandle poolHandle = Arc.container().instance(Pool.class); + if (!poolHandle.isAvailable()) { + throw new IllegalStateException("No pool has been defined for persistence unit " + persistenceUnitName); + } + pool = poolHandle.get(); + } catch (RuntimeException e) { + throw PersistenceUnitUtil.unableToFindDataSource(persistenceUnitName, datasourceName, e); } - serviceRegistry.addInitiator(new QuarkusReactiveConnectionPoolInitiator(poolHandle.get())); + serviceRegistry.addInitiator(new QuarkusReactiveConnectionPoolInitiator(pool)); InstanceHandle vertxHandle = Arc.container().instance(Vertx.class); if (!vertxHandle.isAvailable()) { diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java index 630742c7adc6c..5eb2f7ae1094c 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java @@ -24,10 +24,14 @@ public FastBootReactiveEntityManagerFactoryBuilder(PrevalidatedQuarkusMetadata m @Override public EntityManagerFactory build() { - final SessionFactoryOptionsBuilder optionsBuilder = metadata.buildSessionFactoryOptionsBuilder(); - optionsBuilder.enableCollectionInDefaultFetchGroup(true); - populate(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME, optionsBuilder, standardServiceRegistry); - SessionFactoryOptions options = optionsBuilder.buildOptions(); - return new ReactiveSessionFactoryImpl(metadata, options, metadata.getBootstrapContext()); + try { + final SessionFactoryOptionsBuilder optionsBuilder = metadata.buildSessionFactoryOptionsBuilder(); + optionsBuilder.enableCollectionInDefaultFetchGroup(true); + populate(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME, optionsBuilder, standardServiceRegistry); + SessionFactoryOptions options = optionsBuilder.buildOptions(); + return new ReactiveSessionFactoryImpl(metadata, options, metadata.getBootstrapContext()); + } catch (Exception e) { + throw persistenceException("Unable to build Hibernate Reactive SessionFactory", e); + } } } diff --git a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java index 8c2a597126e5f..ab68ddd370514 100644 --- a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java +++ b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/LiquibaseProcessor.java @@ -306,10 +306,13 @@ ServiceStartBuildItem startLiquibase(LiquibaseRecorder recorder, BuildProducer initializationCompleteBuildItem, BuildProducer schemaReadyBuildItem) { - recorder.doStartActions(); + Set dataSourceNames = getDataSourceNames(jdbcDataSourceBuildItems); + for (String dataSourceName : dataSourceNames) { + recorder.doStartActions(dataSourceName); + } // once we are done running the migrations, we produce a build item indicating that the // schema is "ready" - schemaReadyBuildItem.produce(new JdbcDataSourceSchemaReadyBuildItem(getDataSourceNames(jdbcDataSourceBuildItems))); + schemaReadyBuildItem.produce(new JdbcDataSourceSchemaReadyBuildItem(dataSourceNames)); initializationCompleteBuildItem.produce(new InitTaskCompletedBuildItem("liquibase")); return new ServiceStartBuildItem("liquibase"); diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseDefaultDatasourceTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..ed35e12bfd237 --- /dev/null +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,39 @@ +package io.quarkus.liquibase.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.QuarkusUnitTest; + +public class LiquibaseExtensionConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + Instance liquibaseForDefaultDatasource; + + @Test + @DisplayName("If the default datasource is deactivated, the application should boot, but Liquibase should be deactivated for that datasource") + public void testBootSucceedsButLiquibaseDeactivated() { + assertThatThrownBy(() -> liquibaseForDefaultDatasource.get().getConfiguration()) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Liquibase", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + +} diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseNamedDatasourceTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..583e2d934dadc --- /dev/null +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,48 @@ +package io.quarkus.liquibase.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.liquibase.LiquibaseDataSource; +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.QuarkusUnitTest; + +public class LiquibaseExtensionConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.users.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise it's going to be the *default* datasource making everything fail + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + @Inject + @LiquibaseDataSource("users") + Instance liquibaseForNamedDatasource; + + @Test + @DisplayName("If a named datasource is deactivated, the application should boot, but Liquibase should be deactivated for that datasource") + public void testBootSucceedsButLiquibaseDeactivated() { + assertThatThrownBy(() -> liquibaseForNamedDatasource.get().getConfiguration()) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Liquibase", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..dccc77efe2e06 --- /dev/null +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest.java @@ -0,0 +1,42 @@ +package io.quarkus.liquibase.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.QuarkusUnitTest; + +public class LiquibaseExtensionMigrateAtStartDefaultDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/changeLog.xml", "db/changeLog.xml")) + .overrideConfigKey("quarkus.datasource.active", "false") + .overrideConfigKey("quarkus.liquibase.migrate-at-start", "true"); + + @Inject + Instance liquibaseForDefaultDatasource; + + @Test + @DisplayName("If the default datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Liquibase should be deactivated for that datasource") + public void testBootSucceedsButLiquibaseDeactivated() { + assertThatThrownBy(() -> liquibaseForDefaultDatasource.get().getConfiguration()) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource '' for Liquibase", + "Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + +} diff --git a/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java new file mode 100644 index 0000000000000..bfc68585954d6 --- /dev/null +++ b/extensions/liquibase/deployment/src/test/java/io/quarkus/liquibase/test/LiquibaseExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest.java @@ -0,0 +1,52 @@ +package io.quarkus.liquibase.test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.liquibase.LiquibaseDataSource; +import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.test.QuarkusUnitTest; + +public class LiquibaseExtensionMigrateAtStartNamedDatasourceConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/changeLog.xml", "db/changeLog.xml")) + .overrideConfigKey("quarkus.datasource.users.active", "false") + .overrideConfigKey("quarkus.liquibase.users.migrate-at-start", "true") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.users.db-kind", "h2") + // We need this otherwise it's going to be the *default* datasource making everything fail + .overrideConfigKey("quarkus.datasource.db-kind", "h2") + .overrideConfigKey("quarkus.datasource.username", "sa") + .overrideConfigKey("quarkus.datasource.password", "sa") + .overrideConfigKey("quarkus.datasource.jdbc.url", + "jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1"); + + @Inject + @LiquibaseDataSource("users") + Instance liquibaseForNamedDatasource; + + @Test + @DisplayName("If a named datasource is deactivated, even if migrate-at-start is enabled, the application should boot, but Liquibase should be deactivated for that datasource") + public void testBootSucceedsButLiquibaseDeactivated() { + assertThatThrownBy(() -> liquibaseForNamedDatasource.get().getConfiguration()) + .isInstanceOf(CreationException.class) + .cause() + .hasMessageContainingAll("Unable to find datasource 'users' for Liquibase", + "Datasource 'users' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"users\".active'" + + " to 'true' and configure datasource 'users'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseFactoryUtil.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseFactoryUtil.java new file mode 100644 index 0000000000000..c3d4698cfe4c6 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseFactoryUtil.java @@ -0,0 +1,45 @@ +package io.quarkus.liquibase.runtime; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.inject.Default; + +import io.quarkus.agroal.runtime.DataSources; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.datasource.common.runtime.DataSourceUtil; +import io.quarkus.liquibase.LiquibaseDataSource; +import io.quarkus.liquibase.LiquibaseFactory; + +public final class LiquibaseFactoryUtil { + private LiquibaseFactoryUtil() { + } + + public static InstanceHandle getLiquibaseFactory(String dataSourceName) { + return Arc.container().instance(LiquibaseFactory.class, + getLiquibaseFactoryQualifier(dataSourceName)); + } + + public static List> getActiveLiquibaseFactories() { + List> result = new ArrayList<>(); + for (String datasourceName : Arc.container().instance(DataSources.class).get().getActiveDataSourceNames()) { + InstanceHandle handle = Arc.container().instance(LiquibaseFactory.class, + getLiquibaseFactoryQualifier(datasourceName)); + if (!handle.isAvailable()) { + continue; + } + result.add(handle); + } + return result; + } + + public static Annotation getLiquibaseFactoryQualifier(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return Default.Literal.INSTANCE; + } + + return LiquibaseDataSource.LiquibaseDataSourceLiteral.of(dataSourceName); + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java index 4b045a357417b..f368806383203 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseRecorder.java @@ -5,13 +5,11 @@ import javax.sql.DataSource; -import jakarta.enterprise.inject.Any; import jakarta.enterprise.inject.UnsatisfiedResolutionException; import io.quarkus.agroal.runtime.DataSources; import io.quarkus.agroal.runtime.UnconfiguredDataSource; import io.quarkus.arc.Arc; -import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.InstanceHandle; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.datasource.common.runtime.DataSourceUtil; @@ -52,49 +50,45 @@ public LiquibaseFactory apply(SyntheticCreationalContext conte }; } - public void doStartActions() { + public void doStartActions(String dataSourceName) { if (!config.getValue().enabled) { return; } + // Liquibase is active when the datasource itself is active. + if (!Arc.container().instance(DataSources.class).get().getActiveDataSourceNames().contains(dataSourceName)) { + return; + } + InstanceHandle liquibaseFactoryHandle = LiquibaseFactoryUtil.getLiquibaseFactory(dataSourceName); try { - InjectableInstance liquibaseFactoryInstance = Arc.container() - .select(LiquibaseFactory.class, Any.Literal.INSTANCE); - if (liquibaseFactoryInstance.isUnsatisfied()) { + LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); + var config = liquibaseFactory.getConfiguration(); + if (!config.cleanAtStart && !config.migrateAtStart) { return; } - - for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { - try { - LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); - var config = liquibaseFactory.getConfiguration(); - if (!config.cleanAtStart && !config.migrateAtStart) { - continue; - } - try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { - if (config.cleanAtStart) { - liquibase.dropAll(); - } - if (config.migrateAtStart) { - var lockService = LockServiceFactory.getInstance() - .getLockService(liquibase.getDatabase()); - lockService.waitForLock(); - try { - if (config.validateOnMigrate) { - liquibase.validate(); - } - liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); - } finally { - lockService.releaseLock(); - } + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + if (config.cleanAtStart) { + liquibase.dropAll(); + } + if (config.migrateAtStart) { + var lockService = LockServiceFactory.getInstance() + .getLockService(liquibase.getDatabase()); + lockService.waitForLock(); + try { + if (config.validateOnMigrate) { + liquibase.validate(); } + liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); + } finally { + lockService.releaseLock(); } - } catch (UnsatisfiedResolutionException e) { - //ignore, the DS is not configured } } + } catch (UnsatisfiedResolutionException e) { + //ignore, the DS is not configured } catch (Exception e) { throw new IllegalStateException("Error starting Liquibase", e); } } + } diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java index d3bc30ca0ed90..35701758bd6f1 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseSchemaProvider.java @@ -1,10 +1,7 @@ package io.quarkus.liquibase.runtime; -import jakarta.enterprise.inject.Any; import jakarta.enterprise.inject.UnsatisfiedResolutionException; -import io.quarkus.arc.Arc; -import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.InstanceHandle; import io.quarkus.datasource.runtime.DatabaseSchemaProvider; import io.quarkus.liquibase.LiquibaseFactory; @@ -15,20 +12,11 @@ public class LiquibaseSchemaProvider implements DatabaseSchemaProvider { @Override public void resetDatabase(String dbName) { try { - InjectableInstance liquibaseFactoryInstance = Arc.container() - .select(LiquibaseFactory.class, Any.Literal.INSTANCE); - if (liquibaseFactoryInstance.isUnsatisfied()) { - return; - } - for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { - try { - LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); - if (liquibaseFactory.getDataSourceName().equals(dbName)) { - doReset(liquibaseFactory); - } - } catch (UnsatisfiedResolutionException e) { - //ignore, the DS is not configured - } + try { + LiquibaseFactory liquibaseFactory = LiquibaseFactoryUtil.getLiquibaseFactory(dbName).get(); + doReset(liquibaseFactory); + } catch (UnsatisfiedResolutionException e) { + //ignore, the DS is not configured } } catch (Exception e) { throw new IllegalStateException("Error starting Liquibase", e); @@ -38,12 +26,7 @@ public void resetDatabase(String dbName) { @Override public void resetAllDatabases() { try { - InjectableInstance liquibaseFactoryInstance = Arc.container() - .select(LiquibaseFactory.class, Any.Literal.INSTANCE); - if (liquibaseFactoryInstance.isUnsatisfied()) { - return; - } - for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { + for (InstanceHandle liquibaseFactoryHandle : LiquibaseFactoryUtil.getActiveLiquibaseFactories()) { try { LiquibaseFactory liquibaseFactory = liquibaseFactoryHandle.get(); doReset(liquibaseFactory); diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java index ccd6b43863542..87a10e56483fd 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java @@ -1,35 +1,25 @@ package io.quarkus.liquibase.runtime.devui; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.Set; import java.util.TreeSet; import java.util.function.Supplier; -import jakarta.enterprise.inject.Any; - -import io.quarkus.arc.Arc; -import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.InstanceHandle; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.liquibase.LiquibaseFactory; +import io.quarkus.liquibase.runtime.LiquibaseFactoryUtil; public class LiquibaseFactoriesSupplier implements Supplier> { @Override public Collection get() { - InjectableInstance liquibaseFactoryInstance = Arc.container().select(LiquibaseFactory.class, - Any.Literal.INSTANCE); - if (liquibaseFactoryInstance.isUnsatisfied()) { - return Collections.emptySet(); - } - - Set liquibaseFactories = new TreeSet<>(LiquibaseFactoryComparator.INSTANCE); - for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { - liquibaseFactories.add(liquibaseFactoryHandle.get()); + Set containers = new TreeSet<>(LiquibaseFactoryComparator.INSTANCE); + for (InstanceHandle handle : LiquibaseFactoryUtil.getActiveLiquibaseFactories()) { + containers.add(handle.get()); } - return liquibaseFactories; + return containers; } private static class LiquibaseFactoryComparator implements Comparator { diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index 486b768c12024..b3c9f05ff21d4 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -14,83 +14,80 @@ public class OidcCommonConfig { /** * The base URL of the OpenID Connect (OIDC) server, for example, `https://host:port/auth`. - * OIDC discovery endpoint will be called by default by appending a '.well-known/openid-configuration' path to this URL. - * Note if you work with Keycloak OIDC server, make sure the base URL is in the following format: - * `https://host:port/realms/{realm}` where `{realm}` has to be replaced by the name of the Keycloak realm. + * Do not set this property if the public key verification ({@link #publicKey}) or certificate chain verification only + * ({@link #certificateChain}) is required. + * The OIDC discovery endpoint is called by default by appending a `.well-known/openid-configuration` path to this URL. + * For Keycloak, use `https://host:port/realms/{realm}`, replacing `{realm}` with the Keycloak realm name. */ @ConfigItem public Optional authServerUrl = Optional.empty(); /** - * Enables OIDC discovery. - * If the discovery is disabled then the OIDC endpoint URLs must be configured individually. + * Discovery of the OIDC endpoints. + * If not enabled, you must configure the OIDC endpoint URLs individually. */ @ConfigItem(defaultValueDocumentation = "true") public Optional discoveryEnabled = Optional.empty(); /** - * Relative path or absolute URL of the OIDC token endpoint which issues access and refresh tokens. + * The OIDC token endpoint that issues access and refresh tokens; + * specified as a relative path or absolute URL. + * Set if {@link #discoveryEnabled} is `false` or a discovered token endpoint path must be customized. */ @ConfigItem public Optional tokenPath = Optional.empty(); /** - * Relative path or absolute URL of the OIDC token revocation endpoint. + * The relative path or absolute URL of the OIDC token revocation endpoint. */ @ConfigItem public Optional revokePath = Optional.empty(); /** - * The client-id of the application. Each application has a client-id that is used to identify the application + * The client id of the application. Each application has a client id that is used to identify the application. + * Setting the client id is not required if {@link #applicationType} is `service` and no token introspection is required. */ @ConfigItem public Optional clientId = Optional.empty(); /** - * The maximum amount of time connecting to the currently unavailable OIDC server will be attempted for. - * The number of times the connection request will be repeated is calculated by dividing the value of this property by 2. - * For example, setting it to `20S` will allow for requesting the connection up to 10 times with a 2 seconds delay between - * the retries. - * Note this property is only effective when the initial OIDC connection is created, - * for example, when requesting a well-known OIDC configuration. - * Use the 'connection-retry-count' property to support trying to re-establish an already available connection which may - * have been - * dropped. + * The duration to attempt the initial connection to an OIDC server. + * For example, setting the duration to `20S` allows 10 retries, each 2 seconds apart. + * This property is only effective when the initial OIDC connection is created. + * For dropped connections, use the `connection-retry-count` property instead. */ @ConfigItem public Optional connectionDelay = Optional.empty(); /** - * The number of times an attempt to re-establish an already available connection will be repeated. - * Note this property is different from the `connection-delay` property, which is only effective during the initial OIDC - * connection creation. - * This property is used to try to recover an existing connection that may have been temporarily lost. - * For example, if a request to the OIDC token endpoint fails due to a connection exception, then the request will be - * retried the number of times configured by this property. + * The number of times to retry re-establishing an existing OIDC connection if it is temporarily lost. + * Different from `connection-delay`, which applies only to initial connection attempts. + * For instance, if a request to the OIDC token endpoint fails due to a connection issue, it will be retried as per this + * setting. */ @ConfigItem(defaultValue = "3") public int connectionRetryCount = 3; /** - * The amount of time after which the current OIDC connection request will time out. + * The number of seconds after which the current OIDC connection request times out. */ @ConfigItem(defaultValue = "10s") public Duration connectionTimeout = Duration.ofSeconds(10); /** - * The maximum size of the connection pool used by the WebClient + * The maximum size of the connection pool used by the WebClient. */ @ConfigItem public OptionalInt maxPoolSize = OptionalInt.empty(); /** - * Credentials which the OIDC adapter will use to authenticate to the OIDC server. + * Credentials the OIDC adapter uses to authenticate to the OIDC server. */ @ConfigItem public Credentials credentials = new Credentials(); /** - * Options to configure a proxy that OIDC adapter will use for talking with OIDC server. + * Options to configure the proxy the OIDC adapter uses to talk with the OIDC server. */ @ConfigItem public Proxy proxy = new Proxy(); @@ -105,15 +102,16 @@ public class OidcCommonConfig { public static class Credentials { /** - * Client secret which is used for a `client_secret_basic` authentication method. - * Note that a 'client-secret.value' can be used instead but both properties are mutually exclusive. + * The client secret used by the `client_secret_basic` authentication method. + * Must be set unless a secret is set in {@link #clientSecret} or {@link #jwt} client authentication is required. + * You can use `client-secret.value` instead, but both properties are mutually exclusive. */ @ConfigItem public Optional secret = Optional.empty(); /** - * Client secret which can be used for the `client_secret_basic` (default) and `client_secret_post` - * and 'client_secret_jwt' authentication methods. + * The client secret used by the `client_secret_basic` (default), `client_secret_post`, or `client_secret_jwt` + * authentication methods. * Note that a `secret.value` property can be used instead to support the `client_secret_basic` method * but both properties are mutually exclusive. */ @@ -121,7 +119,7 @@ public static class Credentials { public Secret clientSecret = new Secret(); /** - * Client JWT authentication methods + * Client JSON Web Token (JWT) authentication methods */ @ConfigItem public Jwt jwt = new Jwt(); @@ -151,7 +149,7 @@ public void setJwt(Jwt jwt) { } /** - * Supports the client authentication methods which involve sending a client secret. + * Supports the client authentication methods that involve sending a client secret. * * @see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication @@ -161,20 +159,21 @@ public static class Secret { public static enum Method { /** - * client_secret_basic (default): client id and secret are submitted with the HTTP Authorization Basic scheme + * `client_secret_basic` (default): The client id and secret are submitted with the HTTP Authorization Basic + * scheme. */ BASIC, /** - * client_secret_post: client id and secret are submitted as the `client_id` and `client_secret` form - * parameters. + * `client_secret_post`: The client id and secret are submitted as the `client_id` and `client_secret` + * form parameters. */ POST, /** - * client_secret_jwt: client id and generated JWT secret are submitted as the `client_id` and `client_secret` - * form - * parameters. + * `client_secret_jwt`: The client id and generated JWT secret are submitted as the `client_id` and + * `client_secret` + * form parameters. */ POST_JWT, @@ -186,19 +185,21 @@ public static enum Method { } /** - * The client secret value - it will be ignored if 'credentials.secret' is set + * The client secret value. This value is ignored if `credentials.secret` is set. + * Must be set unless a secret is set in {@link #clientSecret} or {@link #jwt} client authentication is required. */ @ConfigItem public Optional value = Optional.empty(); /** - * The Secret CredentialsProvider + * The Secret CredentialsProvider. */ @ConfigItem public Provider provider = new Provider(); /** - * Authentication method. + * The authentication method. + * If the `clientSecret.value` secret is set, this method is `basic` by default. */ @ConfigItem public Optional method = Optional.empty(); @@ -229,9 +230,8 @@ public void setSecretProvider(Provider secretProvider) { } /** - * Supports the client authentication 'client_secret_jwt' and `private_key_jwt` methods which involve sending a JWT - * token - * assertion signed with either a client secret or private key. + * Supports the client authentication `client_secret_jwt` and `private_key_jwt` methods, which involves sending a JWT + * token assertion signed with a client secret or private key. * * @see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication @@ -239,20 +239,20 @@ public void setSecretProvider(Provider secretProvider) { @ConfigGroup public static class Jwt { /** - * If provided, indicates that JWT is signed using a secret key + * If provided, indicates that JWT is signed using a secret key. */ @ConfigItem public Optional secret = Optional.empty(); /** - * If provided, indicates that JWT is signed using a secret key provided by Secret CredentialsProvider + * If provided, indicates that JWT is signed using a secret key provided by Secret CredentialsProvider. */ @ConfigItem public Provider secretProvider = new Provider(); /** - * If provided, indicates that JWT is signed using a private key in PEM or JWK format. You can use the - * {@link #signatureAlgorithm} property to specify the key algorithm. + * If provided, indicates that JWT is signed using a private key in PEM or JWK format. + * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`. */ @ConfigItem public Optional keyFile = Optional.empty(); @@ -270,38 +270,38 @@ public static class Jwt { public Optional keyStorePassword; /** - * The private key id/alias + * The private key id or alias. */ @ConfigItem public Optional keyId = Optional.empty(); /** - * The private key password + * The private key password. */ @ConfigItem public Optional keyPassword; /** - * JWT audience ('aud') claim value. + * The JWT audience (`aud`) claim value. * By default, the audience is set to the address of the OpenId Connect Provider's token endpoint. */ @ConfigItem public Optional audience = Optional.empty(); /** - * Key identifier of the signing key added as a JWT 'kid' header + * The key identifier of the signing key added as a JWT `kid` header. */ @ConfigItem public Optional tokenKeyId = Optional.empty(); /** - * Issuer of the signing key added as a JWT `iss` claim (default: client id) + * The issuer of the signing key added as a JWT `iss` claim. The default value is the client id. */ @ConfigItem public Optional issuer = Optional.empty(); /** - * Subject of the signing key added as a JWT 'sub' claim (default: client id) + * Subject of the signing key added as a JWT `sub` claim The default value is the client id. */ @ConfigItem public Optional subject = Optional.empty(); @@ -313,14 +313,16 @@ public static class Jwt { public Map claims = new HashMap<>(); /** - * Signature algorithm, also used for the {@link #keyFile} property. - * Supported values: RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, HS256, HS384, HS512. + * The signature algorithm used for the {@link #keyFile} property. + * Supported values: `RS256` (default), `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `ES256`, `ES384`, `ES512`, + * `HS256`, `HS384`, `HS512`. */ @ConfigItem public Optional signatureAlgorithm = Optional.empty(); /** - * JWT life-span in seconds. It will be added to the time it was issued at to calculate the expiration time. + * The JWT lifespan in seconds. This value is added to the time at which the JWT was issued to calculate the + * expiration time. */ @ConfigItem(defaultValue = "10") public int lifespan = 10; @@ -392,13 +394,14 @@ public void setClaims(Map claims) { } /** - * CredentialsProvider which provides a client secret + * CredentialsProvider, which provides a client secret. */ @ConfigGroup public static class Provider { /** - * The CredentialsProvider name which should only be set if more than one CredentialsProvider is registered + * The CredentialsProvider name, which should only be set if more than one CredentialsProvider is + * registered */ @ConfigItem public Optional name = Optional.empty(); @@ -441,14 +444,15 @@ public enum Verification { CERTIFICATE_VALIDATION, /** - * All certificated are trusted and hostname verification is disabled. + * All certificates are trusted and hostname verification is disabled. */ NONE } /** - * Certificate validation and hostname verification, which can be one of the following {@link Verification} values. - * Default is required. + * Certificate validation and hostname verification, which can be one of the following {@link Verification} + * values. + * Default is `required`. */ @ConfigItem public Optional verification = Optional.empty(); @@ -460,68 +464,67 @@ public enum Verification { public Optional keyStoreFile = Optional.empty(); /** - * An optional parameter to specify type of the keystore file. If not given, the type is automatically detected - * based on the file name. + * The type of the keystore file. If not given, the type is automatically detected based on the file name. */ @ConfigItem public Optional keyStoreFileType = Optional.empty(); /** - * An optional parameter to specify a provider of the keystore file. If not given, the provider is automatically - * detected - * based on the keystore file type. + * The provider of the keystore file. If not given, the provider is automatically detected based on the + * keystore file type. */ @ConfigItem public Optional keyStoreProvider; /** - * A parameter to specify the password of the keystore file. If not given, the default ("password") is used. + * The password of the keystore file. If not given, the default value, `password`, is used. */ @ConfigItem public Optional keyStorePassword; /** - * An optional parameter to select a specific key in the keystore. When SNI is disabled, if the keystore contains - * multiple + * The alias of a specific key in the keystore. + * When SNI is disabled, if the keystore contains multiple * keys and no alias is specified, the behavior is undefined. */ @ConfigItem public Optional keyStoreKeyAlias = Optional.empty(); /** - * An optional parameter to define the password for the key, in case it's different from {@link #keyStorePassword}. + * The password of the key, if it is different from the {@link #keyStorePassword}. */ @ConfigItem public Optional keyStoreKeyPassword = Optional.empty(); /** - * An optional truststore which holds the certificate information of the certificates to trust + * The truststore that holds the certificate information of the certificates to trust. */ @ConfigItem public Optional trustStoreFile = Optional.empty(); /** - * A parameter to specify the password of the truststore file. + * The password of the truststore file. */ @ConfigItem public Optional trustStorePassword = Optional.empty(); /** - * A parameter to specify the alias of the truststore certificate. + * The alias of the truststore certificate. */ @ConfigItem public Optional trustStoreCertAlias = Optional.empty(); /** - * An optional parameter to specify type of the truststore file. If not given, the type is automatically detected + * The type of the truststore file. + * If not given, the type is automatically detected * based on the file name. */ @ConfigItem public Optional trustStoreFileType = Optional.empty(); /** - * An optional parameter to specify a provider of the truststore file. If not given, the provider is automatically - * detected + * The provider of the truststore file. + * If not given, the provider is automatically detected * based on the truststore file type. */ @ConfigItem @@ -581,27 +584,27 @@ public void setTrustStoreProvider(String trustStoreProvider) { public static class Proxy { /** - * The host (name or IP address) of the Proxy.
    - * Note: If OIDC adapter needs to use a Proxy to talk with OIDC server (Provider), - * then at least the "host" config item must be configured to enable the usage of a Proxy. + * The host name or IP address of the Proxy.
    + * Note: If the OIDC adapter requires a Proxy to talk with the OIDC server (Provider), + * set this value to enable the usage of a Proxy. */ @ConfigItem public Optional host = Optional.empty(); /** - * The port number of the Proxy. Default value is 80. + * The port number of the Proxy. The default value is `80`. */ @ConfigItem(defaultValue = "80") public int port = 80; /** - * The username, if Proxy needs authentication. + * The username, if the Proxy needs authentication. */ @ConfigItem public Optional username = Optional.empty(); /** - * The password, if Proxy needs authentication. + * The password, if the Proxy needs authentication. */ @ConfigItem public Optional password = Optional.empty(); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index a12af4f1bd7bb..b3ae53ab3282b 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -14,52 +14,49 @@ public class DevServicesConfig { /** - * If DevServices has been explicitly enabled or disabled. - *

    - * When DevServices is enabled Quarkus will attempt to automatically configure and start - * Keycloak when running in Dev or Test mode and when Docker is running. + * Flag to enable (default) or disable Dev Services. + * + * When enabled, Dev Services for Keycloak automatically configures and starts Keycloak in Dev or Test mode, and when Docker + * is running. */ @ConfigItem(defaultValue = "true") public boolean enabled = true; /** - * The container image name to use, for container-based DevServices providers. + * The container image name for Dev Services providers. * - * Image with a Quarkus based distribution is used by default. - * Image with a WildFly based distribution can be selected instead, for example: + * Defaults to a Quarkus-based Keycloak image. For a WildFly-based distribution, use an image like * `quay.io/keycloak/keycloak:19.0.3-legacy`. - *

    - * Note Keycloak Quarkus and Keycloak WildFly images are initialized differently. - * By default, Dev Services for Keycloak will assume it is a Keycloak Quarkus image if the image version does not end with a - * '-legacy' - * string. - * Set 'quarkus.keycloak.devservices.keycloak-x-image' to override this check. + * + * Keycloak Quarkus and WildFly images are initialized differently. Dev Services for Keycloak will assume it is a Keycloak + * Quarkus image unless the image version + * ends with `-legacy`. + * Override with `quarkus.keycloak.devservices.keycloak-x-image`. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:23.0.3") + @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:23.0.4") public String imageName; /** - * If Keycloak-X image is used. + * Indicates if a Keycloak-X image is used. * - * By default, Dev Services for Keycloak will assume a Keycloak-X image is used if the image name contains a 'keycloak-x' - * string. - * Set 'quarkus.keycloak.devservices.keycloak-x-image' to override this check which may be necessary if you build custom - * Keycloak-X or Keycloak images. + * By default, the image is identified by `keycloak-x` in the image name. + * For custom images, override with `quarkus.keycloak.devservices.keycloak-x-image`. * You do not need to set this property if the default check works. */ @ConfigItem public Optional keycloakXImage; /** - * Indicates if the Keycloak container managed by Quarkus Dev Services is shared. - * When shared, Quarkus looks for running containers using label-based service discovery. - * If a matching container is found, it is used, and so a second one is not started. - * Otherwise, Dev Services for Keycloak starts a new container. - *

    - * The discovery uses the {@code quarkus-dev-service-label} label. - * The value is configured using the {@code service-name} property. - *

    - * Container sharing is only used in dev mode. + * Determines if the Keycloak container is shared. + * + * When shared, Quarkus uses label-based service discovery to find and reuse a running Keycloak container, so a second one + * is not started. + * Otherwise, if a matching container is not is found, a new container is started. + * + * The service discovery uses the {@code quarkus-dev-service-label} label, whose value is set by the {@code service-name} + * property. + * + * Container sharing is available only in dev mode. */ @ConfigItem(defaultValue = "true") public boolean shared; @@ -69,29 +66,38 @@ public class DevServicesConfig { * This property is used when {@code shared} is set to {@code true}. * In this case, before starting a container, Dev Services for Keycloak looks for a container with the * {@code quarkus-dev-service-keycloak} label - * set to the configured value. If found, it will use this container instead of starting a new one. Otherwise, it + * set to the configured value. If found, it uses this container instead of starting a new one. Otherwise, it * starts a new container with the {@code quarkus-dev-service-keycloak} label set to the specified value. *

    * Container sharing is only used in dev mode. */ + /** + * The value of the `quarkus-dev-service-keycloak` label for identifying the Keycloak container. + * + * Used in shared mode to locate an existing container with this label. If not found, a new container is initialized with + * this label. + * + * Applicable only in dev mode. + */ @ConfigItem(defaultValue = "quarkus") public String serviceName; /** - * The comma-separated list of class or file system paths to Keycloak realm files which will be used to initialize Keycloak. - * The first value in this list will be used to initialize default tenant connection properties. + * A comma-separated list of class or file system paths to Keycloak realm files. + * This list is used to initialize Keycloak. + * The first value in this list is used to initialize default tenant connection properties. */ @ConfigItem public Optional> realmPath; /** - * Aliases to additional class or file system resources which will be used to initialize Keycloak. + * Aliases to additional class or file system resources that are used to initialize Keycloak. * Each map entry represents a mapping between an alias and a class or file system resource path. */ @ConfigItem public Map resourceAliases; /** - * Additional class or file system resources which will be used to initialize Keycloak. + * Additional class or file system resources that are used to initialize Keycloak. * Each map entry represents a mapping between a class or file system resource path alias and the Keycloak container * location. */ @@ -99,7 +105,7 @@ public class DevServicesConfig { public Map resourceMappings; /** - * The JAVA_OPTS passed to the keycloak JVM + * The `JAVA_OPTS` passed to the keycloak JVM */ @ConfigItem public Optional javaOpts; @@ -113,55 +119,54 @@ public class DevServicesConfig { /** * Keycloak start command. * Use this property to experiment with Keycloak start options, see {@link https://www.keycloak.org/server/all-config}. - * Note it will be ignored when loading legacy Keycloak WildFly images. + * Note, it is ignored when loading legacy Keycloak WildFly images. */ @ConfigItem public Optional startCommand; /** - * The Keycloak realm name. - * This property will be used to create the realm if the realm file pointed to by the `realm-path` property does not exist, - * default value is `quarkus` in this case. - * If the realm file pointed to by the `realm-path` property exists then it is still recommended to set this property - * for Dev Services for Keycloak to avoid parsing the realm file to determine the realm name. + * The name of the Keycloak realm. * + * This property is used to create the realm if the realm file pointed to by the `realm-path` property does not exist. + * The default value is `quarkus` in this case. + * It is recommended to always set this property so that Dev Services for Keycloak can identify the realm name without + * parsing the realm file. */ @ConfigItem public Optional realmName; /** - * Indicates if the Keycloak realm has to be created when the realm file pointed to by the `realm-path` property does not - * exist. + * Specifies whether to create the Keycloak realm when no realm file is found at the `realm-path`. * - * Disable it if you'd like to create a realm using Keycloak Administration Console - * or Keycloak Admin API from {@linkplain io.quarkus.test.common.QuarkusTestResourceLifecycleManager}. + * Set to `false` if the realm is to be created using either the Keycloak Administration Console or + * the Keycloak Admin API provided by {@linkplain io.quarkus.test.common.QuarkusTestResourceLifecycleManager}. */ @ConfigItem(defaultValue = "true") public boolean createRealm; /** - * The Keycloak users map containing the username and password pairs. - * If this map is empty then two users, 'alice' and 'bob' with the passwords matching their names will be created. - * This property will be used to create the Keycloak users if the realm file pointed to by the `realm-path` property does - * not exist. + * A map of Keycloak usernames to passwords. + * + * If empty, default users `alice` and `bob` are created with their names as passwords. + * This map is used for user creation when no realm file is found at the `realm-path`. */ @ConfigItem public Map users; /** - * The Keycloak user roles. - * If this map is empty then a user named 'alice' will get 'admin' and 'user' roles and all other users will get a 'user' - * role. - * This property will be used to create the Keycloak roles if the realm file pointed to by the `realm-path` property does - * not exist. + * A map of roles for Keycloak users. + * + * If empty, default roles are assigned: `alice` receives `admin` and `user` roles, while other users receive + * `user` role. + * This map is used for role creation when no realm file is found at the `realm-path`. */ @ConfigItem public Map> roles; /** - * Grant type. + * Specifies the grant type. * - * @deprecated Use {@link DevUiConfig#grant}. + * @deprecated This field is deprecated. Use {@link DevUiConfig#grant} instead. */ @Deprecated public Grant grant = new Grant(); @@ -170,21 +175,21 @@ public class DevServicesConfig { public static class Grant { public static enum Type { /** - * 'client_credentials' grant + * `client_credentials` grant */ CLIENT("client_credentials"), /** - * 'password' grant + * `password` grant */ PASSWORD("password"), /** - * 'authorization_code' grant + * `authorization_code` grant */ CODE("code"), /** - * 'implicit' grant + * `implicit` grant */ IMPLICIT("implicit"); @@ -200,22 +205,22 @@ public String getGrantType() { } /** - * Grant type which will be used to acquire a token to test the OIDC 'service' applications + * Defines the grant type for aquiring tokens for testing OIDC `service` applications. */ @ConfigItem(defaultValue = "code") public Type type = Type.CODE; } /** - * Optional fixed port the dev service will listen to. + * The specific port for the dev service to listen on. *

    - * If not defined, the port will be chosen randomly. + * If not specified, a random port is selected. */ @ConfigItem public OptionalInt port; /** - * Environment variables that are passed to the container. + * Environment variables to be passed to the container. */ @ConfigItem public Map containerEnv; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index 805099be88105..77cbdaa69fa9f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -23,8 +23,8 @@ public class OidcTenantConfig extends OidcCommonConfig { /** - * A unique tenant identifier. It must be set by {@code TenantConfigResolver} providers which - * resolve the tenant configuration dynamically and is optional in all other cases. + * A unique tenant identifier. It can be set by {@code TenantConfigResolver} providers, which + * resolve the tenant configuration dynamically. */ @ConfigItem public Optional tenantId = Optional.empty(); @@ -32,10 +32,10 @@ public class OidcTenantConfig extends OidcCommonConfig { /** * If this tenant configuration is enabled. * - * Note that the default tenant will be disabled if it is not configured but either - * {@link TenantConfigResolver} which will resolve tenant configurations is registered + * The default tenant is disabled if it is not configured but + * a {@link TenantConfigResolver} that resolves tenant configurations is registered, * or named tenants are configured. - * You do not have to disable the default tenant in this case. + * In this case, you do not need to disable the default tenant. */ @ConfigItem(defaultValue = "true") public boolean tenantEnabled = true; @@ -47,51 +47,54 @@ public class OidcTenantConfig extends OidcCommonConfig { public Optional applicationType = Optional.empty(); /** - * Relative path or absolute URL of the OIDC authorization endpoint which authenticates the users. - * This property must be set for the 'web-app' applications if OIDC discovery is disabled. - * This property will be ignored if the discovery is enabled. + * The relative path or absolute URL of the OpenID Connect (OIDC) authorization endpoint, which authenticates + * users. + * You must set this property for `web-app` applications if OIDC discovery is disabled. + * This property is ignored if OIDC discovery is enabled. */ @ConfigItem public Optional authorizationPath = Optional.empty(); /** - * Relative path or absolute URL of the OIDC UserInfo endpoint. - * This property must only be set for the 'web-app' applications if OIDC discovery is disabled - * and `authentication.user-info-required` property is enabled. - * This property will be ignored if the discovery is enabled. + * The relative path or absolute URL of the OIDC UserInfo endpoint. + * You must set this property for `web-app` applications if OIDC discovery is disabled + * and the `authentication.user-info-required` property is enabled. + * This property is ignored if OIDC discovery is enabled. */ @ConfigItem public Optional userInfoPath = Optional.empty(); /** - * Relative path or absolute URL of the OIDC RFC7662 introspection endpoint which can introspect both opaque and JWT tokens. - * This property must be set if OIDC discovery is disabled and 1) the opaque bearer access tokens have to be verified - * or 2) JWT tokens have to be verified while the cached JWK verification set with no matching JWK is being refreshed. - * This property will be ignored if the discovery is enabled. + * Relative path or absolute URL of the OIDC RFC7662 introspection endpoint which can introspect both opaque and + * JSON Web Token (JWT) tokens. + * This property must be set if OIDC discovery is disabled and 1) the opaque bearer access tokens must be verified + * or 2) JWT tokens must be verified while the cached JWK verification set with no matching JWK is being refreshed. + * This property is ignored if the discovery is enabled. */ @ConfigItem public Optional introspectionPath = Optional.empty(); /** - * Relative path or absolute URL of the OIDC JWKS endpoint which returns a JSON Web Key Verification Set. + * Relative path or absolute URL of the OIDC JSON Web Key Set (JWKS) endpoint which returns a JSON Web Key + * Verification Set. * This property should be set if OIDC discovery is disabled and the local JWT verification is required. - * This property will be ignored if the discovery is enabled. + * This property is ignored if the discovery is enabled. */ @ConfigItem public Optional jwksPath = Optional.empty(); /** * Relative path or absolute URL of the OIDC end_session_endpoint. - * This property must be set if OIDC discovery is disabled and RP Initiated Logout support for the 'web-app' applications is + * This property must be set if OIDC discovery is disabled and RP Initiated Logout support for the `web-app` applications is * required. - * This property will be ignored if the discovery is enabled. + * This property is ignored if the discovery is enabled. */ @ConfigItem public Optional endSessionPath = Optional.empty(); /** - * Public key for the local JWT token verification. - * OIDC server connection will not be created when this property is set. + * The public key for the local JWT token verification. + * OIDC server connection is not created when this property is set. */ @ConfigItem public Optional publicKey = Optional.empty(); @@ -122,7 +125,7 @@ public static class IntrospectionCredentials { public Optional secret = Optional.empty(); /** - * Include OpenId Connect Client ID configured with 'quarkus.oidc.client-id' + * Include OpenId Connect Client ID configured with `quarkus.oidc.client-id`. */ @ConfigItem(defaultValue = "true") public boolean includeClientId = true; @@ -173,7 +176,7 @@ public void setIncludeClientId(boolean includeClientId) { /** * Configuration of the certificate chain which can be used to verify tokens. - * If the certificate chain trusstore is configured then the tokens can be verified using the certificate + * If the certificate chain trusstore is configured, the tokens can be verified using the certificate * chain inlined in the Base64-encoded format as an `x5c` header in the token itself. */ @ConfigItem @@ -182,7 +185,7 @@ public void setIncludeClientId(boolean includeClientId) { @ConfigGroup public static class CertificateChain { /** - * Truststore file which keeps thumbprints of the trusted certificates + * Truststore file which keeps thumbprints of the trusted certificates. */ @ConfigItem public Optional trustStoreFile = Optional.empty(); @@ -200,7 +203,8 @@ public static class CertificateChain { public Optional trustStoreCertAlias = Optional.empty(); /** - * An optional parameter to specify type of the truststore file. If not given, the type is automatically detected + * An optional parameter to specify type of the truststore file. If not given, the type is automatically + * detected * based on the file name. */ @ConfigItem @@ -278,14 +282,16 @@ public void setTrustStoreFileType(Optional trustStoreFileType) { public static class Logout { /** - * The relative path of the logout endpoint at the application. If provided, the application is able to initiate the + * The relative path of the logout endpoint at the application. If provided, the application is able to + * initiate the * logout through this endpoint in conformance with the OpenID Connect RP-Initiated Logout specification. */ @ConfigItem public Optional path = Optional.empty(); /** - * Relative path of the application endpoint where the user should be redirected to after logging out from the OpenID + * Relative path of the application endpoint where the user should be redirected to after logging out from the + * OpenID * Connect Provider. * This endpoint URI must be properly registered at the OpenID Connect Provider as a valid redirect URI. */ @@ -293,13 +299,13 @@ public static class Logout { public Optional postLogoutPath = Optional.empty(); /** - * Name of the post logout URI parameter which will be added as a query parameter to the logout redirect URI. + * Name of the post logout URI parameter which is added as a query parameter to the logout redirect URI. */ @ConfigItem(defaultValue = OidcConstants.POST_LOGOUT_REDIRECT_URI) public String postLogoutUriParam; /** - * Additional properties which will be added as the query parameters to the logout redirect URI. + * Additional properties which is added as the query parameters to the logout redirect URI. */ @ConfigItem public Map extraParams; @@ -388,13 +394,13 @@ public static class Backchannel { /** * Token cache timer interval. - * If this property is set then a timer will check and remove the stale entries periodically. + * If this property is set, a timer checks and removes the stale entries periodically. */ @ConfigItem public Optional cleanUpTimerInterval = Optional.empty(); /** - * Logout token claim whose value will be used as a key for caching the tokens. + * Logout token claim whose value is used as a key for caching the tokens. * Only `sub` (subject) and `sid` (session id) claims can be used as keys. * Set it to `sid` only if ID tokens issued by the OIDC provider have no `sub` but have `sid` claim. */ @@ -455,7 +461,7 @@ public static class Jwks { * If JWK verification keys should be fetched at the moment a connection to the OIDC provider * is initialized. *

    - * Disabling this property will delay the key acquisition until the moment the current token + * Disabling this property delays the key acquisition until the moment the current token * has to be verified. Typically it can only be necessary if the token or other telated request properties * provide an additional context which is required to resolve the keys correctly. */ @@ -464,22 +470,22 @@ public static class Jwks { /** * Maximum number of JWK keys that can be cached. - * This property will be ignored if the {@link #resolveEarly} property is set to true. + * This property is ignored if the {@link #resolveEarly} property is set to true. */ @ConfigItem(defaultValue = "10") public int cacheSize = 10; /** * Number of minutes a JWK key can be cached for. - * This property will be ignored if the {@link #resolveEarly} property is set to true. + * This property is ignored if the {@link #resolveEarly} property is set to true. */ @ConfigItem(defaultValue = "10M") public Duration cacheTimeToLive = Duration.ofMinutes(10); /** * Cache timer interval. - * If this property is set then a timer will check and remove the stale entries periodically. - * This property will be ignored if the {@link #resolveEarly} property is set to true. + * If this property is set, a timer checks and removes the stale entries periodically. + * This property is ignored if the {@link #resolveEarly} property is set to true. */ @ConfigItem public Optional cleanUpTimerInterval = Optional.empty(); @@ -573,23 +579,23 @@ public enum Strategy { public boolean splitTokens; /** - * Mandates that the Default TokenStateManager will encrypt the session cookie that stores the tokens. + * Mandates that the Default TokenStateManager encrypt the session cookie that stores the tokens. */ @ConfigItem(defaultValue = "true") public boolean encryptionRequired = true; /** - * Secret which will be used by the Default TokenStateManager to encrypt the session cookie + * The secret used by the Default TokenStateManager to encrypt the session cookie * storing the tokens when {@link #encryptionRequired} property is enabled. *

    * If this secret is not set, the client secret configured with - * either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.client-secret.value` will be checked. - * Finally, `quarkus.oidc.credentials.jwt.secret` which can be used for `client_jwt_secret` authentication will be + * either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.client-secret.value` is checked. + * Finally, `quarkus.oidc.credentials.jwt.secret` which can be used for `client_jwt_secret` authentication is * checked. - * The secret will be auto-generated if it remains uninitialized after checking all of these properties. + * The secret is auto-generated if it remains uninitialized after checking all of these properties. *

    - * The length of the secret which will be used to encrypt the tokens should be at least 32 characters long. - * Warning will be logged if the secret length is less than 16 characters. + * The length of the secret used to encrypt the tokens should be at least 32 characters long. + * A warning is logged if the secret length is less than 16 characters. */ @ConfigItem public Optional encryptionSecret = Optional.empty(); @@ -738,18 +744,19 @@ public static Roles fromClaimPathAndSeparator(List path, String sep) { } /** - * List of paths to claims containing an array of groups. Each path starts from the top level JWT JSON object - * and can contain multiple segments where each segment represents a JSON object name only, - * example: "realm/groups". Use double quotes with the namespace qualified claim names. - * This property can be used if a token has no 'groups' claim but has the groups set in one or more different - * claims. + * A list of paths to claims containing an array of groups. + * Each path starts from the top level JWT JSON object + * and can contain multiple segments. + * Each segment represents a JSON object name only; for example: "realm/groups". + * Use double quotes with the namespace-qualified claim names. + * This property can be used if a token has no `groups` claim but has the groups set in one or more different claims. */ @ConfigItem public Optional> roleClaimPath = Optional.empty(); /** - * Separator for splitting a string which may contain multiple group values. - * It will only be used if the "role-claim-path" property points to one or more custom claims whose values are strings. - * A single space will be used by default because the standard 'scope' claim may contain a space separated sequence. + * The separator for splitting strings that contain multiple group values. + * It is only used if the "role-claim-path" property points to one or more custom claims whose values are strings. + * A single space is used by default because the standard `scope` claim can contain a space-separated sequence. */ @ConfigItem public Optional roleClaimSeparator = Optional.empty(); @@ -787,13 +794,13 @@ public void setSource(Source source) { // Source of the principal roles public static enum Source { /** - * ID Token - the default value for the 'web-app' applications. + * ID Token - the default value for the `web-app` applications. */ idtoken, /** - * Access Token - the default value for the 'service' applications; - * can also be used as the source of roles for the 'web-app' applications. + * Access Token - the default value for the `service` applications; + * can also be used as the source of roles for the `web-app` applications. */ accesstoken, @@ -831,59 +838,60 @@ public enum ResponseMode { /** * Authorization response parameters are encoded as HTML form values that are auto-submitted in the browser - * and transmitted via the HTTP POST method using the application/x-www-form-urlencoded content type + * and transmitted by the HTTP POST method using the application/x-www-form-urlencoded content type */ FORM_POST } /** - * Authorization code flow response mode + * Authorization code flow response mode. */ @ConfigItem(defaultValueDocumentation = "query") public Optional responseMode = Optional.empty(); /** - * Relative path for calculating a `redirect_uri` query parameter. - * It has to start from a forward slash and will be appended to the request URI's host and port. - * For example, if the current request URI is 'https://localhost:8080/service' then a `redirect_uri` parameter - * will be set to 'https://localhost:8080/' if this property is set to '/' and be the same as the request URI + * The relative path for calculating a `redirect_uri` query parameter. + * It has to start from a forward slash and is appended to the request URI's host and port. + * For example, if the current request URI is `https://localhost:8080/service`, a `redirect_uri` parameter + * is set to `https://localhost:8080/` if this property is set to `/` and be the same as the request URI * if this property has not been configured. - * Note the original request URI will be restored after the user has authenticated if 'restorePathAfterRedirect' is set + * Note the original request URI is restored after the user has authenticated if `restorePathAfterRedirect` is set * to `true`. */ @ConfigItem public Optional redirectPath = Optional.empty(); /** - * If this property is set to `true` then the original request URI which was used before - * the authentication will be restored after the user has been redirected back to the application. + * If this property is set to `true`, the original request URI which was used before + * the authentication is restored after the user has been redirected back to the application. * - * Note if `redirectPath` property is not set, the original request URI will be restored even if this property is + * Note if `redirectPath` property is not set, the original request URI is restored even if this property is * disabled. */ @ConfigItem(defaultValue = "false") public boolean restorePathAfterRedirect; /** - * Remove the query parameters such as 'code' and 'state' set by the OIDC server on the redirect URI + * Remove the query parameters such as `code` and `state` set by the OIDC server on the redirect URI * after the user has authenticated by redirecting a user to the same URI but without the query parameters. */ @ConfigItem(defaultValue = "true") public boolean removeRedirectParameters = true; /** - * Relative path to the public endpoint which will process the error response from the OIDC authorization endpoint. - * If the user authentication has failed then the OIDC provider will return an `error` and an optional + * Relative path to the public endpoint which processes the error response from the OIDC authorization + * endpoint. + * If the user authentication has failed, the OIDC provider returns an `error` and an optional * `error_description` - * parameters, instead of the expected authorization 'code'. + * parameters, instead of the expected authorization `code`. * - * If this property is set then the user will be redirected to the endpoint which can return a user-friendly - * error description page. It has to start from a forward slash and will be appended to the request URI's host and port. - * For example, if it is set as '/error' and the current request URI is - * 'https://localhost:8080/callback?error=invalid_scope' - * then a redirect will be made to 'https://localhost:8080/error?error=invalid_scope'. + * If this property is set, the user is redirected to the endpoint which can return a user-friendly + * error description page. It has to start from a forward slash and is appended to the request URI's host and port. + * For example, if it is set as `/error` and the current request URI is + * `https://localhost:8080/callback?error=invalid_scope`, + * a redirect is made to `https://localhost:8080/error?error=invalid_scope`. * - * If this property is not set then HTTP 401 status will be returned in case of the user authentication failure. + * If this property is not set, HTTP 401 status is returned in case of the user authentication failure. */ @ConfigItem public Optional errorPath = Optional.empty(); @@ -895,8 +903,8 @@ public enum ResponseMode { * Access token is not verified by default since it is meant to be propagated to the downstream services. * The verification of the access token should be enabled if it is injected as a JWT token. * - * Access tokens obtained as part of the code flow will always be verified if `quarkus.oidc.roles.source` - * property is set to `accesstoken` which means the authorization decision will be based on the roles extracted from the + * Access tokens obtained as part of the code flow are always verified if `quarkus.oidc.roles.source` + * property is set to `accesstoken` which means the authorization decision is based on the roles extracted from the * access token. * * Bearer access tokens are always verified. @@ -905,8 +913,9 @@ public enum ResponseMode { public boolean verifyAccessToken; /** - * Force 'https' as the `redirect_uri` parameter scheme when running behind an SSL/TLS terminating reverse proxy. - * This property, if enabled, will also affect the logout `post_logout_redirect_uri` and the local redirect requests. + * Force `https` as the `redirect_uri` parameter scheme when running behind an SSL/TLS terminating reverse + * proxy. + * This property, if enabled, also affects the logout `post_logout_redirect_uri` and the local redirect requests. */ @ConfigItem(defaultValueDocumentation = "false") public Optional forceRedirectHttpsScheme = Optional.empty(); @@ -927,29 +936,29 @@ public enum ResponseMode { public boolean nonceRequired = false; /** - * Add the `openid` scope automatically to the list of scopes. This is required for OpenId Connect providers - * but will not work for OAuth2 providers such as Twitter OAuth2 which does not accept that scope and throws an error. + * Add the `openid` scope automatically to the list of scopes. This is required for OpenId Connect providers, + * but does not work for OAuth2 providers such as Twitter OAuth2, which do not accept this scope and throw errors. */ @ConfigItem(defaultValueDocumentation = "true") public Optional addOpenidScope = Optional.empty(); /** - * Additional properties which will be added as the query parameters to the authentication redirect URI. + * Additional properties added as query parameters to the authentication redirect URI. */ @ConfigItem public Map extraParams = new HashMap<>(); /** - * Request URL query parameters which, if present, will be added to the authentication redirect URI. + * Request URL query parameters which, if present, are added to the authentication redirect URI. */ @ConfigItem @ConvertWith(TrimmedStringConverter.class) public Optional> forwardParams = Optional.empty(); /** - * If enabled the state, session and post logout cookies will have their 'secure' parameter set to `true` - * when HTTP is used. It may be necessary when running behind an SSL/TLS terminating reverse proxy. - * The cookies will always be secure if HTTPS is used even if this property is set to false. + * If enabled the state, session, and post logout cookies have their `secure` parameter set to `true` + * when HTTP is used. It might be necessary when running behind an SSL/TLS terminating reverse proxy. + * The cookies are always secure if HTTPS is used, even if this property is set to false. */ @ConfigItem(defaultValue = "false") public boolean cookieForceSecure; @@ -963,23 +972,23 @@ public enum ResponseMode { public Optional cookieSuffix = Optional.empty(); /** - * Cookie path parameter value which, if set, will be used to set a path parameter for the session, state and post + * Cookie path parameter value which, if set, is used to set a path parameter for the session, state and post * logout cookies. - * The `cookie-path-header` property, if set, will be checked first. + * The `cookie-path-header` property, if set, is checked first. */ @ConfigItem(defaultValue = "/") public String cookiePath = "/"; /** * Cookie path header parameter value which, if set, identifies the incoming HTTP header - * whose value will be used to set a path parameter for the session, state and post logout cookies. - * If the header is missing then the `cookie-path` property will be checked. + * whose value is used to set a path parameter for the session, state and post logout cookies. + * If the header is missing, the `cookie-path` property is checked. */ @ConfigItem public Optional cookiePathHeader = Optional.empty(); /** - * Cookie domain parameter value which, if set, will be used for the session, state and post logout cookies. + * Cookie domain parameter value which, if set, is used for the session, state and post logout cookies. */ @ConfigItem public Optional cookieDomain = Optional.empty(); @@ -991,11 +1000,11 @@ public enum ResponseMode { public CookieSameSite cookieSameSite = CookieSameSite.LAX; /** - * If a state cookie is present then a `state` query parameter must also be present and both the state - * cookie name suffix and state cookie value have to match the value of the `state` query parameter when + * If a state cookie is present, a `state` query parameter must also be present and both the state + * cookie name suffix and state cookie value must match the value of the `state` query parameter when * the redirect path matches the current path. * However, if multiple authentications are attempted from the same browser, for example, from the different - * browser tabs, then the currently available state cookie may represent the authentication flow + * browser tabs, then the currently available state cookie might represent the authentication flow * initiated from another tab and not related to the current request. * Disable this property to permit only a single authorization code flow in the same browser. * @@ -1010,29 +1019,29 @@ public enum ResponseMode { * matches the original request URL, the stale state cookie might remain in the browser cache from * the earlier failed redirect to an OpenId Connect provider and be visible during the current request. * For example, if Single-page application (SPA) uses XHR to handle redirects to the provider - * which does not support CORS for its authorization endpoint, the browser will block it - * and the state cookie created by Quarkus will remain in the browser cache. - * Quarkus will report an authentication failure when it will detect such an old state cookie but find no matching state + * which does not support CORS for its authorization endpoint, the browser blocks it + * and the state cookie created by Quarkus remains in the browser cache. + * Quarkus reports an authentication failure when it detects such an old state cookie but find no matching state * query parameter. *

    - * Reporting HTTP 401 error is usually the right thing to do in such cases, it will minimize a risk of the + * Reporting HTTP 401 error is usually the right thing to do in such cases, it minimizes a risk of the * browser redirect loop but also can identify problems in the way SPA or Quarkus application manage redirects. * For example, enabling {@link #javaScriptAutoRedirect} or having the provider redirect to URL configured - * with {@link #redirectPath} may be needed to avoid such errors. + * with {@link #redirectPath} might be needed to avoid such errors. *

    - * However, setting this property to `false` may help if the above options are not suitable. - * It will cause a new authentication redirect to OpenId Connect provider. Doing so may increase the + * However, setting this property to `false` might help if the above options are not suitable. + * It causes a new authentication redirect to OpenId Connect provider. Doing so might increase the * risk of browser redirect loops. */ @ConfigItem(defaultValue = "false") public boolean failOnMissingStateParam = false; /** - * If this property is set to `true` then an OIDC UserInfo endpoint will be called. - * This property will be enabled if `quarkus.oidc.roles.source` is `userinfo` + * If this property is set to `true`, an OIDC UserInfo endpoint is called. + * This property is enabled if `quarkus.oidc.roles.source` is `userinfo`. * or `quarkus.oidc.token.verify-access-token-with-user-info` is `true` * or `quarkus.oidc.authentication.id-token-required` is set to `false`, - * you do not have to enable this property manually in these cases. + * you do not need to enable this property manually in these cases. */ @ConfigItem(defaultValueDocumentation = "false") public Optional userInfoRequired = Optional.empty(); @@ -1040,24 +1049,25 @@ public enum ResponseMode { /** * Session age extension in minutes. * The user session age property is set to the value of the ID token life-span by default and - * the user will be redirected to the OIDC provider to re-authenticate once the session has expired. - * If this property is set to a non-zero value, then the expired ID token can be refreshed before + * the user is redirected to the OIDC provider to re-authenticate once the session has expired. + * If this property is set to a nonzero value, then the expired ID token can be refreshed before * the session has expired. - * This property will be ignored if the `token.refresh-expired` property has not been enabled. + * This property is ignored if the `token.refresh-expired` property has not been enabled. */ @ConfigItem(defaultValue = "5M") public Duration sessionAgeExtension = Duration.ofMinutes(5); /** - * If this property is set to `true` then a normal 302 redirect response will be returned - * if the request was initiated via JavaScript API such as XMLHttpRequest or Fetch and the current user needs to be - * (re)authenticated which may not be desirable for Single-page applications (SPA) since - * it automatically following the redirect may not work given that OIDC authorization endpoints typically do not support + * If this property is set to `true`, a normal 302 redirect response is returned + * if the request was initiated by a JavaScript API such as XMLHttpRequest or Fetch and the current user needs to be + * (re)authenticated, which might not be desirable for Single-page applications (SPA) since + * it automatically following the redirect might not work given that OIDC authorization endpoints typically do not + * support * CORS. *

    - * If this property is set to 'false' then a status code of '499' will be returned to allow + * If this property is set to `false`, a status code of `499` is returned to allow * SPA to handle the redirect manually if a request header identifying current request as a JavaScript request is found. - * 'X-Requested-With' request header with its value set to either `JavaScript` or `XMLHttpRequest` is expected by + * `X-Requested-With` request header with its value set to either `JavaScript` or `XMLHttpRequest` is expected by * default if * this property is enabled. You can register a custom {@linkplain JavaScriptRequestChecker} to do a custom JavaScript * request check instead. @@ -1068,7 +1078,7 @@ public enum ResponseMode { /** * Requires that ID token is available when the authorization code flow completes. * Disable this property only when you need to use the authorization code flow with OAuth2 providers which do not return - * ID token - an internal IdToken will be generated in such cases. + * ID token - an internal IdToken is generated in such cases. */ @ConfigItem(defaultValueDocumentation = "true") public Optional idTokenRequired = Optional.empty(); @@ -1087,29 +1097,30 @@ public enum ResponseMode { public Optional pkceRequired = Optional.empty(); /** - * Secret which will be used to encrypt a Proof Key for Code Exchange (PKCE) code verifier in the code flow state. + * Secret used to encrypt a Proof Key for Code Exchange (PKCE) code verifier in the code flow state. * This secret should be at least 32 characters long. * - * @deprecated Use {@link #stateSecret} property instead. + * @deprecated This field is deprecated. Use {@link #stateSecret} instead. + * */ @ConfigItem @Deprecated(forRemoval = true) public Optional pkceSecret = Optional.empty(); /** - * Secret which will be used to encrypt Proof Key for Code Exchange (PKCE) code verifier and/or nonce in the code flow + * Secret used to encrypt Proof Key for Code Exchange (PKCE) code verifier and/or nonce in the code flow * state. * This secret should be at least 32 characters long. *

    * If this secret is not set, the client secret configured with - * either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.client-secret.value` will be checked. - * Finally, `quarkus.oidc.credentials.jwt.secret` which can be used for `client_jwt_secret` authentication will be - * checked. Client secret will not be used as a state encryption secret if it is less than 32 characters + * either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.client-secret.value` is checked. + * Finally, `quarkus.oidc.credentials.jwt.secret` which can be used for `client_jwt_secret` authentication is + * checked. A client secret is not be used as a state encryption secret if it is less than 32 characters * long. *

    - * The secret will be auto-generated if it remains uninitialized after checking all of these properties. + * The secret is auto-generated if it remains uninitialized after checking all of these properties. *

    - * Error will be reported if the secret length is less than 16 characters. + * Error is reported if the secret length is less than 16 characters. */ @ConfigItem public Optional stateSecret = Optional.empty(); @@ -1341,13 +1352,13 @@ public static class CodeGrant { /** * Additional parameters, in addition to the required `code` and `redirect-uri` parameters, - * which have to be included to complete the authorization code grant request. + * which must be included to complete the authorization code grant request. */ @ConfigItem public Map extraParams = new HashMap<>(); /** - * Custom HTTP headers which have to be sent to complete the authorization code grant request. + * Custom HTTP headers which must be sent to complete the authorization code grant request. */ @ConfigItem public Map headers = new HashMap<>(); @@ -1411,11 +1422,11 @@ public static Token fromAudience(String... audience) { } /** - * Expected issuer `iss` claim value. - * Note this property overrides the `issuer` property which may be set in OpenId Connect provider's well-known + * The expected issuer `iss` claim value. + * This property overrides the `issuer` property, which might be set in OpenId Connect provider's well-known * configuration. - * If the `iss` claim value varies depending on the host/IP address or tenant id of the provider then you may skip the - * issuer verification by setting this property to 'any' but it should be done only when other options (such as + * If the `iss` claim value varies depending on the host, IP address, or tenant id of the provider, you can skip the + * issuer verification by setting this property to `any`, but it should be done only when other options (such as * configuring * the provider to use the fixed `iss` claim value) are not possible. */ @@ -1423,15 +1434,15 @@ public static Token fromAudience(String... audience) { public Optional issuer = Optional.empty(); /** - * Expected audience 'aud' claim value which may be a string or an array of strings. + * The expected audience `aud` claim value, which can be a string or an array of strings. * - * Note the audience claim will be verified for ID tokens by default. + * Note the audience claim is verified for ID tokens by default. * ID token audience must be equal to the value of `quarkus.oidc.client-id` property. * Use this property to override the expected value if your OpenID Connect provider * sets a different audience claim value in ID tokens. Set it to `any` if your provider * does not set ID token audience` claim. * - * Audience verification for access tokens will only be done if this property is configured. + * Audience verification for access tokens is only done if this property is configured. */ @ConfigItem public Optional> audience = Optional.empty(); @@ -1439,7 +1450,7 @@ public static Token fromAudience(String... audience) { /** * Require that the token includes a `sub` (subject) claim which is a unique * and never reassigned identifier for the current user. - * Note that if you enable this property and if UserInfo is also required then + * Note that if you enable this property and if UserInfo is also required, * both the token and UserInfo `sub` claims must be present and match each other. */ @ConfigItem(defaultValue = "false") @@ -1476,22 +1487,23 @@ public static Token fromAudience(String... audience) { * Token age. * * It allows for the number of seconds to be specified that must not elapse since the `iat` (issued at) time. - * A small leeway to account for clock skew which can be configured with 'quarkus.oidc.token.lifespan-grace' to verify + * A small leeway to account for clock skew which can be configured with `quarkus.oidc.token.lifespan-grace` to verify * the token expiry time * can also be used to verify the token age property. * * Note that setting this property does not relax the requirement that Bearer and Code Flow JWT tokens - * must have a valid ('exp') expiry claim value. The only exception where setting this property relaxes the requirement + * must have a valid (`exp`) expiry claim value. The only exception where setting this property relaxes the requirement * is when a logout token is sent with a back-channel logout request since the current - * OpenId Connect Back-Channel specification does not explicitly require the logout tokens to contain an 'exp' claim. - * However, even if the current logout token is allowed to have no 'exp' claim, the `exp` claim will be still verified + * OpenId Connect Back-Channel specification does not explicitly require the logout tokens to contain an `exp` claim. + * However, even if the current logout token is allowed to have no `exp` claim, the `exp` claim is still verified * if the logout token contains it. */ @ConfigItem public Optional age = Optional.empty(); /** - * Name of the claim which contains a principal name. By default, the `upn`, `preferred_username` and `sub` claims are + * Name of the claim which contains a principal name. By default, the `upn`, `preferred_username` and `sub` + * claims are * checked. */ @ConfigItem @@ -1499,34 +1511,34 @@ public static Token fromAudience(String... audience) { /** * Refresh expired authorization code flow ID or access tokens. - * If this property is enabled then a refresh token request will be performed if the authorization code - * ID or access token has expired and, if successful, the local session will be updated with the new set of tokens. - * Otherwise, the local session will be invalidated and the user redirected to the OpenID Provider to re-authenticate. - * In this case the user may not be challenged again if the OIDC provider session is still active. + * If this property is enabled, a refresh token request is performed if the authorization code + * ID or access token has expired and, if successful, the local session is updated with the new set of tokens. + * Otherwise, the local session is invalidated and the user redirected to the OpenID Provider to re-authenticate. + * In this case, the user might not be challenged again if the OIDC provider session is still active. * - * For this option be effective the `authentication.session-age-extension` property should also be set to a non-zero + * For this option be effective the `authentication.session-age-extension` property should also be set to a nonzero * value since the refresh token is currently kept in the user session. * * This option is valid only when the application is of type {@link ApplicationType#WEB_APP}}. * - * This property will be enabled if `quarkus.oidc.token.refresh-token-time-skew` is configured, - * you do not have to enable this property manually in this case. + * This property is enabled if `quarkus.oidc.token.refresh-token-time-skew` is configured, + * you do not need to enable this property manually in this case. */ @ConfigItem public boolean refreshExpired; /** - * Refresh token time skew in seconds. - * If this property is enabled then the configured number of seconds is added to the current time + * The refresh token time skew, in seconds. + * If this property is enabled, the configured number of seconds is added to the current time * when checking if the authorization code ID or access token should be refreshed. - * If the sum is greater than the authorization code ID or access token's expiration time then a refresh is going to + * If the sum is greater than the authorization code ID or access token's expiration time, a refresh is going to * happen. */ @ConfigItem public Optional refreshTokenTimeSkew = Optional.empty(); /** - * Forced JWK set refresh interval in minutes. + * The forced JWK set refresh interval in minutes. */ @ConfigItem(defaultValue = "10M") public Duration forcedJwkRefreshInterval = Duration.ofMinutes(10); @@ -1556,12 +1568,11 @@ public static Token fromAudience(String... audience) { * Decryption key location. * JWT tokens can be inner-signed and encrypted by OpenId Connect providers. * However, it is not always possible to remotely introspect such tokens because - * the providers may not control the private decryption keys. + * the providers might not control the private decryption keys. * In such cases set this property to point to the file containing the decryption private key in * PEM or JSON Web Key (JWK) format. - * Note that if a `private_key_jwt` client authentication method is used then the private key - * which is used to sign client authentication JWT tokens will be used to try to decrypt an encrypted ID token - * if this property is not set. + * If this property is not set and the `private_key_jwt` client authentication method is used, the private key + * used to sign the client authentication JWT tokens are also used to decrypt the encrypted ID tokens. */ @ConfigItem public Optional decryptionKeyLocation = Optional.empty(); @@ -1569,10 +1580,10 @@ public static Token fromAudience(String... audience) { /** * Allow the remote introspection of JWT tokens when no matching JWK key is available. * - * Note this property is set to `true` by default for backward-compatibility reasons and will be set to `false` - * instead in one of the next releases. + * This property is set to `true` by default for backward-compatibility reasons. It is planned that this default value + * will be changed to `false` in an upcoming release. * - * Also note this property will be ignored if JWK endpoint URI is not available and introspecting the tokens is + * Also note this property is ignored if JWK endpoint URI is not available and introspecting the tokens is * the only verification option. */ @ConfigItem(defaultValue = "true") @@ -1588,7 +1599,7 @@ public static Token fromAudience(String... audience) { /** * Allow the remote introspection of the opaque tokens. * - * Set this property to 'false' if only JWT tokens are expected. + * Set this property to `false` if only JWT tokens are expected. */ @ConfigItem(defaultValue = "true") public boolean allowOpaqueTokenIntrospection = true; @@ -1606,9 +1617,9 @@ public static Token fromAudience(String... audience) { /** * Indirectly verify that the opaque (binary) access token is valid by using it to request UserInfo. * Opaque access token is considered valid if the provider accepted this token and returned a valid UserInfo. - * You should only enable this option if the opaque access tokens have to be accepted but OpenId Connect + * You should only enable this option if the opaque access tokens must be accepted but OpenId Connect * provider does not have a token introspection endpoint. - * This property will have no effect when JWT tokens have to be verified. + * This property has no effect when JWT tokens must be verified. */ @ConfigItem(defaultValueDocumentation = "false") public Optional verifyAccessTokenWithUserInfo = Optional.empty(); @@ -1790,7 +1801,7 @@ public static enum ApplicationType { /** * A combined {@code SERVICE} and {@code WEB_APP} client. - * For this type of client, the Bearer Authorization method will be used if the Authorization header is set + * For this type of client, the Bearer Authorization method is used if the Authorization header is set * and Authorization Code Flow - if not. */ HYBRID diff --git a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/AdditionalJpaOperations.java b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/AdditionalJpaOperations.java index f136bf2ec6cf4..e0c9a7c729f1a 100644 --- a/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/AdditionalJpaOperations.java +++ b/extensions/panache/hibernate-orm-panache/runtime/src/main/java/io/quarkus/hibernate/orm/panache/runtime/AdditionalJpaOperations.java @@ -32,7 +32,7 @@ public class AdditionalJpaOperations { public static PanacheQuery find(AbstractJpaOperations jpaOperations, Class entityClass, String query, String countQuery, Sort sort, Map params) { String findQuery = createFindQuery(entityClass, query, jpaOperations.paramCount(params)); - EntityManager em = jpaOperations.getEntityManager(); + EntityManager em = jpaOperations.getEntityManager(entityClass); Query jpaQuery = em.createQuery(sort != null ? findQuery + toOrderBy(sort) : findQuery); JpaOperations.bindParameters(jpaQuery, params); return new CustomCountPanacheQuery(em, jpaQuery, countQuery, params); @@ -47,14 +47,14 @@ public static PanacheQuery find(AbstractJpaOperations jpaOperations, Class public static PanacheQuery find(AbstractJpaOperations jpaOperations, Class entityClass, String query, String countQuery, Sort sort, Object... params) { String findQuery = createFindQuery(entityClass, query, jpaOperations.paramCount(params)); - EntityManager em = jpaOperations.getEntityManager(); + EntityManager em = jpaOperations.getEntityManager(entityClass); Query jpaQuery = em.createQuery(sort != null ? findQuery + toOrderBy(sort) : findQuery); JpaOperations.bindParameters(jpaQuery, params); return new CustomCountPanacheQuery(em, jpaQuery, countQuery, params); } public static long deleteAllWithCascade(AbstractJpaOperations jpaOperations, Class entityClass) { - EntityManager em = jpaOperations.getEntityManager(); + EntityManager em = jpaOperations.getEntityManager(entityClass); //detecting the case where there are cascade-delete associations, and do the bulk delete query otherwise. if (deleteOnCascadeDetected(jpaOperations, entityClass)) { int count = 0; @@ -77,7 +77,7 @@ public static long deleteAllWithCascade(AbstractJpaOperations jpaOperations, * @return true if cascading delete is needed. False otherwise */ private static boolean deleteOnCascadeDetected(AbstractJpaOperations jpaOperations, Class entityClass) { - EntityManager em = jpaOperations.getEntityManager(); + EntityManager em = jpaOperations.getEntityManager(entityClass); Metamodel metamodel = em.getMetamodel(); EntityType entity1 = metamodel.entity(entityClass); Set> declaredAttributes = ((EntityTypeImpl) entity1).getDeclaredAttributes(); @@ -96,7 +96,7 @@ private static boolean deleteOnCascadeDetected(AbstractJpaOperations jpaOpera public static long deleteWithCascade(AbstractJpaOperations jpaOperations, Class entityClass, String query, Object... params) { - EntityManager em = jpaOperations.getEntityManager(); + EntityManager em = jpaOperations.getEntityManager(entityClass); if (deleteOnCascadeDetected(jpaOperations, entityClass)) { int count = 0; List objects = jpaOperations.list(jpaOperations.find(entityClass, query, params)); @@ -112,7 +112,7 @@ public static long deleteWithCascade(AbstractJpaOperations long deleteWithCascade(AbstractJpaOperations jpaOperations, Class entityClass, String query, Map params) { - EntityManager em = jpaOperations.getEntityManager(); + EntityManager em = jpaOperations.getEntityManager(entityClass); if (deleteOnCascadeDetected(jpaOperations, entityClass)) { int count = 0; List objects = jpaOperations.list(jpaOperations.find(entityClass, query, params)); diff --git a/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java b/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java index 60187addf5680..4c94cc18e68c5 100644 --- a/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java +++ b/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/ReactiveDB2ClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -208,6 +209,7 @@ private void createPoolIfDefined(DB2PoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -222,6 +224,7 @@ private void createPoolIfDefined(DB2PoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyDB2Pool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java index d5e372b88779b..85e16402885f6 100644 --- a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java +++ b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/DB2PoolRecorder.java @@ -25,6 +25,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -91,6 +92,9 @@ private DB2Pool initialize(VertxInternal vertx, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, DataSourceReactiveDB2Config dataSourceReactiveDB2Config, SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveDB2Config); DB2ConnectOptions db2ConnectOptions = toConnectOptions(dataSourceName, dataSourceRuntimeConfig, diff --git a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/health/ReactiveDB2DataSourcesHealthCheck.java b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/health/ReactiveDB2DataSourcesHealthCheck.java index 72c046f4c33cb..fe4d3a860c1a7 100644 --- a/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/health/ReactiveDB2DataSourcesHealthCheck.java +++ b/extensions/reactive-db2-client/runtime/src/main/java/io/quarkus/reactive/db2/client/runtime/health/ReactiveDB2DataSourcesHealthCheck.java @@ -20,7 +20,7 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.vertx.mutiny.db2client.DB2Pool; @@ -37,8 +37,8 @@ class ReactiveDB2DataSourcesHealthCheck implements HealthCheck { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(DB2Pool.class, Any.Literal.INSTANCE).handles()) { String db2PoolName = getDB2PoolName(handle.getBean()); if (!excludedNames.contains(db2PoolName)) { diff --git a/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java b/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java index 2707e8aba1424..4dcafa3c6ab01 100644 --- a/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java +++ b/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/ReactiveMSSQLClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -207,6 +208,7 @@ private void createPoolIfDefined(MSSQLPoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -221,6 +223,7 @@ private void createPoolIfDefined(MSSQLPoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyMSSQLPool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..8ad9fbeb363d6 --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,133 @@ +package io.quarkus.reactive.mssql.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mssqlclient.MSSQLPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + MSSQLPool pool = Arc.container().instance(MSSQLPool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.mssqlclient.MSSQLPool pool = Arc.container().instance(io.vertx.mutiny.mssqlclient.MSSQLPool.class) + .get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseNamedDatasourceTest.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..352e3bb17086f --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,140 @@ +package io.quarkus.reactive.mssql.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mssqlclient.MSSQLPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "mssql"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + MSSQLPool pool = Arc.container().instance(MSSQLPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.mssqlclient.MSSQLPool pool = Arc.container().instance(io.vertx.mutiny.mssqlclient.MSSQLPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @ReactiveDataSource("ds-1") + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..08f702e03f64d --- /dev/null +++ b/extensions/reactive-mssql-client/deployment/src/test/java/io/quarkus/reactive/mssql/client/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.mssql.client; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "mssql") + .overrideConfigKey("quarkus.datasource.brokenDS.reactive.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java index dad7ed85e5f86..b3c2c8cf1da72 100644 --- a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java +++ b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/MSSQLPoolRecorder.java @@ -25,6 +25,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -61,6 +62,9 @@ public Function, MSSQLPool> configureMSSQL return new Function<>() { @Override public MSSQLPool apply(SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } MSSQLPool pool = initialize((VertxInternal) vertx.getValue(), eventLoopCount.get(), dataSourceName, diff --git a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/health/ReactiveMSSQLDataSourcesHealthCheck.java b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/health/ReactiveMSSQLDataSourcesHealthCheck.java index 35537917a3399..a9a2c28a34685 100644 --- a/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/health/ReactiveMSSQLDataSourcesHealthCheck.java +++ b/extensions/reactive-mssql-client/runtime/src/main/java/io/quarkus/reactive/mssql/client/runtime/health/ReactiveMSSQLDataSourcesHealthCheck.java @@ -11,7 +11,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.runtime.ReactiveDatasourceHealthCheck; import io.vertx.mssqlclient.MSSQLPool; @@ -26,8 +26,8 @@ public ReactiveMSSQLDataSourcesHealthCheck() { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(MSSQLPool.class, Any.Literal.INSTANCE).handles()) { String poolName = getPoolName(handle.getBean()); if (!excludedNames.contains(poolName)) { diff --git a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java index 4bb36e3156dd0..2d03f42ef6e7a 100644 --- a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java +++ b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/ReactiveMySQLClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -208,6 +209,7 @@ private void createPoolIfDefined(MySQLPoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -222,6 +224,7 @@ private void createPoolIfDefined(MySQLPoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyMySQLPool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..0a7e67f1f6fe1 --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,133 @@ +package io.quarkus.reactive.mysql.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + MySQLPool pool = Arc.container().instance(MySQLPool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.mysqlclient.MySQLPool pool = Arc.container().instance(io.vertx.mutiny.mysqlclient.MySQLPool.class) + .get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseNamedDatasourceTest.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..5a6ba912c67cd --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,140 @@ +package io.quarkus.reactive.mysql.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.mysqlclient.MySQLPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "mysql"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + MySQLPool pool = Arc.container().instance(MySQLPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.mysqlclient.MySQLPool pool = Arc.container().instance(io.vertx.mutiny.mysqlclient.MySQLPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @ReactiveDataSource("ds-1") + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..40f9830df3787 --- /dev/null +++ b/extensions/reactive-mysql-client/deployment/src/test/java/io/quarkus/reactive/mysql/client/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.mysql.client; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "mysql") + .overrideConfigKey("quarkus.datasource.brokenDS.reactive.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java index 8b0d101285326..fda8b5372f2a1 100644 --- a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java +++ b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java @@ -25,6 +25,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -91,6 +92,9 @@ private MySQLPool initialize(VertxInternal vertx, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, DataSourceReactiveMySQLConfig dataSourceReactiveMySQLConfig, SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveMySQLConfig); List mySQLConnectOptions = toMySQLConnectOptions(dataSourceName, dataSourceRuntimeConfig, diff --git a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/health/ReactiveMySQLDataSourcesHealthCheck.java b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/health/ReactiveMySQLDataSourcesHealthCheck.java index 81470e5d9d0e6..656d585acce53 100644 --- a/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/health/ReactiveMySQLDataSourcesHealthCheck.java +++ b/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/health/ReactiveMySQLDataSourcesHealthCheck.java @@ -11,7 +11,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.runtime.ReactiveDatasourceHealthCheck; import io.vertx.mysqlclient.MySQLPool; @@ -26,8 +26,8 @@ public ReactiveMySQLDataSourcesHealthCheck() { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(MySQLPool.class, Any.Literal.INSTANCE).handles()) { String poolName = getPoolName(handle.getBean()); if (!excludedNames.contains(poolName)) { diff --git a/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java b/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java index ab1cf2dceff79..ee5ed3fd99376 100644 --- a/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java +++ b/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/ReactiveOracleClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -209,6 +210,7 @@ private void createPoolIfDefined(OraclePoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -223,6 +225,7 @@ private void createPoolIfDefined(OraclePoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyOraclePool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..d71964ecbf67d --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,133 @@ +package io.quarkus.reactive.oracle.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.oracleclient.OraclePool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + OraclePool pool = Arc.container().instance(OraclePool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.oracleclient.OraclePool pool = Arc.container().instance(io.vertx.mutiny.oracleclient.OraclePool.class) + .get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseNamedDatasourceTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..546c7179e563a --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,140 @@ +package io.quarkus.reactive.oracle.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.oracleclient.OraclePool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "oracle"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + OraclePool pool = Arc.container().instance(OraclePool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.oracleclient.OraclePool pool = Arc.container().instance(io.vertx.mutiny.oracleclient.OraclePool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @ReactiveDataSource("ds-1") + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..7791728100211 --- /dev/null +++ b/extensions/reactive-oracle-client/deployment/src/test/java/io/quarkus/reactive/oracle/client/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.oracle.client; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "oracle") + .overrideConfigKey("quarkus.datasource.brokenDS.reactive.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java index dc49a6eafd261..54ef9588855e6 100644 --- a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java +++ b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/OraclePoolRecorder.java @@ -19,6 +19,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -87,6 +88,9 @@ private OraclePool initialize(VertxInternal vertx, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, DataSourceReactiveOracleConfig dataSourceReactiveOracleConfig, SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveOracleConfig); OracleConnectOptions oracleConnectOptions = toOracleConnectOptions(dataSourceName, dataSourceRuntimeConfig, diff --git a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/health/ReactiveOracleDataSourcesHealthCheck.java b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/health/ReactiveOracleDataSourcesHealthCheck.java index fa462c96ac2b5..cc3370616f4eb 100644 --- a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/health/ReactiveOracleDataSourcesHealthCheck.java +++ b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/health/ReactiveOracleDataSourcesHealthCheck.java @@ -11,7 +11,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.runtime.ReactiveDatasourceHealthCheck; import io.vertx.oracleclient.OraclePool; @@ -26,8 +26,8 @@ public ReactiveOracleDataSourcesHealthCheck() { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(OraclePool.class, Any.Literal.INSTANCE).handles()) { String poolName = getPoolName(handle.getBean()); if (!excludedNames.contains(poolName)) { diff --git a/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java b/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java index cce55cfa31a5c..9f87c5535811b 100644 --- a/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java +++ b/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/ReactivePgClientProcessor.java @@ -32,6 +32,7 @@ import io.quarkus.datasource.deployment.spi.DefaultDataSourceDbKindBuildItem; import io.quarkus.datasource.deployment.spi.DevServicesDatasourceConfigurationHandlerBuildItem; import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.deployment.Capabilities; @@ -213,6 +214,7 @@ private void createPoolIfDefined(PgPoolRecorder recorder, .addType(Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -227,6 +229,7 @@ private void createPoolIfDefined(PgPoolRecorder recorder, .addType(io.vertx.mutiny.sqlclient.Pool.class) .scope(ApplicationScoped.class) .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .addInjectionPoint(ClassType.create(DataSourceSupport.class)) .createWith(recorder.mutinyPgPool(poolFunction)) .unremovable() .setRuntimeInit(); diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseDefaultDatasourceTest.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseDefaultDatasourceTest.java new file mode 100644 index 0000000000000..8dbb1f8094e8a --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseDefaultDatasourceTest.java @@ -0,0 +1,133 @@ +package io.quarkus.reactive.pg.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.pgclient.PgPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseDefaultDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.active", "false"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + PgPool pool = Arc.container().instance(PgPool.class).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.pgclient.PgPool pool = Arc.container().instance(io.vertx.mutiny.pgclient.PgPool.class) + .get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource '' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.active'" + + " to 'true' and configure datasource ''", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseNamedDatasourceTest.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseNamedDatasourceTest.java new file mode 100644 index 0000000000000..e40b129e6cabd --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/ConfigActiveFalseNamedDatasourceTest.java @@ -0,0 +1,140 @@ +package io.quarkus.reactive.pg.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.reactive.datasource.ReactiveDataSource; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.pgclient.PgPool; +import io.vertx.sqlclient.Pool; + +public class ConfigActiveFalseNamedDatasourceTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.datasource.ds-1.active", "false") + // We need at least one build-time property for the datasource, + // otherwise it's considered unconfigured at build time... + .overrideConfigKey("quarkus.datasource.ds-1.db-kind", "postgresql"); + + @Inject + MyBean myBean; + + @Test + public void pool() { + Pool pool = Arc.container().instance(Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyPool() { + io.vertx.mutiny.sqlclient.Pool pool = Arc.container().instance(io.vertx.mutiny.sqlclient.Pool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void vendorPool() { + PgPool pool = Arc.container().instance(PgPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void mutinyVendorPool() { + io.vertx.mutiny.pgclient.PgPool pool = Arc.container().instance(io.vertx.mutiny.pgclient.PgPool.class, + new ReactiveDataSource.ReactiveDataSourceLiteral("ds-1")).get(); + + // The bean is always available to be injected during static init + // since we don't know whether the datasource will be active at runtime. + // So the bean cannot be null. + assertThat(pool).isNotNull(); + // However, any attempt to use it at runtime will fail. + assertThatThrownBy(() -> pool.getConnection()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @Test + public void injectedBean() { + assertThatThrownBy(() -> myBean.usePool()) + .isInstanceOf(RuntimeException.class) + .cause() + .isInstanceOf(ConfigurationException.class) + .hasMessageContainingAll("Datasource 'ds-1' was deactivated through configuration properties.", + "To solve this, avoid accessing this datasource at runtime, for instance by deactivating consumers (persistence units, ...).", + "Alternatively, activate the datasource by setting configuration property 'quarkus.datasource.\"ds-1\".active'" + + " to 'true' and configure datasource 'ds-1'", + "Refer to https://quarkus.io/guides/datasource for guidance."); + } + + @ApplicationScoped + public static class MyBean { + @Inject + @ReactiveDataSource("ds-1") + Pool pool; + + public CompletionStage usePool() { + return pool.getConnection().toCompletionStage(); + } + } +} diff --git a/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/DataSourceHealthCheckConfigActiveFalseTest.java b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/DataSourceHealthCheckConfigActiveFalseTest.java new file mode 100644 index 0000000000000..1b9c7d61639ff --- /dev/null +++ b/extensions/reactive-pg-client/deployment/src/test/java/io/quarkus/reactive/pg/client/DataSourceHealthCheckConfigActiveFalseTest.java @@ -0,0 +1,30 @@ +package io.quarkus.reactive.pg.client; + +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class DataSourceHealthCheckConfigActiveFalseTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("application-default-datasource.properties") + .overrideConfigKey("quarkus.datasource.health.enabled", "true") + // this data source is broken, but will be deactivated, + // so the overall check should pass + .overrideConfigKey("quarkus.datasource.brokenDS.db-kind", "postgresql") + .overrideConfigKey("quarkus.datasource.brokenDS.reactive.url", "BROKEN") + .overrideConfigKey("quarkus.datasource.brokenDS.active", "false"); + + @Test + public void testDataSourceHealthCheckExclusion() { + RestAssured.when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } + +} diff --git a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java index 18149f6e67b1a..4afab742f0163 100644 --- a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java +++ b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/PgPoolRecorder.java @@ -24,6 +24,7 @@ import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; import io.quarkus.datasource.runtime.DataSourceRuntimeConfig; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.datasource.runtime.DataSourcesRuntimeConfig; import io.quarkus.reactive.datasource.ReactiveDataSource; import io.quarkus.reactive.datasource.runtime.ConnectOptionsSupplier; @@ -90,6 +91,9 @@ private PgPool initialize(VertxInternal vertx, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, DataSourceReactivePostgreSQLConfig dataSourceReactivePostgreSQLConfig, SyntheticCreationalContext context) { + if (context.getInjectedReference(DataSourceSupport.class).getInactiveNames().contains(dataSourceName)) { + throw DataSourceUtil.dataSourceInactive(dataSourceName); + } PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactivePostgreSQLConfig); List pgConnectOptionsList = toPgConnectOptions(dataSourceName, dataSourceRuntimeConfig, diff --git a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/health/ReactivePgDataSourcesHealthCheck.java b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/health/ReactivePgDataSourcesHealthCheck.java index c0228a3c99869..9cfc47a61dc2a 100644 --- a/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/health/ReactivePgDataSourcesHealthCheck.java +++ b/extensions/reactive-pg-client/runtime/src/main/java/io/quarkus/reactive/pg/client/runtime/health/ReactivePgDataSourcesHealthCheck.java @@ -11,7 +11,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; -import io.quarkus.datasource.runtime.DataSourcesHealthSupport; +import io.quarkus.datasource.runtime.DataSourceSupport; import io.quarkus.reactive.datasource.runtime.ReactiveDatasourceHealthCheck; import io.vertx.pgclient.PgPool; @@ -26,8 +26,8 @@ public ReactivePgDataSourcesHealthCheck() { @PostConstruct protected void init() { ArcContainer container = Arc.container(); - DataSourcesHealthSupport excluded = container.instance(DataSourcesHealthSupport.class).get(); - Set excludedNames = excluded.getExcludedNames(); + DataSourceSupport support = container.instance(DataSourceSupport.class).get(); + Set excludedNames = support.getInactiveOrHealthCheckExcludedNames(); for (InstanceHandle handle : container.select(PgPool.class, Any.Literal.INSTANCE).handles()) { String poolName = getPoolName(handle.getBean()); if (!excludedNames.contains(poolName)) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java index fae3611ccbfab..375324b773f92 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java @@ -31,6 +31,7 @@ import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.gizmo.ClassOutput; import io.quarkus.resteasy.reactive.common.deployment.JaxRsResourceIndexBuildItem; +import io.quarkus.resteasy.reactive.links.RestLinkId; import io.quarkus.resteasy.reactive.links.runtime.GetterAccessorsContainer; import io.quarkus.resteasy.reactive.links.runtime.GetterAccessorsContainerRecorder; import io.quarkus.resteasy.reactive.links.runtime.LinkInfo; @@ -150,35 +151,7 @@ private RuntimeValue implementPathParameterValueGetter validateClassHasFieldId(index, entityType); for (String parameterName : linkInfo.getPathParameters()) { - FieldInfoSupplier byParamName = new FieldInfoSupplier(c -> c.field(parameterName), className, index); - - // We implement a getter inside a class that has the required field. - // We later map that getter's accessor with an entity type. - // If a field is inside a parent class, the getter accessor will be mapped to each subclass which - // has REST links that need access to that field. - FieldInfo fieldInfo = byParamName.get(); - if ((fieldInfo == null) && parameterName.equals("id")) { - // this is a special case where we want to go through the fields of the class - // and see if any is annotated with any sort of @Id annotation - // N.B. as this module does not depend on any other module that could supply this @Id annotation - // (like Panache), we need this general lookup - FieldInfoSupplier byIdAnnotation = new FieldInfoSupplier( - c -> { - for (FieldInfo field : c.fields()) { - List annotationInstances = field.annotations(); - for (AnnotationInstance annotationInstance : annotationInstances) { - if (annotationInstance.name().toString().endsWith("persistence.Id")) { - return field; - } - } - } - return null; - }, - className, - index); - fieldInfo = byIdAnnotation.get(); - } - + FieldInfo fieldInfo = resolveField(index, parameterName, className); if (fieldInfo != null) { GetterMetadata getterMetadata = new GetterMetadata(fieldInfo); if (!implementedGetters.contains(getterMetadata)) { @@ -187,7 +160,8 @@ private RuntimeValue implementPathParameterValueGetter } getterAccessorsContainerRecorder.addAccessor(getterAccessorsContainer, - entityType, getterMetadata.getFieldName(), getterMetadata.getGetterAccessorName()); + entityType, parameterName, + getterMetadata.getGetterAccessorName()); } } } @@ -196,6 +170,52 @@ private RuntimeValue implementPathParameterValueGetter return getterAccessorsContainer; } + private FieldInfo resolveField(IndexView index, String parameterName, DotName className) { + FieldInfoSupplier byParamName = new FieldInfoSupplier(c -> c.field(parameterName), className, index); + + // check if we have field matching the name + FieldInfo fieldInfo = byParamName.get(); + if (parameterName.equals("id")) { + // this is a special case where we want to go through the fields of the class + // and see if any is annotated with any sort of @persistence.Id/@RestLinkId annotation + // N.B. as this module does not depend on any other module that could supply this @Id annotation + // (like Panache), we need this general lookup + // the order of preference for the annotations is @RestLinkId > @persistence.Id > id + FieldInfoSupplier byAnnotation = new FieldInfoSupplier( + c -> { + FieldInfo persistenceId = null; + for (FieldInfo field : c.fields()) { + // prefer RestLinId over Id + if (field.hasAnnotation(RestLinkId.class)) { + return field; + } + // keep the first found @persistence.Id annotation in case not @RestLinkId is found + if (fieldAnnotatedWith(field, "persistence.Id") && persistenceId == null) { + persistenceId = field; + } + } + return persistenceId; + }, + className, + index); + FieldInfo annotatedField = byAnnotation.get(); + if (annotatedField != null) { + fieldInfo = annotatedField; + } + } + return fieldInfo; + } + + private boolean fieldAnnotatedWith(FieldInfo field, String annotation) { + List annotationInstances = field.annotations(); + for (AnnotationInstance annotationInstance : annotationInstances) { + if (annotationInstance.name().toString().endsWith(annotation)) { + return true; + } + } + return false; + } + /** * Validates if the given classname contains a field `id` or annotated with `@Id` * @@ -227,15 +247,26 @@ private void validateRec(IndexView index, String entityType, ClassInfo classInfo .filter(a -> a.name().toString().endsWith("persistence.Id")) .toList(); + List fieldsAnnotatedWithRestLinkId = classInfo.fields().stream() + .flatMap(f -> f.annotations(RestLinkId.class).stream()) + .toList(); + + // @RestLinkId annotation count > 1 is not allowed + if (fieldsAnnotatedWithRestLinkId.size() > 1) { + throw new IllegalStateException("Cannot generate web links for the class " + entityType + + " because it has multiple fields annotated with `@RestLinkId`, where a maximum of one is allowed"); + } + // Id field found, break the loop - if (!fieldsNamedId.isEmpty() || !fieldsAnnotatedWithId.isEmpty()) + if (!fieldsNamedId.isEmpty() || !fieldsAnnotatedWithId.isEmpty() || !fieldsAnnotatedWithRestLinkId.isEmpty()) { return; + } // Id field not found and hope is gone DotName superClassName = classInfo.superName(); if (superClassName == null) { throw new IllegalStateException("Cannot generate web links for the class " + entityType + - " because is either missing an `id` field or a field with an `@Id` annotation"); + " because it is either missing an `id` field, a field with an `@Id` annotation or a field with a `@RestLinkId annotation"); } // Id field not found but there's still hope diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java index 5245b1fcec275..99f71ebaac2b0 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java @@ -28,4 +28,66 @@ void shouldGetHalLinksForInstance() { assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); } + + @Test + void shouldGetHalLinksForIdAndPersistenceIdAndRestLinkId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-id-and-persistence-id-and-rest-link-id/100") + .thenReturn(); + + assertThat(response.body() + .jsonPath() + .getString("_links.self.href")).endsWith("/records/with-id-and-persistence-id-and-rest-link-id/100"); + } + + @Test + void shouldGetHalLinksForIdAndPersistenceId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-id-and-persistence-id/10") + .thenReturn(); + + assertThat(response.body() + .jsonPath() + .getString("_links.self.href")).endsWith("/records/with-id-and-persistence-id/10"); + } + + @Test + void shouldGetHalLinksForIdAndRestLinkId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-id-and-rest-link-id/100") + .thenReturn(); + + assertThat(response.body() + .jsonPath() + .getString("_links.self.href")).endsWith("/records/with-id-and-rest-link-id/100"); + } + + @Test + void shouldGetHalLinksForPersistenceIdAndRestLinkId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-persistence-id-and-rest-link-id/100") + .thenReturn(); + + assertThat(response.body() + .jsonPath() + .getString("_links.self.href")).endsWith("/records/with-persistence-id-and-rest-link-id/100"); + } + + @Test + void shouldGetHalLinksForPersistenceId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-persistence-id/10") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/records/with-persistence-id/10"); + } + + @Test + void shouldGetHalLinksForRestLinkId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-rest-link-id/100") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/records/with-rest-link-id/100"); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java index b37da8dcb601f..6a35906db9843 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java @@ -12,7 +12,10 @@ public class HalLinksWithJacksonTest extends AbstractHalLinksTest { @RegisterExtension static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar - .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class)) + .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class, + TestRecordWithIdAndPersistenceIdAndRestLinkId.class, TestRecordWithIdAndRestLinkId.class, + TestRecordWithIdAndPersistenceId.class, TestRecordWithPersistenceId.class, + TestRecordWithRestLinkId.class, TestRecordWithPersistenceIdAndRestLinkId.class)) .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-resteasy-reactive-jackson", Version.getVersion()), Dependency.of("io.quarkus", "quarkus-hal", Version.getVersion()))) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java index 778847fbdbdea..9cf3b61e7e91d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java @@ -12,7 +12,10 @@ public class HalLinksWithJsonbTest extends AbstractHalLinksTest { @RegisterExtension static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar - .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class)) + .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class, + TestRecordWithIdAndPersistenceIdAndRestLinkId.class, TestRecordWithIdAndRestLinkId.class, + TestRecordWithIdAndPersistenceId.class, TestRecordWithPersistenceId.class, + TestRecordWithRestLinkId.class, TestRecordWithPersistenceIdAndRestLinkId.class)) .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-resteasy-reactive-jsonb", Version.getVersion()), Dependency.of("io.quarkus", "quarkus-hal", Version.getVersion()))) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java index f8610662664fe..1efbc3f9d384a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java @@ -19,7 +19,10 @@ public class RestLinksInjectionTest { @RegisterExtension static final QuarkusUnitTest TEST = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class)); + .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class, + TestRecordWithIdAndPersistenceIdAndRestLinkId.class, TestRecordWithIdAndRestLinkId.class, + TestRecordWithIdAndPersistenceId.class, TestRecordWithPersistenceId.class, + TestRecordWithRestLinkId.class, TestRecordWithPersistenceIdAndRestLinkId.class)); @TestHTTPResource("records") String recordsUrl; diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionMultipleRestLinkIdTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionMultipleRestLinkIdTest.java new file mode 100644 index 0000000000000..0b85bcbc39050 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionMultipleRestLinkIdTest.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.util.ExceptionUtil; +import io.quarkus.test.QuarkusUnitTest; + +public class RestLinksWithFailureInjectionMultipleRestLinkIdTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot( + jar -> jar.addClasses(TestRecordMultipleRestLinkIds.class, TestResourceMultipleRestLinkIds.class)) + .assertException(t -> { + Throwable rootCause = ExceptionUtil.getRootCause(t); + assertThat(rootCause).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Cannot generate web links for the class " + + "io.quarkus.resteasy.reactive.links.deployment.TestRecordMultipleRestLinkIds" + + " because it has multiple fields annotated with `@RestLinkId`, where a maximum of one is allowed"); + }); + + @Test + void validationFailed() { + // Should not be reached: verify + fail(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionTest.java index 72791f897f1b6..b338136024942 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionTest.java @@ -1,7 +1,7 @@ package io.quarkus.resteasy.reactive.links.deployment; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -17,13 +17,13 @@ public class RestLinksWithFailureInjectionTest { Throwable rootCause = ExceptionUtil.getRootCause(t); assertThat(rootCause).isInstanceOf(IllegalStateException.class) .hasMessageContaining("Cannot generate web links for the class " + - "io.quarkus.resteasy.reactive.links.deployment.TestRecordNoId because is either " + - "missing an `id` field or a field with an `@Id` annotation"); + "io.quarkus.resteasy.reactive.links.deployment.TestRecordNoId because it is " + + "either missing an `id` field, a field with an `@Id` annotation or a field with a `@RestLinkId annotation"); }); @Test void validationFailed() { // Should not be reached: verify - assertTrue(false); + fail(); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordMultipleRestLinkIds.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordMultipleRestLinkIds.java new file mode 100644 index 0000000000000..90706ba0a1d32 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordMultipleRestLinkIds.java @@ -0,0 +1,46 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; + +public class TestRecordMultipleRestLinkIds { + + @RestLinkId + private long idOne; + @RestLinkId + private long idTwo; + + private String name; + + public TestRecordMultipleRestLinkIds() { + } + + public TestRecordMultipleRestLinkIds(long idOne, long idTwo, String name) { + this.idOne = idOne; + this.idTwo = idTwo; + this.name = name; + } + + public long getIdOne() { + return idOne; + } + + public void setIdOne(long idOne) { + this.idOne = idOne; + } + + public long getIdTwo() { + return idTwo; + } + + public void setIdTwo(long idTwo) { + this.idTwo = idTwo; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceId.java new file mode 100644 index 0000000000000..aa991505e4045 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceId.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.deployment.persistence.Id; + +public class TestRecordWithIdAndPersistenceId { + + @Id + private int persistenceId; + private int id; + private String name; + + public TestRecordWithIdAndPersistenceId() { + } + + public TestRecordWithIdAndPersistenceId(int persistenceId, int id, String value) { + this.persistenceId = persistenceId; + this.id = id; + this.name = value; + } + + public int getPersistenceId() { + return persistenceId; + } + + public void setPersistenceId(int persistenceId) { + this.persistenceId = persistenceId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceIdAndRestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceIdAndRestLinkId.java new file mode 100644 index 0000000000000..3f03638b8175b --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceIdAndRestLinkId.java @@ -0,0 +1,56 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; +import io.quarkus.resteasy.reactive.links.deployment.persistence.Id; + +public class TestRecordWithIdAndPersistenceIdAndRestLinkId { + + @RestLinkId + private int restLinkId; + @Id + private int persistenceId; + private int id; + private String name; + + public TestRecordWithIdAndPersistenceIdAndRestLinkId() { + } + + public TestRecordWithIdAndPersistenceIdAndRestLinkId(int restLinkId, int persistenceId, int id, String name) { + this.restLinkId = restLinkId; + this.persistenceId = persistenceId; + this.id = id; + this.name = name; + } + + public int getRestLinkId() { + return restLinkId; + } + + public void setRestLinkId(int restLinkId) { + this.restLinkId = restLinkId; + } + + public int getPersistenceId() { + return persistenceId; + } + + public void setPersistenceId(int persistenceId) { + this.persistenceId = persistenceId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndRestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndRestLinkId.java new file mode 100644 index 0000000000000..d0de5c6c2b73f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndRestLinkId.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; + +public class TestRecordWithIdAndRestLinkId { + + @RestLinkId + private int restLinkId; + private int id; + private String name; + + public TestRecordWithIdAndRestLinkId() { + } + + public TestRecordWithIdAndRestLinkId(int restLinkId, int id, String value) { + this.restLinkId = restLinkId; + this.id = id; + this.name = value; + } + + public int getRestLinkId() { + return restLinkId; + } + + public void setRestLinkId(int restLinkId) { + this.restLinkId = restLinkId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceId.java new file mode 100644 index 0000000000000..8ab9924a5d6da --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceId.java @@ -0,0 +1,34 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.deployment.persistence.Id; + +public class TestRecordWithPersistenceId { + + @Id + private int persistenceId; + private String name; + + public TestRecordWithPersistenceId() { + } + + public TestRecordWithPersistenceId(int persistenceId, String value) { + this.persistenceId = persistenceId; + this.name = value; + } + + public int getPersistenceId() { + return persistenceId; + } + + public void setPersistenceId(int persistenceId) { + this.persistenceId = persistenceId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceIdAndRestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceIdAndRestLinkId.java new file mode 100644 index 0000000000000..bb12576819eb8 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceIdAndRestLinkId.java @@ -0,0 +1,46 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; +import io.quarkus.resteasy.reactive.links.deployment.persistence.Id; + +public class TestRecordWithPersistenceIdAndRestLinkId { + + @RestLinkId + private int restLinkId; + @Id + private int persistenceId; + private String name; + + public TestRecordWithPersistenceIdAndRestLinkId() { + } + + public TestRecordWithPersistenceIdAndRestLinkId(int restLinkId, int persistenceId, String name) { + this.restLinkId = restLinkId; + this.persistenceId = persistenceId; + this.name = name; + } + + public int getRestLinkId() { + return restLinkId; + } + + public void setRestLinkId(int restLinkId) { + this.restLinkId = restLinkId; + } + + public int getPersistenceId() { + return persistenceId; + } + + public void setPersistenceId(int persistenceId) { + this.persistenceId = persistenceId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithRestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithRestLinkId.java new file mode 100644 index 0000000000000..163a19e8551b2 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithRestLinkId.java @@ -0,0 +1,34 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; + +public class TestRecordWithRestLinkId { + + @RestLinkId + private int restLinkId; + private String name; + + public TestRecordWithRestLinkId() { + } + + public TestRecordWithRestLinkId(int restLinkId, String value) { + this.restLinkId = restLinkId; + this.name = value; + } + + public int getRestLinkId() { + return restLinkId; + } + + public void setRestLinkId(int restLinkId) { + this.restLinkId = restLinkId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java index cbfd69aa57159..50255cd5f39cb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java @@ -29,6 +29,34 @@ public class TestResource { new TestRecord(ID_COUNTER.incrementAndGet(), "first", "First value"), new TestRecord(ID_COUNTER.incrementAndGet(), "second", "Second value"))); + private static final List ID_AND_PERSISTENCE_ID_AND_REST_LINK_IDS = new LinkedList<>( + Arrays.asList( + new TestRecordWithIdAndPersistenceIdAndRestLinkId(100, 10, 1, "One"), + new TestRecordWithIdAndPersistenceIdAndRestLinkId(101, 11, 2, "Two"))); + + private static final List PERSISTENCE_ID_AND_REST_LINK_IDS = new LinkedList<>( + Arrays.asList( + new TestRecordWithPersistenceIdAndRestLinkId(100, 10, "One"), + new TestRecordWithPersistenceIdAndRestLinkId(101, 11, "Two"))); + + private static final List ID_AND_PERSISTENCE_ID_RECORDS = new LinkedList<>( + Arrays.asList( + new TestRecordWithIdAndPersistenceId(10, 1, "One"), + new TestRecordWithIdAndPersistenceId(11, 2, "Two"))); + + private static final List ID_AND_REST_LINK_ID_RECORDS = new LinkedList<>( + Arrays.asList( + new TestRecordWithIdAndRestLinkId(100, 1, "One"), + new TestRecordWithIdAndRestLinkId(101, 2, "Two"))); + + private static final List PERSISTENCE_ID_RECORDS = new LinkedList<>(Arrays.asList( + new TestRecordWithPersistenceId(10, "One"), + new TestRecordWithPersistenceId(11, "Two"))); + + private static final List REST_LINK_ID_RECORDS = new LinkedList<>(Arrays.asList( + new TestRecordWithRestLinkId(100, "One"), + new TestRecordWithRestLinkId(101, "Two"))); + @GET @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) @RestLink(entityType = TestRecord.class) @@ -80,4 +108,77 @@ public TestRecord getBySlugOrId(@PathParam("slugOrId") String slugOrId) { .findFirst() .orElseThrow(NotFoundException::new); } + + @GET + @Path("/with-id-and-persistence-id-and-rest-link-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithIdAndPersistenceIdAndRestLinkId.class) + @InjectRestLinks + public TestRecordWithIdAndPersistenceIdAndRestLinkId getWithIdAndPersistenceIdAndRestLinkId(@PathParam("id") int id) { + return ID_AND_PERSISTENCE_ID_AND_REST_LINK_IDS.stream() + .filter(record -> record.getRestLinkId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-persistence-id-and-rest-link-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithPersistenceIdAndRestLinkId.class) + @InjectRestLinks + public TestRecordWithPersistenceIdAndRestLinkId getWithPersistenceIdAndRestLinkId(@PathParam("id") int id) { + return PERSISTENCE_ID_AND_REST_LINK_IDS.stream() + .filter(record -> record.getRestLinkId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-id-and-persistence-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithIdAndPersistenceId.class) + @InjectRestLinks + public TestRecordWithIdAndPersistenceId getWithIdAndPersistenceId(@PathParam("id") int id) { + return ID_AND_PERSISTENCE_ID_RECORDS.stream() + .filter(record -> record.getPersistenceId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-id-and-rest-link-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithIdAndRestLinkId.class) + @InjectRestLinks + public TestRecordWithIdAndRestLinkId getWithIdAndRestLinkId(@PathParam("id") int id) { + return ID_AND_REST_LINK_ID_RECORDS.stream() + .filter(record -> record.getRestLinkId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-persistence-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithPersistenceId.class) + @InjectRestLinks + public TestRecordWithPersistenceId getWithPersistenceId(@PathParam("id") int id) { + return PERSISTENCE_ID_RECORDS.stream() + .filter(record -> record.getPersistenceId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-rest-link-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithRestLinkId.class) + @InjectRestLinks + public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) { + return REST_LINK_ID_RECORDS.stream() + .filter(record -> record.getRestLinkId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResourceMultipleRestLinkIds.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResourceMultipleRestLinkIds.java new file mode 100644 index 0000000000000..bb1bdfc449d0e --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResourceMultipleRestLinkIds.java @@ -0,0 +1,33 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.time.Duration; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.common.util.RestMediaType; + +import io.quarkus.resteasy.reactive.links.InjectRestLinks; +import io.quarkus.resteasy.reactive.links.RestLink; +import io.smallrye.mutiny.Uni; + +@Path("/recordsMultipleRestLinkIds") +@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) +public class TestResourceMultipleRestLinkIds { + + private static final List RECORDS = new LinkedList<>(Arrays.asList( + new TestRecordMultipleRestLinkIds(10, 20, "first_value"), + new TestRecordMultipleRestLinkIds(11, 22, "second_value"))); + + @GET + @RestLink(entityType = TestRecordMultipleRestLinkIds.class) + @InjectRestLinks + public Uni> getAll() { + return Uni.createFrom().item(RECORDS).onItem().delayIt().by(Duration.ofMillis(100)); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/persistence/Id.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/persistence/Id.java new file mode 100644 index 0000000000000..7879297ffd300 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/persistence/Id.java @@ -0,0 +1,7 @@ +package io.quarkus.resteasy.reactive.links.deployment.persistence; + +/** + * Dummy annotation to test the persistence.Id annotation functionality. + */ +public @interface Id { +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkId.java new file mode 100644 index 0000000000000..dd6164644a6f9 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkId.java @@ -0,0 +1,17 @@ +package io.quarkus.resteasy.reactive.links; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark a field on a resource to be a provider for 'id' parameters in associated Web links. + *

    + * The RestLinkId annotation can be used at field level. + *

    + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +public @interface RestLinkId { +} diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java index 3179c70c9f734..f2139649f24a1 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java @@ -31,20 +31,18 @@ public class QuarkusIdentityProviderManagerImpl implements IdentityProviderManag private static final Logger log = Logger.getLogger(QuarkusIdentityProviderManagerImpl.class); private final Map, List> providers; - private final List augmenters; - private final BlockingSecurityExecutor blockingExecutor; - - private final AuthenticationRequestContext blockingRequestContext = new AuthenticationRequestContext() { - @Override - public Uni runBlocking(Supplier function) { - return blockingExecutor.executeBlocking(function); - } - }; + private final SecurityIdentityAugmentor[] augmenters; + private final AuthenticationRequestContext blockingRequestContext; QuarkusIdentityProviderManagerImpl(Builder builder) { this.providers = builder.providers; - this.augmenters = builder.augmentors; - this.blockingExecutor = builder.blockingExecutor; + this.augmenters = builder.augmentors.toArray(SecurityIdentityAugmentor[]::new); + this.blockingRequestContext = new AuthenticationRequestContext() { + @Override + public Uni runBlocking(Supplier function) { + return builder.blockingExecutor.executeBlocking(function); + } + }; } /** @@ -66,7 +64,7 @@ public Uni authenticate(AuthenticationRequest request) { if (providers.size() == 1) { return handleSingleProvider(providers.get(0), request); } - return handleProvider(0, (List) providers, request, blockingRequestContext); + return handleProvider(0, (List) providers, request); } catch (Throwable t) { return Uni.createFrom().failure(t); } @@ -81,12 +79,12 @@ public Throwable get() { return new AuthenticationFailedException(); } }); - if (!augmenters.isEmpty()) { + if (augmenters.length > 0) { authenticated = authenticated .flatMap(new Function>() { @Override public Uni apply(SecurityIdentity securityIdentity) { - return handleIdentityFromProvider(0, securityIdentity, blockingRequestContext); + return handleIdentityFromProvider(0, securityIdentity, request.getAttributes()); } }); } @@ -108,47 +106,48 @@ public SecurityIdentity authenticateBlocking(AuthenticationRequest request) { throw new IllegalArgumentException( "No IdentityProviders were registered to handle AuthenticationRequest " + request); } - return (SecurityIdentity) handleProvider(0, (List) providers, request, blockingRequestContext).await().indefinitely(); + return (SecurityIdentity) handleProvider(0, (List) providers, request).await().indefinitely(); } private Uni handleProvider(int pos, - List> providers, T request, AuthenticationRequestContext context) { + List> providers, T request) { if (pos == providers.size()) { //we failed to authentication log.debug("Authentication failed as providers would authenticate the request"); return Uni.createFrom().failure(new AuthenticationFailedException()); } IdentityProvider current = providers.get(pos); - Uni cs = current.authenticate(request, context) + Uni cs = current.authenticate(request, blockingRequestContext) .onItem().transformToUni(new Function>() { @Override public Uni apply(SecurityIdentity securityIdentity) { if (securityIdentity != null) { return Uni.createFrom().item(securityIdentity); } - return handleProvider(pos + 1, providers, request, context); + return handleProvider(pos + 1, providers, request); } }); return cs.onItem().transformToUni(new Function>() { @Override public Uni apply(SecurityIdentity securityIdentity) { - return handleIdentityFromProvider(0, securityIdentity, context); + return handleIdentityFromProvider(0, securityIdentity, request.getAttributes()); } }); } private Uni handleIdentityFromProvider(int pos, SecurityIdentity identity, - AuthenticationRequestContext context) { - if (pos == augmenters.size()) { + Map attributes) { + if (pos == augmenters.length) { return Uni.createFrom().item(identity); } - SecurityIdentityAugmentor a = augmenters.get(pos); - return a.augment(identity, context).flatMap(new Function>() { - @Override - public Uni apply(SecurityIdentity securityIdentity) { - return handleIdentityFromProvider(pos + 1, securityIdentity, context); - } - }); + SecurityIdentityAugmentor a = augmenters[pos]; + return a.augment(identity, blockingRequestContext, attributes) + .flatMap(new Function>() { + @Override + public Uni apply(SecurityIdentity securityIdentity) { + return handleIdentityFromProvider(pos + 1, securityIdentity, attributes); + } + }); } /** diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java index 3899c03b69169..69d292bd81634 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java @@ -5,7 +5,6 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -217,7 +216,6 @@ public void defineHealthRoutes(BuildProducer routes, .routeConfigKey("quarkus.smallrye-health.root-path") .handler(new SmallRyeHealthHandler()) .displayOnNotFoundPage() - .blockingRoute() .build()); // Register the liveness handler @@ -226,7 +224,6 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.livenessPath) .handler(new SmallRyeLivenessHandler()) .displayOnNotFoundPage() - .blockingRoute() .build()); // Register the readiness handler @@ -235,29 +232,14 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.readinessPath) .handler(new SmallRyeReadinessHandler()) .displayOnNotFoundPage() - .blockingRoute() .build()); - // Find all health groups - Set healthGroups = new HashSet<>(); - // with simple @HealthGroup annotations - for (AnnotationInstance healthGroupAnnotation : index.getAnnotations(HEALTH_GROUP)) { - healthGroups.add(healthGroupAnnotation.value().asString()); - } - // with @HealthGroups repeatable annotations - for (AnnotationInstance healthGroupsAnnotation : index.getAnnotations(HEALTH_GROUPS)) { - for (AnnotationInstance healthGroupAnnotation : healthGroupsAnnotation.value().asNestedArray()) { - healthGroups.add(healthGroupAnnotation.value().asString()); - } - } - // Register the health group handlers routes.produce(nonApplicationRootPathBuildItem.routeBuilder() .management("quarkus.smallrye-health.management.enabled") .nestedRoute(healthConfig.rootPath, healthConfig.groupPath) .handler(new SmallRyeHealthGroupHandler()) .displayOnNotFoundPage() - .blockingRoute() .build()); SmallRyeIndividualHealthGroupHandler handler = new SmallRyeIndividualHealthGroupHandler(); @@ -266,7 +248,6 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.groupPath + "/*") .handler(handler) .displayOnNotFoundPage() - .blockingRoute() .build()); // Register the wellness handler @@ -275,7 +256,6 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.wellnessPath) .handler(new SmallRyeWellnessHandler()) .displayOnNotFoundPage() - .blockingRoute() .build()); // Register the startup handler @@ -284,7 +264,6 @@ public void defineHealthRoutes(BuildProducer routes, .nestedRoute(healthConfig.rootPath, healthConfig.startupPath) .handler(new SmallRyeStartupHandler()) .displayOnNotFoundPage() - .blockingRoute() .build()); } diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/BlockingNonBlockingTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/BlockingNonBlockingTest.java new file mode 100644 index 0000000000000..d1ef2e076d593 --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/BlockingNonBlockingTest.java @@ -0,0 +1,78 @@ +package io.quarkus.smallrye.health.test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; + +import java.time.Duration; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.web.Route; +import io.restassured.RestAssured; +import io.restassured.parsing.Parser; +import io.smallrye.common.annotation.Blocking; +import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; + +public class BlockingNonBlockingTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(BlockingHealthCheck.class, Routes.class) + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void testRegisterHealthOnBlockingThreadStep1() { + // initial startup health blocking call on worker thread + given() + .when().get("/start-health") + .then().statusCode(200); + + try { + RestAssured.defaultParser = Parser.JSON; + // repeat the call a few times since the block isn't always logged + for (int i = 0; i < 3; i++) { + RestAssured.when().get("/q/health").then() + .body("status", is("UP"), + "checks.status", contains("UP"), + "checks.name", contains("blocking")); + } + } finally { + RestAssured.reset(); + } + } + + @Liveness + static final class BlockingHealthCheck implements HealthCheck { + @Override + public HealthCheckResponse call() { + // await() is illegal on the executor thread + Uni.createFrom().item(42).onItem().delayIt().by(Duration.ofMillis(10)).await().indefinitely(); + return HealthCheckResponse.up("blocking"); + } + } + + @ApplicationScoped + static final class Routes { + + @Inject + SmallRyeHealthReporter smallRyeHealthReporter; + + @Route(path = "/start-health", methods = Route.HttpMethod.GET) + @Blocking + public String health() { + return smallRyeHealthReporter.getHealth().toString(); + } + } +} diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java index de343b4f38b85..cfec52c933b1b 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java @@ -5,7 +5,6 @@ import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; -import io.quarkus.runtime.BlockingOperationControl; import io.smallrye.health.AsyncHealthCheckFactory; import io.smallrye.health.api.AsyncHealthCheck; import io.smallrye.mutiny.Uni; @@ -28,14 +27,12 @@ public QuarkusAsyncHealthCheckFactory(Vertx vertx) { @Override public Uni callSync(HealthCheck healthCheck) { Uni healthCheckResponseUni = super.callSync(healthCheck); - return BlockingOperationControl.isBlockingAllowed() ? healthCheckResponseUni - : healthCheckResponseUni.runSubscriptionOn(MutinyHelper.blockingExecutor(vertx, false)); + return healthCheckResponseUni.runSubscriptionOn(MutinyHelper.blockingExecutor(vertx, false)); } @Override public Uni callAsync(AsyncHealthCheck asyncHealthCheck) { Uni healthCheckResponseUni = super.callAsync(asyncHealthCheck); - return !BlockingOperationControl.isBlockingAllowed() ? healthCheckResponseUni - : healthCheckResponseUni.runSubscriptionOn(MutinyHelper.executor(vertx)); + return healthCheckResponseUni.runSubscriptionOn(MutinyHelper.executor(vertx)); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java index 84c5c6fa62d0c..95b87746c1b08 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthGroupHandler.java @@ -2,12 +2,13 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeHealthGroupHandler extends SmallRyeHealthHandlerBase { @Override - protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getHealthGroups(); + protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getHealthGroupsAsync(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java index 6960bb284bce9..6d9d33066e8fb 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandler.java @@ -2,12 +2,13 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeHealthHandler extends SmallRyeHealthHandlerBase { @Override - protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getHealth(); + protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getHealthAsync(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java index fff1485398fbc..e999375418769 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java @@ -10,7 +10,11 @@ import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.vertx.MutinyHelper; +import io.vertx.core.Context; import io.vertx.core.Handler; +import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerResponse; @@ -18,7 +22,7 @@ abstract class SmallRyeHealthHandlerBase implements Handler { - protected abstract SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext); + protected abstract Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext); @Override public void handle(RoutingContext ctx) { @@ -41,19 +45,21 @@ private void doHandle(RoutingContext ctx) { Arc.container().instance(CurrentIdentityAssociation.class).get().setIdentity(user.getSecurityIdentity()); } SmallRyeHealthReporter reporter = Arc.container().instance(SmallRyeHealthReporter.class).get(); - SmallRyeHealth health = getHealth(reporter, ctx); - HttpServerResponse resp = ctx.response(); - if (health.isDown()) { - resp.setStatusCode(503); - } - resp.headers().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8"); - Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks - try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) { - reporter.reportHealth(outputStream, health); - resp.end(buffer); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + Context context = Vertx.currentContext(); + getHealth(reporter, ctx).emitOn(MutinyHelper.executor(context)) + .subscribe().with(health -> { + HttpServerResponse resp = ctx.response(); + if (health.isDown()) { + resp.setStatusCode(503); + } + resp.headers().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8"); + Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks + try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) { + reporter.reportHealth(outputStream, health); + resp.end(buffer); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } - } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java index 66f960791ad8d..e0c7ba3874439 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeIndividualHealthGroupHandler.java @@ -2,13 +2,14 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeIndividualHealthGroupHandler extends SmallRyeHealthHandlerBase { @Override - protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { String group = ctx.normalizedPath().substring(ctx.normalizedPath().lastIndexOf("/") + 1); - return reporter.getHealthGroup(group); + return reporter.getHealthGroupAsync(group); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java index a5cf3dd904cbe..ad33e824ff3d7 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeLivenessHandler.java @@ -2,12 +2,13 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeLivenessHandler extends SmallRyeHealthHandlerBase { @Override - protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { - return reporter.getLiveness(); + protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getLivenessAsync(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java index a23a3e1f9d538..18c652bd673bd 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeReadinessHandler.java @@ -2,12 +2,13 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeReadinessHandler extends SmallRyeHealthHandlerBase { @Override - protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { - return reporter.getReadiness(); + protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getReadinessAsync(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java index c450430735ecb..cd1ae14846cc9 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeStartupHandler.java @@ -2,12 +2,13 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeStartupHandler extends SmallRyeHealthHandlerBase { @Override - protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { - return reporter.getStartup(); + protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getStartupAsync(); } } diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java index 84ca3860c1cae..e2131f51de416 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeWellnessHandler.java @@ -2,12 +2,13 @@ import io.smallrye.health.SmallRyeHealth; import io.smallrye.health.SmallRyeHealthReporter; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; public class SmallRyeWellnessHandler extends SmallRyeHealthHandlerBase { @Override - protected SmallRyeHealth getHealth(SmallRyeHealthReporter reporter, RoutingContext routingContext) { - return reporter.getWellness(); + protected Uni getHealth(SmallRyeHealthReporter reporter, RoutingContext ctx) { + return reporter.getWellnessAsync(); } } diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/StockMethodsAdder.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/StockMethodsAdder.java index c719ced1638ae..f8fa8fce729a0 100644 --- a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/StockMethodsAdder.java +++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/generate/StockMethodsAdder.java @@ -87,9 +87,9 @@ public void add(ClassCreator classCreator, FieldDescriptor entityClassFieldDescr // and if so generate the implementation while also keeping the proper records generateSave(classCreator, generatedClassName, entityDotName, entityTypeStr, - allMethodsToBeImplementedToResult); + allMethodsToBeImplementedToResult, entityClassFieldDescriptor); generateSaveAndFlush(classCreator, generatedClassName, entityDotName, entityTypeStr, - allMethodsToBeImplementedToResult); + allMethodsToBeImplementedToResult, entityClassFieldDescriptor); generateSaveAll(classCreator, entityClassFieldDescriptor, generatedClassName, entityDotName, entityTypeStr, allMethodsToBeImplementedToResult); generateFlush(classCreator, generatedClassName, allMethodsToBeImplementedToResult); @@ -121,7 +121,8 @@ public void add(ClassCreator classCreator, FieldDescriptor entityClassFieldDescr private void generateSave(ClassCreator classCreator, String generatedClassName, DotName entityDotName, String entityTypeStr, - Map allMethodsToBeImplementedToResult) { + Map allMethodsToBeImplementedToResult, + FieldDescriptor entityClassFieldDescriptor) { MethodDescriptor saveDescriptor = MethodDescriptor.ofMethod(generatedClassName, "save", entityTypeStr, entityTypeStr); @@ -144,7 +145,7 @@ private void generateSave(ClassCreator classCreator, String generatedClassName, entity); BranchResult isNewBranch = save.ifTrue(isNew); generatePersistAndReturn(entity, isNewBranch.trueBranch()); - generateMergeAndReturn(entity, isNewBranch.falseBranch()); + generateMergeAndReturn(entity, isNewBranch.falseBranch(), entityClassFieldDescriptor); } else { AnnotationTarget idAnnotationTarget = getIdAnnotationTarget(entityDotName, index); ResultHandle idValue = generateObtainValue(save, entityDotName, entity, idAnnotationTarget); @@ -167,7 +168,7 @@ private void generateSave(ClassCreator classCreator, String generatedClassName, versionValueTarget.get()); BranchResult versionValueIsNullBranch = save.ifNull(versionValue); generatePersistAndReturn(entity, versionValueIsNullBranch.trueBranch()); - generateMergeAndReturn(entity, versionValueIsNullBranch.falseBranch()); + generateMergeAndReturn(entity, versionValueIsNullBranch.falseBranch(), entityClassFieldDescriptor); } BytecodeCreator idValueUnset; @@ -192,7 +193,7 @@ private void generateSave(ClassCreator classCreator, String generatedClassName, idValueUnset = idValueNullBranch.trueBranch(); } generatePersistAndReturn(entity, idValueUnset); - generateMergeAndReturn(entity, idValueSet); + generateMergeAndReturn(entity, idValueSet, entityClassFieldDescriptor); } } try (MethodCreator bridgeSave = classCreator.getMethodCreator(bridgeSaveDescriptor)) { @@ -236,10 +237,13 @@ private void generatePersistAndReturn(ResultHandle entity, BytecodeCreator bytec bytecodeCreator.returnValue(entity); } - private void generateMergeAndReturn(ResultHandle entity, BytecodeCreator bytecodeCreator) { + private void generateMergeAndReturn(ResultHandle entity, BytecodeCreator bytecodeCreator, + FieldDescriptor entityClassFieldDescriptor) { + ResultHandle entityClass = bytecodeCreator.readInstanceField(entityClassFieldDescriptor, bytecodeCreator.getThis()); ResultHandle entityManager = bytecodeCreator.invokeVirtualMethod( - ofMethod(AbstractJpaOperations.class, "getEntityManager", EntityManager.class), - bytecodeCreator.readStaticField(operationsField)); + ofMethod(AbstractJpaOperations.class, "getEntityManager", EntityManager.class, Class.class), + bytecodeCreator.readStaticField(operationsField), + entityClass); entity = bytecodeCreator.invokeInterfaceMethod( MethodDescriptor.ofMethod(EntityManager.class, "merge", Object.class, Object.class), entityManager, entity); @@ -280,7 +284,7 @@ private Type getTypeOfTarget(AnnotationTarget idAnnotationTarget) { private void generateSaveAndFlush(ClassCreator classCreator, String generatedClassName, DotName entityDotName, String entityTypeStr, - Map allMethodsToBeImplementedToResult) { + Map allMethodsToBeImplementedToResult, FieldDescriptor entityClassFieldDescriptor) { MethodDescriptor saveAndFlushDescriptor = MethodDescriptor.ofMethod(generatedClassName, "saveAndFlush", entityTypeStr, entityTypeStr); @@ -298,7 +302,7 @@ private void generateSaveAndFlush(ClassCreator classCreator, // we need to force the generation of findById since this method depends on it allMethodsToBeImplementedToResult.put(save, false); generateSave(classCreator, generatedClassName, entityDotName, entityTypeStr, - allMethodsToBeImplementedToResult); + allMethodsToBeImplementedToResult, entityClassFieldDescriptor); try (MethodCreator saveAndFlush = classCreator.getMethodCreator(saveAndFlushDescriptor)) { saveAndFlush.addAnnotation(Transactional.class); diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/FixedLocaleJavaType.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/FixedLocaleJavaType.java index bc3819dc7287c..83eaa58a4af0d 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/FixedLocaleJavaType.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/FixedLocaleJavaType.java @@ -1,9 +1,3 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ package io.quarkus.spring.data.deployment; import java.util.Locale; diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/MultiplePersistenceUnitConfigTest.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/MultiplePersistenceUnitConfigTest.java index 03bdfddb810eb..5c9a54bd7a93c 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/MultiplePersistenceUnitConfigTest.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/MultiplePersistenceUnitConfigTest.java @@ -1,9 +1,20 @@ package io.quarkus.spring.data.deployment.multiple_pu; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Supplier; + +import jakarta.inject.Inject; + import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.spring.data.deployment.multiple_pu.first.FirstEntity; import io.quarkus.spring.data.deployment.multiple_pu.first.FirstEntityRepository; import io.quarkus.spring.data.deployment.multiple_pu.second.SecondEntity; @@ -21,6 +32,17 @@ public class MultiplePersistenceUnitConfigTest { PanacheTestResource.class) .addAsResource("application-multiple-persistence-units.properties", "application.properties")); + @Inject + private FirstEntityRepository repository1; + @Inject + private SecondEntityRepository repository2; + + @BeforeEach + void beforeEach() { + repository1.deleteAll(); + repository2.deleteAll(); + } + @Test public void panacheOperations() { /** @@ -35,4 +57,64 @@ public void panacheOperations() { RestAssured.when().get("/persistence-unit/second/name-1").then().body(Matchers.is("1")); RestAssured.when().get("/persistence-unit/second/name-2").then().body(Matchers.is("2")); } + + @Test + public void entityLifecycle() { + var detached = repository2.save(new SecondEntity()); + assertThat(detached.id).isNotNull(); + assertThat(inTx(repository2::count)).isEqualTo(1); + + detached.name = "name"; + repository2.save(detached); + assertThat(inTx(repository2::count)).isEqualTo(1); + + inTx(() -> { + var lazyRef = repository2.getOne(detached.id); + assertThat(lazyRef.name).isEqualTo(detached.name); + return null; + }); + + repository2.deleteByName("otherThan" + detached.name); + assertThat(inTx(() -> repository2.findById(detached.id))).isPresent(); + + repository2.deleteByName(detached.name); + assertThat(inTx(() -> repository2.findById(detached.id))).isEmpty(); + } + + @Test + void pagedQueries() { + var newEntity = new SecondEntity(); + newEntity.name = "name"; + var detached = repository2.save(newEntity); + + Pageable pageable = PageRequest.of(0, 10, Sort.Direction.DESC, "id"); + + var page = inTx(() -> repository2.findByName(detached.name, pageable)); + assertThat(page.getContent()).extracting(e -> e.id).containsExactly(detached.id); + + var pageIndexParam = inTx(() -> repository2.findByNameQueryIndexed(detached.name, pageable)); + assertThat(pageIndexParam.getContent()).extracting(e -> e.id).containsExactly(detached.id); + + var pageNamedParam = inTx(() -> repository2.findByNameQueryNamed(detached.name, pageable)); + assertThat(pageNamedParam.getContent()).extracting(e -> e.id).containsExactly(detached.id); + } + + @Test + void cascading() { + var newParent = new SecondEntity(); + newParent.name = "parent"; + var newChild = new SecondEntity(); + newChild.name = "child"; + newParent.child = newChild; + var detachedParent = repository2.save(newParent); + + assertThat(inTx(repository2::count)).isEqualTo(2); + + repository2.deleteByName(detachedParent.name); + assertThat(inTx(repository2::count)).isZero(); + } + + private T inTx(Supplier action) { + return QuarkusTransaction.requiringNew().call(action::get); + } } diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/second/SecondEntity.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/second/SecondEntity.java index c0753077f8a0b..9bc0ce353c449 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/second/SecondEntity.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/second/SecondEntity.java @@ -1,8 +1,6 @@ package io.quarkus.spring.data.deployment.multiple_pu.second; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; +import jakarta.persistence.*; @Entity public class SecondEntity { @@ -12,4 +10,7 @@ public class SecondEntity { public Long id; public String name; + + @OneToOne(cascade = CascadeType.ALL) + public SecondEntity child; } diff --git a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/second/SecondEntityRepository.java b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/second/SecondEntityRepository.java index d35fbaab8775f..f0ddfd7d4bfab 100644 --- a/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/second/SecondEntityRepository.java +++ b/extensions/spring-data-jpa/deployment/src/test/java/io/quarkus/spring/data/deployment/multiple_pu/second/SecondEntityRepository.java @@ -1,5 +1,11 @@ package io.quarkus.spring.data.deployment.multiple_pu.second; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -8,4 +14,21 @@ public interface SecondEntityRepository extends org.springframework.data.reposit SecondEntity save(SecondEntity entity); long count(); + + Optional findById(Long id); + + SecondEntity getOne(Long id); + + void deleteAll(); + + void deleteByName(String name); + + Page findByName(String name, Pageable pageable); + + @Query(value = "SELECT se FROM SecondEntity se WHERE name=?1", countQuery = "SELECT COUNT(*) FROM SecondEntity se WHERE name=?1") + Page findByNameQueryIndexed(String name, Pageable pageable); + + @Query(value = "SELECT se FROM SecondEntity se WHERE name=:name", countQuery = "SELECT COUNT(*) FROM SecondEntity se WHERE name=:name") + Page findByNameQueryNamed(@Param("name") String name, Pageable pageable); + } diff --git a/extensions/spring-data-jpa/runtime/src/main/java/io/quarkus/spring/data/runtime/RepositorySupport.java b/extensions/spring-data-jpa/runtime/src/main/java/io/quarkus/spring/data/runtime/RepositorySupport.java index 53e91d906f023..2da94e173b573 100644 --- a/extensions/spring-data-jpa/runtime/src/main/java/io/quarkus/spring/data/runtime/RepositorySupport.java +++ b/extensions/spring-data-jpa/runtime/src/main/java/io/quarkus/spring/data/runtime/RepositorySupport.java @@ -42,7 +42,7 @@ public static void deleteAll(AbstractJpaOperations> operations, } public static Object getOne(AbstractJpaOperations> operations, Class entityClass, Object id) { - return operations.getEntityManager().getReference(entityClass, id); + return operations.getEntityManager(entityClass).getReference(entityClass, id); } public static void clear(Class clazz) { diff --git a/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/RouteBuildItem.java b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/RouteBuildItem.java index d43538ce33c76..ba6828cdb9377 100644 --- a/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/RouteBuildItem.java +++ b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/RouteBuildItem.java @@ -70,23 +70,41 @@ public enum RouteType { ABSOLUTE_ROUTE } - private RouteType typeOfRoute = RouteType.APPLICATION_ROUTE; + private final RouteType typeOfRoute; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private OptionalInt order = OptionalInt.empty(); + private final OptionalInt order; - private String path; - private Consumer customizer; + private final String path; + private final Consumer customizer; - private boolean isManagement; + private final boolean isManagement; - private Handler handler; + private final Handler handler; - private HandlerType typeOfHandler = HandlerType.NORMAL; + private final HandlerType typeOfHandler = HandlerType.NORMAL; - private boolean displayOnNotFoundPage; - private String notFoundPageTitle; + private final boolean displayOnNotFoundPage; + private final String notFoundPageTitle; - private String routeConfigKey; + private final String routeConfigKey; + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public RouteBuildItem(RouteType typeOfRoute, String path, Consumer customizer, + boolean isManagement, + Handler handler, + boolean displayOnNotFoundPage, + String notFoundPageTitle, + String routeConfigKey, OptionalInt order) { + this.order = order; + this.typeOfRoute = typeOfRoute; + this.path = path; + this.handler = handler; + this.displayOnNotFoundPage = displayOnNotFoundPage; + this.notFoundPageTitle = notFoundPageTitle; + this.routeConfigKey = routeConfigKey; + this.customizer = customizer; + this.isManagement = isManagement; + } public RouteType getTypeOfRoute() { return typeOfRoute; @@ -246,13 +264,27 @@ public boolean isManagement() { */ public static class Builder { - private final RouteBuildItem item; + private final RouteType typeOfRoute; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private OptionalInt order = OptionalInt.empty(); + + private final String path; + private final boolean isManagement; + private Consumer customizer; + + private Handler handler; + + private HandlerType typeOfHandler = HandlerType.NORMAL; + + private boolean displayOnNotFoundPage; + private String notFoundPageTitle; + + private String routeConfigKey; private Builder(RouteType type, String path, boolean isManagement) { - item = new RouteBuildItem(); - item.typeOfRoute = type; - item.path = path; - item.isManagement = isManagement; + this.typeOfRoute = type; + this.path = path; + this.isManagement = isManagement; } /** @@ -262,7 +294,7 @@ private Builder(RouteType type, String path, boolean isManagement) { * @return the current builder */ public Builder withRouteCustomizer(Consumer customizer) { - item.customizer = customizer; + this.customizer = customizer; return this; } @@ -273,7 +305,7 @@ public Builder withRouteCustomizer(Consumer customizer) { * @return the current builder */ public Builder withOrder(int order) { - item.order = OptionalInt.of(order); + this.order = OptionalInt.of(order); return this; } @@ -284,7 +316,7 @@ public Builder withOrder(int order) { * @return the current builder */ public Builder withRequestHandler(Handler handler) { - item.handler = handler; + this.handler = handler; return this; } @@ -295,10 +327,10 @@ public Builder withRequestHandler(Handler handler) { * @return the current builder */ public Builder asBlockingRoute() { - if (item.typeOfHandler == HandlerType.FAILURE) { + if (this.typeOfHandler == HandlerType.FAILURE) { throw new IllegalArgumentException("A failure route cannot be a blocking route"); } - item.typeOfHandler = HandlerType.BLOCKING; + this.typeOfHandler = HandlerType.BLOCKING; return this; } @@ -309,10 +341,10 @@ public Builder asBlockingRoute() { * @return the current builder */ public Builder asFailureRoute() { - if (item.typeOfHandler == HandlerType.BLOCKING) { + if (this.typeOfHandler == HandlerType.BLOCKING) { throw new IllegalArgumentException("A blocking route cannot be a failure route"); } - item.typeOfHandler = HandlerType.FAILURE; + this.typeOfHandler = HandlerType.FAILURE; return this; } @@ -322,7 +354,7 @@ public Builder asFailureRoute() { * @return the current builder */ public Builder displayOnNotFoundPage() { - item.displayOnNotFoundPage = true; + this.displayOnNotFoundPage = true; return this; } @@ -333,8 +365,8 @@ public Builder displayOnNotFoundPage() { * @return the current builder */ public Builder displayOnNotFoundPage(String notFoundPageTitle) { - item.displayOnNotFoundPage = true; - item.notFoundPageTitle = notFoundPageTitle; + this.displayOnNotFoundPage = true; + this.notFoundPageTitle = notFoundPageTitle; return this; } @@ -345,7 +377,7 @@ public Builder displayOnNotFoundPage(String notFoundPageTitle) { * @return the current builder */ public Builder withRoutePathConfigKey(String attributeName) { - item.routeConfigKey = attributeName; + this.routeConfigKey = attributeName; return this; } @@ -355,11 +387,13 @@ public Builder withRoutePathConfigKey(String attributeName) { * @return the route build item */ public RouteBuildItem build() { - if (item.handler == null) { + if (this.handler == null) { throw new IllegalArgumentException("The route handler must be set"); } - return item; + return new RouteBuildItem(typeOfRoute, path, customizer, isManagement, handler, displayOnNotFoundPage, + notFoundPageTitle, + routeConfigKey, order); } } diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js index c1e7285349342..c376f251e58ad 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js @@ -71,16 +71,14 @@ export class QwcConfigurationEditor extends LitElement { return html`Error: ${this._error}`; } - if(this._value){ - return html` - ${this._renderToolbar()} - - `; - } + return html` + ${this._renderToolbar()} + + `; } _renderToolbar(){ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 6345c47d66458..ae7e5ca6c2360 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -1,5 +1,7 @@ package io.quarkus.vertx.http.runtime.security; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.setRoutingContextAttribute; + import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.Reader; @@ -284,7 +286,8 @@ public void onItem(SecurityIdentity identity) { } if (identity == null) { Uni anon = authenticator.getIdentityProviderManager() - .authenticate(AnonymousAuthenticationRequest.INSTANCE); + .authenticate( + setRoutingContextAttribute(new AnonymousAuthenticationRequest(), event)); anon.subscribe().withSubscriber(new UniSubscriber() { @Override public void onSubscribe(UniSubscription subscription) { @@ -340,7 +343,8 @@ public Uni apply(SecurityIdentity securityIdentity) //if it is null we use the anonymous identity if (securityIdentity == null) { return authenticator.getIdentityProviderManager() - .authenticate(AnonymousAuthenticationRequest.INSTANCE); + .authenticate( + setRoutingContextAttribute(new AnonymousAuthenticationRequest(), event)); } return Uni.createFrom().item(securityIdentity); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java index 8d5fe1f8a57fa..d4a4ab27e494a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityUtils.java @@ -1,5 +1,7 @@ package io.quarkus.vertx.http.runtime.security; +import java.util.Map; + import io.quarkus.security.identity.request.AuthenticationRequest; import io.vertx.ext.web.RoutingContext; @@ -18,4 +20,8 @@ public static AuthenticationRequest setRoutingContextAttribute(AuthenticationReq public static RoutingContext getRoutingContextAttribute(AuthenticationRequest request) { return request.getAttribute(ROUTING_CONTEXT_ATTRIBUTE); } + + public static RoutingContext getRoutingContextAttribute(Map authenticationRequestAttributes) { + return (RoutingContext) authenticationRequestAttributes.get(ROUTING_CONTEXT_ATTRIBUTE); + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/QuarkusHttpUser.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/QuarkusHttpUser.java index edf4c1975180a..a567faaca889e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/QuarkusHttpUser.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/QuarkusHttpUser.java @@ -1,5 +1,7 @@ package io.quarkus.vertx.http.runtime.security; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.setRoutingContextAttribute; + import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; @@ -92,7 +94,10 @@ public static SecurityIdentity getSecurityIdentityBlocking(RoutingContext routin return deferred.await().indefinitely(); } if (identityProviderManager != null) { - return identityProviderManager.authenticate(AnonymousAuthenticationRequest.INSTANCE).await().indefinitely(); + return identityProviderManager + .authenticate(setRoutingContextAttribute(new AnonymousAuthenticationRequest(), routingContext)) + .await() + .indefinitely(); } return null; } @@ -125,7 +130,8 @@ public static Uni getSecurityIdentity(RoutingContext routingCo return Uni.createFrom().item(existing.getSecurityIdentity()); } if (identityProviderManager != null) { - return identityProviderManager.authenticate(AnonymousAuthenticationRequest.INSTANCE); + return identityProviderManager + .authenticate(setRoutingContextAttribute(new AnonymousAuthenticationRequest(), routingContext)); } return Uni.createFrom().nullItem(); } diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 519be532d3367..c3ccc961bf2d6 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -54,7 +54,7 @@ 5.10.1 1.9.22 1.7.3 - 5.8.0 + 5.9.0 1.7.0.Final 2.0.1 diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index f75a326bb6ec5..586f96c7c8a95 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -638,12 +638,24 @@ Collection extractQualifiers(AnnotationInstance annotation) * This returns a collection because in case of repeating interceptor bindings there can be multiple. * For most instances this will be a singleton instance (if given annotation is an interceptor binding) or * an empty list for cases where the annotation is not an interceptor binding. + *

    + * In addition to repeating annotations, the result also includes transitive interceptor bindings. * * @param annotation annotation to be inspected * @return a collection of interceptor bindings or an empty collection */ public Collection extractInterceptorBindings(AnnotationInstance annotation) { - return extractAnnotations(annotation, interceptorBindings, repeatingInterceptorBindingAnnotations); + Collection result = extractAnnotations(annotation, interceptorBindings, + repeatingInterceptorBindingAnnotations); + if (result.isEmpty()) { + return result; + } + Set transitive = transitiveInterceptorBindings.get(annotation.name()); + if (transitive != null) { + result = new HashSet<>(result); + result.addAll(transitive); + } + return result; } private static Collection extractAnnotations(AnnotationInstance annotation, @@ -667,10 +679,6 @@ ClassInfo getInterceptorBinding(DotName name) { return interceptorBindings.get(name); } - Set getTransitiveInterceptorBindings(DotName name) { - return transitiveInterceptorBindings.get(name); - } - Map> getTransitiveInterceptorBindings() { return transitiveInterceptorBindings; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index 14aea5e82c65b..02089c9471e18 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -25,6 +25,7 @@ import jakarta.enterprise.inject.spi.InterceptionType; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationInstanceEquivalenceProxy; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.ClassInfo; @@ -857,7 +858,7 @@ private void addClassLevelBindings(ClassInfo targetClass, Collection bindings, Set skip, boolean onlyInherited) { beanDeployment.getAnnotations(classInfo).stream() - .filter(a -> beanDeployment.getInterceptorBinding(a.name()) != null) + .flatMap(a -> beanDeployment.extractInterceptorBindings(a).stream()) .filter(a -> !skip.contains(a.name())) .filter(a -> !onlyInherited || beanDeployment.hasAnnotation(beanDeployment.getInterceptorBinding(a.name()), DotNames.INHERITED)) @@ -977,6 +978,13 @@ boolean isEmpty() { return interceptors.isEmpty(); } + Set bindingsEquivalenceProxies() { + Set result = new HashSet<>(); + for (AnnotationInstance binding : bindings) { + result.add(binding.createEquivalenceProxy()); + } + return result; + } } static class DecorationInfo { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorResolver.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorResolver.java index 4ef049210a6ba..2276c91e01b76 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorResolver.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorResolver.java @@ -61,19 +61,6 @@ private boolean hasInterceptorBinding(Collection bindings, A for (AnnotationInstance binding : bindings) { if (isInterceptorBinding(interceptorBinding, binding)) { return true; - } else { - // could be transitive binding - Set transitiveInterceptorBindings = beanDeployment - .getTransitiveInterceptorBindings(binding.name()); - if (transitiveInterceptorBindings == null) { - continue; - } - for (AnnotationInstance transitiveBindingInstance : transitiveInterceptorBindings) { - if (isInterceptorBinding(interceptorBinding, - transitiveBindingInstance)) { - return true; - } - } } } return false; @@ -83,7 +70,6 @@ private boolean isInterceptorBinding(AnnotationInstance interceptorBinding, Anno ClassInfo interceptorBindingClass = beanDeployment.getInterceptorBinding(interceptorBinding.name()); if (candidate.name().equals(interceptorBinding.name())) { // Must have the same annotation member value for each member which is not annotated @Nonbinding - boolean matches = true; Set nonBindingFields = beanDeployment.getInterceptorNonbindingMembers(interceptorBinding.name()); for (AnnotationValue value : candidate.valuesWithDefaults(beanDeployment.getBeanArchiveIndex())) { String annotationField = value.name(); @@ -91,13 +77,10 @@ private boolean isInterceptorBinding(AnnotationInstance interceptorBinding, Anno && !nonBindingFields.contains(annotationField) && !value.equals( interceptorBinding.valueWithDefault(beanDeployment.getBeanArchiveIndex(), annotationField))) { - matches = false; - break; + return false; } } - if (matches) { - return true; - } + return true; } return false; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Interceptors.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Interceptors.java index dc8f13581c7fe..84b15eec8b863 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Interceptors.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Interceptors.java @@ -85,12 +85,6 @@ private static void addBindings(BeanDeployment beanDeployment, ClassInfo classIn } bindings.addAll(beanDeployment.extractInterceptorBindings(annotation)); - // can also be a transitive binding - Set transitiveInterceptorBindings = beanDeployment - .getTransitiveInterceptorBindings(annotation.name()); - if (transitiveInterceptorBindings != null) { - bindings.addAll(transitiveInterceptorBindings); - } } if (classInfo.superName() != null && !classInfo.superName().equals(DotNames.OBJECT)) { @@ -102,22 +96,13 @@ private static void addBindings(BeanDeployment beanDeployment, ClassInfo classIn } // similar logic already exists in InterceptorResolver, but it doesn't validate + // when called, `bindings` always include transitive bindings static void checkClassLevelInterceptorBindings(Collection bindings, ClassInfo targetClass, BeanDeployment beanDeployment) { - // when called from `createInterceptor` above, `bindings` already include transitive bindings, - // but when called from outside, that isn't guaranteed - Set allBindings = new HashSet<>(bindings); - for (AnnotationInstance binding : bindings) { - Set transitive = beanDeployment.getTransitiveInterceptorBindings(binding.name()); - if (transitive != null) { - allBindings.addAll(transitive); - } - } - IndexView index = beanDeployment.getBeanArchiveIndex(); Map> seenBindings = new HashMap<>(); - for (AnnotationInstance binding : allBindings) { + for (AnnotationInstance binding : bindings) { DotName name = binding.name(); if (beanDeployment.hasAnnotation(index.getClassByName(name), DotNames.REPEATABLE)) { // don't validate @Repeatable interceptor bindings, repeatability is their entire point diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java index f665300bc4444..d5b558580a9ff 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java @@ -17,19 +17,18 @@ import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Collectors; import jakarta.enterprise.context.spi.CreationalContext; import jakarta.enterprise.inject.spi.InterceptionType; import jakarta.interceptor.InvocationContext; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationInstanceEquivalenceProxy; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; @@ -247,19 +246,19 @@ protected FieldDescriptor createConstructor(ClassOutput classOutput, BeanInfo be IntegerHolder chainIdx = new IntegerHolder(); IntegerHolder bindingIdx = new IntegerHolder(); Map, String> interceptorChainKeys = new HashMap<>(); - Map, String> bindingKeys = new HashMap<>(); + Map, String> bindingKeys = new HashMap<>(); ResultHandle interceptorChainMap = constructor.newInstance(MethodDescriptor.ofConstructor(HashMap.class)); ResultHandle bindingsMap = constructor.newInstance(MethodDescriptor.ofConstructor(HashMap.class)); // Shared interceptor bindings literals - Map bindingsLiterals = new HashMap<>(); - Function bindingsLiteralFun = new Function() { + Map bindingsLiterals = new HashMap<>(); + Function bindingsLiteralFun = new Function() { @Override - public ResultHandle apply(BindingKey key) { + public ResultHandle apply(AnnotationInstanceEquivalenceProxy binding) { // Create annotation literal if needed - ClassInfo bindingClass = bean.getDeployment().getInterceptorBinding(key.annotation.name()); - return annotationLiterals.create(constructor, bindingClass, key.annotation); + ClassInfo bindingClass = bean.getDeployment().getInterceptorBinding(binding.get().name()); + return annotationLiterals.create(constructor, bindingClass, binding.get()); } }; @@ -295,18 +294,18 @@ public String apply(List interceptors) { } }; - Function, String> bindingsFun = new Function, String>() { + Function, String> bindingsFun = new Function, String>() { @Override - public String apply(List keys) { + public String apply(Set bindings) { String key = "b" + bindingIdx.i++; - if (keys.size() == 1) { + if (bindings.size() == 1) { constructor.invokeInterfaceMethod(MethodDescriptors.MAP_PUT, bindingsMap, constructor.load(key), constructor.invokeStaticMethod(MethodDescriptors.COLLECTIONS_SINGLETON, - bindingsLiterals.computeIfAbsent(keys.iterator().next(), bindingsLiteralFun))); + bindingsLiterals.computeIfAbsent(bindings.iterator().next(), bindingsLiteralFun))); } else { - ResultHandle bindingsArray = constructor.newArray(Object.class, keys.size()); + ResultHandle bindingsArray = constructor.newArray(Object.class, bindings.size()); int bindingsIndex = 0; - for (BindingKey binding : keys) { + for (AnnotationInstanceEquivalenceProxy binding : bindings) { constructor.writeArrayValue(bindingsArray, bindingsIndex++, bindingsLiterals.computeIfAbsent(binding, bindingsLiteralFun)); } @@ -325,8 +324,7 @@ public String apply(List keys) { subclass.getFieldCreator("arc$" + methodIdx++, InterceptedMethodMetadata.class.getName()) .setModifiers(ACC_PRIVATE); interceptorChainKeys.computeIfAbsent(interception.interceptors, interceptorChainKeysFun); - bindingKeys.computeIfAbsent(interception.bindings.stream().map(BindingKey::new).collect(Collectors.toList()), - bindingsFun); + bindingKeys.computeIfAbsent(interception.bindingsEquivalenceProxies(), bindingsFun); } } @@ -412,9 +410,8 @@ public String apply(List keys) { paramsHandles); // 3. Interceptor bindings - // Note that we use a shared list if possible - String bindingKey = bindingKeys.get( - interception.bindings.stream().map(BindingKey::new).collect(Collectors.toList())); + // Note that we use a shared set if possible + String bindingKey = bindingKeys.get(interception.bindingsEquivalenceProxies()); ResultHandle bindingsHandle = bindingsHandles.computeIfAbsent(bindingKey, ignored -> { return initMetadataMethodFinal.invokeInterfaceMethod(MethodDescriptors.MAP_GET, initMetadataMethodFinal.getMethodParam(1), initMetadataMethodFinal.load(bindingKey)); @@ -990,43 +987,8 @@ protected void createDestroy(ClassOutput classOutput, BeanInfo bean, ClassCreato } } - /** - * We cannot use {@link AnnotationInstance#equals(Object)} and {@link AnnotationInstance#hashCode()} because it includes the - * annotation target. - */ - static class BindingKey { - - final AnnotationInstance annotation; - - public BindingKey(AnnotationInstance annotation) { - this.annotation = Objects.requireNonNull(annotation); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - BindingKey key = (BindingKey) o; - return annotation.name().equals(key.annotation.name()) && annotation.values().equals(key.annotation.values()); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + annotation.name().hashCode(); - result = prime * result + annotation.values().hashCode(); - return result; - } - - } - private static class IntegerHolder { private int i = 1; - }; + } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/CounterInterceptor.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/CounterInterceptor.java index fc89b4acb2ecb..0a8f7e974538c 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/CounterInterceptor.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/CounterInterceptor.java @@ -1,20 +1,29 @@ package io.quarkus.arc.test.interceptors.bindings.transitive; +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Set; + import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; import jakarta.interceptor.InvocationContext; +import io.quarkus.arc.ArcInvocationContext; + @Interceptor @Priority(1) @CounterBinding public class CounterInterceptor { - public static Integer timesInvoked = 0; + public static int timesInvoked = 0; + + public static Set lastBindings = new HashSet<>(); @AroundInvoke public Object aroundInvoke(InvocationContext context) throws Exception { timesInvoked++; + lastBindings = (Set) context.getContextData().get(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS); return context.proceed(); } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/TransitiveCounterInterceptor.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/TransitiveCounterInterceptor.java index 2aff42936aaa1..5598e7413a185 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/TransitiveCounterInterceptor.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/TransitiveCounterInterceptor.java @@ -1,21 +1,30 @@ package io.quarkus.arc.test.interceptors.bindings.transitive; +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Set; + import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; import jakarta.interceptor.InvocationContext; +import io.quarkus.arc.ArcInvocationContext; + @Interceptor @Priority(2) @SomeAnnotation // this is transitive binding, it also brings in @CounterBinding public class TransitiveCounterInterceptor { - public static Integer timesInvoked = 0; + public static int timesInvoked = 0; + + public static Set lastBindings = new HashSet<>(); @AroundInvoke public Object aroundInvoke(InvocationContext context) throws Exception { // it should effectively interrupt all that CounterInterceptor does timesInvoked++; + lastBindings = (Set) context.getContextData().get(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS); return context.proceed(); } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/TransitiveInterceptorBindingTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/TransitiveInterceptorBindingTest.java index 134a5b45e3bff..70efa57707725 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/TransitiveInterceptorBindingTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/TransitiveInterceptorBindingTest.java @@ -1,6 +1,13 @@ package io.quarkus.arc.test.interceptors.bindings.transitive; -import org.junit.jupiter.api.Assertions; +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.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Set; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -17,10 +24,10 @@ public class TransitiveInterceptorBindingTest { @Test public void testInterceptorsAreInvoked() { - Assertions.assertTrue(Arc.container().instance(MethodLevelInterceptedBean.class).isAvailable()); - Assertions.assertTrue(Arc.container().instance(ClassLevelInterceptedBean.class).isAvailable()); - Assertions.assertTrue(Arc.container().instance(TwoLevelsDeepClassLevelInterceptedBean.class).isAvailable()); - Assertions.assertTrue(Arc.container().instance(NotInterceptedBean.class).isAvailable()); + assertTrue(Arc.container().instance(MethodLevelInterceptedBean.class).isAvailable()); + assertTrue(Arc.container().instance(ClassLevelInterceptedBean.class).isAvailable()); + assertTrue(Arc.container().instance(TwoLevelsDeepClassLevelInterceptedBean.class).isAvailable()); + assertTrue(Arc.container().instance(NotInterceptedBean.class).isAvailable()); MethodLevelInterceptedBean methodLevelInterceptedBean = Arc.container().instance(MethodLevelInterceptedBean.class) .get(); ClassLevelInterceptedBean classLevelInterceptedBean = Arc.container().instance(ClassLevelInterceptedBean.class).get(); @@ -28,27 +35,49 @@ public void testInterceptorsAreInvoked() { .instance(TwoLevelsDeepClassLevelInterceptedBean.class).get(); NotInterceptedBean notIntercepted = Arc.container().instance(NotInterceptedBean.class).get(); - Assertions.assertTrue(CounterInterceptor.timesInvoked == 0); - Assertions.assertTrue(TransitiveCounterInterceptor.timesInvoked == 0); + assertEquals(0, CounterInterceptor.timesInvoked); + assertEquals(0, TransitiveCounterInterceptor.timesInvoked); + assertBindings(); // empty methodLevelInterceptedBean.oneLevelDeepBinding(); - Assertions.assertTrue(CounterInterceptor.timesInvoked == 1); - Assertions.assertTrue(TransitiveCounterInterceptor.timesInvoked == 1); + assertEquals(1, CounterInterceptor.timesInvoked); + assertEquals(1, TransitiveCounterInterceptor.timesInvoked); + assertBindings(SomeAnnotation.class, CounterBinding.class); methodLevelInterceptedBean.twoLevelsDeepBinding(); - Assertions.assertTrue(CounterInterceptor.timesInvoked == 2); - Assertions.assertTrue(TransitiveCounterInterceptor.timesInvoked == 2); + assertEquals(2, CounterInterceptor.timesInvoked); + assertEquals(2, TransitiveCounterInterceptor.timesInvoked); + assertBindings(AnotherAnnotation.class, SomeAnnotation.class, CounterBinding.class); classLevelInterceptedBean.ping(); - Assertions.assertTrue(CounterInterceptor.timesInvoked == 3); - Assertions.assertTrue(TransitiveCounterInterceptor.timesInvoked == 3); + assertEquals(3, CounterInterceptor.timesInvoked); + assertEquals(3, TransitiveCounterInterceptor.timesInvoked); + assertBindings(SomeAnnotation.class, CounterBinding.class); deeperHierarchyBean.ping(); - Assertions.assertTrue(CounterInterceptor.timesInvoked == 4); - Assertions.assertTrue(TransitiveCounterInterceptor.timesInvoked == 4); + assertEquals(4, CounterInterceptor.timesInvoked); + assertEquals(4, TransitiveCounterInterceptor.timesInvoked); + assertBindings(AnotherAnnotation.class, SomeAnnotation.class, CounterBinding.class); + CounterInterceptor.lastBindings = new HashSet<>(); + TransitiveCounterInterceptor.lastBindings = new HashSet<>(); // following two invocations use @NotABinding which should not trigger interception notIntercepted.ping(); - Assertions.assertTrue(CounterInterceptor.timesInvoked == 4); - Assertions.assertTrue(TransitiveCounterInterceptor.timesInvoked == 4); + assertEquals(4, CounterInterceptor.timesInvoked); + assertEquals(4, TransitiveCounterInterceptor.timesInvoked); + assertBindings(); // empty methodLevelInterceptedBean.shouldNotBeIntercepted(); - Assertions.assertTrue(CounterInterceptor.timesInvoked == 4); - Assertions.assertTrue(TransitiveCounterInterceptor.timesInvoked == 4); + assertEquals(4, CounterInterceptor.timesInvoked); + assertEquals(4, TransitiveCounterInterceptor.timesInvoked); + assertBindings(); // empty } + @SafeVarargs + static void assertBindings(Class... bindings) { + assertBindings(CounterInterceptor.lastBindings, bindings); + assertBindings(TransitiveCounterInterceptor.lastBindings, bindings); + } + + private static void assertBindings(Set actualBindings, Class[] expectedBindings) { + assertNotNull(actualBindings); + assertEquals(expectedBindings.length, actualBindings.size()); + for (Class expectedBinding : expectedBindings) { + assertTrue(actualBindings.stream().anyMatch(it -> it.annotationType() == expectedBinding)); + } + } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/MuchCoolerInterceptor.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/MuchCoolerInterceptor.java index 30e0819254445..053d43e47f859 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/MuchCoolerInterceptor.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/MuchCoolerInterceptor.java @@ -1,20 +1,29 @@ package io.quarkus.arc.test.interceptors.bindings.transitive.with.transformer; +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Set; + import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; import jakarta.interceptor.InvocationContext; +import io.quarkus.arc.ArcInvocationContext; + @Interceptor @Priority(1) @MuchCoolerBinding public class MuchCoolerInterceptor { - public static Integer timesInvoked = 0; + public static int timesInvoked = 0; + + public static Set lastBindings = new HashSet<>(); @AroundInvoke public Object aroundInvoke(InvocationContext context) throws Exception { timesInvoked++; + lastBindings = (Set) context.getContextData().get(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS); return context.proceed(); } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/PlainInterceptor.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/PlainInterceptor.java index c92371a7f3c64..2bfcc63eee62c 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/PlainInterceptor.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/PlainInterceptor.java @@ -1,21 +1,30 @@ package io.quarkus.arc.test.interceptors.bindings.transitive.with.transformer; +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Set; + import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; import jakarta.interceptor.InvocationContext; +import io.quarkus.arc.ArcInvocationContext; + @Interceptor @Priority(1) @PlainBinding // @MuchCoolerBinding is added programmatically public class PlainInterceptor { - public static Integer timesInvoked = 0; + public static int timesInvoked = 0; + + public static Set lastBindings = new HashSet<>(); @AroundInvoke public Object aroundInvoke(InvocationContext context) throws Exception { timesInvoked++; + lastBindings = (Set) context.getContextData().get(ArcInvocationContext.KEY_INTERCEPTOR_BINDINGS); return context.proceed(); } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/TransitiveInterceptionWithTransformerApplicationTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/TransitiveInterceptionWithTransformerApplicationTest.java index a213cad67deaf..f28b55bfe6ac8 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/TransitiveInterceptionWithTransformerApplicationTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/bindings/transitive/with/transformer/TransitiveInterceptionWithTransformerApplicationTest.java @@ -1,7 +1,13 @@ package io.quarkus.arc.test.interceptors.bindings.transitive.with.transformer; +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.lang.annotation.Annotation; +import java.util.Set; + import org.jboss.jandex.AnnotationTarget; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -21,14 +27,16 @@ public class TransitiveInterceptionWithTransformerApplicationTest { @Test public void testTransformersAreApplied() { - Assertions.assertTrue(Arc.container().instance(DummyBean.class).isAvailable()); + assertTrue(Arc.container().instance(DummyBean.class).isAvailable()); DummyBean bean = Arc.container().instance(DummyBean.class).get(); - Assertions.assertTrue(PlainInterceptor.timesInvoked == 0); - Assertions.assertTrue(MuchCoolerInterceptor.timesInvoked == 0); + assertEquals(0, PlainInterceptor.timesInvoked); + assertEquals(0, MuchCoolerInterceptor.timesInvoked); + assertBindings(); // empty bean.ping(); - Assertions.assertTrue(PlainInterceptor.timesInvoked == 1); - Assertions.assertTrue(MuchCoolerInterceptor.timesInvoked == 1); + assertEquals(1, PlainInterceptor.timesInvoked); + assertEquals(1, MuchCoolerInterceptor.timesInvoked); + assertBindings(PlainBinding.class, MuchCoolerBinding.class); } static class MyTransformer implements AnnotationsTransformer { @@ -47,4 +55,18 @@ public void transform(TransformationContext context) { } } + + @SafeVarargs + static void assertBindings(Class... bindings) { + assertBindings(PlainInterceptor.lastBindings, bindings); + assertBindings(MuchCoolerInterceptor.lastBindings, bindings); + } + + private static void assertBindings(Set actualBindings, Class[] expectedBindings) { + assertNotNull(actualBindings); + assertEquals(expectedBindings.length, actualBindings.size()); + for (Class expectedBinding : expectedBindings) { + assertTrue(actualBindings.stream().anyMatch(it -> it.annotationType() == expectedBinding)); + } + } } diff --git a/independent-projects/extension-maven-plugin/pom.xml b/independent-projects/extension-maven-plugin/pom.xml index ecc19bd3a68fc..8a6ca6f0b3e3f 100644 --- a/independent-projects/extension-maven-plugin/pom.xml +++ b/independent-projects/extension-maven-plugin/pom.xml @@ -393,7 +393,7 @@ org.mockito mockito-core - 5.8.0 + 5.9.0 test diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java index 84042ece5b1c8..583fe35aadbe7 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/IncludeSectionHelper.java @@ -7,7 +7,9 @@ import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; import java.util.function.Supplier; +import java.util.regex.Pattern; import io.quarkus.qute.Template.Fragment; @@ -110,7 +112,7 @@ public MissingEndTagStrategy missingEndTagStrategy() { } @Override - protected boolean ignoreParameterInit(String key, String value) { + protected boolean ignoreParameterInit(Supplier firstParamSupplier, String key, String value) { return key.equals(TEMPLATE) // {#include foo _isolated=true /} || key.equals(ISOLATED) @@ -147,6 +149,7 @@ static abstract class AbstractIncludeFactory implements static final String ISOLATED = "_isolated"; static final String UNISOLATED = "_unisolated"; static final String IGNORE_FRAGMENTS = "_ignoreFragments"; + static final Pattern WHITESPACE = Pattern.compile("\\s"); @Override public boolean treatUnknownSectionsAsBlocks() { @@ -161,6 +164,7 @@ void addDefaultParams(ParametersInfo.Builder builder) { builder .addParameter(Parameter.builder(ISOLATED).defaultValue(isolatedDefaultValue()).optional() .valuePredicate(ISOLATED::equals).build()) + .addParameter(Parameter.builder(UNISOLATED).optional().valuePredicate(UNISOLATED::equals).build()) .addParameter(Parameter.builder(IGNORE_FRAGMENTS).defaultValue(Boolean.FALSE.toString()).optional() .valuePredicate(IGNORE_FRAGMENTS::equals).build()) .build(); @@ -172,13 +176,7 @@ public Scope initializeBlock(Scope outerScope, BlockInfo block) { for (Entry entry : block.getParameters().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); - if (ignoreParameterInit(key, value)) { - continue; - } else if (useDefaultedKey(key, value)) { - // As "order" in {#include foo order /} - key = value; - } - block.addExpression(key, value); + handleParam(key, value, () -> block.getParameter(0), (k, v) -> block.addExpression(k, v)); } return outerScope; } else { @@ -228,7 +226,7 @@ public T initialize(SectionInitContext context) { ignoreFragments = true; continue; } - handleParamInit(key, value, context, params); + handleParam(key, value, () -> context.getParameter(0), (k, v) -> params.put(k, context.getExpression(k))); } } @@ -312,13 +310,19 @@ protected String getFragmentId(String templateId, SectionInitContext context) { return null; } - protected void handleParamInit(String key, String value, SectionInitContext context, Map params) { - if (ignoreParameterInit(key, value)) { + protected void handleParam(String key, String value, Supplier firstParamSupplier, + BiConsumer paramConsumer) { + if (ignoreParameterInit(firstParamSupplier, key, value)) { return; } else if (useDefaultedKey(key, value)) { - key = value; + if (LiteralSupport.isStringLiteral(value)) { + // {#include "foo" /} => {#include foo="foo" /} + key = value.substring(1, value.length() - 1); + } else { + key = value; + } } - params.put(key, context.getExpression(key)); + paramConsumer.accept(key, value); } protected boolean useDefaultedKey(String key, String value) { @@ -331,10 +335,10 @@ protected boolean useDefaultedKey(String key, String value) { } protected boolean isSinglePart(String value) { - return Expressions.splitParts(value).size() == 1; + return Expressions.splitParts(value).size() == 1 && !WHITESPACE.matcher(value).find(); } - protected boolean ignoreParameterInit(String key, String value) { + protected boolean ignoreParameterInit(Supplier firstParamSupplier, String key, String value) { return key.equals(IGNORE_FRAGMENTS); } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java index 65afaf5eb21a0..e2bd413a21a50 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java @@ -23,8 +23,7 @@ static Object getLiteralValue(String literal) { if (literal == null || literal.isEmpty()) { return value; } - char firstChar = literal.charAt(0); - if (isStringLiteralSeparator(firstChar) && literal.charAt(literal.length() - 1) == firstChar) { + if (isStringLiteral(literal)) { value = literal.substring(1, literal.length() - 1); } else if (literal.equals("true")) { value = Boolean.TRUE; @@ -33,6 +32,7 @@ static Object getLiteralValue(String literal) { } else if (literal.equals("null")) { value = null; } else { + char firstChar = literal.charAt(0); if (Character.isDigit(firstChar) || firstChar == '-' || firstChar == '+') { if (INTEGER_LITERAL_PATTERN.matcher(literal).matches()) { try { @@ -77,4 +77,12 @@ static boolean isStringLiteralSeparator(char character) { return character == '"' || character == '\''; } + static boolean isStringLiteral(String value) { + if (value == null || value.isEmpty()) { + return false; + } + char firstChar = value.charAt(0); + return isStringLiteralSeparator(firstChar) && value.charAt(value.length() - 1) == firstChar; + } + } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index b6eba251f7d9b..3b43d28e9cb81 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -707,42 +707,24 @@ private void processParams(String tag, String label, Iterator iter, Sect LOGGER.debugf(builder.toString()); } + List parametersPositions = new ArrayList<>(paramValues.size()); + // Process named params first - for (Iterator it = paramValues.iterator(); it.hasNext();) { - String param = it.next(); + for (String param : paramValues) { int equalsPosition = getFirstDeterminingEqualsCharPosition(param); if (equalsPosition != -1) { // Named param - params.put(param.substring(0, equalsPosition), param.substring(equalsPosition + 1, - param.length())); - it.remove(); + String val = param.substring(equalsPosition + 1, param.length()); + params.put(param.substring(0, equalsPosition), val); + parametersPositions.add(val); + } else { + parametersPositions.add(null); } } - Predicate included = params::containsKey; // Then process positional params - if (actualSize < factoryParams.size()) { - // The number of actual params is less than factory params - // We need to choose the best fit for positional params - for (String param : paramValues) { - Parameter found = findFactoryParameter(param, factoryParams, included, true); - if (found != null) { - params.put(found.name, param); - } - } - } else { - // The number of actual params is greater or equals to factory params - int generatedIdx = 0; - for (String param : paramValues) { - // Positional param - Parameter found = findFactoryParameter(param, factoryParams, included, false); - if (found != null) { - params.put(found.name, param); - } else { - params.put("" + generatedIdx++, param); - } - } - } + // When the number of actual params is less than factory params then we need to choose the best fit for positional params + processPositionalParams(paramValues, parametersPositions, factoryParams, params, actualSize < factoryParams.size()); // Use the default values if needed for (Parameter param : factoryParams) { @@ -767,8 +749,26 @@ private void processParams(String tag, String label, Iterator iter, Sect .build(); } - for (Entry e : params.entrySet()) { - block.addParameter(e.getKey(), e.getValue()); + params.entrySet().forEach(block::addParameter); + block.setParametersPositions(parametersPositions); + } + + private void processPositionalParams(List paramValues, List parametersPositions, + List factoryParams, Map params, boolean noDefaultValueTakesPrecedence) { + int generatedIdx = 0; + int idx = 0; + Predicate included = params::containsKey; + for (String param : paramValues) { + if (parametersPositions.isEmpty() || parametersPositions.get(idx) == null) { + Parameter found = findFactoryParameter(param, factoryParams, included, noDefaultValueTakesPrecedence); + if (found != null) { + params.put(found.name, param); + } else { + params.put("" + generatedIdx++, param); + } + parametersPositions.set(idx, param); + } + idx++; } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java index f3cfe26a22a29..a584e9be57fe3 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionBlock.java @@ -5,6 +5,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.function.Predicate; @@ -29,6 +30,10 @@ public final class SectionBlock implements WithOrigin, ErrorInitializer { public final String label; /** * An unmodifiable ordered map of parsed parameters. + *

    + * Note that the order does not necessary reflect the original positions of the parameters but the parsing order. + * + * @see SectionHelperFactory#getParameters() */ public final Map parameters; @@ -42,21 +47,33 @@ public final class SectionBlock implements WithOrigin, ErrorInitializer { */ List nodes; + private final List positionalParameters; + public SectionBlock(Origin origin, String id, String label, Map parameters, Map expressions, - List nodes) { + List nodes, List positionalParameters) { this.origin = origin; this.id = id; this.label = label; this.parameters = parameters; this.expressions = expressions; this.nodes = ImmutableList.copyOf(nodes); + this.positionalParameters = positionalParameters; } public boolean isEmpty() { return nodes.isEmpty(); } + /** + * + * @param position + * @return the parameter for the specified position, or {@code null} if no such parameter exists + */ + public String getParameter(int position) { + return positionalParameters.get(position); + } + List getExpressions() { List expressions = new ArrayList<>(); expressions.addAll(this.expressions.values()); @@ -230,6 +247,7 @@ static class Builder implements BlockInfo { private Origin origin; private String label; private Map parameters; + private List parametersPositions = List.of(); private final List nodes; private Map expressions; private final Parser parser; @@ -257,11 +275,16 @@ SectionBlock.Builder setLabel(String label) { return this; } - SectionBlock.Builder addParameter(String name, String value) { + SectionBlock.Builder addParameter(Entry entry) { if (parameters == null) { parameters = new LinkedHashMap<>(); } - parameters.put(name, value); + parameters.put(entry.getKey(), entry.getValue()); + return this; + } + + SectionBlock.Builder setParametersPositions(List parametersPositions) { + this.parametersPositions = Collections.unmodifiableList(parametersPositions); return this; } @@ -279,6 +302,11 @@ public Map getParameters() { return parameters == null ? Collections.emptyMap() : Collections.unmodifiableMap(parameters); } + @Override + public String getParameter(int position) { + return parametersPositions.get(position); + } + public String getLabel() { return label; } @@ -310,7 +338,7 @@ SectionBlock build() { } else { expressions = Collections.unmodifiableMap(expressions); } - return new SectionBlock(origin, id, label, parameters, expressions, nodes); + return new SectionBlock(origin, id, label, parameters, expressions, nodes, parametersPositions); } } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java index 4b542c527db18..bee4473a4d500 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionHelperFactory.java @@ -159,9 +159,12 @@ interface BlockInfo extends ParserDelegate, WithOrigin { String getLabel(); /** - * Undeclared params with default values are included. + * An unmodifiable ordered map of parsed parameters. + *

    + * Note that the order does not necessary reflect the original positions of the parameters but the parsing order. * * @return the map of parameters + * @see SectionHelperFactory#getParameters() */ Map getParameters(); @@ -173,6 +176,14 @@ default boolean hasParameter(String name) { return getParameters().containsKey(name); } + /** + * + * @param position + * @return the parameter for the specified position, or {@code null} if no such parameter exists + * @see SectionBlock#getParameter(int) + */ + String getParameter(int position); + /** * Parse and register an expression for the specified parameter. *

    @@ -197,6 +208,7 @@ public interface SectionInitContext extends ParserDelegate { /** * * @return the parameters of the main block + * @see SectionBlock#parameters */ default public Map getParameters() { return getBlocks().get(0).parameters; @@ -219,6 +231,15 @@ default public String getParameter(String name) { return getParameters().get(name); } + /** + * + * @return the parameter for the specified position + * @see SectionBlock#getParameter(int) + */ + default public String getParameter(int position) { + return getBlocks().get(0).getParameter(position); + } + /** * * @param name diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java index 5781d6d52d82c..7754638e9f476 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/UserTagSectionHelper.java @@ -8,6 +8,8 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Predicate; import java.util.function.Supplier; public class UserTagSectionHelper extends IncludeSectionHelper implements SectionHelper { @@ -15,11 +17,16 @@ public class UserTagSectionHelper extends IncludeSectionHelper implements Sectio private static final String NESTED_CONTENT = "nested-content"; protected final boolean isNestedContentNeeded; + private final HtmlEscaper htmlEscaper; + private final String itKey; UserTagSectionHelper(Supplier