diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 0b5aab120c7fa..19bca0d7cb51f 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -99,10 +99,10 @@ 1.5.1 - 6.2.7.Final + 6.2.9.Final 1.14.7 6.0.6.Final - 2.0.4.Final + 2.0.5.Final 8.0.1.Final 6.2.1.Final @@ -169,8 +169,8 @@ 9.21.2 3.0.3 - 4.23.1 - 4.23.0 + 4.23.2 + 4.23.1 2.1 6.0.0 4.10.2 @@ -198,7 +198,7 @@ 2.1 4.7.4 1.1.0 - 1.23.0 + 1.24.0 1.10.0 2.10.1 1.1.1.Final @@ -215,7 +215,7 @@ 2.7 2.4 2.4.0 - 6.6.0.202305301015-r + 6.6.1.202309021850-r 0.12.0 9.31 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index fd33610c90ca0..326fd9c0b924a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -1013,8 +1013,8 @@ private Converter getConverter(SmallRyeConfig config, Field field, ConverterT * properties in all profiles (active or not), so it is safe to fall back to different default on another * profile. *
- * We also filter the properties coming from System or Env with the registered roots, because we don't want to - * record properties set by the compiling JVM (or other properties that are only related to the build). + * We also filter the properties coming from System, Env or Build with the registered roots, because we don't + * want to record properties set by the compiling JVM (or other properties that are only related to the build). */ private Set getAllProperties(final Set registeredRoots) { Set properties = new HashSet<>(); @@ -1023,7 +1023,8 @@ private Set getAllProperties(final Set registeredRoots) { } for (ConfigSource configSource : config.getConfigSources()) { - if (configSource instanceof SysPropConfigSource || configSource instanceof EnvConfigSource) { + if (configSource instanceof SysPropConfigSource || configSource instanceof EnvConfigSource + || "PropertiesConfigSource[source=Build system]".equals(configSource.getName())) { for (String property : configSource.getPropertyNames()) { NameIterator ni = new NameIterator(property); if (ni.hasNext() && PropertiesUtil.isPropertyInRoot(registeredRoots, ni)) { diff --git a/docs/pom.xml b/docs/pom.xml index 7177ab511802c..43da6420781c5 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -27,7 +27,7 @@ 2.26.0.Final 21 11.1.0 - 6.6.0.202305301015-r + 6.6.1.202309021850-r https://quarkus.io https://github.com/quarkusio/quarkus diff --git a/docs/src/main/asciidoc/aws-lambda-http.adoc b/docs/src/main/asciidoc/aws-lambda-http.adoc index f94a0927f1e29..ccddd58c3e3a8 100644 --- a/docs/src/main/asciidoc/aws-lambda-http.adoc +++ b/docs/src/main/asciidoc/aws-lambda-http.adoc @@ -576,4 +576,4 @@ the `QUARKUS_AWS_LAMBDA_FORCE_USER_NAME` environment variable == SnapStart -To optimize your application for Lambda SnapStart, check xref:amazon-snapstart.adoc[the SnapStart Configuration Documentation]. +To optimize your application for Lambda SnapStart, check xref:aws-lambda-snapstart.adoc[the SnapStart Configuration Documentation]. diff --git a/docs/src/main/asciidoc/aws-lambda.adoc b/docs/src/main/asciidoc/aws-lambda.adoc index 05ebc142f0bc6..9e6e70a2129f0 100644 --- a/docs/src/main/asciidoc/aws-lambda.adoc +++ b/docs/src/main/asciidoc/aws-lambda.adoc @@ -715,4 +715,4 @@ That's all there is to it! == SnapStart -To optimize your application for Lambda SnapStart, check xref:amazon-snapstart.adoc[the SnapStart Configuration Documentation]. \ No newline at end of file +To optimize your application for Lambda SnapStart, check xref:aws-lambda-snapstart.adoc[the SnapStart Configuration Documentation]. \ No newline at end of file diff --git a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc index 6eff93e0e0fa0..7ba55fe448308 100644 --- a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc @@ -14,6 +14,16 @@ breadth of an Object Relational Mapper allowing you to access your database over It makes complex mappings possible, but it does not make simple and common mappings trivial. Hibernate Reactive with Panache focuses on making your entities trivial and fun to write in Quarkus. +[NOTE] +==== +Hibernate Reactive is not a replacement for xref:hibernate-orm-panache.adoc[Hibernate ORM] or the future of Hibernate ORM. +It is a different stack tailored for reactive use cases where you need high-concurrency. + +Furthermore, using RESTEasy Reactive, our default REST layer, does not require the use of Hibernate Reactive. +It is perfectly valid to use RESTEasy Reactive with Hibernate ORM, +and if you do not need high-concurrency, or are not accustomed to the reactive paradigm, it is recommended to use Hibernate ORM. +==== + == First: an example What we're doing in Panache allows you to write your Hibernate Reactive entities like this: @@ -685,7 +695,7 @@ matching the values returned by the select clause: ---- import io.quarkus.runtime.annotations.RegisterForReflection; -@RegisterForReflection +@RegisterForReflection public class RaceWeight { public final String race; public final Double weight @@ -736,7 +746,7 @@ NOTE: Note that a Panache entity may not be used from a blocking thread. See als Also make sure to wrap methods that modify the database or involve multiple queries (e.g. `entity.persist()`) within a transaction. You can annotate a CDI business method that returns `Uni` with the `@WithTransaction` annotation. The method will be intercepted and the returned `Uni` is triggered within a transaction boundary. -Alternatively, you can use the `Panache.withTransaction()` method for the same effect. +Alternatively, you can use the `Panache.withTransaction()` method for the same effect. IMPORTANT: You cannot use the `@Transactional` annotation with Hibernate Reactive for your transactions: you must use `@WithTransaction`, and your annotated method must return a `Uni` to be non-blocking. @@ -871,9 +881,9 @@ public class SomeTest { @Test @RunOnVertxContext - public void testEntity(TransactionalUniAsserter asserter) { + public void testEntity(TransactionalUniAsserter asserter) { asserter.execute(() -> new MyEntity().persist()); <1> - asserter.assertEquals(() -> MyEntity.count(), 1l); <2> + asserter.assertEquals(() -> MyEntity.count(), 1l); <2> asserter.execute(() -> MyEntity.deleteAll()); <3> } } @@ -1062,7 +1072,7 @@ public class PanacheFunctionalityTest { @RunOnVertxContext // <1> @Test public void testPanacheRepositoryMocking(UniAsserter asserter) { // <2> - + // Mocked classes always return a default value asserter.assertEquals(() -> mockablePersonRepository.count(), 0l); @@ -1091,7 +1101,7 @@ public class PanacheFunctionalityTest { }); asserter.assertThat(() -> mockablePersonRepository.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key))); asserter.assertNull(() -> mockablePersonRepository.findById(42l)); - + // Mock throwing asserter.execute(() -> Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException())); asserter.assertFailedWith(() -> { @@ -1113,7 +1123,7 @@ public class PanacheFunctionalityTest { Mockito.verify(mockablePersonRepository).persist(Mockito. any()); Mockito.verifyNoMoreInteractions(mockablePersonRepository); }); - + // IMPORTANT: We need to execute the asserter within a reactive session asserter.surroundWith(u -> Panache.withSession(() -> u)); } diff --git a/docs/src/main/asciidoc/hibernate-reactive.adoc b/docs/src/main/asciidoc/hibernate-reactive.adoc index edd4b83ab2774..d6ee6d9e0fd57 100644 --- a/docs/src/main/asciidoc/hibernate-reactive.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive.adoc @@ -9,10 +9,19 @@ include::_attributes.adoc[] :reactive-doc-url-prefix: https://hibernate.org/reactive/documentation/1.1/reference/html_single/#getting-started :extension-status: preview - link:https://hibernate.org/reactive/[Hibernate Reactive] is a reactive API for Hibernate ORM, supporting non-blocking database drivers and a reactive style of interaction with the database. +[NOTE] +==== +Hibernate Reactive is not a replacement for xref:hibernate-orm.adoc[Hibernate ORM] or the future of Hibernate ORM. +It is a different stack tailored for reactive use cases where you need high-concurrency. + +Also, using RESTEasy Reactive, our default REST layer, does not require the use of Hibernate Reactive. +It is perfectly valid to use RESTEasy Reactive with Hibernate ORM, +and if you do not need high-concurrency, or are not accustomed to the reactive paradigm, it is recommended to use Hibernate ORM. +==== + [NOTE] ==== Hibernate Reactive works with the same annotations and most of the configuration described in the diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index a25f8d88588c9..1a2785433df2c 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -247,7 +247,8 @@ A category configuration recursively applies to all subcategories of that catego The parent of all logging categories is called the "root category". This category, being the ultimate parent, may contain configuration which applies globally to all other categories. This includes the globally configured handlers and formatters. -Thus, configurations made under `quarkus.log.console.*`, `quarkus.log.file.*`, and `quarkus.log.syslog.*`, are global and apply for all categories. For more information, see <>. +Thus, configurations made under `quarkus.log.console.+*+`, `quarkus.log.file.+*+`, and `quarkus.log.syslog.+*+` are global and apply for all categories. +For more information, see <>. If you want to configure something extra for a specific category, create a named handler like `quarkus.log.handler.[console|file|syslog]..*` and set it up for that category by using `quarkus.log.category..handlers`. diff --git a/docs/src/main/asciidoc/spring-data-jpa.adoc b/docs/src/main/asciidoc/spring-data-jpa.adoc index b0be429b39b5d..627f0b393d8fc 100644 --- a/docs/src/main/asciidoc/spring-data-jpa.adoc +++ b/docs/src/main/asciidoc/spring-data-jpa.adoc @@ -602,6 +602,7 @@ is not used at all (since all the necessary plumbing is done at build time). Sim * Native and named queries when using `@Query` * https://github.com/spring-projects/spring-data-jpa/blob/main/src/main/asciidoc/jpa.adoc#entity-state-detection-strategies[Entity State-detection Strategies] via `EntityInformation`. +* The use of `org.springframework.data.jpa.repository.Lock` The Quarkus team is exploring various alternatives to bridging the gap between the JPA and Reactive worlds. diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index 03f0df969bcbd..dddaa383af560 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -1316,6 +1316,11 @@ public interface SomeConfig { } ---- <1> This will generate a configuration map key named `quarkus.some."cache-name"` instead of `quarkus.some."namespace"`. + +It is possible to write a textual explanation for the documentation default value, this is useful when it is generated: `@ConfigDocDefault("explain how this is generated")`. + +`@ConfigDocEnumValue` gives a way to explicitly customize the string displayed in the documentation when listing accepted values for an enum. + ==== ===== Writing section documentation diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java index c8716e52eee85..694b690fe797f 100644 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java +++ b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java @@ -243,7 +243,8 @@ private String getCookieToken(RoutingContext routing, CsrfReactiveConfig config) } private boolean isCsrfTokenRequired(RoutingContext routing, CsrfReactiveConfig config) { - return config.createTokenPath.isPresent() ? config.createTokenPath.get().contains(routing.request().path()) : true; + return config.createTokenPath + .map(value -> value.contains(routing.normalizedPath())).orElse(true); } private void createCookie(String csrfToken, RoutingContext routing, CsrfReactiveConfig config) { diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayAlwaysEnabledProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayAlwaysEnabledProcessor.java new file mode 100644 index 0000000000000..bff1407d07cf1 --- /dev/null +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayAlwaysEnabledProcessor.java @@ -0,0 +1,44 @@ +package io.quarkus.flyway.deployment; + +import org.flywaydb.core.extensibility.Plugin; + +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.IndexDependencyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; + +public class FlywayAlwaysEnabledProcessor { + + @BuildStep + void build(BuildProducer featureProducer) { + featureProducer.produce(new FeatureBuildItem(Feature.FLYWAY)); + } + + /** + * Reinitialize {@code InsertRowLock} to avoid using a cached seed when invoking {@code getNextRandomString} + */ + @BuildStep + public RuntimeReinitializedClassBuildItem reinitInsertRowLock() { + return new RuntimeReinitializedClassBuildItem( + "org.flywaydb.core.internal.database.InsertRowLock"); + } + + @BuildStep + public NativeImageResourceBuildItem resources() { + return new NativeImageResourceBuildItem("org/flywaydb/database/version.txt"); + } + + @BuildStep + IndexDependencyBuildItem indexFlyway() { + return new IndexDependencyBuildItem("org.flywaydb", "flyway-core"); + } + + @BuildStep + public ServiceProviderBuildItem flywayPlugins() { + return ServiceProviderBuildItem.allProvidersFromClassPath(Plugin.class.getName()); + } +} diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayCallbacksLocator.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayCallbacksLocator.java similarity index 99% rename from extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayCallbacksLocator.java rename to extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayCallbacksLocator.java index 8d90513e8de45..92e5a125affc5 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayCallbacksLocator.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayCallbacksLocator.java @@ -1,4 +1,4 @@ -package io.quarkus.flyway; +package io.quarkus.flyway.deployment; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayEnabled.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayEnabled.java new file mode 100644 index 0000000000000..928e009e8ef16 --- /dev/null +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayEnabled.java @@ -0,0 +1,24 @@ +package io.quarkus.flyway.deployment; + +import java.util.function.BooleanSupplier; + +import io.quarkus.flyway.runtime.FlywayBuildTimeConfig; + +/** + * Supplier that can be used to only run build steps + * if the Flyway extension is enabled. + */ +public class FlywayEnabled implements BooleanSupplier { + + private final FlywayBuildTimeConfig config; + + FlywayEnabled(FlywayBuildTimeConfig config) { + this.config = config; + } + + @Override + public boolean getAsBoolean() { + return config.enabled; + } + +} diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java similarity index 89% rename from extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java rename to extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java index 1fb06fce2b9c2..ecd2439175b89 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java @@ -1,4 +1,4 @@ -package io.quarkus.flyway; +package io.quarkus.flyway.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; @@ -25,7 +25,6 @@ import org.flywaydb.core.api.Location; import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.migration.JavaMigration; -import org.flywaydb.core.extensibility.Plugin; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; @@ -43,34 +42,33 @@ import io.quarkus.arc.processor.DotNames; import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.annotations.Consume; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Produce; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; -import io.quarkus.deployment.builditem.IndexDependencyBuildItem; import io.quarkus.deployment.builditem.InitTaskBuildItem; import io.quarkus.deployment.builditem.InitTaskCompletedBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.deployment.recording.RecorderContext; +import io.quarkus.flyway.FlywayDataSource; import io.quarkus.flyway.runtime.FlywayBuildTimeConfig; import io.quarkus.flyway.runtime.FlywayContainer; import io.quarkus.flyway.runtime.FlywayContainerProducer; +import io.quarkus.flyway.runtime.FlywayDataSourceBuildTimeConfig; import io.quarkus.flyway.runtime.FlywayRecorder; import io.quarkus.flyway.runtime.FlywayRuntimeConfig; import io.quarkus.runtime.util.ClassPathUtils; +@BuildSteps(onlyIf = FlywayEnabled.class) class FlywayProcessor { private static final String CLASSPATH_APPLICATION_MIGRATIONS_PROTOCOL = "classpath"; @@ -82,32 +80,26 @@ class FlywayProcessor { private static final Logger LOGGER = Logger.getLogger(FlywayProcessor.class); - FlywayBuildTimeConfig flywayBuildConfig; - - @BuildStep - IndexDependencyBuildItem indexFlyway() { - return new IndexDependencyBuildItem("org.flywaydb", "flyway-core"); - } - @Record(STATIC_INIT) @BuildStep - MigrationStateBuildItem build(BuildProducer featureProducer, - BuildProducer resourceProducer, + MigrationStateBuildItem build(BuildProducer resourceProducer, BuildProducer reflectiveClassProducer, BuildProducer hotDeploymentProducer, FlywayRecorder recorder, RecorderContext context, CombinedIndexBuildItem combinedIndexBuildItem, - List jdbcDataSourceBuildItems) throws Exception { - - featureProducer.produce(new FeatureBuildItem(Feature.FLYWAY)); + List jdbcDataSourceBuildItems, + FlywayBuildTimeConfig flywayBuildTimeConfig) throws Exception { Collection dataSourceNames = getDataSourceNames(jdbcDataSourceBuildItems); Map> applicationMigrationsToDs = new HashMap<>(); - for (var i : dataSourceNames) { + for (var dataSourceName : dataSourceNames) { + FlywayDataSourceBuildTimeConfig flywayDataSourceBuildTimeConfig = flywayBuildTimeConfig + .getConfigForDataSourceName(dataSourceName); + Collection migrationLocations = discoverApplicationMigrations( - flywayBuildConfig.getConfigForDataSourceName(i).locations); - applicationMigrationsToDs.put(i, migrationLocations); + flywayDataSourceBuildTimeConfig.locations); + applicationMigrationsToDs.put(dataSourceName, migrationLocations); } Set datasourcesWithMigrations = new HashSet<>(); Set datasourcesWithoutMigrations = new HashSet<>(); @@ -138,7 +130,7 @@ MigrationStateBuildItem build(BuildProducer featureProducer, final Map> callbacks = FlywayCallbacksLocator.with( dataSourceNames, - flywayBuildConfig, + flywayBuildTimeConfig, combinedIndexBuildItem, reflectiveClassProducer).getCallbacks(); recorder.setApplicationCallbackClasses(callbacks); @@ -170,7 +162,8 @@ void createBeans(FlywayRecorder recorder, List sqlGeneratorBuildItems, BuildProducer additionalBeans, BuildProducer syntheticBeanBuildItemBuildProducer, - MigrationStateBuildItem migrationsBuildItem) { + MigrationStateBuildItem migrationsBuildItem, + FlywayBuildTimeConfig flywayBuildTimeConfig) { // make a FlywayContainerProducer bean additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClasses(FlywayContainerProducer.class).setUnremovable() .setDefaultScope(DotNames.SINGLETON).build()); @@ -251,9 +244,14 @@ public ServiceStartBuildItem startActions(FlywayRecorder recorder, FlywayRuntimeConfig config, BuildProducer schemaReadyBuildItem, BuildProducer initializationCompleteBuildItem, + List jdbcDataSourceBuildItems, MigrationStateBuildItem migrationsBuildItem) { - recorder.doStartActions(); + Collection 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" @@ -341,25 +339,6 @@ private Set getApplicationMigrationsFromPath(final String location, fina } } - /** - * Reinitialize {@code InsertRowLock} to avoid using a cached seed when invoking {@code getNextRandomString} - */ - @BuildStep - public RuntimeReinitializedClassBuildItem reinitInsertRowLock() { - return new RuntimeReinitializedClassBuildItem( - "org.flywaydb.core.internal.database.InsertRowLock"); - } - - @BuildStep - public NativeImageResourceBuildItem resources() { - return new NativeImageResourceBuildItem("org/flywaydb/database/version.txt"); - } - - @BuildStep - public ServiceProviderBuildItem flywayPlugins() { - return ServiceProviderBuildItem.allProvidersFromClassPath(Plugin.class.getName()); - } - public static final class MigrationStateBuildItem extends SimpleBuildItem { final Set hasMigrations; diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/devui/FlywayDevUIProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/devui/FlywayDevUIProcessor.java similarity index 97% rename from extensions/flyway/deployment/src/main/java/io/quarkus/flyway/devui/FlywayDevUIProcessor.java rename to extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/devui/FlywayDevUIProcessor.java index e1d1c7cfcb838..90dd8c6ad9e19 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/devui/FlywayDevUIProcessor.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/devui/FlywayDevUIProcessor.java @@ -1,4 +1,4 @@ -package io.quarkus.flyway.devui; +package io.quarkus.flyway.deployment.devui; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateNamedDataSourcesInactiveTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateNamedDataSourcesInactiveTest.java new file mode 100644 index 0000000000000..f4b5fb39d1c18 --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionBaselineOnMigrateNamedDataSourcesInactiveTest.java @@ -0,0 +1,70 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfo; +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 FlywayExtensionBaselineOnMigrateNamedDataSourcesInactiveTest { + + @Inject + @FlywayDataSource("users") + Flyway flywayUsers; + + @Inject + @FlywayDataSource("laptops") + Flyway flywayLaptops; + + static final FlywayH2TestCustomizer customizerUsers = FlywayH2TestCustomizer + .withDbName("quarkus-flyway-baseline-on-named-ds-users") + .withPort(11302) + .withInitSqlFile("src/test/resources/h2-init-data.sql"); + + static final FlywayH2TestCustomizer customizerLaptops = FlywayH2TestCustomizer + .withDbName("quarkus-flyway-baseline-on-named-ds-laptops") + .withPort(11303) + .withInitSqlFile("src/test/resources/h2-init-data.sql"); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setBeforeAllCustomizer(new Runnable() { + @Override + public void run() { + customizerUsers.startH2(); + customizerLaptops.startH2(); + } + }) + .setAfterAllCustomizer(new Runnable() { + @Override + public void run() { + customizerUsers.stopH2(); + customizerLaptops.stopH2(); + } + }) + .withApplicationRoot((jar) -> jar + .addClass(FlywayH2TestCustomizer.class) + .addAsResource("baseline-on-migrate-named-datasources-inactive.properties", "application.properties")); + + @Test + @DisplayName("Create history table correctly") + public void testFlywayInitialBaselineInfo() { + MigrationInfo baselineInfo = flywayUsers.info().applied()[0]; + + assertEquals("0.0.1", baselineInfo.getVersion().getVersion()); + assertEquals("Initial description for test", baselineInfo.getDescription()); + } + + @Test + @DisplayName("History table not created if inactive") + public void testFlywayInitialBaselineInfoInactive() { + assertEquals(0, flywayLaptops.info().applied().length); + } +} diff --git a/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionDisabledTest.java b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionDisabledTest.java new file mode 100644 index 0000000000000..c5fa111d1074f --- /dev/null +++ b/extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionDisabledTest.java @@ -0,0 +1,31 @@ +package io.quarkus.flyway.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +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 FlywayExtensionDisabledTest { + + @Inject + Instance flyway; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addAsResource("db/migration/V1.0.0__Quarkus.sql") + .addAsResource("disabled-config.properties", "application.properties")); + + @Test + @DisplayName("No Flyway instance available if disabled") + public void testFlywayConfigInjection() { + assertTrue(flyway.isUnsatisfied()); + } +} diff --git a/extensions/flyway/deployment/src/test/resources/baseline-on-migrate-named-datasources-inactive.properties b/extensions/flyway/deployment/src/test/resources/baseline-on-migrate-named-datasources-inactive.properties new file mode 100644 index 0000000000000..d816d7b5a8451 --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/baseline-on-migrate-named-datasources-inactive.properties @@ -0,0 +1,24 @@ +quarkus.datasource.users.db-kind=h2 +quarkus.datasource.users.username=sa +quarkus.datasource.users.password=sa +quarkus.datasource.users.jdbc.url=jdbc:h2:tcp://localhost:11302/mem:quarkus-flyway-baseline-on-named-ds-users + +# Flyway config properties +quarkus.flyway.users.migrate-at-start=true +quarkus.flyway.users.table=test_flyway_history +quarkus.flyway.users.baseline-on-migrate=true +quarkus.flyway.users.baseline-version=0.0.1 +quarkus.flyway.users.baseline-description=Initial description for test + +quarkus.datasource.laptops.db-kind=h2 +quarkus.datasource.laptops.username=sa +quarkus.datasource.laptops.password=sa +quarkus.datasource.laptops.jdbc.url=jdbc:h2:tcp://localhost:11302/mem:quarkus-flyway-baseline-on-named-ds-laptops + +# Flyway config properties +quarkus.flyway.laptops.active=false +quarkus.flyway.laptops.migrate-at-start=true +quarkus.flyway.laptops.table=test_flyway_history +quarkus.flyway.laptops.baseline-on-migrate=true +quarkus.flyway.laptops.baseline-version=0.0.1 +quarkus.flyway.laptops.baseline-description=Initial description for test diff --git a/extensions/flyway/deployment/src/test/resources/disabled-config.properties b/extensions/flyway/deployment/src/test/resources/disabled-config.properties new file mode 100644 index 0000000000000..7bf03c9db53df --- /dev/null +++ b/extensions/flyway/deployment/src/test/resources/disabled-config.properties @@ -0,0 +1,8 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:test-quarkus-migrate-at-start;DB_CLOSE_DELAY=-1 + +# Flyway config properties +quarkus.flyway.enabled=false +quarkus.flyway.migrate-at-start=true diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildTimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildTimeConfig.java index 47e7a4878c7f4..5560e96941cc4 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildTimeConfig.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayBuildTimeConfig.java @@ -21,6 +21,16 @@ public FlywayDataSourceBuildTimeConfig getConfigForDataSourceName(String dataSou return namedDataSources.getOrDefault(dataSourceName, FlywayDataSourceBuildTimeConfig.defaultConfig()); } + /** + * Whether Flyway is enabled *during the build*. + * + * If Flyway is disabled, the Flyway beans won't be created and Flyway won't be usable. + * + * @asciidoclet + */ + @ConfigItem(defaultValue = "true") + public boolean enabled; + /** * Flyway configuration for the default datasource. */ 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 79f4c961a0e5a..726ada0de89fd 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 @@ -22,6 +22,12 @@ public static FlywayDataSourceRuntimeConfig defaultConfig() { return new FlywayDataSourceRuntimeConfig(); } + /** + * Flag to activate/deactivate Flyway for a specific datasource at runtime. + */ + @ConfigItem(defaultValue = "true") + public boolean active = true; + /** * 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 2bb0fffb51c72..f8df08e4e7825 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 @@ -8,7 +8,6 @@ import javax.sql.DataSource; import jakarta.enterprise.inject.Default; -import jakarta.enterprise.inject.UnsatisfiedResolutionException; import org.flywaydb.core.Flyway; import org.flywaydb.core.FlywayExecutor; @@ -67,7 +66,7 @@ public Function, FlywayContainer> fl public FlywayContainer apply(SyntheticCreationalContext context) { DataSource dataSource = context.getInjectedReference(DataSources.class).getDataSource(dataSourceName); if (dataSource instanceof UnconfiguredDataSource) { - throw new UnsatisfiedResolutionException("No datasource present"); + return new UnconfiguredDataSourceFlywayContainer(dataSourceName); } FlywayContainerProducer flywayProducer = context.getInjectedReference(FlywayContainerProducer.class); @@ -82,44 +81,58 @@ public Function, Flyway> flywayFunction(Strin return new Function<>() { @Override public Flyway apply(SyntheticCreationalContext context) { - Annotation flywayContainerQualifier; - if (DataSourceUtil.isDefault(dataSourceName)) { - flywayContainerQualifier = Default.Literal.INSTANCE; - } else { - flywayContainerQualifier = FlywayDataSourceLiteral.of(dataSourceName); - } - - FlywayContainer flywayContainer = context.getInjectedReference(FlywayContainer.class, flywayContainerQualifier); + FlywayContainer flywayContainer = context.getInjectedReference(FlywayContainer.class, + getFlywayContainerQualifier(dataSourceName)); return flywayContainer.getFlyway(); } }; } - public void doStartActions() { - if (!config.getValue().enabled) { + public void doStartActions(String dataSourceName) { + FlywayDataSourceRuntimeConfig flywayDataSourceRuntimeConfig = config.getValue() + .getConfigForDataSourceName(dataSourceName); + + if (!config.getValue().getConfigForDataSourceName(dataSourceName).active) { return; } - for (InstanceHandle flywayContainerHandle : Arc.container().listAll(FlywayContainer.class)) { - FlywayContainer flywayContainer = flywayContainerHandle.get(); + InstanceHandle flywayContainerInstanceHandle = Arc.container().instance(FlywayContainer.class, + getFlywayContainerQualifier(dataSourceName)); - if (flywayContainer.isCleanAtStart()) { - flywayContainer.getFlyway().clean(); - } - if (flywayContainer.isValidateAtStart()) { - flywayContainer.getFlyway().validate(); - } - if (flywayContainer.isBaselineAtStart()) { - new FlywayExecutor(flywayContainer.getFlyway().getConfiguration()) - .execute(new BaselineCommand(flywayContainer.getFlyway()), true, null); - } - if (flywayContainer.isRepairAtStart()) { - flywayContainer.getFlyway().repair(); - } - if (flywayContainer.isMigrateAtStart()) { - flywayContainer.getFlyway().migrate(); - } + if (!flywayContainerInstanceHandle.isAvailable()) { + return; } + + FlywayContainer flywayContainer = flywayContainerInstanceHandle.get(); + + if (flywayContainer instanceof UnconfiguredDataSourceFlywayContainer) { + return; + } + + if (flywayContainer.isCleanAtStart()) { + flywayContainer.getFlyway().clean(); + } + if (flywayContainer.isValidateAtStart()) { + flywayContainer.getFlyway().validate(); + } + if (flywayContainer.isBaselineAtStart()) { + new FlywayExecutor(flywayContainer.getFlyway().getConfiguration()) + .execute(new BaselineCommand(flywayContainer.getFlyway()), true, null); + } + if (flywayContainer.isRepairAtStart()) { + flywayContainer.getFlyway().repair(); + } + if (flywayContainer.isMigrateAtStart()) { + flywayContainer.getFlyway().migrate(); + } + } + + private static Annotation getFlywayContainerQualifier(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return Default.Literal.INSTANCE; + } + + return FlywayDataSourceLiteral.of(dataSourceName); } static class BaselineCommand implements FlywayExecutor.Command { diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRuntimeConfig.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRuntimeConfig.java index 481ee6e821539..703e3cd0b00a1 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRuntimeConfig.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRuntimeConfig.java @@ -21,13 +21,6 @@ public FlywayDataSourceRuntimeConfig getConfigForDataSourceName(String dataSourc return namedDataSources.getOrDefault(dataSourceName, FlywayDataSourceRuntimeConfig.defaultConfig()); } - /** - * Flag to enable / disable Flyway. - * - */ - @ConfigItem(defaultValue = "true") - public boolean enabled; - /** * Flyway configuration for the default datasource. */ diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/UnconfiguredDataSourceFlywayContainer.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/UnconfiguredDataSourceFlywayContainer.java new file mode 100644 index 0000000000000..a3206cd8141ae --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/UnconfiguredDataSourceFlywayContainer.java @@ -0,0 +1,16 @@ +package io.quarkus.flyway.runtime; + +import org.flywaydb.core.Flyway; + +public class UnconfiguredDataSourceFlywayContainer extends FlywayContainer { + + public UnconfiguredDataSourceFlywayContainer(String dataSourceName) { + super(null, false, false, false, false, false, dataSourceName, false, false); + } + + @Override + public Flyway getFlyway() { + throw new UnsupportedOperationException( + "Cannot get a Flyway instance for unconfigured datasource " + getDataSourceName()); + } +} diff --git a/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/graal/Substitute_JandexBehavior.java b/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/graal/Substitute_JandexBehavior.java index 18bfe47e63283..7315ade3084a6 100644 --- a/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/graal/Substitute_JandexBehavior.java +++ b/extensions/hibernate-search-orm-elasticsearch/runtime/src/main/java/io/quarkus/hibernate/search/orm/elasticsearch/runtime/graal/Substitute_JandexBehavior.java @@ -13,7 +13,7 @@ final class Substitute_JandexBehavior { @Substitute - public static T doWithJandex(JandexBehavior.JandexOperation operation) { + public static void doWithJandex(JandexBehavior.JandexOperation operation) { throw new IllegalStateException("Jandex should not be used at runtime."); } diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java index abd5fa3db6d43..fa7831fb6e266 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java @@ -44,7 +44,8 @@ public Uni checkPermission(RoutingContext request, Uni apply(Throwable t) { t.getCause()))); } // Token has expired, try to refresh + if (isRpInitiatedLogout(context, configContext)) { + LOG.debug("Session has expired, performing an RP initiated logout"); + fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED_SESSION_EXPIRED, + Map.of(SecurityEvent.SESSION_TOKENS_PROPERTY, session)); + return Uni.createFrom().item((SecurityIdentity) null) + .call(() -> buildLogoutRedirectUriUni(context, configContext, + currentIdToken)); + } if (session.getRefreshToken() == null) { LOG.debug( "Token has expired, token refresh is not possible because the refresh token is null"); @@ -981,6 +989,12 @@ private void fireEvent(SecurityEvent.Type eventType, SecurityIdentity securityId } } + private void fireEvent(SecurityEvent.Type eventType, Map properties) { + if (resolver.isSecurityEventObserved()) { + resolver.getSecurityEvent().fire(new SecurityEvent(eventType, properties)); + } + } + private String getRedirectPath(OidcTenantConfig oidcConfig, RoutingContext context) { Authentication auth = oidcConfig.getAuthentication(); return auth.getRedirectPath().isPresent() ? auth.getRedirectPath().get() : context.request().path(); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java index 7c4c540ebeb6f..c9a66cf76f5d9 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcJsonWebTokenProducer.java @@ -79,7 +79,9 @@ private JsonWebToken getTokenCredential(Class type) { return new OidcJwtCallerPrincipal(jwtClaims, credential); } String tokenType = type == AccessTokenCredential.class ? "access" : "ID"; - LOG.tracef("Current identity is not associated with an %s token", tokenType); + LOG.warnf( + "Identity is not associated with an %s token. Access 'JsonWebToken' with '@IdToken' qualifier if ID token is required and 'JsonWebToken' without this qualifier when JWT access token is required. Inject either 'io.quarkus.security.identity.SecurityIdentity' or 'io.quarkus.oidc.UserInfo' if you need to have the same endpoint code working for both authorization code and bearer token authentication flows.", + tokenType); return new NullJsonWebToken(); } } diff --git a/extensions/opentelemetry/runtime/pom.xml b/extensions/opentelemetry/runtime/pom.xml index 57677019dddab..7befd1be1b261 100644 --- a/extensions/opentelemetry/runtime/pom.xml +++ b/extensions/opentelemetry/runtime/pom.xml @@ -177,6 +177,11 @@ assertj-core test + + org.mockito + mockito-core + test + diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtil.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtil.java index 6ab6ffac43112..de8326f931aeb 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtil.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtil.java @@ -1,9 +1,12 @@ package io.quarkus.opentelemetry.runtime; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; @@ -16,6 +19,7 @@ public final class OpenTelemetryUtil { public static final String SPAN_ID = "spanId"; public static final String SAMPLED = "sampled"; public static final String PARENT_ID = "parentId"; + private static final Set SPAN_DATA_KEYS = Set.of(TRACE_ID, SPAN_ID, SAMPLED, PARENT_ID); private OpenTelemetryUtil() { } @@ -54,22 +58,45 @@ public static Map convertKeyValueListToMap(List headers) * @param vertxContext vertx context */ public static void setMDCData(Context context, io.vertx.core.Context vertxContext) { + setMDCData(getSpanData(context), vertxContext); + } + + public static void setMDCData(Map spanData, io.vertx.core.Context vertxContext) { + if (spanData == null) { + return; + } + + for (Entry entry : spanData.entrySet()) { + if (SPAN_DATA_KEYS.contains(entry.getKey())) { + VertxMDC.INSTANCE.put(entry.getKey(), entry.getValue(), vertxContext); + } + } + } + + /** + * Gets current span data from the MDC context. + * + * @param context opentelemetry context + */ + public static Map getSpanData(Context context) { + if (context == null) { + return Collections.emptyMap(); + } Span span = Span.fromContextOrNull(context); + Map spanData = new HashMap<>(); if (span != null) { SpanContext spanContext = span.getSpanContext(); - VertxMDC vertxMDC = VertxMDC.INSTANCE; - vertxMDC.put(SPAN_ID, spanContext.getSpanId(), vertxContext); - vertxMDC.put(TRACE_ID, spanContext.getTraceId(), vertxContext); - vertxMDC.put(SAMPLED, Boolean.toString(spanContext.isSampled()), vertxContext); + spanData.put(SPAN_ID, spanContext.getSpanId()); + spanData.put(TRACE_ID, spanContext.getTraceId()); + spanData.put(SAMPLED, Boolean.toString(spanContext.isSampled())); if (span instanceof ReadableSpan) { SpanContext parentSpanContext = ((ReadableSpan) span).getParentSpanContext(); - if (parentSpanContext.isValid()) { - vertxMDC.put(PARENT_ID, parentSpanContext.getSpanId(), vertxContext); - } else { - vertxMDC.remove(PARENT_ID, vertxContext); + if (parentSpanContext != null && parentSpanContext.isValid()) { + spanData.put(PARENT_ID, parentSpanContext.getSpanId()); } } } + return spanData; } /** diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/QuarkusContextStorage.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/QuarkusContextStorage.java index 7a30e5bcdae2d..ebba2e69aee64 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/QuarkusContextStorage.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/QuarkusContextStorage.java @@ -3,6 +3,8 @@ import static io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.setContextSafe; import static io.smallrye.common.vertx.VertxContext.isDuplicatedContext; +import java.util.Map; + import org.jboss.logging.Logger; import io.opentelemetry.context.Context; @@ -64,14 +66,18 @@ public Scope attach(io.vertx.core.Context vertxContext, Context toAttach) { return Scope.noop(); } vertxContext.putLocal(OTEL_CONTEXT, toAttach); - OpenTelemetryUtil.setMDCData(toAttach, vertxContext); + final Map spanDataToAttach = OpenTelemetryUtil.getSpanData(toAttach); + OpenTelemetryUtil.setMDCData(spanDataToAttach, vertxContext); return new Scope() { @Override public void close() { - if (getContext(vertxContext) != toAttach) { - log.warn("Context in storage not the expected context, Scope.close was not called correctly"); + final Context before = getContext(vertxContext); + if (before != toAttach) { + log.warn("Context in storage not the expected context, Scope.close was not called correctly. Details:" + + " OTel context before: " + OpenTelemetryUtil.getSpanData(before) + + ". OTel context toAttach: " + spanDataToAttach); } if (beforeAttach == null) { diff --git a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtilTest.java b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtilTest.java index f1842ccc980a5..eebe37eee8a85 100644 --- a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtilTest.java +++ b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtilTest.java @@ -1,5 +1,8 @@ package io.quarkus.opentelemetry.runtime; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + import java.util.AbstractMap; import java.util.Arrays; import java.util.Collections; @@ -8,6 +11,13 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SpanProcessor; + public class OpenTelemetryUtilTest { @Test @@ -58,4 +68,54 @@ public void testConvertKeyValueListToMap_empty_value() { .convertKeyValueListToMap(Collections.emptyList()); Assertions.assertThat(actual).containsExactly(); } + + @Test + public void testGetSpanData() { + SpanProcessor mockedSpanProcessor = mock(SpanProcessor.class); + + SdkTracerProvider tracerSdkFactory = SdkTracerProvider.builder() + .addSpanProcessor(mockedSpanProcessor) + .build(); + Tracer spanBuilderSdkTest = tracerSdkFactory.get("SpanBuilderSdkTest"); + SpanBuilder spanBuilder = spanBuilderSdkTest.spanBuilder("SpanName"); + + Span parent = spanBuilder.startSpan(); + Context contextParent = Context.current().with(parent); + + Span child = spanBuilder.setParent(contextParent).startSpan(); + Context contextChild = Context.current().with(child); + + Map actual = OpenTelemetryUtil.getSpanData(contextChild); + assertEquals(4, actual.size()); + assertEquals(child.getSpanContext().getSpanId(), actual.get("spanId")); + assertEquals(child.getSpanContext().getTraceId(), actual.get("traceId")); + assertEquals("true", actual.get("sampled")); + assertEquals(parent.getSpanContext().getSpanId(), actual.get("parentId")); + } + + @Test + public void testGetSpanData_noParent() { + SpanProcessor mockedSpanProcessor = mock(SpanProcessor.class); + SdkTracerProvider tracerSdkFactory = SdkTracerProvider.builder() + .addSpanProcessor(mockedSpanProcessor) + .build(); + Tracer spanBuilderSdkTest = tracerSdkFactory.get("SpanBuilderSdkTest"); + + SpanBuilder spanBuilder = spanBuilderSdkTest.spanBuilder("SpanName"); + + Span child = spanBuilder.startSpan(); + Context contextChild = Context.current().with(child); + + Map actual = OpenTelemetryUtil.getSpanData(contextChild); + assertEquals(3, actual.size()); + assertEquals(child.getSpanContext().getSpanId(), actual.get("spanId")); + assertEquals(child.getSpanContext().getTraceId(), actual.get("traceId")); + assertEquals("true", actual.get("sampled")); + } + + @Test + public void testGetSpanData_nullValue() { + Map actual = OpenTelemetryUtil.getSpanData(null); + assertEquals(0, actual.size()); + } } diff --git a/extensions/reactive-datasource/deployment/src/main/java/io/quarkus/reactive/datasource/deployment/VertxPoolBuildItem.java b/extensions/reactive-datasource/deployment/src/main/java/io/quarkus/reactive/datasource/deployment/VertxPoolBuildItem.java index 01e475b64c66f..ced3f83275f99 100644 --- a/extensions/reactive-datasource/deployment/src/main/java/io/quarkus/reactive/datasource/deployment/VertxPoolBuildItem.java +++ b/extensions/reactive-datasource/deployment/src/main/java/io/quarkus/reactive/datasource/deployment/VertxPoolBuildItem.java @@ -10,28 +10,26 @@ * If you inject this build item when recording runtime init template calls, you are guaranteed the Pool configuration * has been injected and Pools can be created. */ +@Deprecated(forRemoval = true) public final class VertxPoolBuildItem extends MultiBuildItem { - private final RuntimeValue vertxPool; - private final String dbKind; - private final boolean isDefault; + public VertxPoolBuildItem() { + } public VertxPoolBuildItem(RuntimeValue vertxPool, String dbKind, boolean isDefault) { - this.vertxPool = vertxPool; - this.dbKind = dbKind; - this.isDefault = isDefault; + } public RuntimeValue getPool() { - return vertxPool; + throw new IllegalStateException("should never be called"); } public String getDbKind() { - return dbKind; + throw new IllegalStateException("should never be called"); } public boolean isDefault() { - return isDefault; + throw new IllegalStateException("should never be called"); } } diff --git a/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/DB2PoolBuildItem.java b/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/DB2PoolBuildItem.java index 95b5c18a89872..a63b88b6c2796 100644 --- a/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/DB2PoolBuildItem.java +++ b/extensions/reactive-db2-client/deployment/src/main/java/io/quarkus/reactive/db2/client/deployment/DB2PoolBuildItem.java @@ -1,17 +1,19 @@ package io.quarkus.reactive.db2.client.deployment; +import java.util.function.Function; + +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.runtime.RuntimeValue; import io.vertx.db2client.DB2Pool; public final class DB2PoolBuildItem extends MultiBuildItem { private final String dataSourceName; - private final RuntimeValue db2Pool; + private final Function, DB2Pool> db2Pool; - public DB2PoolBuildItem(String dataSourceName, RuntimeValue db2Pool) { + public DB2PoolBuildItem(String dataSourceName, Function, DB2Pool> db2Pool) { this.dataSourceName = dataSourceName; this.db2Pool = db2Pool; } @@ -20,7 +22,7 @@ public String getDataSourceName() { return dataSourceName; } - public RuntimeValue getDB2Pool() { + public Function, DB2Pool> getDB2Pool() { return db2Pool; } 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 d20b951f8d517..d21106cfd1baf 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 @@ -6,15 +6,20 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; @@ -51,7 +56,6 @@ import io.quarkus.reactive.db2.client.runtime.DB2PoolRecorder; import io.quarkus.reactive.db2.client.runtime.DB2ServiceBindingConverter; import io.quarkus.reactive.db2.client.runtime.DataSourcesReactiveDB2Config; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; import io.quarkus.vertx.core.deployment.EventLoopCountBuildItem; import io.quarkus.vertx.deployment.VertxBuildItem; @@ -60,6 +64,12 @@ class ReactiveDB2ClientProcessor { + private static final ParameterizedType POOL_INJECTION_TYPE = ParameterizedType.create(DotName.createSimple(Instance.class), + new Type[] { ClassType.create(DotName.createSimple(DB2PoolCreator.class.getName())) }, null); + private static final AnnotationInstance[] EMPTY_ANNOTATIONS = new AnnotationInstance[0]; + + private static final DotName REACTIVE_DATASOURCE = DotName.createSimple(ReactiveDataSource.class); + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) ServiceStartBuildItem build(BuildProducer feature, @@ -81,7 +91,7 @@ ServiceStartBuildItem build(BuildProducer feature, feature.produce(new FeatureBuildItem(Feature.REACTIVE_DB2_CLIENT)); for (String dataSourceName : dataSourcesBuildTimeConfig.dataSources().keySet()) { - createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, db2Pool, vertxPool, syntheticBeans, dataSourceName, + createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, db2Pool, syntheticBeans, dataSourceName, dataSourcesBuildTimeConfig, dataSourcesRuntimeConfig, dataSourcesReactiveBuildTimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactiveDB2Config, defaultDataSourceDbKindBuildItems, curateOutcomeBuildItem); @@ -90,6 +100,7 @@ ServiceStartBuildItem build(BuildProducer feature, // Enable SSL support by default sslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.REACTIVE_DB2_CLIENT)); + vertxPool.produce(new VertxPoolBuildItem()); return new ServiceStartBuildItem("reactive-db2-client"); } @@ -168,7 +179,6 @@ private void createPoolIfDefined(DB2PoolRecorder recorder, EventLoopCountBuildItem eventLoopCount, ShutdownContextBuildItem shutdown, BuildProducer db2Pool, - BuildProducer vertxPool, BuildProducer syntheticBeans, String dataSourceName, DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, @@ -184,20 +194,21 @@ private void createPoolIfDefined(DB2PoolRecorder recorder, return; } - RuntimeValue pool = recorder.configureDB2Pool(vertx.getVertx(), + Function, DB2Pool> poolFunction = recorder.configureDB2Pool(vertx.getVertx(), eventLoopCount.getEventLoopCount(), dataSourceName, dataSourcesRuntimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactiveDB2Config, shutdown); - db2Pool.produce(new DB2PoolBuildItem(dataSourceName, pool)); + db2Pool.produce(new DB2PoolBuildItem(dataSourceName, poolFunction)); ExtendedBeanConfigurator db2PoolBeanConfigurator = SyntheticBeanBuildItem.configure(DB2Pool.class) .defaultBean() .addType(Pool.class) .scope(ApplicationScoped.class) - .runtimeValue(pool) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -209,14 +220,21 @@ private void createPoolIfDefined(DB2PoolRecorder recorder, .configure(io.vertx.mutiny.db2client.DB2Pool.class) .defaultBean() .scope(ApplicationScoped.class) - .runtimeValue(recorder.mutinyDB2Pool(pool)) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(recorder.mutinyDB2Pool(poolFunction)) .setRuntimeInit(); addQualifiers(mutinyDB2PoolConfigurator, dataSourceName); syntheticBeans.produce(mutinyDB2PoolConfigurator.done()); + } - vertxPool.produce(new VertxPoolBuildItem(pool, DatabaseKind.DB2, DataSourceUtil.isDefault(dataSourceName))); + private AnnotationInstance[] injectionPointAnnotations(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return EMPTY_ANNOTATIONS; + } + return new AnnotationInstance[] { + AnnotationInstance.builder(REACTIVE_DATASOURCE).add("value", dataSourceName).build() }; } private static boolean isReactiveDB2PoolDefined(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, 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 90681e5e63bde..d5e372b88779b 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 @@ -12,13 +12,15 @@ import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.util.TypeLiteral; import org.jboss.logging.Logger; -import io.quarkus.arc.Arc; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; @@ -44,28 +46,42 @@ public class DB2PoolRecorder { private static final Logger log = Logger.getLogger(DB2PoolRecorder.class); + private static final TypeLiteral> TYPE_LITERAL = new TypeLiteral<>() { + }; - public RuntimeValue configureDB2Pool(RuntimeValue vertx, + public Function, DB2Pool> configureDB2Pool(RuntimeValue vertx, Supplier eventLoopCount, String dataSourceName, DataSourcesRuntimeConfig dataSourcesRuntimeConfig, DataSourcesReactiveRuntimeConfig dataSourcesReactiveRuntimeConfig, DataSourcesReactiveDB2Config dataSourcesReactiveDB2Config, ShutdownContext shutdown) { - - DB2Pool db2Pool = initialize((VertxInternal) vertx.getValue(), - eventLoopCount.get(), - dataSourceName, - dataSourcesRuntimeConfig.dataSources().get(dataSourceName), - dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), - dataSourcesReactiveDB2Config.dataSources().get(dataSourceName).reactive().db2()); - - shutdown.addShutdownTask(db2Pool::close); - return new RuntimeValue<>(db2Pool); + return new Function<>() { + @Override + public DB2Pool apply(SyntheticCreationalContext context) { + DB2Pool db2Pool = initialize((VertxInternal) vertx.getValue(), + eventLoopCount.get(), + dataSourceName, + dataSourcesRuntimeConfig.dataSources().get(dataSourceName), + dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), + dataSourcesReactiveDB2Config.dataSources().get(dataSourceName).reactive().db2(), + context); + + shutdown.addShutdownTask(db2Pool::close); + return db2Pool; + } + }; } - public RuntimeValue mutinyDB2Pool(RuntimeValue db2Pool) { - return new RuntimeValue<>(io.vertx.mutiny.db2client.DB2Pool.newInstance(db2Pool.getValue())); + public Function, io.vertx.mutiny.db2client.DB2Pool> mutinyDB2Pool( + Function, DB2Pool> function) { + return new Function<>() { + @SuppressWarnings("unchecked") + @Override + public io.vertx.mutiny.db2client.DB2Pool apply(SyntheticCreationalContext context) { + return io.vertx.mutiny.db2client.DB2Pool.newInstance(function.apply(context)); + } + }; } private DB2Pool initialize(VertxInternal vertx, @@ -73,14 +89,15 @@ private DB2Pool initialize(VertxInternal vertx, String dataSourceName, DataSourceRuntimeConfig dataSourceRuntimeConfig, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, - DataSourceReactiveDB2Config dataSourceReactiveDB2Config) { + DataSourceReactiveDB2Config dataSourceReactiveDB2Config, + SyntheticCreationalContext context) { PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveDB2Config); DB2ConnectOptions db2ConnectOptions = toConnectOptions(dataSourceName, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveDB2Config); Supplier> databasesSupplier = toDatabasesSupplier(vertx, List.of(db2ConnectOptions), dataSourceRuntimeConfig); - return createPool(vertx, poolOptions, db2ConnectOptions, dataSourceName, databasesSupplier); + return createPool(vertx, poolOptions, db2ConnectOptions, dataSourceName, databasesSupplier, context); } private Supplier> toDatabasesSupplier(Vertx vertx, List db2ConnectOptionsList, @@ -213,12 +230,13 @@ private DB2ConnectOptions toConnectOptions(String dataSourceName, DataSourceRunt } private DB2Pool createPool(Vertx vertx, PoolOptions poolOptions, DB2ConnectOptions dB2ConnectOptions, - String dataSourceName, Supplier> databases) { + String dataSourceName, Supplier> databases, + SyntheticCreationalContext context) { Instance instance; if (DataSourceUtil.isDefault(dataSourceName)) { - instance = Arc.container().select(DB2PoolCreator.class); + instance = context.getInjectedReference(TYPE_LITERAL); } else { - instance = Arc.container().select(DB2PoolCreator.class, + instance = context.getInjectedReference(TYPE_LITERAL, new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); } if (instance.isResolvable()) { diff --git a/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/MSSQLPoolBuildItem.java b/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/MSSQLPoolBuildItem.java index 0eb56a6071d9d..639f59dac3e25 100644 --- a/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/MSSQLPoolBuildItem.java +++ b/extensions/reactive-mssql-client/deployment/src/main/java/io/quarkus/reactive/mssql/client/deployment/MSSQLPoolBuildItem.java @@ -1,17 +1,19 @@ package io.quarkus.reactive.mssql.client.deployment; +import java.util.function.Function; + +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.runtime.RuntimeValue; import io.vertx.mssqlclient.MSSQLPool; public final class MSSQLPoolBuildItem extends MultiBuildItem { private final String dataSourceName; - private final RuntimeValue mssqlPool; + private final Function, MSSQLPool> mssqlPool; - public MSSQLPoolBuildItem(String dataSourceName, RuntimeValue mssqlPool) { + public MSSQLPoolBuildItem(String dataSourceName, Function, MSSQLPool> mssqlPool) { this.dataSourceName = dataSourceName; this.mssqlPool = mssqlPool; } @@ -20,7 +22,7 @@ public String getDataSourceName() { return dataSourceName; } - public RuntimeValue getMSSQLPool() { + public Function, MSSQLPool> getMSSQLPool() { return mssqlPool; } 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 5e5d06baf8bdd..fc29eb683d158 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 @@ -6,15 +6,20 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; @@ -51,7 +56,6 @@ import io.quarkus.reactive.mssql.client.runtime.DataSourcesReactiveMSSQLConfig; import io.quarkus.reactive.mssql.client.runtime.MSSQLPoolRecorder; import io.quarkus.reactive.mssql.client.runtime.MsSQLServiceBindingConverter; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; import io.quarkus.vertx.core.deployment.EventLoopCountBuildItem; import io.quarkus.vertx.deployment.VertxBuildItem; @@ -60,6 +64,11 @@ class ReactiveMSSQLClientProcessor { + private static final ParameterizedType POOL_INJECTION_TYPE = ParameterizedType.create(DotName.createSimple(Instance.class), + new Type[] { ClassType.create(DotName.createSimple(MSSQLPoolCreator.class.getName())) }, null); + private static final AnnotationInstance[] EMPTY_ANNOTATIONS = new AnnotationInstance[0]; + private static final DotName REACTIVE_DATASOURCE = DotName.createSimple(ReactiveDataSource.class); + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) ServiceStartBuildItem build(BuildProducer feature, @@ -81,7 +90,7 @@ ServiceStartBuildItem build(BuildProducer feature, feature.produce(new FeatureBuildItem(Feature.REACTIVE_MSSQL_CLIENT)); for (String dataSourceName : dataSourcesBuildTimeConfig.dataSources().keySet()) { - createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, msSQLPool, vertxPool, syntheticBeans, dataSourceName, + createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, msSQLPool, syntheticBeans, dataSourceName, dataSourcesBuildTimeConfig, dataSourcesRuntimeConfig, dataSourcesReactiveBuildTimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactiveMSSQLConfig, defaultDataSourceDbKindBuildItems, curateOutcomeBuildItem); @@ -90,6 +99,7 @@ ServiceStartBuildItem build(BuildProducer feature, // Enable SSL support by default sslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.REACTIVE_MSSQL_CLIENT)); + vertxPool.produce(new VertxPoolBuildItem()); return new ServiceStartBuildItem("reactive-mssql-client"); } @@ -168,7 +178,6 @@ private void createPoolIfDefined(MSSQLPoolRecorder recorder, EventLoopCountBuildItem eventLoopCount, ShutdownContextBuildItem shutdown, BuildProducer msSQLPool, - BuildProducer vertxPool, BuildProducer syntheticBeans, String dataSourceName, DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, @@ -184,20 +193,21 @@ private void createPoolIfDefined(MSSQLPoolRecorder recorder, return; } - RuntimeValue pool = recorder.configureMSSQLPool(vertx.getVertx(), + Function, MSSQLPool> poolFunction = recorder.configureMSSQLPool(vertx.getVertx(), eventLoopCount.getEventLoopCount(), dataSourceName, dataSourcesRuntimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactiveMSSQLConfig, shutdown); - msSQLPool.produce(new MSSQLPoolBuildItem(dataSourceName, pool)); + msSQLPool.produce(new MSSQLPoolBuildItem(dataSourceName, poolFunction)); ExtendedBeanConfigurator msSQLPoolBeanConfigurator = SyntheticBeanBuildItem.configure(MSSQLPool.class) .defaultBean() .addType(Pool.class) .scope(ApplicationScoped.class) - .runtimeValue(pool) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -209,14 +219,21 @@ private void createPoolIfDefined(MSSQLPoolRecorder recorder, .configure(io.vertx.mutiny.mssqlclient.MSSQLPool.class) .defaultBean() .scope(ApplicationScoped.class) - .runtimeValue(recorder.mutinyMSSQLPool(pool)) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(recorder.mutinyMSSQLPool(poolFunction)) .setRuntimeInit(); addQualifiers(mutinyMSSQLPoolConfigurator, dataSourceName); syntheticBeans.produce(mutinyMSSQLPoolConfigurator.done()); + } - vertxPool.produce(new VertxPoolBuildItem(pool, DatabaseKind.MSSQL, DataSourceUtil.isDefault(dataSourceName))); + private AnnotationInstance[] injectionPointAnnotations(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return EMPTY_ANNOTATIONS; + } + return new AnnotationInstance[] { + AnnotationInstance.builder(REACTIVE_DATASOURCE).add("value", dataSourceName).build() }; } private static boolean isReactiveMSSQLPoolDefined(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, 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 3c9dc91c63deb..dad7ed85e5f86 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 @@ -12,13 +12,15 @@ import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.util.TypeLiteral; import org.jboss.logging.Logger; -import io.quarkus.arc.Arc; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; @@ -43,9 +45,12 @@ @Recorder public class MSSQLPoolRecorder { + private static final TypeLiteral> TYPE_LITERAL = new TypeLiteral<>() { + }; + private static final Logger log = Logger.getLogger(MSSQLPoolRecorder.class); - public RuntimeValue configureMSSQLPool(RuntimeValue vertx, + public Function, MSSQLPool> configureMSSQLPool(RuntimeValue vertx, Supplier eventLoopCount, String dataSourceName, DataSourcesRuntimeConfig dataSourcesRuntimeConfig, @@ -53,33 +58,47 @@ public RuntimeValue configureMSSQLPool(RuntimeValue vertx, DataSourcesReactiveMSSQLConfig dataSourcesReactiveMSSQLConfig, ShutdownContext shutdown) { - MSSQLPool mssqlPool = initialize((VertxInternal) vertx.getValue(), - eventLoopCount.get(), - dataSourceName, - dataSourcesRuntimeConfig.dataSources().get(dataSourceName), - dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), - dataSourcesReactiveMSSQLConfig.dataSources().get(dataSourceName).reactive().mssql()); - - shutdown.addShutdownTask(mssqlPool::close); - return new RuntimeValue<>(mssqlPool); + return new Function<>() { + @Override + public MSSQLPool apply(SyntheticCreationalContext context) { + MSSQLPool pool = initialize((VertxInternal) vertx.getValue(), + eventLoopCount.get(), + dataSourceName, + dataSourcesRuntimeConfig.dataSources().get(dataSourceName), + dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), + dataSourcesReactiveMSSQLConfig.dataSources().get(dataSourceName).reactive().mssql(), + context); + + shutdown.addShutdownTask(pool::close); + return pool; + } + }; } - public RuntimeValue mutinyMSSQLPool(RuntimeValue mssqlPool) { - return new RuntimeValue<>(io.vertx.mutiny.mssqlclient.MSSQLPool.newInstance(mssqlPool.getValue())); + public Function, io.vertx.mutiny.mssqlclient.MSSQLPool> mutinyMSSQLPool( + Function, MSSQLPool> function) { + return new Function, io.vertx.mutiny.mssqlclient.MSSQLPool>() { + @Override + @SuppressWarnings("unchecked") + public io.vertx.mutiny.mssqlclient.MSSQLPool apply(SyntheticCreationalContext context) { + return io.vertx.mutiny.mssqlclient.MSSQLPool.newInstance(function.apply(context)); + } + }; } private MSSQLPool initialize(VertxInternal vertx, Integer eventLoopCount, String dataSourceName, DataSourceRuntimeConfig dataSourceRuntimeConfig, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, - DataSourceReactiveMSSQLConfig dataSourceReactiveMSSQLConfig) { + DataSourceReactiveMSSQLConfig dataSourceReactiveMSSQLConfig, + SyntheticCreationalContext context) { PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveMSSQLConfig); MSSQLConnectOptions mssqlConnectOptions = toMSSQLConnectOptions(dataSourceName, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveMSSQLConfig); Supplier> databasesSupplier = toDatabasesSupplier(vertx, List.of(mssqlConnectOptions), dataSourceRuntimeConfig); - return createPool(vertx, poolOptions, mssqlConnectOptions, dataSourceName, databasesSupplier); + return createPool(vertx, poolOptions, mssqlConnectOptions, dataSourceName, databasesSupplier, context); } private Supplier> toDatabasesSupplier(Vertx vertx, @@ -214,12 +233,13 @@ private MSSQLConnectOptions toMSSQLConnectOptions(String dataSourceName, DataSou } private MSSQLPool createPool(Vertx vertx, PoolOptions poolOptions, MSSQLConnectOptions mSSQLConnectOptions, - String dataSourceName, Supplier> databases) { + String dataSourceName, Supplier> databases, + SyntheticCreationalContext context) { Instance instance; if (DataSourceUtil.isDefault(dataSourceName)) { - instance = Arc.container().select(MSSQLPoolCreator.class); + instance = context.getInjectedReference(TYPE_LITERAL); } else { - instance = Arc.container().select(MSSQLPoolCreator.class, + instance = context.getInjectedReference(TYPE_LITERAL, new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); } if (instance.isResolvable()) { diff --git a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/MySQLPoolBuildItem.java b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/MySQLPoolBuildItem.java index b8c40a65cef57..0b64cbf31d0c5 100644 --- a/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/MySQLPoolBuildItem.java +++ b/extensions/reactive-mysql-client/deployment/src/main/java/io/quarkus/reactive/mysql/client/deployment/MySQLPoolBuildItem.java @@ -1,17 +1,19 @@ package io.quarkus.reactive.mysql.client.deployment; +import java.util.function.Function; + +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.runtime.RuntimeValue; import io.vertx.mysqlclient.MySQLPool; public final class MySQLPoolBuildItem extends MultiBuildItem { private final String dataSourceName; - private final RuntimeValue mysqlPool; + private final Function, MySQLPool> mysqlPool; - public MySQLPoolBuildItem(String dataSourceName, RuntimeValue mysqlPool) { + public MySQLPoolBuildItem(String dataSourceName, Function, MySQLPool> mysqlPool) { this.dataSourceName = dataSourceName; this.mysqlPool = mysqlPool; } @@ -20,7 +22,7 @@ public String getDataSourceName() { return dataSourceName; } - public RuntimeValue getMySQLPool() { + public Function, MySQLPool> getMySQLPool() { return mysqlPool; } 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 b512986ade9ee..7f61ab1eb9231 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 @@ -6,15 +6,20 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; @@ -51,7 +56,6 @@ import io.quarkus.reactive.mysql.client.runtime.DataSourcesReactiveMySQLConfig; import io.quarkus.reactive.mysql.client.runtime.MySQLPoolRecorder; import io.quarkus.reactive.mysql.client.runtime.MySQLServiceBindingConverter; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; import io.quarkus.vertx.core.deployment.EventLoopCountBuildItem; import io.quarkus.vertx.deployment.VertxBuildItem; @@ -60,6 +64,11 @@ class ReactiveMySQLClientProcessor { + private static final ParameterizedType POOL_INJECTION_TYPE = ParameterizedType.create(DotName.createSimple(Instance.class), + new Type[] { ClassType.create(DotName.createSimple(MySQLPoolCreator.class.getName())) }, null); + private static final AnnotationInstance[] EMPTY_ANNOTATIONS = new AnnotationInstance[0]; + private static final DotName REACTIVE_DATASOURCE = DotName.createSimple(ReactiveDataSource.class); + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) ServiceStartBuildItem build(BuildProducer feature, @@ -81,7 +90,7 @@ ServiceStartBuildItem build(BuildProducer feature, feature.produce(new FeatureBuildItem(Feature.REACTIVE_MYSQL_CLIENT)); for (String dataSourceName : dataSourcesBuildTimeConfig.dataSources().keySet()) { - createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, mySQLPool, vertxPool, syntheticBeans, dataSourceName, + createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, mySQLPool, syntheticBeans, dataSourceName, dataSourcesBuildTimeConfig, dataSourcesRuntimeConfig, dataSourcesReactiveBuildTimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactiveMySQLConfig, defaultDataSourceDbKindBuildItems, curateOutcomeBuildItem); @@ -90,6 +99,7 @@ ServiceStartBuildItem build(BuildProducer feature, // Enable SSL support by default sslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.REACTIVE_MYSQL_CLIENT)); + vertxPool.produce(new VertxPoolBuildItem()); return new ServiceStartBuildItem("reactive-mysql-client"); } @@ -169,7 +179,6 @@ private void createPoolIfDefined(MySQLPoolRecorder recorder, EventLoopCountBuildItem eventLoopCount, ShutdownContextBuildItem shutdown, BuildProducer mySQLPool, - BuildProducer vertxPool, BuildProducer syntheticBeans, String dataSourceName, DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, @@ -185,20 +194,21 @@ private void createPoolIfDefined(MySQLPoolRecorder recorder, return; } - RuntimeValue pool = recorder.configureMySQLPool(vertx.getVertx(), + Function, MySQLPool> poolFunction = recorder.configureMySQLPool(vertx.getVertx(), eventLoopCount.getEventLoopCount(), dataSourceName, dataSourcesRuntimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactiveMySQLConfig, shutdown); - mySQLPool.produce(new MySQLPoolBuildItem(dataSourceName, pool)); + mySQLPool.produce(new MySQLPoolBuildItem(dataSourceName, poolFunction)); ExtendedBeanConfigurator mySQLPoolBeanConfigurator = SyntheticBeanBuildItem.configure(MySQLPool.class) .defaultBean() .addType(Pool.class) .scope(ApplicationScoped.class) - .runtimeValue(pool) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -210,14 +220,21 @@ private void createPoolIfDefined(MySQLPoolRecorder recorder, .configure(io.vertx.mutiny.mysqlclient.MySQLPool.class) .defaultBean() .scope(ApplicationScoped.class) - .runtimeValue(recorder.mutinyMySQLPool(pool)) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(recorder.mutinyMySQLPool(poolFunction)) .setRuntimeInit(); addQualifiers(mutinyMySQLPoolConfigurator, dataSourceName); syntheticBeans.produce(mutinyMySQLPoolConfigurator.done()); + } - vertxPool.produce(new VertxPoolBuildItem(pool, DatabaseKind.MYSQL, DataSourceUtil.isDefault(dataSourceName))); + private AnnotationInstance[] injectionPointAnnotations(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return EMPTY_ANNOTATIONS; + } + return new AnnotationInstance[] { + AnnotationInstance.builder(REACTIVE_DATASOURCE).add("value", dataSourceName).build() }; } private static boolean isReactiveMySQLPoolDefined(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, 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 b3db354d08583..8b0d101285326 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 @@ -14,11 +14,13 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.util.TypeLiteral; -import io.quarkus.arc.Arc; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; @@ -44,27 +46,42 @@ @Recorder public class MySQLPoolRecorder { - public RuntimeValue configureMySQLPool(RuntimeValue vertx, + private static final TypeLiteral> TYPE_LITERAL = new TypeLiteral<>() { + }; + + public Function, MySQLPool> configureMySQLPool(RuntimeValue vertx, Supplier eventLoopCount, String dataSourceName, DataSourcesRuntimeConfig dataSourcesRuntimeConfig, DataSourcesReactiveRuntimeConfig dataSourcesReactiveRuntimeConfig, DataSourcesReactiveMySQLConfig dataSourcesReactiveMySQLConfig, ShutdownContext shutdown) { - - MySQLPool mysqlPool = initialize((VertxInternal) vertx.getValue(), - eventLoopCount.get(), - dataSourceName, - dataSourcesRuntimeConfig.dataSources().get(dataSourceName), - dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), - dataSourcesReactiveMySQLConfig.dataSources().get(dataSourceName).reactive().mysql()); - - shutdown.addShutdownTask(mysqlPool::close); - return new RuntimeValue<>(mysqlPool); + return new Function<>() { + @Override + public MySQLPool apply(SyntheticCreationalContext context) { + MySQLPool pool = initialize((VertxInternal) vertx.getValue(), + eventLoopCount.get(), + dataSourceName, + dataSourcesRuntimeConfig.dataSources().get(dataSourceName), + dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), + dataSourcesReactiveMySQLConfig.dataSources().get(dataSourceName).reactive().mysql(), + context); + + shutdown.addShutdownTask(pool::close); + return pool; + } + }; } - public RuntimeValue mutinyMySQLPool(RuntimeValue mysqlPool) { - return new RuntimeValue<>(io.vertx.mutiny.mysqlclient.MySQLPool.newInstance(mysqlPool.getValue())); + public Function, io.vertx.mutiny.mysqlclient.MySQLPool> mutinyMySQLPool( + Function, MySQLPool> function) { + return new Function<>() { + @Override + @SuppressWarnings("unchecked") + public io.vertx.mutiny.mysqlclient.MySQLPool apply(SyntheticCreationalContext context) { + return io.vertx.mutiny.mysqlclient.MySQLPool.newInstance(function.apply(context)); + } + }; } private MySQLPool initialize(VertxInternal vertx, @@ -72,14 +89,15 @@ private MySQLPool initialize(VertxInternal vertx, String dataSourceName, DataSourceRuntimeConfig dataSourceRuntimeConfig, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, - DataSourceReactiveMySQLConfig dataSourceReactiveMySQLConfig) { + DataSourceReactiveMySQLConfig dataSourceReactiveMySQLConfig, + SyntheticCreationalContext context) { PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveMySQLConfig); List mySQLConnectOptions = toMySQLConnectOptions(dataSourceName, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveMySQLConfig); Supplier> databasesSupplier = toDatabasesSupplier(vertx, mySQLConnectOptions, dataSourceRuntimeConfig); - return createPool(vertx, poolOptions, mySQLConnectOptions, dataSourceName, databasesSupplier); + return createPool(vertx, poolOptions, mySQLConnectOptions, dataSourceName, databasesSupplier, context); } private Supplier> toDatabasesSupplier(Vertx vertx, @@ -232,12 +250,13 @@ private List toMySQLConnectOptions(String dataSourceName, } private MySQLPool createPool(Vertx vertx, PoolOptions poolOptions, List mySQLConnectOptionsList, - String dataSourceName, Supplier> databases) { + String dataSourceName, Supplier> databases, + SyntheticCreationalContext context) { Instance instance; if (DataSourceUtil.isDefault(dataSourceName)) { - instance = Arc.container().select(MySQLPoolCreator.class); + instance = context.getInjectedReference(TYPE_LITERAL); } else { - instance = Arc.container().select(MySQLPoolCreator.class, + instance = context.getInjectedReference(TYPE_LITERAL, new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); } if (instance.isResolvable()) { diff --git a/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/OraclePoolBuildItem.java b/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/OraclePoolBuildItem.java index 4a8a264324cfe..a7bba6abd8c71 100644 --- a/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/OraclePoolBuildItem.java +++ b/extensions/reactive-oracle-client/deployment/src/main/java/io/quarkus/reactive/oracle/client/deployment/OraclePoolBuildItem.java @@ -1,17 +1,19 @@ package io.quarkus.reactive.oracle.client.deployment; +import java.util.function.Function; + +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.runtime.RuntimeValue; import io.vertx.oracleclient.OraclePool; public final class OraclePoolBuildItem extends MultiBuildItem { private final String dataSourceName; - private final RuntimeValue oraclePool; + private final Function, OraclePool> oraclePool; - public OraclePoolBuildItem(String dataSourceName, RuntimeValue oraclePool) { + public OraclePoolBuildItem(String dataSourceName, Function, OraclePool> oraclePool) { this.dataSourceName = dataSourceName; this.oraclePool = oraclePool; } @@ -20,7 +22,7 @@ public String getDataSourceName() { return dataSourceName; } - public RuntimeValue getOraclePool() { + public Function, OraclePool> getOraclePool() { return oraclePool; } 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 25918da0fca34..a82812d3d8409 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 @@ -6,15 +6,20 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; @@ -51,7 +56,6 @@ import io.quarkus.reactive.oracle.client.runtime.DataSourcesReactiveOracleConfig; import io.quarkus.reactive.oracle.client.runtime.OraclePoolRecorder; import io.quarkus.reactive.oracle.client.runtime.OracleServiceBindingConverter; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; import io.quarkus.vertx.core.deployment.EventLoopCountBuildItem; import io.quarkus.vertx.deployment.VertxBuildItem; @@ -60,6 +64,11 @@ class ReactiveOracleClientProcessor { + private static final ParameterizedType POOL_INJECTION_TYPE = ParameterizedType.create(DotName.createSimple(Instance.class), + new Type[] { ClassType.create(DotName.createSimple(OraclePoolCreator.class.getName())) }, null); + private static final AnnotationInstance[] EMPTY_ANNOTATIONS = new AnnotationInstance[0]; + private static final DotName REACTIVE_DATASOURCE = DotName.createSimple(ReactiveDataSource.class); + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) ServiceStartBuildItem build(BuildProducer feature, @@ -81,7 +90,7 @@ ServiceStartBuildItem build(BuildProducer feature, feature.produce(new FeatureBuildItem(Feature.REACTIVE_ORACLE_CLIENT)); for (String dataSourceName : dataSourcesBuildTimeConfig.dataSources().keySet()) { - createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, oraclePool, vertxPool, syntheticBeans, + createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, oraclePool, syntheticBeans, dataSourceName, dataSourcesBuildTimeConfig, dataSourcesRuntimeConfig, dataSourcesReactiveBuildTimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactiveOracleConfig, defaultDataSourceDbKindBuildItems, @@ -91,6 +100,7 @@ ServiceStartBuildItem build(BuildProducer feature, // Enable SSL support by default sslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.REACTIVE_ORACLE_CLIENT)); + vertxPool.produce(new VertxPoolBuildItem()); return new ServiceStartBuildItem("reactive-oracle-client"); } @@ -169,7 +179,6 @@ private void createPoolIfDefined(OraclePoolRecorder recorder, EventLoopCountBuildItem eventLoopCount, ShutdownContextBuildItem shutdown, BuildProducer oraclePool, - BuildProducer vertxPool, BuildProducer syntheticBeans, String dataSourceName, DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, @@ -185,20 +194,22 @@ private void createPoolIfDefined(OraclePoolRecorder recorder, return; } - RuntimeValue pool = recorder.configureOraclePool(vertx.getVertx(), + Function, OraclePool> poolFunction = recorder.configureOraclePool( + vertx.getVertx(), eventLoopCount.getEventLoopCount(), dataSourceName, dataSourcesRuntimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactiveOracleConfig, shutdown); - oraclePool.produce(new OraclePoolBuildItem(dataSourceName, pool)); + oraclePool.produce(new OraclePoolBuildItem(dataSourceName, poolFunction)); ExtendedBeanConfigurator oraclePoolBeanConfigurator = SyntheticBeanBuildItem.configure(OraclePool.class) .defaultBean() .addType(Pool.class) .scope(ApplicationScoped.class) - .runtimeValue(pool) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -210,14 +221,21 @@ private void createPoolIfDefined(OraclePoolRecorder recorder, .configure(io.vertx.mutiny.oracleclient.OraclePool.class) .defaultBean() .scope(ApplicationScoped.class) - .runtimeValue(recorder.mutinyOraclePool(pool)) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(recorder.mutinyOraclePool(poolFunction)) .setRuntimeInit(); addQualifiers(mutinyOraclePoolConfigurator, dataSourceName); syntheticBeans.produce(mutinyOraclePoolConfigurator.done()); + } - vertxPool.produce(new VertxPoolBuildItem(pool, DatabaseKind.ORACLE, DataSourceUtil.isDefault(dataSourceName))); + private AnnotationInstance[] injectionPointAnnotations(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return EMPTY_ANNOTATIONS; + } + return new AnnotationInstance[] { + AnnotationInstance.builder(REACTIVE_DATASOURCE).add("value", dataSourceName).build() }; } private static boolean isReactiveOraclePoolDefined(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, 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 e217cc1a818af..dc49a6eafd261 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 @@ -6,13 +6,15 @@ import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.util.TypeLiteral; import org.jboss.logging.Logger; -import io.quarkus.arc.Arc; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; @@ -38,29 +40,44 @@ @Recorder public class OraclePoolRecorder { + private static final TypeLiteral> TYPE_LITERAL = new TypeLiteral<>() { + }; + private static final Logger log = Logger.getLogger(OraclePoolRecorder.class); - public RuntimeValue configureOraclePool(RuntimeValue vertx, + public Function, OraclePool> configureOraclePool(RuntimeValue vertx, Supplier eventLoopCount, String dataSourceName, DataSourcesRuntimeConfig dataSourcesRuntimeConfig, DataSourcesReactiveRuntimeConfig dataSourcesReactiveRuntimeConfig, DataSourcesReactiveOracleConfig dataSourcesReactiveOracleConfig, ShutdownContext shutdown) { - - OraclePool oraclePool = initialize((VertxInternal) vertx.getValue(), - eventLoopCount.get(), - dataSourceName, - dataSourcesRuntimeConfig.dataSources().get(dataSourceName), - dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), - dataSourcesReactiveOracleConfig.dataSources().get(dataSourceName).reactive().oracle()); - - shutdown.addShutdownTask(oraclePool::close); - return new RuntimeValue<>(oraclePool); + return new Function, OraclePool>() { + @Override + public OraclePool apply(SyntheticCreationalContext context) { + OraclePool pool = initialize((VertxInternal) vertx.getValue(), + eventLoopCount.get(), + dataSourceName, + dataSourcesRuntimeConfig.dataSources().get(dataSourceName), + dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), + dataSourcesReactiveOracleConfig.dataSources().get(dataSourceName).reactive().oracle(), + context); + + shutdown.addShutdownTask(pool::close); + return pool; + } + }; } - public RuntimeValue mutinyOraclePool(RuntimeValue oraclePool) { - return new RuntimeValue<>(io.vertx.mutiny.oracleclient.OraclePool.newInstance(oraclePool.getValue())); + public Function, io.vertx.mutiny.oracleclient.OraclePool> mutinyOraclePool( + Function, OraclePool> function) { + return new Function<>() { + @SuppressWarnings("unchecked") + @Override + public io.vertx.mutiny.oracleclient.OraclePool apply(SyntheticCreationalContext context) { + return io.vertx.mutiny.oracleclient.OraclePool.newInstance(function.apply(context)); + } + }; } private OraclePool initialize(VertxInternal vertx, @@ -68,14 +85,15 @@ private OraclePool initialize(VertxInternal vertx, String dataSourceName, DataSourceRuntimeConfig dataSourceRuntimeConfig, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, - DataSourceReactiveOracleConfig dataSourceReactiveOracleConfig) { + DataSourceReactiveOracleConfig dataSourceReactiveOracleConfig, + SyntheticCreationalContext context) { PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveOracleConfig); OracleConnectOptions oracleConnectOptions = toOracleConnectOptions(dataSourceName, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactiveOracleConfig); Supplier> databasesSupplier = toDatabasesSupplier(vertx, List.of(oracleConnectOptions), dataSourceRuntimeConfig); - return createPool(vertx, poolOptions, oracleConnectOptions, dataSourceName, databasesSupplier); + return createPool(vertx, poolOptions, oracleConnectOptions, dataSourceName, databasesSupplier, context); } private Supplier> toDatabasesSupplier(Vertx vertx, @@ -185,12 +203,13 @@ private OracleConnectOptions toOracleConnectOptions(String dataSourceName, DataS } private OraclePool createPool(Vertx vertx, PoolOptions poolOptions, OracleConnectOptions oracleConnectOptions, - String dataSourceName, Supplier> databases) { + String dataSourceName, Supplier> databases, + SyntheticCreationalContext context) { Instance instance; if (DataSourceUtil.isDefault(dataSourceName)) { - instance = Arc.container().select(OraclePoolCreator.class); + instance = context.getInjectedReference(TYPE_LITERAL); } else { - instance = Arc.container().select(OraclePoolCreator.class, + instance = context.getInjectedReference(TYPE_LITERAL, new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); } if (instance.isResolvable()) { diff --git a/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/PgPoolBuildItem.java b/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/PgPoolBuildItem.java index 518bf7df817a8..0d19c802ead05 100644 --- a/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/PgPoolBuildItem.java +++ b/extensions/reactive-pg-client/deployment/src/main/java/io/quarkus/reactive/pg/client/deployment/PgPoolBuildItem.java @@ -1,17 +1,19 @@ package io.quarkus.reactive.pg.client.deployment; +import java.util.function.Function; + +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.datasource.common.runtime.DataSourceUtil; -import io.quarkus.runtime.RuntimeValue; import io.vertx.pgclient.PgPool; public final class PgPoolBuildItem extends MultiBuildItem { private final String dataSourceName; - private final RuntimeValue pgPool; + private final Function, PgPool> pgPool; - public PgPoolBuildItem(String dataSourceName, RuntimeValue pgPool) { + public PgPoolBuildItem(String dataSourceName, Function, PgPool> pgPool) { this.dataSourceName = dataSourceName; this.pgPool = pgPool; } @@ -20,7 +22,7 @@ public String getDataSourceName() { return dataSourceName; } - public RuntimeValue getPgPool() { + public Function, PgPool> getPgPool() { return pgPool; } 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 77690e5430e3d..e1db7a21692b0 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 @@ -6,15 +6,20 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; @@ -52,7 +57,6 @@ import io.quarkus.reactive.pg.client.runtime.DataSourcesReactivePostgreSQLConfig; import io.quarkus.reactive.pg.client.runtime.PgPoolRecorder; import io.quarkus.reactive.pg.client.runtime.PostgreSQLServiceBindingConverter; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; import io.quarkus.vertx.core.deployment.EventLoopCountBuildItem; import io.quarkus.vertx.deployment.VertxBuildItem; @@ -61,6 +65,11 @@ class ReactivePgClientProcessor { + private static final ParameterizedType POOL_INJECTION_TYPE = ParameterizedType.create(DotName.createSimple(Instance.class), + new Type[] { ClassType.create(DotName.createSimple(PgPoolCreator.class.getName())) }, null); + private static final AnnotationInstance[] EMPTY_ANNOTATIONS = new AnnotationInstance[0]; + private static final DotName REACTIVE_DATASOURCE = DotName.createSimple(ReactiveDataSource.class); + @BuildStep NativeImageConfigBuildItem config() { return NativeImageConfigBuildItem.builder().addRuntimeInitializedClass("io.vertx.pgclient.impl.codec.StartupMessage") @@ -93,7 +102,7 @@ ServiceStartBuildItem build(BuildProducer feature, feature.produce(new FeatureBuildItem(Feature.REACTIVE_PG_CLIENT)); for (String dataSourceName : dataSourcesBuildTimeConfig.dataSources().keySet()) { - createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, pgPool, vertxPool, syntheticBeans, dataSourceName, + createPoolIfDefined(recorder, vertx, eventLoopCount, shutdown, pgPool, syntheticBeans, dataSourceName, dataSourcesBuildTimeConfig, dataSourcesRuntimeConfig, dataSourcesReactiveBuildTimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactivePostgreSQLConfig, defaultDataSourceDbKindBuildItems, curateOutcomeBuildItem); @@ -102,6 +111,7 @@ ServiceStartBuildItem build(BuildProducer feature, // Enable SSL support by default sslNativeSupport.produce(new ExtensionSslNativeSupportBuildItem(Feature.REACTIVE_PG_CLIENT)); + vertxPool.produce(new VertxPoolBuildItem()); return new ServiceStartBuildItem("reactive-pg-client"); } @@ -174,7 +184,6 @@ private void createPoolIfDefined(PgPoolRecorder recorder, EventLoopCountBuildItem eventLoopCount, ShutdownContextBuildItem shutdown, BuildProducer pgPool, - BuildProducer vertxPool, BuildProducer syntheticBeans, String dataSourceName, DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, @@ -190,20 +199,21 @@ private void createPoolIfDefined(PgPoolRecorder recorder, return; } - RuntimeValue pool = recorder.configurePgPool(vertx.getVertx(), + Function, PgPool> poolFunction = recorder.configurePgPool(vertx.getVertx(), eventLoopCount.getEventLoopCount(), dataSourceName, dataSourcesRuntimeConfig, dataSourcesReactiveRuntimeConfig, dataSourcesReactivePostgreSQLConfig, shutdown); - pgPool.produce(new PgPoolBuildItem(dataSourceName, pool)); + pgPool.produce(new PgPoolBuildItem(dataSourceName, poolFunction)); ExtendedBeanConfigurator pgPoolBeanConfigurator = SyntheticBeanBuildItem.configure(PgPool.class) .defaultBean() .addType(Pool.class) .scope(ApplicationScoped.class) - .runtimeValue(pool) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(poolFunction) .unremovable() .setRuntimeInit(); @@ -215,14 +225,21 @@ private void createPoolIfDefined(PgPoolRecorder recorder, .configure(io.vertx.mutiny.pgclient.PgPool.class) .defaultBean() .scope(ApplicationScoped.class) - .runtimeValue(recorder.mutinyPgPool(pool)) + .addInjectionPoint(POOL_INJECTION_TYPE, injectionPointAnnotations(dataSourceName)) + .createWith(recorder.mutinyPgPool(poolFunction)) .setRuntimeInit(); addQualifiers(mutinyPgPoolConfigurator, dataSourceName); syntheticBeans.produce(mutinyPgPoolConfigurator.done()); + } - vertxPool.produce(new VertxPoolBuildItem(pool, DatabaseKind.POSTGRESQL, DataSourceUtil.isDefault(dataSourceName))); + private AnnotationInstance[] injectionPointAnnotations(String dataSourceName) { + if (DataSourceUtil.isDefault(dataSourceName)) { + return EMPTY_ANNOTATIONS; + } + return new AnnotationInstance[] { + AnnotationInstance.builder(REACTIVE_DATASOURCE).add("value", dataSourceName).build() }; } private static boolean isReactivePostgreSQLPoolDefined(DataSourcesBuildTimeConfig dataSourcesBuildTimeConfig, 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 7988a6f86afa7..053c68a56708f 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 @@ -13,11 +13,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.function.Supplier; import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.util.TypeLiteral; -import io.quarkus.arc.Arc; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.datasource.common.runtime.DataSourceUtil; @@ -43,27 +45,42 @@ @Recorder public class PgPoolRecorder { - public RuntimeValue configurePgPool(RuntimeValue vertx, + private static final TypeLiteral> TYPE_LITERAL = new TypeLiteral<>() { + }; + + public Function, PgPool> configurePgPool(RuntimeValue vertx, Supplier eventLoopCount, String dataSourceName, DataSourcesRuntimeConfig dataSourcesRuntimeConfig, DataSourcesReactiveRuntimeConfig dataSourcesReactiveRuntimeConfig, DataSourcesReactivePostgreSQLConfig dataSourcesReactivePostgreSQLConfig, ShutdownContext shutdown) { - - PgPool pgPool = initialize((VertxInternal) vertx.getValue(), - eventLoopCount.get(), - dataSourceName, - dataSourcesRuntimeConfig.dataSources().get(dataSourceName), - dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), - dataSourcesReactivePostgreSQLConfig.dataSources().get(dataSourceName).reactive().postgresql()); - - shutdown.addShutdownTask(pgPool::close); - return new RuntimeValue<>(pgPool); + return new Function<>() { + @Override + public PgPool apply(SyntheticCreationalContext context) { + PgPool pgPool = initialize((VertxInternal) vertx.getValue(), + eventLoopCount.get(), + dataSourceName, + dataSourcesRuntimeConfig.dataSources().get(dataSourceName), + dataSourcesReactiveRuntimeConfig.getDataSourceReactiveRuntimeConfig(dataSourceName), + dataSourcesReactivePostgreSQLConfig.dataSources().get(dataSourceName).reactive().postgresql(), + context); + + shutdown.addShutdownTask(pgPool::close); + return pgPool; + } + }; } - public RuntimeValue mutinyPgPool(RuntimeValue pgPool) { - return new RuntimeValue<>(io.vertx.mutiny.pgclient.PgPool.newInstance(pgPool.getValue())); + public Function, io.vertx.mutiny.pgclient.PgPool> mutinyPgPool( + Function, PgPool> function) { + return new Function<>() { + @SuppressWarnings("unchecked") + @Override + public io.vertx.mutiny.pgclient.PgPool apply(SyntheticCreationalContext context) { + return io.vertx.mutiny.pgclient.PgPool.newInstance(function.apply(context)); + } + }; } private PgPool initialize(VertxInternal vertx, @@ -71,14 +88,15 @@ private PgPool initialize(VertxInternal vertx, String dataSourceName, DataSourceRuntimeConfig dataSourceRuntimeConfig, DataSourceReactiveRuntimeConfig dataSourceReactiveRuntimeConfig, - DataSourceReactivePostgreSQLConfig dataSourceReactivePostgreSQLConfig) { + DataSourceReactivePostgreSQLConfig dataSourceReactivePostgreSQLConfig, + SyntheticCreationalContext context) { PoolOptions poolOptions = toPoolOptions(eventLoopCount, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactivePostgreSQLConfig); List pgConnectOptionsList = toPgConnectOptions(dataSourceName, dataSourceRuntimeConfig, dataSourceReactiveRuntimeConfig, dataSourceReactivePostgreSQLConfig); Supplier> databasesSupplier = toDatabasesSupplier(vertx, pgConnectOptionsList, dataSourceRuntimeConfig); - return createPool(vertx, poolOptions, pgConnectOptionsList, dataSourceName, databasesSupplier); + return createPool(vertx, poolOptions, pgConnectOptionsList, dataSourceName, databasesSupplier, context); } private Supplier> toDatabasesSupplier(Vertx vertx, List pgConnectOptionsList, @@ -220,12 +238,13 @@ private List toPgConnectOptions(String dataSourceName, DataSou } private PgPool createPool(Vertx vertx, PoolOptions poolOptions, List pgConnectOptionsList, - String dataSourceName, Supplier> databases) { + String dataSourceName, Supplier> databases, + SyntheticCreationalContext context) { Instance instance; if (DataSourceUtil.isDefault(dataSourceName)) { - instance = Arc.container().select(PgPoolCreator.class); + instance = context.getInjectedReference(TYPE_LITERAL); } else { - instance = Arc.container().select(PgPoolCreator.class, + instance = context.getInjectedReference(TYPE_LITERAL, new ReactiveDataSource.ReactiveDataSourceLiteral(dataSourceName)); } if (instance.isResolvable()) { diff --git a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java index 7ba1c30da0eed..2a60256424809 100644 --- a/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java +++ b/extensions/redis-cache/runtime/src/main/java/io/quarkus/cache/redis/runtime/RedisCacheImpl.java @@ -1,5 +1,6 @@ package io.quarkus.cache.redis.runtime; +import java.net.ConnectException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; @@ -10,6 +11,8 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import org.jboss.logging.Logger; + import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.cache.CacheException; @@ -35,6 +38,8 @@ */ public class RedisCacheImpl extends AbstractCache implements RedisCache { + private static final Logger log = Logger.getLogger(RedisCacheImpl.class); + private static final Map> PRIMITIVE_TO_CLASS_MAPPING = Map.of( "int", Integer.class, "byte", Byte.class, @@ -123,6 +128,19 @@ private String encodeKey(K key) { return new String(marshaller.encode(key), StandardCharsets.UTF_8); } + private Uni computeValue(K key, Function valueLoader, boolean isWorkerThread) { + if (isWorkerThread) { + return Uni.createFrom().item(new Supplier() { + @Override + public V get() { + return valueLoader.apply(key); + } + }).runSubscriptionOn(MutinyHelper.blockingExecutor(vertx.getDelegate())); + } else { + return Uni.createFrom().item(valueLoader.apply(key)); + } + } + @Override public Uni get(K key, Class clazz, Function valueLoader) { // With optimistic locking: @@ -148,17 +166,7 @@ public Uni apply(V cached) throws Exception { if (cached != null) { return Uni.createFrom().item(new StaticSupplier<>(cached)); } else { - Uni uni; - if (isWorkerThread) { - uni = Uni.createFrom().item(new Supplier() { - @Override - public V get() { - return valueLoader.apply(key); - } - }).runSubscriptionOn(MutinyHelper.blockingExecutor(vertx.getDelegate())); - } else { - uni = Uni.createFrom().item(valueLoader.apply(key)); - } + Uni uni = computeValue(key, valueLoader, isWorkerThread); return uni.onItem().call(new Function>() { @Override @@ -185,7 +193,15 @@ public Uni apply(V value) { } })); } - }); + }) + + .onFailure(ConnectException.class).recoverWithUni(new Function>() { + @Override + public Uni apply(Throwable e) { + log.warn("Unable to connect to Redis, recomputing cached value", e); + return computeValue(key, valueLoader, isWorkerThread); + } + }); } @Override @@ -215,7 +231,11 @@ public Uni apply(RedisConnection connection) { } }); } - }); + }) + .onFailure(ConnectException.class).recoverWithUni(e -> { + log.warn("Unable to connect to Redis, recomputing cached value", e); + return valueLoader.apply(key); + }); } @Override diff --git a/extensions/redis-cache/runtime/src/test/java/io/quarkus/cache/redis/runtime/RedisCacheImplTest.java b/extensions/redis-cache/runtime/src/test/java/io/quarkus/cache/redis/runtime/RedisCacheImplTest.java index c69a50ff13796..c1e7466a4ffbe 100644 --- a/extensions/redis-cache/runtime/src/test/java/io/quarkus/cache/redis/runtime/RedisCacheImplTest.java +++ b/extensions/redis-cache/runtime/src/test/java/io/quarkus/cache/redis/runtime/RedisCacheImplTest.java @@ -29,7 +29,12 @@ class RedisCacheImplTest extends RedisCacheTestBase { @AfterEach void clear() { - redis.send(Request.cmd(Command.FLUSHALL).arg("SYNC")).await().atMost(Duration.ofSeconds(10)); + try { + redis.send(Request.cmd(Command.FLUSHALL).arg("SYNC")).await() + .atMost(Duration.ofSeconds(10)); + } catch (Exception ignored) { + // ignored. + } } @Test @@ -45,6 +50,18 @@ public void testPutInTheCache() { assertThat(r).isNotNull(); } + @Test + public void testPutInTheCacheWithoutRedis() { + String k = UUID.randomUUID().toString(); + RedisCacheInfo info = new RedisCacheInfo(); + info.name = "foo"; + info.valueType = String.class.getName(); + info.expireAfterWrite = Optional.of(Duration.ofSeconds(2)); + RedisCacheImpl cache = new RedisCacheImpl(info, vertx, redis, BLOCKING_ALLOWED); + server.close(); + assertThat(cache.get(k, s -> "hello").await().indefinitely()).isEqualTo("hello"); + } + @Test public void testPutInTheCacheWithOptimisticLocking() { String k = UUID.randomUUID().toString(); @@ -355,6 +372,32 @@ void testAsyncGetWithDefaultType() { })); } + @Test + void testAsyncGetWithDefaultTypeWithoutRedis() { + RedisCacheInfo info = new RedisCacheInfo(); + info.name = "star-wars"; + info.expireAfterWrite = Optional.of(Duration.ofSeconds(2)); + info.valueType = Person.class.getName(); + RedisCacheImpl cache = new RedisCacheImpl(info, vertx, redis, BLOCKING_ALLOWED); + + server.close(); + + assertThat(cache + .getAsync("test", + x -> Uni.createFrom().item(new Person("luke", "skywalker")) + .runSubscriptionOn(Infrastructure.getDefaultExecutor())) + .await().indefinitely()).satisfies(p -> { + assertThat(p.firstName).isEqualTo("luke"); + assertThat(p.lastName).isEqualTo("skywalker"); + }); + + assertThat(cache.getAsync("test", x -> Uni.createFrom().item(new Person("leia", "organa"))) + .await().indefinitely()).satisfies(p -> { + assertThat(p.firstName).isEqualTo("leia"); + assertThat(p.lastName).isEqualTo("organa"); + }); + } + @Test void testAsyncGetWithDefaultTypeWithOptimisticLocking() { RedisCacheInfo info = new RedisCacheInfo(); diff --git a/extensions/resteasy-classic/rest-client/runtime/pom.xml b/extensions/resteasy-classic/rest-client/runtime/pom.xml index b0095161d1a09..2ec750c3592c3 100644 --- a/extensions/resteasy-classic/rest-client/runtime/pom.xml +++ b/extensions/resteasy-classic/rest-client/runtime/pom.xml @@ -25,12 +25,6 @@ io.quarkus quarkus-resteasy-common - - - org.jboss.resteasy - resteasy-cdi - - io.quarkus diff --git a/extensions/resteasy-classic/resteasy-common/runtime/pom.xml b/extensions/resteasy-classic/resteasy-common/runtime/pom.xml index dc485b199d7e1..51726a0e168ab 100644 --- a/extensions/resteasy-classic/resteasy-common/runtime/pom.xml +++ b/extensions/resteasy-classic/resteasy-common/runtime/pom.xml @@ -81,10 +81,6 @@ jakarta.ws.rs jakarta.ws.rs-api - - org.jboss.resteasy - resteasy-cdi - org.jboss.resteasy resteasy-json-binding-provider diff --git a/extensions/resteasy-classic/resteasy-server-common/runtime/pom.xml b/extensions/resteasy-classic/resteasy-server-common/runtime/pom.xml index 9175af66fa019..00b5a3bebcd99 100644 --- a/extensions/resteasy-classic/resteasy-server-common/runtime/pom.xml +++ b/extensions/resteasy-classic/resteasy-server-common/runtime/pom.xml @@ -25,6 +25,10 @@ io.quarkus quarkus-resteasy-common + + org.jboss.resteasy + resteasy-cdi + jakarta.validation jakarta.validation-api diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java new file mode 100644 index 0000000000000..ed21f896dc164 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java @@ -0,0 +1,230 @@ +package io.quarkus.resteasy.test.security; + +import static org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URL; +import java.time.Duration; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.smallrye.mutiny.Uni; +import io.vertx.core.http.HttpMethod; +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.core.http.HttpClientRequest; + +public class JakartaRestResourceHttpPermissionTest { + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(20); + private static final String APP_PROPS = "quarkus.http.auth.permission.foo.paths=/api/foo,/api/foo/\n" + + "quarkus.http.auth.permission.foo.policy=authenticated\n" + + "quarkus.http.auth.permission.bar.paths=api/bar*\n" + + "quarkus.http.auth.permission.bar.policy=authenticated\n" + + "quarkus.http.auth.permission.baz-fum-pub.paths=/api/baz/fum\n" + + "quarkus.http.auth.permission.baz-fum-pub.policy=permit\n" + + "quarkus.http.auth.permission.baz-fum.paths=/api/baz/fum*\n" + + "quarkus.http.auth.permission.baz-fum.policy=authenticated\n" + + "quarkus.http.auth.permission.root.paths=/\n" + + "quarkus.http.auth.permission.root.policy=authenticated\n" + + "quarkus.http.auth.permission.dot.paths=dot,dot/\n" + + "quarkus.http.auth.permission.dot.policy=authenticated\n"; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestIdentityProvider.class, TestIdentityController.class, ApiResource.class, + RootResource.class, PublicResource.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("test", "test", "test"); + } + + @TestHTTPResource + URL url; + + @Inject + Vertx vertx; + + @ParameterizedTest + @ValueSource(strings = { + // path without wildcard, with leading slashes in both policy and @Path + "/api/foo/", "/api/foo", + // path with wildcard, without leading slashes in both policy and @Path + "/api/bar", "/api/bar/", "/api/bar/irish", + // combination of permit and authenticated policies, paths are resolved to /api/baz/fum/ and auth required + "/api/baz/fum/" + }) + public void testEmptyPathSegments(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, getLastNonEmptySegmentContent(path)); + } + + @ParameterizedTest + @ValueSource(strings = { "/", "///", "/?stuff", "" }) + public void testRootPath(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { "/one/", "/three?stuff" }) + public void testNotSecuredPaths(String path) { + // negative testing - all paths are public unless auth policy is applied + assurePath(path, 200); + } + + @ParameterizedTest + @ValueSource(strings = { "/api/foo///", "////api/foo", "/api//foo", "/api/bar///irish", "/api/bar///irish/", + "/api//baz/fum//", + "/api///foo", "////api/bar", "/api///bar", "/api//bar" }) + public void testSecuredNotFound(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, 404); + } + + private static String getLastNonEmptySegmentContent(String path) { + while (path.endsWith("/") || path.endsWith(".")) { + path = path.substring(0, path.length() - 1); + } + return path.substring(path.lastIndexOf('/') + 1); + } + + @Path("/api") + public static class ApiResource { + + @GET + @Path("/foo") + public String foo() { + return "foo"; + } + + @GET + @Path("/bar") + public String bar() { + return "bar"; + } + + @GET + @Path("/bar/irish") + public String irishBar() { + return "irish"; + } + + @GET + @Path("/baz/fum") + public String bazFum() { + return "fum"; + } + + } + + @Path("/") + public static class RootResource { + + @GET + public String get() { + return "root"; + } + + } + + @Path("/") + public static class PublicResource { + + @Path("one") + @GET + public String one() { + return "one"; + } + + @Path("/two") + @GET + public String two() { + return "two"; + } + + @Path("/three") + @GET + public String three() { + return "three"; + } + + @Path("four") + @GET + public String four() { + return "four"; + } + + @Path("/four#stuff") + @GET + public String fourWitFragment() { + return "four#stuff"; + } + + @Path("five") + @GET + public String five() { + return "five"; + } + + } + + private void assurePath(String path, int expectedStatusCode) { + assurePath(path, expectedStatusCode, null, false); + } + + private void assurePathAuthenticated(String path) { + assurePath(path, 200, null, true); + } + + private void assurePathAuthenticated(String path, int statusCode) { + assurePath(path, statusCode, null, true); + } + + private void assurePathAuthenticated(String path, String body) { + assurePath(path, 200, body, true); + } + + private void assurePath(String path, int expectedStatusCode, String body, boolean auth) { + var httpClient = vertx.createHttpClient(); + try { + httpClient + .request(HttpMethod.GET, url.getPort(), url.getHost(), path) + .map(r -> { + if (auth) { + r.putHeader("Authorization", "Basic " + encodeBase64URLSafeString("test:test".getBytes())); + } + return r; + }) + .flatMap(HttpClientRequest::send) + .invoke(r -> assertEquals(expectedStatusCode, r.statusCode(), path)) + .flatMap(r -> { + if (body != null) { + return r.body().invoke(b -> assertEquals(b.toString(), body, path)); + } else { + return Uni.createFrom().nullItem(); + } + }) + .await() + .atMost(REQUEST_TIMEOUT); + } finally { + httpClient + .close() + .await() + .atMost(REQUEST_TIMEOUT); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java index 6b55e2a725a56..7f8bd584a0244 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java @@ -140,23 +140,12 @@ public void testParser() { } private void testParser(String event, String data, String comment, String lastId, String name, long reconnectDelay) { - if (data != null) { - testParser(Collections.singletonList(event), Collections.singletonList(new InboundSseEventImpl(null, null) - .setData(data) - .setComment(comment) - .setId(lastId) - .setName(name) - .setReconnectDelay(reconnectDelay))); - } else if (comment != null) { - testParser(Collections.singletonList(event), Collections.singletonList(new InboundSseEventImpl(null, null) - .setData(null) - .setComment(comment) - .setId(lastId) - .setName(name) - .setReconnectDelay(reconnectDelay))); - } else { - testParser(Collections.singletonList(event), Collections.emptyList()); - } + testParser(Collections.singletonList(event), Collections.singletonList(new InboundSseEventImpl(null, null) + .setData(data) + .setComment(comment) + .setId(lastId) + .setName(name) + .setReconnectDelay(reconnectDelay))); } private void testParser(List events, List expectedEvents) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java new file mode 100644 index 0000000000000..ee706eae57662 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java @@ -0,0 +1,224 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URL; +import java.time.Duration; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.vertx.core.Vertx; +import io.vertx.ext.web.client.WebClient; + +public class JakartaRestResourceHttpPermissionTest { + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(20); + private static final String APP_PROPS = "quarkus.http.auth.permission.foo.paths=/api/foo,/api/foo/\n" + + "quarkus.http.auth.permission.foo.policy=authenticated\n" + + "quarkus.http.auth.permission.bar.paths=api/bar*\n" + + "quarkus.http.auth.permission.bar.policy=authenticated\n" + + "quarkus.http.auth.permission.baz-fum-pub.paths=/api/baz/fum\n" + + "quarkus.http.auth.permission.baz-fum-pub.policy=permit\n" + + "quarkus.http.auth.permission.baz-fum.paths=/api/baz/fum*\n" + + "quarkus.http.auth.permission.baz-fum.policy=authenticated\n" + + "quarkus.http.auth.permission.root.paths=/\n" + + "quarkus.http.auth.permission.root.policy=authenticated\n" + + "quarkus.http.auth.permission.fragment.paths=/#stuff,/#stuff/\n" + + "quarkus.http.auth.permission.fragment.policy=authenticated\n"; + private static WebClient client; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestIdentityProvider.class, TestIdentityController.class, ApiResource.class, + RootResource.class, PublicResource.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("test", "test", "test"); + } + + @AfterAll + public static void cleanup() { + if (client != null) { + client.close(); + } + } + + @Inject + Vertx vertx; + + @TestHTTPResource + URL url; + + private WebClient getClient() { + if (client == null) { + client = WebClient.create(vertx); + } + return client; + } + + @ParameterizedTest + @ValueSource(strings = { + // path without wildcard, with leading slashes in both policy and @Path + "////api/foo", "/api/foo", "/api//foo", "/api//foo", "/api///foo", "/api/foo/", "/api/foo///", + "/api/foo///.", "/api/foo/./", + // path with wildcard, without leading slashes in both policy and @Path + "////api/bar", "/api///bar", "/api//bar", "/api/bar", "/api/bar/", "/api/bar/irish", + "/api/bar///irish", "/api/bar///irish/.", "/../api/bar///irish/.", + // combination of permit and authenticated policies, paths are resolved to /api/baz/fum/ and auth required + "/api/baz/fum/", "/api//baz/fum//", "/api//baz/fum/." + }) + public void testEmptyPathSegments(String path) { + assurePath(path, 401); + + assurePathAuthenticated(path, getLastNonEmptySegmentContent(path)); + } + + @ParameterizedTest + @ValueSource(strings = { "/", "///", "/?stuff", "/#stuff/", "" }) + public void testRootPath(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { "/one/", "///two", "/three?stuff", "/four#stuff", "/.////five" }) + public void testNotSecuredPaths(String path) { + // negative testing - all paths are public unless auth policy is applied + assurePathAuthenticated(path); + } + + private static String getLastNonEmptySegmentContent(String path) { + while (path.endsWith("/") || path.endsWith(".")) { + path = path.substring(0, path.length() - 1); + } + return path.substring(path.lastIndexOf('/') + 1); + } + + @Path("/api") + public static class ApiResource { + + @GET + @Path("/foo") + public String foo() { + return "foo"; + } + + @GET + @Path("bar") + public String bar() { + return "bar"; + } + + @GET + @Path("bar/irish") + public String irishBar() { + return "irish"; + } + + @GET + @Path("baz/fum") + public String bazFum() { + return "fum"; + } + + } + + @Path("/") + public static class RootResource { + + @GET + public String get() { + return "root"; + } + + @Path("#stuff") + @GET + public String fragment() { + return "#stuff"; + } + + } + + @Path("/") + public static class PublicResource { + + @Path("one") + @GET + public String one() { + return "one"; + } + + @Path("/two") + @GET + public String two() { + return "two"; + } + + @Path("/three") + @GET + public String three() { + return "three"; + } + + @Path("four") + @GET + public String four() { + return "four"; + } + + @Path("four#stuff") + @GET + public String fourFragment() { + return "four#stuff"; + } + + @Path("five") + @GET + public String five() { + return "five"; + } + + } + + private void assurePath(String path, int expectedStatusCode) { + assurePath(path, expectedStatusCode, null, false); + } + + private void assurePathAuthenticated(String path) { + assurePath(path, 200, null, true); + } + + private void assurePathAuthenticated(String path, String body) { + assurePath(path, 200, body, true); + } + + private void assurePath(String path, int expectedStatusCode, String body, boolean auth) { + var req = getClient().get(url.getPort(), url.getHost(), path); + if (auth) { + req.basicAuthentication("test", "test"); + } + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (body != null) { + Assertions.assertTrue(result.result().bodyAsString().contains(body), path); + } + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index e4483f6586bb7..04fd0a2d54494 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java @@ -39,6 +39,7 @@ import jakarta.ws.rs.Priorities; import jakarta.ws.rs.RuntimeType; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -60,6 +61,7 @@ import org.jboss.resteasy.reactive.client.spi.MissingMessageBodyReaderErrorMessageContextualizer; import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore; +import org.jboss.resteasy.reactive.common.util.QuarkusMultivaluedHashMap; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; @@ -331,12 +333,15 @@ void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, } } - Map generatedProviders = new HashMap<>(); - populateClientExceptionMapperFromAnnotations(generatedClasses, reflectiveClasses, index, generatedProviders); - populateClientRedirectHandlerFromAnnotations(generatedClasses, reflectiveClasses, index, generatedProviders); + MultivaluedMap generatedProviders = new QuarkusMultivaluedHashMap<>(); + populateClientExceptionMapperFromAnnotations(generatedClasses, reflectiveClasses, index) + .forEach(generatedProviders::add); + populateClientRedirectHandlerFromAnnotations(generatedClasses, reflectiveClasses, index) + .forEach(generatedProviders::add); for (AnnotationToRegisterIntoClientContextBuildItem annotation : annotationsToRegisterIntoClientContext) { - populateClientProviderFromAnnotations(annotation, generatedClasses, reflectiveClasses, - index, generatedProviders); + populateClientProviderFromAnnotations(annotation, generatedClasses, reflectiveClasses, index) + .forEach(generatedProviders::add); + } addGeneratedProviders(index, constructor, annotationsByClassName, generatedProviders); @@ -551,77 +556,83 @@ && isImplementorOf(index, target.asClass(), RESPONSE_EXCEPTION_MAPPER, Set.of(AP } } - private void populateClientExceptionMapperFromAnnotations(BuildProducer generatedClasses, - BuildProducer reflectiveClasses, IndexView index, - Map generatedProviders) { + private Map populateClientExceptionMapperFromAnnotations( + BuildProducer generatedClasses, + BuildProducer reflectiveClasses, IndexView index) { + var result = new HashMap(); ClientExceptionMapperHandler clientExceptionMapperHandler = new ClientExceptionMapperHandler( new GeneratedClassGizmoAdaptor(generatedClasses, true)); for (AnnotationInstance instance : index.getAnnotations(CLIENT_EXCEPTION_MAPPER)) { - GeneratedClassResult result = clientExceptionMapperHandler.generateResponseExceptionMapper(instance); - if (result == null) { + GeneratedClassResult classResult = clientExceptionMapperHandler.generateResponseExceptionMapper(instance); + if (classResult == null) { continue; } - if (generatedProviders.containsKey(result.interfaceName)) { + if (result.containsKey(classResult.interfaceName)) { throw new IllegalStateException("Only a single instance of '" + CLIENT_EXCEPTION_MAPPER - + "' is allowed per REST Client interface. Offending class is '" + result.interfaceName + "'"); + + "' is allowed per REST Client interface. Offending class is '" + classResult.interfaceName + "'"); } - generatedProviders.put(result.interfaceName, result); - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(result.generatedClassName) + result.put(classResult.interfaceName, classResult); + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(classResult.generatedClassName) .serialization(false).build()); } + return result; } - private void populateClientRedirectHandlerFromAnnotations(BuildProducer generatedClasses, - BuildProducer reflectiveClasses, IndexView index, - Map generatedProviders) { + private Map populateClientRedirectHandlerFromAnnotations( + BuildProducer generatedClasses, + BuildProducer reflectiveClasses, IndexView index) { + var result = new HashMap(); ClientRedirectHandler clientHandler = new ClientRedirectHandler(new GeneratedClassGizmoAdaptor(generatedClasses, true)); for (AnnotationInstance instance : index.getAnnotations(CLIENT_REDIRECT_HANDLER)) { - GeneratedClassResult result = clientHandler.generateResponseExceptionMapper(instance); - if (result == null) { + GeneratedClassResult classResult = clientHandler.generateResponseExceptionMapper(instance); + if (classResult == null) { continue; } - GeneratedClassResult existing = generatedProviders.get(result.interfaceName); - if (existing != null && existing.priority == result.priority) { + GeneratedClassResult existing = result.get(classResult.interfaceName); + if (existing != null && existing.priority == classResult.priority) { throw new IllegalStateException("Only a single instance of '" + CLIENT_REDIRECT_HANDLER + "' with the same priority is allowed per REST Client interface. " - + "Offending class is '" + result.interfaceName + "'"); - } else if (existing == null || existing.priority < result.priority) { - generatedProviders.put(result.interfaceName, result); - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(result.generatedClassName) + + "Offending class is '" + classResult.interfaceName + "'"); + } else if (existing == null || existing.priority < classResult.priority) { + result.put(classResult.interfaceName, classResult); + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(classResult.generatedClassName) .serialization(false).build()); } } + return result; } - private void populateClientProviderFromAnnotations(AnnotationToRegisterIntoClientContextBuildItem annotationBuildItem, + private Map populateClientProviderFromAnnotations( + AnnotationToRegisterIntoClientContextBuildItem annotationBuildItem, BuildProducer generatedClasses, - BuildProducer reflectiveClasses, IndexView index, - Map generatedProviders) { + BuildProducer reflectiveClasses, IndexView index) { + var result = new HashMap(); ClientContextResolverHandler handler = new ClientContextResolverHandler(annotationBuildItem.getAnnotation(), annotationBuildItem.getExpectedReturnType(), new GeneratedClassGizmoAdaptor(generatedClasses, true)); for (AnnotationInstance instance : index.getAnnotations(annotationBuildItem.getAnnotation())) { - GeneratedClassResult result = handler.generateContextResolver(instance); - if (result == null) { + GeneratedClassResult classResult = handler.generateContextResolver(instance); + if (classResult == null) { continue; } - if (generatedProviders.containsKey(result.interfaceName)) { + if (result.containsKey(classResult.interfaceName)) { throw new IllegalStateException("Only a single instance of '" + annotationBuildItem.getAnnotation() - + "' is allowed per REST Client interface. Offending class is '" + result.interfaceName + "'"); + + "' is allowed per REST Client interface. Offending class is '" + classResult.interfaceName + "'"); } - generatedProviders.put(result.interfaceName, result); - reflectiveClasses.produce(ReflectiveClassBuildItem.builder(result.generatedClassName) + result.put(classResult.interfaceName, classResult); + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(classResult.generatedClassName) .serialization(false).build()); } + return result; } private void addGeneratedProviders(IndexView index, MethodCreator constructor, Map> annotationsByClassName, - Map generatedProviders) { + Map> generatedProviders) { for (Map.Entry> annotationsForClass : annotationsByClassName.entrySet()) { ResultHandle map = constructor.newInstance(MethodDescriptor.ofConstructor(HashMap.class)); for (AnnotationInstance value : annotationsForClass.getValue()) { @@ -641,18 +652,24 @@ private void addGeneratedProviders(IndexView index, MethodCreator constructor, if (generatedProviders.containsKey(ifaceName)) { // remove the interface from the generated provider since it's going to be handled now // the remaining entries will be handled later - GeneratedClassResult result = generatedProviders.remove(ifaceName); - constructor.invokeInterfaceMethod(MAP_PUT, map, constructor.loadClass(result.generatedClassName), - constructor.load(result.priority)); + List providers = generatedProviders.remove(ifaceName); + for (GeneratedClassResult classResult : providers) { + constructor.invokeInterfaceMethod(MAP_PUT, map, constructor.loadClass(classResult.generatedClassName), + constructor.load(classResult.priority)); + } + } addProviders(constructor, ifaceName, map); } - for (Map.Entry entry : generatedProviders.entrySet()) { + for (Map.Entry> entry : generatedProviders.entrySet()) { ResultHandle map = constructor.newInstance(MethodDescriptor.ofConstructor(HashMap.class)); - constructor.invokeInterfaceMethod(MAP_PUT, map, constructor.loadClass(entry.getValue().generatedClassName), - constructor.load(entry.getValue().priority)); - addProviders(constructor, entry.getKey(), map); + for (GeneratedClassResult classResult : entry.getValue()) { + constructor.invokeInterfaceMethod(MAP_PUT, map, constructor.loadClass(classResult.generatedClassName), + constructor.load(classResult.priority)); + addProviders(constructor, entry.getKey(), map); + } + } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/redirect/MultipleProvidersFromAnnotationTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/redirect/MultipleProvidersFromAnnotationTest.java new file mode 100644 index 0000000000000..237e3189a9ad1 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/redirect/MultipleProvidersFromAnnotationTest.java @@ -0,0 +1,83 @@ +package io.quarkus.rest.client.reactive.redirect; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.rest.client.reactive.ClientExceptionMapper; +import io.quarkus.rest.client.reactive.ClientRedirectHandler; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class MultipleProvidersFromAnnotationTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, Resource.class)); + + @Test + void test() { + Client client = RestClientBuilder.newBuilder() + .baseUri(uri) + .followRedirects(true) + .build(Client.class); + assertThatThrownBy(() -> client.call(2)).hasMessage("dummy"); + } + + @TestHTTPResource + URI uri; + + @Path("test") + public interface Client { + + @GET + void call(@QueryParam("redirects") Integer numberOfRedirects); + + @ClientRedirectHandler + static URI redirectFor3xx(Response response) { + int status = response.getStatus(); + if (status > 300 && response.getStatus() < 400) { + return response.getLocation(); + } + + return null; + } + + @ClientExceptionMapper + static RuntimeException toException(Response response) { + if (response.getStatus() == 999) { + throw new RuntimeException("dummy") { + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + }; + } + return null; + } + } + + @Path("test") + public static class Resource { + + @GET + public Response redirectedResponse(@QueryParam("redirects") Integer number) { + if (number == null || 0 == number) { + return Response.status(999).build(); + } else { + return Response.status(Response.Status.FOUND).location(URI.create("/test?redirects=" + (number - 1))) + .build(); + } + } + } +} diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/ServletHttpSecurityPolicy.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/ServletHttpSecurityPolicy.java index 1e47c7ceb7859..747314b32c23e 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/ServletHttpSecurityPolicy.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/ServletHttpSecurityPolicy.java @@ -25,7 +25,7 @@ public class ServletHttpSecurityPolicy implements HttpSecurityPolicy { public Uni checkPermission(RoutingContext request, Uni identity, AuthorizationRequestContext requestContext) { - String requestPath = request.request().path(); + String requestPath = request.normalizedPath(); if (!requestPath.startsWith(contextPath)) { //anything outside the context path we don't have anything to do with return Uni.createFrom().item(CheckResult.PERMIT); diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index b2c0692265d0d..1e02df7acaf31 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -67,6 +67,7 @@ import io.undertow.server.HandlerWrapper; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.CanonicalPathHandler; import io.undertow.server.handlers.PathHandler; import io.undertow.server.handlers.ResponseCodeHandler; import io.undertow.server.handlers.resource.CachingResourceManager; @@ -374,6 +375,7 @@ public void run() { .addPrefixPath(manager.getDeployment().getDeploymentInfo().getContextPath(), main); main = pathHandler; } + main = new CanonicalPathHandler(main); currentRoot = main; DefaultExchangeHandler defaultHandler = new DefaultExchangeHandler(ROOT_HANDLER); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/console/ConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/console/ConsoleProcessor.java new file mode 100644 index 0000000000000..54c14ca595937 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/console/ConsoleProcessor.java @@ -0,0 +1,39 @@ +package io.quarkus.vertx.http.deployment.console; + +import static io.quarkus.devui.deployment.ide.IdeProcessor.openBrowser; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Produce; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.builditem.ServiceStartBuildItem; +import io.quarkus.deployment.console.ConsoleCommand; +import io.quarkus.deployment.console.ConsoleStateManager; +import io.quarkus.dev.spi.DevModeType; +import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; + +public class ConsoleProcessor { + + static volatile ConsoleStateManager.ConsoleContext context; + + @Produce(ServiceStartBuildItem.class) + @BuildStep + void setupConsole(HttpRootPathBuildItem rp, NonApplicationRootPathBuildItem np, LaunchModeBuildItem launchModeBuildItem) { + if (launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { + return; + } + if (context == null) { + context = ConsoleStateManager.INSTANCE.createContext("HTTP"); + } + Config c = ConfigProvider.getConfig(); + String host = c.getOptionalValue("quarkus.http.host", String.class).orElse("localhost"); + String port = c.getOptionalValue("quarkus.http.port", String.class).orElse("8080"); + context.reset( + new ConsoleCommand('w', "Open the application in a browser", null, () -> openBrowser(rp, np, "/", host, port)), + new ConsoleCommand('d', "Open the Dev UI in a browser", null, + () -> openBrowser(rp, np, "/q/dev-ui", host, port))); + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java new file mode 100644 index 0000000000000..08679d345bcaa --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java @@ -0,0 +1,187 @@ +package io.quarkus.vertx.http.security; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URL; +import java.time.Duration; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.vertx.core.Vertx; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.client.WebClient; + +public class PathMatchingHttpSecurityPolicyTest { + + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(20); + private static final String APP_PROPS = "quarkus.http.auth.permission.authenticated.paths=/\n" + + "quarkus.http.auth.permission.authenticated.policy=authenticated\n" + + "quarkus.http.auth.permission.public.paths=/api*\n" + + "quarkus.http.auth.permission.public.policy=permit\n" + + "quarkus.http.auth.permission.foo.paths=/api/foo/bar\n" + + "quarkus.http.auth.permission.foo.policy=authenticated\n" + + "quarkus.http.auth.permission.baz.paths=/api/baz\n" + + "quarkus.http.auth.permission.baz.policy=authenticated\n" + + "quarkus.http.auth.permission.static-resource.paths=/static-file.html\n" + + "quarkus.http.auth.permission.static-resource.policy=authenticated\n" + + "quarkus.http.auth.permission.fubar.paths=/api/fubar/baz*\n" + + "quarkus.http.auth.permission.fubar.policy=authenticated\n" + + "quarkus.http.auth.permission.management.paths=/q/*\n" + + "quarkus.http.auth.permission.management.policy=authenticated\n"; + private static WebClient client; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityController.class, TestIdentityProvider.class, PathHandler.class, + RouteHandler.class) + .addAsResource("static-file.html", "META-INF/resources/static-file.html") + .addAsResource(new StringAsset(APP_PROPS), "application.properties")).setForcedDependencies(List.of( + Dependency.of("io.quarkus", "quarkus-smallrye-health", Version.getVersion()), + Dependency.of("io.quarkus", "quarkus-smallrye-openapi", Version.getVersion()))); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("test", "test", "test"); + } + + @AfterAll + public static void cleanup() { + if (client != null) { + client.close(); + } + } + + @Inject + Vertx vertx; + + @TestHTTPResource + URL url; + + private WebClient getClient() { + if (client == null) { + client = WebClient.create(vertx); + } + return client; + } + + @ParameterizedTest + @ValueSource(strings = { + // path policy without wildcard + "/api/foo//bar", "/api/foo///bar", "/api/foo////bar", "/api/foo/////bar", "//api/foo/bar", "///api/foo/bar", + "////api/foo/bar", "//api//foo//bar", "//api/foo//bar", + // path policy with wildcard + "/api/fubar/baz", "/api/fubar/baz/", "/api/fubar/baz//", "/api/fubar/baz/.", "/api/fubar/baz////.", + "/api/fubar/baz/bar", + // routes defined for exact paths + "/api/baz", "//api/baz", "///api////baz", "/api//baz", + // zero length path + "", "/?one=two", + // empty segments only are match with path policy for '/' + "/", "///", "////", "/////" + }) + public void testEmptyPathSegments(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { + "/api/foo/./bar", "/../api/foo///bar", "/api/./foo/.///bar", "/api/foo/./////bar", "/api/fubar/baz/.", + "/..///api/foo/bar", "////../../api/foo/bar", "/./api//foo//bar", "//api/foo/./bar", + "/.", "/..", "/./", "/..//", "/.///", "/..////", "/./////" + }) + public void testDotPathSegments(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { + "/static-file.html", "//static-file.html", "///static-file.html" + }) + public void testStaticResource(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { + "///q/openapi", "/q///openapi", "/q/openapi/", "/q/openapi///" + }) + public void testOpenApiPath(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, "openapi"); + } + + @ParameterizedTest + @ValueSource(strings = { + "/q/health", "/q/health/live", "/q/health/ready", "//q/health", "///q/health", "///q///health", + "/q/health/", "/q///health/", "/q///health////live" + }) + public void testHealthCheckPaths(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, "UP"); + } + + @Test + public void testMiscellaneousPaths() { + // /api/baz with segment indicating version shouldn't match /api/baz path policy + assurePath("/api/baz;v=1.1", 200); + // /api/baz/ is different resource than secured /api/baz, therefore request should succeed + assurePath("/api/baz/", 200); + } + + @ApplicationScoped + public static class RouteHandler { + public void setup(@Observes Router router) { + router.route("/api/baz").order(-1).handler(rc -> rc.response().end("/api/baz response")); + } + } + + private void assurePath(String path, int expectedStatusCode) { + assurePath(path, expectedStatusCode, null, false); + } + + private void assurePathAuthenticated(String path) { + assurePath(path, 200, null, true); + } + + private void assurePathAuthenticated(String path, String body) { + assurePath(path, 200, body, true); + } + + private void assurePath(String path, int expectedStatusCode, String body, boolean auth) { + var req = getClient().get(url.getPort(), url.getHost(), path); + if (auth) { + req.basicAuthentication("test", "test"); + } + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (body != null) { + Assertions.assertTrue(result.result().bodyAsString().contains(body), path); + } + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java index b28d0d7a470cb..b6def745a683c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java @@ -150,6 +150,7 @@ public void handle(RoutingContext event) { } } if (!allowsOrigin) { + LOG.debugf("Invalid origin %s", origin); response.setStatusCode(403); response.setStatusMessage("CORS Rejected - Invalid origin"); } else { @@ -183,7 +184,7 @@ public void handle(RoutingContext event) { //we check that the actual request matches the allowed methods and headers if (!isMethodAllowed(request.method())) { - LOG.debug("Method is not allowed"); + LOG.debugf("Method %s is not allowed", request.method()); response.setStatusCode(403); response.setStatusMessage("CORS Rejected - Invalid method"); response.end(); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java index fd194a02b9a77..d41f6e5fd9819 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractHttpAuthorizer.java @@ -10,6 +10,7 @@ import org.jboss.logging.Logger; import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.AuthenticationRedirectException; import io.quarkus.security.ForbiddenException; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; @@ -120,10 +121,15 @@ public void accept(Throwable throwable) { // the exception twice;at this point, the exception could be failed by the default auth failure handler if (!routingContext.response().ended() && !throwable.equals(routingContext.failure())) { routingContext.fail(throwable); - } else if (!(throwable instanceof AuthenticationFailedException)) { - //don't log auth failure + } else if (throwable instanceof AuthenticationFailedException) { + log.debug("Authentication challenge is required"); + } else if (throwable instanceof AuthenticationRedirectException) { + log.debugf("Completing authentication with a redirect to %s", + ((AuthenticationRedirectException) throwable).getRedirectUri()); + } else { log.error("Exception occurred during authorization", throwable); } + } }); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java index 185387e80061a..14aa4d607cd29 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java @@ -15,12 +15,11 @@ import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.AuthorizationRequestContext; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.CheckResult; import io.smallrye.mutiny.Uni; -import io.vertx.core.http.HttpServerRequest; import io.vertx.ext.web.RoutingContext; /** * A security policy that allows for matching of other security policies based on paths. - * + *

* This is used for the default path/method based RBAC. */ public class AbstractPathMatchingHttpSecurityPolicy { @@ -28,7 +27,7 @@ public class AbstractPathMatchingHttpSecurityPolicy { private final PathMatcher> pathMatcher = new PathMatcher<>(); public String getAuthMechanismName(RoutingContext routingContext) { - PathMatcher.PathMatch> toCheck = pathMatcher.match(routingContext.request().path()); + PathMatcher.PathMatch> toCheck = pathMatcher.match(routingContext.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { return null; } @@ -42,7 +41,7 @@ public String getAuthMechanismName(RoutingContext routingContext) { public Uni checkPermission(RoutingContext routingContext, Uni identity, AuthorizationRequestContext requestContext) { - List permissionCheckers = findPermissionCheckers(routingContext.request()); + List permissionCheckers = findPermissionCheckers(routingContext); return doPermissionCheck(routingContext, identity, 0, null, permissionCheckers, requestContext); } @@ -126,8 +125,8 @@ public void init(Map permissions, } } - public List findPermissionCheckers(HttpServerRequest request) { - PathMatcher.PathMatch> toCheck = pathMatcher.match(request.path()); + public List findPermissionCheckers(RoutingContext context) { + PathMatcher.PathMatch> toCheck = pathMatcher.match(context.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { return Collections.emptyList(); } @@ -136,7 +135,7 @@ public List findPermissionCheckers(HttpServerRequest request for (HttpMatcher i : toCheck.getValue()) { if (i.methods == null || i.methods.isEmpty()) { noMethod.add(i.checker); - } else if (i.methods.contains(request.method().toString())) { + } else if (i.methods.contains(context.request().method().toString())) { methodMatch.add(i.checker); } } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java index 039d339d7b962..7728e87ff7098 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java @@ -144,9 +144,6 @@ private void parseEvent() { } private void dispatchEvent() { - // ignore empty events - if (dataBuffer.length() == 0 && commentBuffer.length() == 0) - return; WebTargetImpl webTarget = sseEventSource.getWebTarget(); InboundSseEventImpl event; // tests don't set a web target, and we don't want them to end up starting vertx just to test parsing diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index f6a954498313c..9ed7306de56e5 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -55,7 +55,7 @@ 2.15.2 4.0.1 5.9.3 - 1.23.0 + 1.24.0 3.5.3.Final 5.3.1 3.2.1 diff --git a/integration-tests/csrf-reactive/pom.xml b/integration-tests/csrf-reactive/pom.xml index b25da69b1cc4e..63e339dc627a4 100644 --- a/integration-tests/csrf-reactive/pom.xml +++ b/integration-tests/csrf-reactive/pom.xml @@ -32,6 +32,16 @@ quarkus-junit5 test + + org.awaitility + awaitility + test + + + io.smallrye.reactive + smallrye-mutiny-vertx-web-client + test + net.sourceforge.htmlunit htmlunit diff --git a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java index ea2478fe815c7..e6d50f0fc39b7 100644 --- a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java +++ b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java @@ -1,14 +1,16 @@ package io.quarkus.it.csrf; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.fail; +import java.net.URL; +import java.time.Duration; import java.util.Base64; import java.util.List; -import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; @@ -20,13 +22,20 @@ import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.util.Cookie; +import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; -import io.restassured.http.Header; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; @QuarkusTest public class CsrfReactiveTest { + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10); + + @TestHTTPResource + URL url; + @Test public void testCsrfTokenInForm() throws Exception { try (final WebClient webClient = createWebClient()) { @@ -196,76 +205,80 @@ public void testWrongCsrfTokenFormValue() throws Exception { @Test public void testCsrfTokenHeaderValue() throws Exception { - try (final WebClient webClient = createWebClient()) { + Vertx vertx = Vertx.vertx(); + io.vertx.ext.web.client.WebClient vertxWebClient = io.vertx.ext.web.client.WebClient.create(vertx); + try { + try (final WebClient webClient = createWebClient()) { - HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithHeader"); - assertEquals("CSRF Token Header Test", htmlPage.getTitleText()); - List inputs = htmlPage.getElementsByIdAndOrName("X-CSRF-TOKEN"); - String csrfToken = inputs.get(0).asNormalizedText(); - - Cookie csrfCookie = webClient.getCookieManager().getCookie("csrftoken"); - assertNotNull(csrfCookie); - - RestAssured.given() - .header("Authorization", basicAuth("alice", "alice")) - .header(new Header("X-CSRF-TOKEN", csrfToken)) - .cookie(csrfCookie.getName(), csrfCookie.getValue()) - .urlEncodingEnabled(true) - .param("csrf-header", "X-CSRF-TOKEN") - .post("/service/csrfTokenWithHeader") - .then() - .body(Matchers.equalTo("verified:true:tokenHeaderIsSet=true")); - webClient.getCookieManager().clearCookies(); + HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithHeader"); + assertEquals("CSRF Token Header Test", htmlPage.getTitleText()); + List inputs = htmlPage.getElementsByIdAndOrName("X-CSRF-TOKEN"); + String csrfToken = inputs.get(0).asNormalizedText(); + + Cookie csrfCookie = webClient.getCookieManager().getCookie("csrftoken"); + assertNotNull(csrfCookie); + + assurePostFormPath(vertxWebClient, "/service/csrfTokenWithHeader", 200, csrfCookie, + csrfToken, "verified:true:tokenHeaderIsSet=true"); + assurePostFormPath(vertxWebClient, "//service/csrfTokenWithHeader", 200, csrfCookie, + csrfToken, "verified:true:tokenHeaderIsSet=true"); + + webClient.getCookieManager().clearCookies(); + } + } finally { + closeVertxWebClient(vertxWebClient, vertx); } } @Test public void testCsrfTokenHeaderValueJson() throws Exception { - try (final WebClient webClient = createWebClient()) { + Vertx vertx = Vertx.vertx(); + io.vertx.ext.web.client.WebClient vertxWebClient = io.vertx.ext.web.client.WebClient.create(vertx); + try { + try (final WebClient webClient = createWebClient()) { - HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithHeader"); - assertEquals("CSRF Token Header Test", htmlPage.getTitleText()); - List inputs = htmlPage.getElementsByIdAndOrName("X-CSRF-TOKEN"); - String csrfToken = inputs.get(0).asNormalizedText(); + HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithHeader"); + assertEquals("CSRF Token Header Test", htmlPage.getTitleText()); + List inputs = htmlPage.getElementsByIdAndOrName("X-CSRF-TOKEN"); + String csrfToken = inputs.get(0).asNormalizedText(); - Cookie csrfCookie = webClient.getCookieManager().getCookie("csrftoken"); - assertNotNull(csrfCookie); + Cookie csrfCookie = webClient.getCookieManager().getCookie("csrftoken"); + assertNotNull(csrfCookie); - RestAssured.given() - .header("Authorization", basicAuth("alice", "alice")) - .header(new Header("X-CSRF-TOKEN", csrfToken)) - .cookie(csrfCookie.getName(), csrfCookie.getValue()) - .header(new Header("Content-Type", "application/json")) - .body("{}") - .post("/service/csrfTokenWithHeader") - .then() - .body(Matchers.equalTo("verified:true:tokenHeaderIsSet=true")); + assurePostJsonPath(vertxWebClient, "/service/csrfTokenWithHeader", 200, csrfCookie, + csrfToken, "verified:true:tokenHeaderIsSet=true"); + assurePostJsonPath(vertxWebClient, "//service/csrfTokenWithHeader", 200, csrfCookie, + csrfToken, "verified:true:tokenHeaderIsSet=true"); - webClient.getCookieManager().clearCookies(); + webClient.getCookieManager().clearCookies(); + } + } finally { + closeVertxWebClient(vertxWebClient, vertx); } } @Test public void testWrongCsrfTokenHeaderValue() throws Exception { - try (final WebClient webClient = createWebClient()) { - - HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithHeader"); - assertEquals("CSRF Token Header Test", htmlPage.getTitleText()); - - Cookie csrfCookie = webClient.getCookieManager().getCookie("csrftoken"); - assertNotNull(csrfCookie); - - RestAssured.given() - .header("Authorization", basicAuth("alice", "alice")) - // CSRF cookie is signed, so passing it as a header value will fail - .header(new Header("X-CSRF-TOKEN", csrfCookie.getValue())) - .cookie(csrfCookie.getName(), csrfCookie.getValue()) - .urlEncodingEnabled(true) - .param("csrf-header", "X-CSRF-TOKEN") - .post("/service/csrfTokenWithHeader") - .then() - .statusCode(400); - webClient.getCookieManager().clearCookies(); + Vertx vertx = Vertx.vertx(); + io.vertx.ext.web.client.WebClient vertxWebClient = io.vertx.ext.web.client.WebClient.create(vertx); + try { + try (final WebClient webClient = createWebClient()) { + + HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithHeader"); + assertEquals("CSRF Token Header Test", htmlPage.getTitleText()); + + Cookie csrfCookie = webClient.getCookieManager().getCookie("csrftoken"); + assertNotNull(csrfCookie); + + // CSRF cookie is signed, so passing it as a header value will fail + assurePostFormPath(vertxWebClient, "/service/csrfTokenWithHeader", 400, csrfCookie, + csrfCookie.getValue(), null); + assurePostFormPath(vertxWebClient, "//service/csrfTokenWithHeader", 400, csrfCookie, + csrfCookie.getValue(), null); + webClient.getCookieManager().clearCookies(); + } + } finally { + closeVertxWebClient(vertxWebClient, vertx); } } @@ -297,4 +310,46 @@ private WebClient createWebClient() { private String basicAuth(String user, String password) { return "Basic " + Base64.getEncoder().encodeToString((user + ":" + password).getBytes()); } + + private void assurePostFormPath(io.vertx.ext.web.client.WebClient vertxWebClient, String path, + int expectedStatus, Cookie csrfCookie, String csrfToken, String responseBody) { + var req = vertxWebClient.post(url.getPort(), url.getHost(), path); + req.basicAuthentication("alice", "alice"); + req.putHeader("X-CSRF-TOKEN", csrfToken); + req.putHeader("Cookie", csrfCookie.getName() + "=" + csrfCookie.getValue()); + + var result = req.sendForm(io.vertx.core.MultiMap.caseInsensitiveMultiMap() + .add("csrf-header", "X-CSRF-TOKEN")); + + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatus, result.result().statusCode(), path); + if (responseBody != null) { + assertEquals(responseBody, result.result().bodyAsString(), path); + } + } + + private void assurePostJsonPath(io.vertx.ext.web.client.WebClient vertxWebClient, String path, + int expectedStatus, Cookie csrfCookie, String csrfToken, String responseBody) { + var req = vertxWebClient.post(url.getPort(), url.getHost(), path); + req.basicAuthentication("alice", "alice"); + req.putHeader("X-CSRF-TOKEN", csrfToken); + req.putHeader("Cookie", csrfCookie.getName() + "=" + csrfCookie.getValue()); + + var result = req.sendJson(new JsonObject("{}")); + + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatus, result.result().statusCode(), path); + if (responseBody != null) { + assertEquals(responseBody, result.result().bodyAsString(), path); + } + } + + private static void closeVertxWebClient(io.vertx.ext.web.client.WebClient vertxWebClient, Vertx vertx) { + if (vertxWebClient != null) { + vertxWebClient.close(); + } + if (vertx != null) { + vertx.close().toCompletionStage().toCompletableFuture().join(); + } + } } diff --git a/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/OpenApiServlet.java b/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/OpenApiServlet.java new file mode 100644 index 0000000000000..0b77b18c46fd7 --- /dev/null +++ b/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/OpenApiServlet.java @@ -0,0 +1,33 @@ +package io.quarkus.it.undertow.elytron; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet(name = "ServletGreeting", urlPatterns = "/openapi/*") +public class OpenApiServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (req.getUserPrincipal().getName() == null) { + throw new RuntimeException("principal was null"); + } + resp.setStatus(200); + resp.addHeader("Content-Type", "text/plain"); + resp.getWriter().write("hello"); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (req.getUserPrincipal().getName() == null) { + throw new RuntimeException("principal was null"); + } + String name = req.getReader().readLine(); + resp.setStatus(200); + resp.addHeader("Content-Type", "text/plain"); + resp.getWriter().write("hello " + name); + } +} diff --git a/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/ServletSecurityAnnotationPermissionsTestCase.java b/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/ServletSecurityAnnotationPermissionsTestCase.java index 9da4fd39aa7a9..9cd6454f3ad25 100644 --- a/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/ServletSecurityAnnotationPermissionsTestCase.java +++ b/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/ServletSecurityAnnotationPermissionsTestCase.java @@ -26,6 +26,11 @@ void testSecuredServletWithNoAuth() { .get("/foo/annotation-secure") .then() .statusCode(401); + given() + .when() + .get("/foo/bar/../annotation-secure") + .then() + .statusCode(401); } @Test diff --git a/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/WebXmlPermissionsTestCase.java b/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/WebXmlPermissionsTestCase.java index 34825a00a2127..9ee6d2c6dd06f 100644 --- a/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/WebXmlPermissionsTestCase.java +++ b/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/WebXmlPermissionsTestCase.java @@ -34,6 +34,16 @@ void testOpenApiNoPermissions() { .get("/foo/openapi") .then() .statusCode(401); + given() + .when() + .get("/r/../foo/openapi") + .then() + .statusCode(401); + given() + .when() + .get("/foo/bar/../openapi") + .then() + .statusCode(401); } @Test diff --git a/integration-tests/keycloak-authorization/pom.xml b/integration-tests/keycloak-authorization/pom.xml index 564819571f10a..f3bc5eb0f4533 100644 --- a/integration-tests/keycloak-authorization/pom.xml +++ b/integration-tests/keycloak-authorization/pom.xml @@ -106,6 +106,11 @@ + + org.awaitility + awaitility + test + net.sourceforge.htmlunit htmlunit diff --git a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java index cdf127a6f5e1f..880b9299d6ac5 100644 --- a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java +++ b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java @@ -1,24 +1,31 @@ package io.quarkus.it.keycloak; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import org.hamcrest.Matchers; +import java.net.URL; +import java.time.Duration; + +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.keycloak.representations.AccessTokenResponse; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.WebResponse; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.util.Cookie; import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; -import io.restassured.http.ContentType; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClient; /** * @author Pedro Igor @@ -26,24 +33,37 @@ @QuarkusTest @QuarkusTestResource(KeycloakLifecycleManager.class) public class PolicyEnforcerTest { - + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10); private static final String KEYCLOAK_REALM = "quarkus"; + @TestHTTPResource + URL url; + + static Vertx vertx = Vertx.vertx(); + static WebClient client = WebClient.create(vertx); + + @AfterAll + public static void closeVertxClient() { + if (client != null) { + client.close(); + client = null; + } + if (vertx != null) { + vertx.close().toCompletionStage().toCompletableFuture().join(); + vertx = null; + } + } + @Test public void testUserHasAdminRoleServiceTenant() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api-permission-tenant") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api-permission-tenant") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("admin")) - .when().get("/api-permission-tenant") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Permission Resource Tenant")); + assureGetPath("/api-permission-tenant", 403, getAccessToken("alice"), null); + assureGetPath("//api-permission-tenant", 403, getAccessToken("alice"), null); + + assureGetPath("/api-permission-tenant", 403, getAccessToken("jdoe"), null); + assureGetPath("//api-permission-tenant", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api-permission-tenant", 200, getAccessToken("admin"), "Permission Resource Tenant"); + assureGetPath("//api-permission-tenant", 200, getAccessToken("admin"), "Permission Resource Tenant"); } @Test @@ -54,7 +74,7 @@ public void testUserHasSuperUserRoleWebTenant() throws Exception { } private void testWebAppTenantAllowed(String user) throws Exception { - try (final WebClient webClient = createWebClient()) { + try (final com.gargoylesoftware.htmlunit.WebClient webClient = createWebClient()) { HtmlPage page = webClient.getPage("http://localhost:8081/api-permission-webapp"); assertEquals("Sign in to quarkus", page.getTitleText()); @@ -66,12 +86,18 @@ private void testWebAppTenantAllowed(String user) throws Exception { WebResponse response = loginForm.getInputByName("login").click().getWebResponse(); assertEquals(200, response.getStatusCode()); assertTrue(response.getContentAsString().contains("Permission Resource WebApp")); + + // Token is encrypted in the cookie + Cookie cookie = webClient.getCookieManager().getCookie("q_session_api-permission-webapp"); + assureGetPathWithCookie("/api-permission-webapp", cookie, 200, null, "Permission Resource WebApp"); + assureGetPathWithCookie("//api-permission-webapp", cookie, 200, null, "Permission Resource WebApp"); + webClient.getCookieManager().clearCookies(); } } private void testWebAppTenantForbidden(String user) throws Exception { - try (final WebClient webClient = createWebClient()) { + try (final com.gargoylesoftware.htmlunit.WebClient webClient = createWebClient()) { HtmlPage page = webClient.getPage("http://localhost:8081/api-permission-webapp"); assertEquals("Sign in to quarkus", page.getTitleText()); @@ -86,186 +112,130 @@ private void testWebAppTenantForbidden(String user) throws Exception { } catch (FailingHttpStatusCodeException ex) { assertEquals(403, ex.getStatusCode()); } + + // Token is encrypted in the cookie + Cookie cookie = webClient.getCookieManager().getCookie("q_session_api-permission-webapp"); + assureGetPathWithCookie("/api-permission-webapp", cookie, 403, null, null); + assureGetPathWithCookie("//api-permission-webapp", cookie, 403, null, null); + webClient.getCookieManager().clearCookies(); } } - private WebClient createWebClient() { - WebClient webClient = new WebClient(); + private com.gargoylesoftware.htmlunit.WebClient createWebClient() { + com.gargoylesoftware.htmlunit.WebClient webClient = new com.gargoylesoftware.htmlunit.WebClient(); webClient.setCssErrorHandler(new SilentCssErrorHandler()); return webClient; } @Test public void testUserHasRoleConfidential() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Permission Resource")); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scope?scope=write") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/annotation/scope-write") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scope?scope=read") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/annotation/scope-read") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); - - RestAssured.given().auth().oauth2(getAccessToken("admin")) - .when().get("/api/permission") - .then() - .statusCode(403); - - RestAssured.given().auth().oauth2(getAccessToken("admin")) - .when().get("/api/permission/entitlements") - .then() - .statusCode(200); + assureGetPath("/api/permission", 403, getAccessToken("alice"), null); + assureGetPath("//api/permission", 403, getAccessToken("alice"), null); + + assureGetPath("/api/permission", 200, getAccessToken("jdoe"), "Permission Resource"); + assureGetPath("//api/permission", 200, getAccessToken("jdoe"), "Permission Resource"); + + assureGetPath("/api/permission/scope?scope=write", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/scope?scope=write", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api/permission/annotation/scope-write", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/annotation/scope-write", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api/permission/scope?scope=read", 200, getAccessToken("jdoe"), "read"); + assureGetPath("//api/permission/scope?scope=read", 200, getAccessToken("jdoe"), "read"); + + assureGetPath("/api/permission", 403, getAccessToken("admin"), null); + assureGetPath("//api/permission", 403, getAccessToken("admin"), null); + + assureGetPath("/api/permission/entitlements", 200, getAccessToken("admin"), null); + assureGetPath("//api/permission/entitlements", 200, getAccessToken("admin"), null); } @Test public void testRequestParameterAsClaim() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/claim-protected?grant=true") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Claim Protected Resource")); - ; - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/claim-protected?grant=false") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/claim-protected") - .then() - .statusCode(403); + assureGetPath("/api/permission/claim-protected?grant=true", 200, getAccessToken("alice"), + "Claim Protected Resource"); + assureGetPath("//api/permission/claim-protected?grant=true", 200, getAccessToken("alice"), + "Claim Protected Resource"); + + assureGetPath("/api/permission/claim-protected?grant=false", 403, getAccessToken("alice"), null); + assureGetPath("//api/permission/claim-protected?grant=false", 403, getAccessToken("alice"), null); + + assureGetPath("/api/permission/claim-protected", 403, getAccessToken("alice"), null); + assureGetPath("//api/permission/claim-protected", 403, getAccessToken("alice"), null); } @Test public void testHttpResponseFromExternalServiceAsClaim() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/http-response-claim-protected") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Http Response Claim Protected Resource")); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/http-response-claim-protected") - .then() - .statusCode(403); + assureGetPath("/api/permission/http-response-claim-protected", 200, getAccessToken("alice"), null); + assureGetPath("//api/permission/http-response-claim-protected", 200, getAccessToken("alice"), null); + + assureGetPath("/api/permission/http-response-claim-protected", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/http-response-claim-protected", 403, getAccessToken("jdoe"), null); } @Test public void testBodyClaim() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .contentType(ContentType.JSON) - .body("{\"from-body\": \"grant\"}") - .when() - .post("/api/permission/body-claim") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Body Claim Protected Resource")); + assurePostPath("/api/permission/body-claim", "{\"from-body\": \"grant\"}", 200, getAccessToken("alice"), + "Body Claim Protected Resource"); } @Test public void testPublicResource() { - RestAssured.given() - .when().get("/api/public") - .then() - .statusCode(204); + assureGetPath("/api/public", 204, null, null); } @Test public void testPublicResourceWithEnforcingPolicy() { - RestAssured.given() - .when().get("/api/public-enforcing") - .then() - .statusCode(401); + assureGetPath("/api/public-enforcing", 401, null, null); + assureGetPath("//api/public-enforcing", 401, null, null); } @Test public void testPublicResourceWithEnforcingPolicyAndToken() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/public-enforcing") - .then() - .statusCode(403); + assureGetPath("/api/public-enforcing", 403, getAccessToken("alice"), null); + assureGetPath("//api/public-enforcing", 403, getAccessToken("alice"), null); } @Test public void testPublicResourceWithDisabledPolicyAndToken() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/public-token") - .then() - .statusCode(204); + assureGetPath("/api/public-token", 204, getAccessToken("alice"), null); } @Test public void testPathConfigurationPrecedenceWhenPathCacheNotDefined() { - RestAssured.given() - .when().get("/api2/resource") - .then() - .statusCode(401); - - RestAssured.given() - .when().get("/hello") - .then() - .statusCode(404); - - RestAssured.given() - .when().get("/") - .then() - .statusCode(404); + assureGetPath("/api2/resource", 401, null, null); + assureGetPath("//api2/resource", 401, null, null); + + assureGetPath("/hello", 404, null, null); + assureGetPath("//hello", 404, null, null); + + assureGetPath("/", 404, null, null); + assureGetPath("//", 400, null, null); } @Test public void testPermissionScopes() { // 'jdoe' has scope 'read' and 'read' is required - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/standard-way") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); + assureGetPath("/api/permission/scopes/standard-way", 200, getAccessToken("jdoe"), "read"); + assureGetPath("//api/permission/scopes/standard-way", 200, getAccessToken("jdoe"), "read"); // 'jdoe' has scope 'read' while 'write' is required - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/standard-way-denied") - .then() - .statusCode(403); - - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/programmatic-way") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); - - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/programmatic-way-denied") - .then() - .statusCode(403); - - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/annotation-way") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); - - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/annotation-way-denied") - .then() - .statusCode(403); + assureGetPath("/api/permission/scopes/standard-way-denied", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/scopes/standard-way-denied", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api/permission/scopes/programmatic-way", 200, getAccessToken("jdoe"), "read"); + assureGetPath("//api/permission/scopes/programmatic-way", 200, getAccessToken("jdoe"), "read"); + + assureGetPath("/api/permission/scopes/programmatic-way-denied", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/scopes/programmatic-way-denied", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api/permission/scopes/annotation-way", 200, getAccessToken("jdoe"), "read"); + assureGetPath("//api/permission/scopes/annotation-way", 200, getAccessToken("jdoe"), "read"); + + assureGetPath("/api/permission/scopes/annotation-way-denied", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/scopes/annotation-way-denied", 403, getAccessToken("jdoe"), null); } protected String getAccessToken(String userName) { @@ -281,4 +251,47 @@ protected String getAccessToken(String userName) { + "/protocol/openid-connect/token") .as(AccessTokenResponse.class).getToken(); } + + private void assureGetPath(String path, int expectedStatusCode, String token, String body) { + var req = client.get(url.getPort(), url.getHost(), path); + if (token != null) { + req.bearerTokenAuthentication(token); + } + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (body != null) { + assertTrue(result.result().bodyAsString().contains(body), path); + } + } + + private void assureGetPathWithCookie(String path, Cookie cookie, int expectedStatusCode, String token, String body) { + var req = client.get(url.getPort(), url.getHost(), path); + if (token != null) { + req.bearerTokenAuthentication(token); + } + req.putHeader("Cookie", cookie.getName() + "=" + cookie.getValue()); + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (body != null) { + assertTrue(result.result().bodyAsString().contains(body), path); + } + } + + private void assurePostPath(String path, String requestBody, int expectedStatusCode, String token, + String responseBody) { + var req = client.post(url.getPort(), url.getHost(), path); + if (token != null) { + req.bearerTokenAuthentication(token); + } + req.putHeader("Content-Type", "application/json"); + var result = req.sendJson(new JsonObject(requestBody)); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (responseBody != null) { + assertTrue(result.result().bodyAsString().contains(responseBody), path); + } + } + } diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties index 7e4b58d206b52..cb4cbaa73822a 100644 --- a/integration-tests/oidc-code-flow/src/main/resources/application.properties +++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties @@ -80,6 +80,8 @@ quarkus.oidc.tenant-logout.application-type=web-app quarkus.oidc.tenant-logout.authentication.cookie-path=/tenant-logout quarkus.oidc.tenant-logout.logout.path=/tenant-logout/logout quarkus.oidc.tenant-logout.logout.post-logout-path=/tenant-logout/post-logout +quarkus.oidc.tenant-logout.authentication.session-age-extension=2M +quarkus.oidc.tenant-logout.token.refresh-expired=true quarkus.oidc.tenant-refresh.auth-server-url=${keycloak.url}/realms/logout-realm quarkus.oidc.tenant-refresh.client-id=quarkus-app diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java index 75e3afffc6d24..f899ff722b4ac 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java @@ -604,6 +604,22 @@ public void testRPInitiatedLogout() throws IOException { page = webClient.getPage("http://localhost:8081/tenant-logout"); assertEquals("Sign in to logout-realm", page.getTitleText()); + + // login again + loginForm = page.getForms().get(0); + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + page = loginForm.getInputByName("login").click(); + assertEquals("Tenant Logout, refreshed: false", page.asNormalizedText()); + + assertNotNull(getSessionCookie(webClient, "tenant-logout")); + + await().atLeast(Duration.ofSeconds(11)); + + page = webClient.getPage("http://localhost:8081/tenant-logout/logout"); + assertTrue(page.asNormalizedText().contains("You were logged out, please login again")); + assertNull(getSessionCookie(webClient, "tenant-logout")); + webClient.getCookieManager().clearCookies(); } } diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index b9c0be214ee86..fd749a8f8668d 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -35,7 +35,7 @@ public Map start() { // revoke refresh tokens so that they can only be used once logoutRealm.setRevokeRefreshToken(true); logoutRealm.setRefreshTokenMaxReuse(0); - logoutRealm.setSsoSessionMaxLifespan(15); + logoutRealm.setSsoSessionMaxLifespan(10); logoutRealm.setAccessTokenLifespan(5); client.createRealm(logoutRealm); realms.add(logoutRealm);