diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 9ff81cbdb108c..67ed6c13d5953 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -51,7 +51,7 @@ 1.2.2 1.0.13 2.7.0 - 2.28.0 + 2.29.0 3.22.0 1.3.3 1.2.1 @@ -110,7 +110,7 @@ 1.0.1.Final 1.20.1.Final 3.4.3.Final - 4.3.5 + 4.3.6 4.5.13 4.4.15 @@ -131,10 +131,10 @@ 5.9.1 1.5.0 6.14.2 - 14.0.2.Final + 14.0.3.Final 4.5.0.Final 3.1.1 - 4.1.85.Final + 4.1.86.Final 1.8.0 1.0.3 3.5.0.Final @@ -153,7 +153,7 @@ 1.6.4 1.4.1 6.2.0 - 3.1.2 + 3.1.3 3.2.0 4.2.0 1.0.11 @@ -162,7 +162,7 @@ 4.17.2 1.33 6.0.0 - 4.8.0 + 4.8.1 1.6.1 0.34.0 3.24.2 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 591a45865970a..1891b4056e154 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -19,7 +19,7 @@ - 3.8.1 + 3.10.1 1.7.21 1.7.20 2.13.8 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java index 58c97c1eb237c..60d9243131ad5 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildLocalContainerRunner.java @@ -116,8 +116,14 @@ protected List getContainerRuntimeBuildArgs() { volumeOutputPath = FileUtil.translateToVolumePath(volumeOutputPath); } + String selinuxBindOption = ":z"; + if (SystemUtils.IS_OS_MAC + && ContainerRuntimeUtil.detectContainerRuntime() == ContainerRuntimeUtil.ContainerRuntime.PODMAN) { + selinuxBindOption = ""; + } + Collections.addAll(containerRuntimeArgs, "-v", - volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + ":z"); + volumeOutputPath + ":" + NativeImageBuildStep.CONTAINER_BUILD_VOLUME_PATH + selinuxBindOption); return containerRuntimeArgs; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/KotlinUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/KotlinUtil.java new file mode 100644 index 0000000000000..af69aa5390ac7 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/KotlinUtil.java @@ -0,0 +1,16 @@ +package io.quarkus.deployment.steps; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; + +final class KotlinUtil { + + private static final DotName KOTLIN_METADATA_ANNOTATION = DotName.createSimple("kotlin.Metadata"); + + private KotlinUtil() { + } + + static boolean isKotlinClass(ClassInfo classInfo) { + return classInfo.hasDeclaredAnnotation(KOTLIN_METADATA_ANNOTATION); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index 6371b57ef3c7c..cfefd34f70fa4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -1,5 +1,6 @@ package io.quarkus.deployment.steps; +import static io.quarkus.deployment.steps.KotlinUtil.isKotlinClass; import static io.quarkus.gizmo.MethodDescriptor.ofConstructor; import static io.quarkus.gizmo.MethodDescriptor.ofMethod; @@ -17,6 +18,7 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ArrayType; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; @@ -341,12 +343,13 @@ public MainClassBuildItem mainClassBuildStep(BuildProducer generatedClass) { ClassCreator file = new ClassCreator(new GeneratedClassGizmoAdaptor(generatedClass, true), MAIN_CLASS, null, diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java index 0018217fa3ae7..b4a085c479f28 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/RegisterForReflectionBuildStep.java @@ -1,5 +1,7 @@ package io.quarkus.deployment.steps; +import static io.quarkus.deployment.steps.KotlinUtil.isKotlinClass; + import java.io.IOException; import java.lang.reflect.Modifier; import java.util.HashSet; @@ -34,8 +36,6 @@ public class RegisterForReflectionBuildStep { private static final Logger log = Logger.getLogger(RegisterForReflectionBuildStep.class); - private static final DotName KOTLIN_METADATA_ANNOTATION = DotName.createSimple("kotlin.Metadata"); - @BuildStep public void build(CombinedIndexBuildItem combinedIndexBuildItem, Capabilities capabilities, BuildProducer reflectiveClass, @@ -98,10 +98,6 @@ public void build(CombinedIndexBuildItem combinedIndexBuildItem, Capabilities ca } } - private static boolean isKotlinClass(ClassInfo classInfo) { - return classInfo.hasDeclaredAnnotation(KOTLIN_METADATA_ANNOTATION); - } - /** * BFS Recursive Method to register a class and it's inner classes for Reflection. * diff --git a/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java new file mode 100644 index 0000000000000..129742b14af23 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/DebugRuntimeConfig.java @@ -0,0 +1,16 @@ +package io.quarkus.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "debug", phase = ConfigPhase.RUN_TIME) +public class DebugRuntimeConfig { + + /** + * If set to {@code true}, Quarkus prints the wall-clock time each build step took to complete. + * This is useful as a first step in debugging slow startup times. + */ + @ConfigItem(defaultValue = "false") + boolean printStartupTimes; +} diff --git a/docs/src/main/asciidoc/building-my-first-extension.adoc b/docs/src/main/asciidoc/building-my-first-extension.adoc index fa8790d26b174..10fdfa2720a44 100644 --- a/docs/src/main/asciidoc/building-my-first-extension.adoc +++ b/docs/src/main/asciidoc/building-my-first-extension.adoc @@ -164,7 +164,7 @@ Your extension is a multi-module project. So let's start by checking out the par runtime - 3.8.1 + 3.10.1 ${surefire-plugin.version} 11 UTF-8 @@ -876,7 +876,7 @@ $ mvn clean compile quarkus:dev [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 1 resource [INFO] -[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ greeting-app --- +[INFO] --- maven-compiler-plugin:3.10.1:compile (default-compile) @ greeting-app --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- quarkus-maven-plugin:{quarkus-version}:dev (default-cli) @ greeting-app --- diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index a390903d28726..2404aa36b1554 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -326,7 +326,7 @@ Adding `--enable-preview` to its `configuration` section is o === Excluding tests when running as a native executable -When running tests this way, the only things that actually run natively are you application endpoints, which +When running tests this way, the only things that actually run natively are your application endpoints, which you can only test via HTTP calls. Your test code does not actually run natively, so if you are testing code that does not call your HTTP endpoints, it's probably not a good idea to run them as part of native tests. @@ -484,7 +484,7 @@ Then, if you didn't delete the generated native executable, you can build the do [source,bash] ---- -docker build -f src/main/docker/Dockerfile.native -t quarkus-quickstart/getting-started . +docker build -f src/main/docker/Dockerfile.native-micro -t quarkus-quickstart/getting-started . ---- And finally, run it with: @@ -653,7 +653,7 @@ Just add your application on top of this image, and you will get a tiny containe Distroless images should not be used in production without rigorous testing. -=== Using a scratch base image +=== Build a container image from scratch IMPORTANT: Scratch image support is experimental. @@ -671,9 +671,9 @@ COPY --chown=quarkus:quarkus mvnw /code/mvnw COPY --chown=quarkus:quarkus .mvn /code/.mvn COPY --chown=quarkus:quarkus pom.xml /code/ RUN mkdir /musl && \ - curl -L -o musl.tar.gz https://more.musl.cc/10.2.1/x86_64-linux-musl/x86_64-linux-musl-native.tgz && \ + curl -L -o musl.tar.gz https://more.musl.cc/11.2.1/x86_64-linux-musl/x86_64-linux-musl-native.tgz && \ tar -xvzf musl.tar.gz -C /musl --strip-components 1 && \ - curl -L -o zlib.tar.gz https://zlib.net/zlib-1.2.12.tar.gz && \ + curl -L -o zlib.tar.gz https://www.zlib.net/zlib-1.2.13.tar.gz && \ mkdir zlib && tar -xvzf zlib.tar.gz -C zlib --strip-components 1 && \ cd zlib && ./configure --static --prefix=/musl && \ make && make install && \ @@ -683,17 +683,20 @@ USER quarkus WORKDIR /code RUN ./mvnw -B org.apache.maven.plugins:maven-dependency-plugin:3.1.2:go-offline COPY src /code/src -RUN ./mvnw package -Pnative -Dquarkus.native.additional-build-args="--static","--libc=musl" +RUN ./mvnw package -Pnative -DskipTests -Dquarkus.native.additional-build-args="--static","--libc=musl" -## Stage 2 : create the docker final image +## Stage 2 : create the final image FROM scratch COPY --from=build /code/target/*-runner /application +EXPOSE 8080 ENTRYPOINT [ "/application" ] ---- Scratch images should not be used in production without rigorous testing. -=== Native executable compression +NOTE: The versions of musl and zlib may need to be updated to meet the native-image executable requirements (and UPX if you use native image compression). + +=== Compress native images Quarkus can compress the produced native executable using UPX. More details on xref:./upx.adoc[UPX Compression documentation]. diff --git a/docs/src/main/asciidoc/cassandra.adoc b/docs/src/main/asciidoc/cassandra.adoc index 9fe0090191a37..b6ebb0c1790e5 100644 --- a/docs/src/main/asciidoc/cassandra.adoc +++ b/docs/src/main/asciidoc/cassandra.adoc @@ -119,7 +119,7 @@ Note also the special return type of the `findAll` method, link:https://docs.datastax.com/en/drivers/java/latest/com/datastax/oss/driver/api/core/PagingIterable.html[`PagingIterable`]: it's the base type of result sets returned by the driver. -Finally, let's create the Mapper interface: +Finally, let's create a Mapper interface: [source,java] ---- @@ -131,10 +131,93 @@ public interface FruitMapper { ---- The `@Mapper` annotation is yet another annotation recognized by the DataStax Object Mapper. A -mapper is responsible for constructing instances of DAOs – in this case, out mapper is constructing +mapper is responsible for constructing DAO instances – in this case, out mapper is constructing an instance of our only DAO, `FruitDao`. -== Creating a Service & JSON REST Endpoint +Think of the mapper interface as a factory for DAO beans. If you intend to construct and inject a +specific DAO bean in your own code, then you first must add a `@DaoFactory` method for it in a +`@Mapper` interface. + +TIP: `@DaoFactory` method names are irrelevant. + +`@DaoFactory` methods should return beans of the following types: + +- Any `@Dao`-annotated interface, e.g. `FruitDao`; +- A `CompletationStage` of any `@Dao`-annotated interface, e.g. `CompletionStage`. +- A `Uni` of any `@Dao`-annotated interface, e.g. `Uni`. + +TIP: `Uni` is a type from the Mutiny library, which is the reactive programming library used by +Quarkus. This will be explained in more detail in the "Reactive Programming" section below. + +== Generating the DAO and mapper implementations + +As you probably guessed already, we are not going to implement the interfaces above. Instead, the +Object Mapper will generate such implementations for us. + +The Object Mapper is composed of 2 pieces: + +1. A (compile-time) annotation processor that scans the classpath for classes annotated with +`@Mapper`, `@Dao` or `@Entity`, and generates code and CQL queries for them; and +2. A runtime module that contains the logic to execute the generated queries. + +Therefore, enabling the Object Mapper requires two steps: + +1. Declare the `cassandra-quarkus-mapper-processor` annotation processor. With Maven, this is done +by modifying the compiler plugin configuration in the project's `pom.xml` file as follows: + +[source,xml] +---- + + maven-compiler-plugin + 3.10.1 + + ${java.version} + ${java.version} + + + com.datastax.oss.quarkus + cassandra-quarkus-mapper-processor + ${cassandra-quarkus.version} + + + + +---- + +With Gradle, this is done by adding the following line to the `build.gradle` file: + +[source,groovy] +---- +annotationProcessor "com.datastax.oss.quarkus:cassandra-quarkus-mapper-processor:${cassandra-quarkus.version}" +---- + +IMPORTANT: Verify that you are enabling the right annotation processor! The Cassandra driver ships +with its Object Mapper annotation processor, called `java-driver-mapper-processor`. But the +Cassandra Quarkus extension also ships with its own annotation processor: +`cassandra-quarkus-mapper-processor`, which has more capabilities than the driver's. This annotation +processor is the only one suitable for use in a Quarkus application, so check that this is the one +in use. Also, never use both annotation processors together. + +[start=2] +1. Declare the `java-driver-mapper-runtime` dependency in compile scope in the project's `pom.xml` + file as follows: + +[source,xml] +---- + + com.datastax.oss + java-driver-mapper-runtime + +---- + +IMPORTANT: Although this module is called "runtime", it must be declared in compile scope. + +If your project is correctly set up, you should now be able to compile it without errors, and you +should see the generated code in the `target/generated-sources/annotations` directory (if you are +using Maven). It's not required to get familiar with the generated code though, as it is mostly +internal machinery to interact with the database. + +== Creating a service & JSON REST endpoint Now let's create a `FruitService` that will be the business layer of our application and store/load the fruits from the Cassandra database. @@ -157,19 +240,20 @@ public class FruitService { ---- Note how the service is being injected a `FruitDao` instance. This DAO instance is injected -automatically. +automatically, thanks to the generated implementations. The Cassandra Quarkus extension allows you to inject any of the following beans in your own components: - All `@Mapper`-annotated interfaces in your project. -- All `@Dao`-annotated interfaces in your project, as long as they are produced by a corresponding -`@DaoFactory`-annotated method declared in a mapper interface from your project. +- You can also inject a `CompletionStage` or `Uni` of any `@Mapper`-annotated interface. +- Any bean returned by a `@DaoFactory` method (see above for possible bean types). - The link:https://javadoc.io/doc/com.datastax.oss.quarkus/cassandra-quarkus-client/latest/com/datastax/oss/quarkus/runtime/api/session/QuarkusCqlSession.html[`QuarkusCqlSession`] bean: this application-scoped, singleton bean is your main entry point to the Cassandra client; it is a specialized Cassandra driver session instance with a few methods tailored especially for Quarkus. Read its javadocs carefully! +- You can also inject `CompletationStage` or `Uni`. In our example, both `FruitMapper` and `FruitDao` could be injected anywhere. We chose to inject `FruitDao` in `FruitService`. @@ -242,7 +326,7 @@ below snippet to your application's ppm.xml file: io.quarkus - quarkus-resteasy-jackson + quarkus-resteasy-reactive-jackson ---- @@ -723,17 +807,19 @@ you can run the native executable as follows: You can then point your browser to `http://localhost:8080/fruits.html` and use your application. -== Eager vs Lazy Initialization +== Choosing between eager and lazy initialization -This extension allows you to inject either: +As explained above, this extension allows you to inject many types of beans: -- a `QuarkusCqlSession` bean; -- or the asynchronous version of this bean, that is, `CompletionStage`; -- or the reactive version of this bean, that is, `Uni`. +- A simple bean like `QuarkusCqlSession` or `FruitDao`; +- The asynchronous version of that bean, for example `CompletionStage` or + `CompletionStage; +- The reactive version of that bean, for example `Uni` or `Uni`. -The most straightforward approach is obviously to inject `QuarkusCqlSession` directly. This should -work just fine for most applications; however, the `QuarkusCqlSession` bean needs to be initialized -before it can be used, and this process is blocking. +The most straightforward approach is obviously to inject the bean directly. This should work just +fine for most applications. However, the `QuarkusCqlSession` bean, and all DAO beans that depend on +it, might take some time to initialize before they can be used for the first time, and this process +is blocking. Fortunately, it is possible to control when the initialization should happen: the `quarkus.cassandra.init.eager-init` parameter determines if the `QuarkusCqlSession` bean should be @@ -744,21 +830,21 @@ that needs to interact with the Cassandra database. Using lazy initialization speeds up your application startup time, and avoids startup failures if the Cassandra database is not available. However, it could also prove dangerous if your code is -fully asynchronous, e.g. if you are using https://quarkus.io/guides/reactive-routes[reactive -routes]: indeed, the lazy initialization could accidentally happen on a thread that is not allowed +fully non-blocking, for example if it uses https://quarkus.io/guides/reactive-routes[reactive +routes]. Indeed, the lazy initialization could accidentally happen on a thread that is not allowed to block, such as a Vert.x event loop thread. Therefore, setting `quarkus.cassandra.init.eager-init` to `false` and injecting `QuarkusCqlSession` should be avoided in these contexts. -If you want to use Vert.x (or any other reactive framework) and keep the lazy initialization -behavior, you should instead inject only `CompletionStage` or -`Uni`. When injecting these beans, the initialization process will be triggered -lazily, but it will happen in the background, in a non-blocking way, leveraging the Vert.x event -loop. This way you don't risk blocking the Vert.x thread. +If you want to use Vert.x (or any other non-blocking framework) and keep the lazy initialization +behavior, you should instead inject only a `CompletionStage` or a `Uni` of the desired bean. When +injecting these beans, the initialization process will be triggered lazily, but it will happen in +the background, in a non-blocking way, leveraging the Vert.x event loop. This way you don't risk +blocking the Vert.x thread. Alternatively, you can set `quarkus.cassandra.init.eager-init` to true: in this case the session -bean will be initialized eagerly during application startup, on the Quarkus main thread. This would -eliminate any risk of blocking a Vert.x thread, at the cost of making your startup time (much) -longer. +bean and all DAO beans will be initialized eagerly during application startup, on the Quarkus main +thread. This would eliminate any risk of blocking a Vert.x thread, at the cost of making your +startup time (much) longer. == Conclusion diff --git a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc index bdf62b6ff05ce..7d37416a04c58 100644 --- a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc +++ b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc @@ -113,7 +113,7 @@ First, add the plugin to your `pom.xml`: com.google.cloud.tools appengine-maven-plugin - 2.4.0 + 2.4.4 GCLOUD_CONFIG <1> gettingstarted @@ -163,7 +163,7 @@ It uses Cloud Build to build your Docker image and deploy it to Google Container When done, the output will display the URL of your application (target url), you can use it with curl or directly open it in your browser using `gcloud app browse`. -NOTE: App Engine Flexible custom runtimes support link:https://cloud.google.com/appengine/docs/flexible/custom-runtimes/configuring-your-app-with-app-yaml#updated_health_checks[health checks], +NOTE: App Engine Flexible custom runtimes support link:https://cloud.google.com/appengine/docs/flexible/reference/app-yaml?tab=java#updated_health_checks[health checks], it is strongly advised to provide them thanks to Quarkus xref:smallrye-health.adoc[Smallrye Health] support. == Deploying to Google Cloud Run @@ -211,7 +211,7 @@ Finally, use Cloud Run to launch your application. [source, shell script] ---- -gcloud run deploy --image gcr.io/PROJECT-ID/helloworld --platform managed +gcloud run deploy --image gcr.io/PROJECT-ID/helloworld ---- Cloud run will ask you questions on the service name, the region and whether unauthenticated calls are allowed. @@ -219,6 +219,9 @@ After you answer to these questions, it will deploy your application. When the deployment is done, the output will display the URL to access your application. +NOTE: Cloud Run supports link:https://cloud.google.com/run/docs/configuring/healthchecks[health checks], +it is strongly advised to provide them thanks to Quarkus xref:smallrye-health.adoc[Smallrye Health] support. + == Using Cloud SQL Google Cloud SQL provides managed instances for MySQL, PostgreSQL and Microsoft SQL Server. @@ -295,4 +298,4 @@ WARNING: This only works when your application is running inside a Google Cloud You can find a set of extensions to access various Google Cloud Services in the Quarkiverse (a GitHub organization for Quarkus extensions maintained by the community), including PubSub, BigQuery, Storage, Spanner, Firestore, Secret Manager (visit the repository for an accurate list of supported services). -You can find some documentation about them in the link:https://github.com/quarkiverse/quarkiverse-google-cloud-services[Quarkiverse Google Cloud Services repository]. +You can find some documentation about them in the link:https://quarkiverse.github.io/quarkiverse-docs/quarkus-google-cloud-services/main/index.html[Quarkiverse Google Cloud Services documentation]. diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 9d22270d17fa3..0d1070fb4884e 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -76,7 +76,7 @@ The full source of the `kubernetes.json` file looks something like this: "kind" : "Deployment", "metadata" : { "annotations": { - "app.quarkus.io/vcs-url" : "", + "app.quarkus.io/vcs-uri" : "", "app.quarkus.io/commit-id" : "", }, "labels" : { @@ -123,7 +123,7 @@ The full source of the `kubernetes.json` file looks something like this: "kind" : "Service", "metadata" : { "annotations": { - "app.quarkus.io/vcs-url" : "", + "app.quarkus.io/vcs-uri" : "", "app.quarkus.io/commit-id" : "", }, "labels" : { @@ -305,7 +305,7 @@ Out of the box, the generated resources will be annotated with version control r [source,json] ---- "annotations": { - "app.quarkus.io/vcs-url" : "", + "app.quarkus.io/vcs-uri" : "", "app.quarkus.io/commit-id" : "", } ---- @@ -1049,7 +1049,7 @@ The full source of the `knative.json` file looks something like this: "kind" : "Service", "metadata" : { "annotations": { - "app.quarkus.io/vcs-url" : "", + "app.quarkus.io/vcs-uri" : "", "app.quarkus.io/commit-id" : "" }, "labels" : { diff --git a/docs/src/main/asciidoc/jreleaser.adoc b/docs/src/main/asciidoc/jreleaser.adoc index 677dca385ec75..d7b54cc7a3378 100644 --- a/docs/src/main/asciidoc/jreleaser.adoc +++ b/docs/src/main/asciidoc/jreleaser.adoc @@ -607,7 +607,7 @@ As a reference, these are the full contents of the `pom.xml`: ${project.build.directory}/distributions - 3.8.1 + 3.10.1 true 11 11 diff --git a/docs/src/main/asciidoc/kubernetes-dev-services.adoc b/docs/src/main/asciidoc/kubernetes-dev-services.adoc index 0f4c4fc0af9be..bd749c6d220e3 100644 --- a/docs/src/main/asciidoc/kubernetes-dev-services.adoc +++ b/docs/src/main/asciidoc/kubernetes-dev-services.adoc @@ -5,7 +5,7 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// = Dev Services for Kubernetes include::_attributes.adoc[] -:categories: messaging +:categories: cloud :summary: Start a Kubernetes API server automatically in dev and test modes. Dev Services for Kubernetes automatically starts a Kubernetes API server in dev mode and when running tests. diff --git a/docs/src/main/asciidoc/maven-tooling.adoc b/docs/src/main/asciidoc/maven-tooling.adoc index 9aac9d4771223..cee4744a2d2e4 100644 --- a/docs/src/main/asciidoc/maven-tooling.adoc +++ b/docs/src/main/asciidoc/maven-tooling.adoc @@ -59,6 +59,10 @@ If you are using the Maven command, the following table lists the attributes you | The version currently recommended by the https://quarkus.io/guides/extension-registry-user[Quarkus Extension Registry] | The version of the platform you want the project to use. It can also accept a version range, in which case the latest from the specified range will be used. +| `javaVersion` +| 17 +| The version of Java you want the project to use. + | `className` | _Not created if omitted_ | The fully qualified name of the generated resource diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index 69d714a5beb28..842ac7f244db2 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -215,7 +215,7 @@ Because we used the `${...}` syntax, the actual value of the parameter will be o [IMPORTANT] ==== -Note that if an interface method contains an argument annotated with `@QueryParam````, that argument will take +Note that if an interface method contains an argument annotated with `@QueryParam`, that argument will take priority over anything specified in any `@ClientQueryParam` annotation. ==== @@ -671,7 +671,7 @@ extensionsService.getByIdAsUni(id) If you use a `CompletionStage`, you would need to call the service's method to retry. This difference comes from the laziness aspect of Mutiny and its subscription protocol. -More details about this can be found in https://smallrye.io/smallrye-mutiny/#_uni_and_multi[the Mutiny documentation]. +More details about this can be found in https://smallrye.io/smallrye-mutiny/latest/reference/uni-and-multi/[the Mutiny documentation]. == Custom headers support diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 97990eb23ff1f..680315e9376d8 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -514,7 +514,7 @@ extensionsService.getByIdAsUni(id) If you use a `CompletionStage`, you would need to call the service's method to retry. This difference comes from the laziness aspect of Mutiny and its subscription protocol. -More details about this can be found in https://smallrye.io/smallrye-mutiny/#_uni_and_multi[the Mutiny documentation]. +More details about this can be found in https://smallrye.io/smallrye-mutiny/latest/reference/uni-and-multi/[the Mutiny documentation]. == Custom headers support diff --git a/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc b/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc index c3f7ad629d664..d535842ec750a 100644 --- a/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc +++ b/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc @@ -175,7 +175,39 @@ public class HelloService { === How to customize authentication exception responses -By default, the authentication security constraints are enforced before the JAX-RS chain starts and only way to handle Quarkus Security authentication exceptions is to provide a failure handler like this one: +You can use JAX-RS `ExceptionMapper` to capture Quarkus Security authentication exceptions such as `io.quarkus.security.AuthenticationFailedException`, for example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import io.quarkus.security.AuthenticationFailedException; + +@Provider +@Priority(Priorities.AUTHENTICATION) +public class AuthenticationFailedExceptionMapper implements ExceptionMapper { + + @Context + UriInfo uriInfo; + + @Override + public Response toResponse(AuthenticationFailedException exception) { + return Response.status(401).header("WWW-Authenticate", "Basic realm=\"Quarkus\"").build(); + } +} +---- + +CAUTION: Some HTTP authentication mechanisms need to handle authentication exceptions themselves in order to create a correct authentication challenge. +For example, `io.quarkus.oidc.runtime.CodeAuthenticationMechanism` which manages OpenId Connect authorization code flow authentication, needs to build a correct redirect URL, cookies, etc. +For that reason, using custom exception mappers to customize authentication exceptions thrown by such mechanisms is not recommended. +In such cases, a safer way to customize authentication exceptions is to make sure the proactive authentication is not disabled and use Vert.x HTTP route failure handlers, as events come to the handler with the correct response status and headers. +To that end, the only thing that needs to be done is to customize the response like this: [source,java] ---- @@ -197,7 +229,7 @@ public class AuthenticationFailedExceptionHandler { @Override public void handle(RoutingContext event) { if (event.failure() instanceof AuthenticationFailedException) { - event.response().setStatusCode(401).end(CUSTOMIZED_RESPONSE); + event.response().end("CUSTOMIZED_RESPONSE"); } else { event.next(); } @@ -207,34 +239,6 @@ public class AuthenticationFailedExceptionHandler { } ---- -Disabling the proactive authentication effectively shifts this process to the moment when the JAX-RS chain starts running thus making it possible to use JAX-RS `ExceptionMapper` to capture Quarkus Security authentication exceptions such as `io.quarkus.security.AuthenticationFailedException`, for example: - -[source,java] ----- -package io.quarkus.it.keycloak; - -import javax.annotation.Priority; -import javax.ws.rs.Priorities; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -import io.quarkus.security.AuthenticationFailedException; - -@Provider -@Priority(Priorities.AUTHENTICATION) -public class AuthenticationFailedExceptionMapper implements ExceptionMapper { - - @Context - UriInfo uriInfo; - - @Override - public Response toResponse(AuthenticationFailedException exception) { - return Response.status(401).header("WWW-Authenticate", "Basic realm=\"Quarkus\"").build(); - } -} ----- - == References * xref:security-overview-concept.adoc[Quarkus Security overview] diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index 0760c51cd7c12..ff3345fa31444 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -70,6 +70,7 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism } ---- +[[dealing-with-more-than-one-http-auth-mechanisms]] == Dealing with more than one HttpAuthenticationMechanism More than one `HttpAuthenticationMechanism` can be combined, for example, the built-in `Basic` or `JWT` mechanism provided by `quarkus-smallrye-jwt` has to be used to verify the service clients credentials passed as the HTTP `Authorization` `Basic` or `Bearer` scheme values while the `Authorization Code` mechanism provided by `quarkus-oidc` has to be used to authenticate the users with Keycloak or other OpenID Connect providers. diff --git a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc index 3fd5893ef29f1..87f0feb9c3b87 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -677,7 +677,7 @@ quarkus.oidc.authentication.pkce-required=true quarkus.oidc.authentication.pkce-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU ---- -If you already have a 32 character long client secret then `quarkus.oidc.authentication.pkce-secret` does not have to be set unless you prefer to use a different secret key. +If you already have a 32 characters long client secret then `quarkus.oidc.authentication.pkce-secret` does not have to be set unless you prefer to use a different secret key. The secret key is required for encrypting a randomly generated `PKCE` `code_verifier` while the user is being redirected with the `code_challenge` query parameter to OpenID Connect Provider to authenticate. The `code_verifier` will be decrypted when the user is redirected back to Quarkus and sent to the token endpoint alongside the `code`, client secret and other parameters to complete the code exchange. The provider will fail the code exchange if a `SHA256` digest of the `code_verifier` does not match the `code_challenge` provided during the authentication request. @@ -700,7 +700,7 @@ public class SecurityEventListener { public void event(@Observes SecurityEvent event) { String tenantId = event.getSecurityIdentity().getAttribute("tenant-id"); - RoutingContext vertxContext = event.getSecurityIdentity().getCredential(IdTokenCredential.class).getRoutingContext(); + RoutingContext vertxContext = event.getSecurityIdentity().getAttribute(RoutingContext.class.getName()); vertxContext.put("listener-message", String.format("event:%s,tenantId:%s", event.getEventType().name(), tenantId)); } } diff --git a/docs/src/main/asciidoc/security-overview-concept.adoc b/docs/src/main/asciidoc/security-overview-concept.adoc index e69dcc9c9fd37..2af860fe09c56 100644 --- a/docs/src/main/asciidoc/security-overview-concept.adoc +++ b/docs/src/main/asciidoc/security-overview-concept.adoc @@ -1,7 +1,7 @@ [id="security-overview-concept"] = Quarkus Security overview include::_attributes.adoc[] -:categories: security, getting-started +:categories: security Quarkus Security is a framework that provides the architecture, multiple authentication and authorization mechanisms, and other tools for you to build secure and production-quality Java applications. diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index f58249f52a563..ddf790248b296 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -559,7 +559,7 @@ The name of the deployment module can be configured in the plugin by setting the ---- plugins { id 'java' - id 'io.quarkus.extensions' + id 'io.quarkus.extension' } quarkusExtension { diff --git a/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml index ba5993f75d90c..b06a4f66bae9b 100644 --- a/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda-http/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -8,7 +8,7 @@ \${version} 3.1.0 - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml index a0518654fe7f2..04a0d6f91da6a 100644 --- a/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda-rest/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -8,7 +8,7 @@ \${version} 3.1.0 - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml index ec1094ddc0d83..d4afeffcb02a5 100644 --- a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -7,7 +7,7 @@ \${artifactId} \${version} - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevBeanInfo.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevBeanInfo.java index d8ce275ab7a3c..7ff0417790d2e 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevBeanInfo.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/DevBeanInfo.java @@ -49,6 +49,7 @@ public static DevBeanInfo from(BeanInfo bean, CompletedApplicationClassPredicate DevBeanKind kind; String memberName; boolean isApplicationBean; + boolean isGenerated = false; Name declaringClass; if (target.kind() == Kind.METHOD) { MethodInfo method = target.asMethod(); @@ -56,35 +57,39 @@ public static DevBeanInfo from(BeanInfo bean, CompletedApplicationClassPredicate kind = DevBeanKind.METHOD; isApplicationBean = predicate.test(bean.getDeclaringBean().getBeanClass()); declaringClass = Name.from(bean.getDeclaringBean().getBeanClass()); + isGenerated = bean.getDeclaringBean().getImplClazz().isSynthetic(); } else if (target.kind() == Kind.FIELD) { FieldInfo field = target.asField(); memberName = field.name(); kind = DevBeanKind.FIELD; isApplicationBean = predicate.test(bean.getDeclaringBean().getBeanClass()); declaringClass = Name.from(bean.getDeclaringBean().getBeanClass()); + isGenerated = bean.getDeclaringBean().getImplClazz().isSynthetic(); } else if (target.kind() == Kind.CLASS) { ClassInfo clazz = target.asClass(); kind = DevBeanKind.CLASS; memberName = null; isApplicationBean = predicate.test(clazz.name()); + isGenerated = clazz.isSynthetic(); declaringClass = null; } else { throw new IllegalArgumentException("Invalid annotation target: " + target); } return new DevBeanInfo(bean.getIdentifier(), kind, isApplicationBean, providerType, memberName, types, qualifiers, scope, declaringClass, - interceptors); + interceptors, isGenerated); } else { // Synthetic bean return new DevBeanInfo(bean.getIdentifier(), DevBeanKind.SYNTHETIC, false, providerType, null, types, qualifiers, scope, null, - interceptors); + interceptors, bean.getImplClazz().isSynthetic()); } } public DevBeanInfo(String id, DevBeanKind kind, boolean isApplicationBean, Name providerType, String memberName, Set types, - Set qualifiers, Name scope, Name declaringClass, List boundInterceptors) { + Set qualifiers, Name scope, Name declaringClass, List boundInterceptors, + boolean isGenerated) { this.id = id; this.kind = kind; this.isApplicationBean = isApplicationBean; @@ -95,6 +100,7 @@ public DevBeanInfo(String id, DevBeanKind kind, boolean isApplicationBean, Name this.scope = scope; this.declaringClass = declaringClass; this.interceptors = boundInterceptors; + this.isGenerated = isGenerated; } private final String id; @@ -107,6 +113,7 @@ public DevBeanInfo(String id, DevBeanKind kind, boolean isApplicationBean, Name private final Name scope; private final Name declaringClass; private final List interceptors; + private final boolean isGenerated; public String getId() { return id; @@ -161,6 +168,10 @@ public List getInterceptors() { return interceptors; } + public boolean isGenerated() { + return isGenerated; + } + public String getDescription() { return description(false); } @@ -203,11 +214,18 @@ public String typeInfo(boolean simple) { @Override public int compareTo(DevBeanInfo o) { - // Application beans should go first - if (isApplicationBean == o.isApplicationBean) { - return providerType.compareTo(o.providerType); + // application beans come first + int result = Boolean.compare(o.isApplicationBean, isApplicationBean); + if (result != 0) { + return result; + } + // generated beans comes last + result = Boolean.compare(isGenerated, o.isGenerated); + if (result != 0) { + return result; } - return isApplicationBean ? -1 : 1; + // fallback to name comparison + return providerType.compareTo(o.providerType); } @Override diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java index 828c49f7ccb4e..027db93f79175 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java @@ -101,6 +101,11 @@ static String createSimple(AnnotationInstance annotation) { @Override public int compareTo(Name other) { + // Quarkus classes should be last + int result = Boolean.compare(isQuarkusClassName(), other.isQuarkusClassName()); + if (result != 0) { + return result; + } return name.compareTo(other.name); } @@ -109,4 +114,7 @@ public String toString() { return name; } + private boolean isQuarkusClassName() { + return name.startsWith("io.quarkus"); + } } diff --git a/extensions/azure-functions-http/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/azure-functions-http/maven-archetype/src/main/resources/archetype-resources/pom.xml index 643e676782a42..9fb478b3726f8 100644 --- a/extensions/azure-functions-http/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/azure-functions-http/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -6,7 +6,7 @@ \${artifactId} \${version} - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index 627712c359bfa..468fff82eae9d 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -200,8 +200,8 @@ private RunningDevService startDevDb(String dbName, boolean explicitlyDisabled = !(dataSourceBuildTimeConfig.devservices.enabled.orElse(true)); if (explicitlyDisabled) { //explicitly disabled - log.debug("Not starting devservices for " + (dbName == null ? "default datasource" : dbName) - + " as it has been disabled in the config"); + log.debug("Not starting Dev Services for " + (dbName == null ? "default datasource" : dbName) + + " as it has been disabled in the configuration"); return null; } @@ -222,8 +222,8 @@ private RunningDevService startDevDb(String dbName, List configHandlers = configurationHandlerBuildItems .get(defaultDbKind.get()); if (devDbProvider == null || configHandlers == null) { - log.warn("Unable to start devservices for " + (dbName == null ? "default datasource" : dbName) - + " as this datasource type (" + defaultDbKind.get() + ") does not support devservices"); + log.warn("Unable to start Dev Services for " + (dbName == null ? "default datasource" : dbName) + + " as this datasource type (" + defaultDbKind.get() + ") does not support Dev Services"); return null; } @@ -232,7 +232,7 @@ private RunningDevService startDevDb(String dbName, if (i.getCheckConfiguredFunction().test(dbName)) { //this database has explicit configuration //we don't start the devservices - log.debug("Not starting devservices for " + (dbName == null ? "default datasource" : dbName) + log.debug("Not starting Dev Services for " + (dbName == null ? "default datasource" : dbName) + " as it has explicit configuration"); return null; } diff --git a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java index 50046b1b90d37..3668d11dcfee4 100644 --- a/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java +++ b/extensions/devservices/mariadb/src/main/java/io/quarkus/devservices/mariadb/deployment/MariaDBDevServicesProcessor.java @@ -118,7 +118,7 @@ public String getEffectiveJdbcUrl() { } public String getReactiveUrl() { - return getEffectiveJdbcUrl().replaceFirst("jdbc:", "vertx-reactive:"); + return getEffectiveJdbcUrl().replaceFirst("jdbc:mariadb:", "vertx-reactive:mysql:"); } } } diff --git a/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml index 919bcad791b75..78ff2cf9929e6 100644 --- a/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/funqy/funqy-amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -7,7 +7,7 @@ \${artifactId} \${version} - 3.8.1 + 3.10.1 true 11 11 diff --git a/extensions/jdbc/jdbc-postgresql/deployment/src/main/java/io/quarkus/jdbc/postgresql/deployment/PostgreSQLJDBCReflections.java b/extensions/jdbc/jdbc-postgresql/deployment/src/main/java/io/quarkus/jdbc/postgresql/deployment/PostgreSQLJDBCReflections.java index daf43576a1516..9c5e85325d0f9 100644 --- a/extensions/jdbc/jdbc-postgresql/deployment/src/main/java/io/quarkus/jdbc/postgresql/deployment/PostgreSQLJDBCReflections.java +++ b/extensions/jdbc/jdbc-postgresql/deployment/src/main/java/io/quarkus/jdbc/postgresql/deployment/PostgreSQLJDBCReflections.java @@ -19,6 +19,9 @@ void build(BuildProducer reflectiveClass) { //We register it for the sake of other users. final String driverName = "org.postgresql.Driver"; reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, driverName)); + + // Needed when quarkus.datasource.jdbc.transactions=xa for the setting of the username and password + reflectiveClass.produce(new ReflectiveClassBuildItem(false, true, false, "org.postgresql.ds.common.BaseDataSource")); } } diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java index 5c8c9d4530585..61d7605485442 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java @@ -30,10 +30,11 @@ public Supplier createAdminClient() { final KeycloakAdminClientConfig config = keycloakAdminClientConfigRuntimeValue.getValue(); validate(config); if (config.serverUrl.isEmpty()) { - return new Supplier() { + return new Supplier<>() { @Override public Keycloak get() { - return null; + throw new IllegalStateException( + "'quarkus.keycloak.admin-client.server-url' must be set in order to use the Keycloak admin client as a CDI bean"); } }; } diff --git a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java index 5ffe9c6f0df6d..ece1316308464 100644 --- a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java @@ -31,10 +31,11 @@ public Supplier createAdminClient() { final KeycloakAdminClientConfig config = keycloakAdminClientConfigRuntimeValue.getValue(); validate(config); if (config.serverUrl.isEmpty()) { - return new Supplier() { + return new Supplier<>() { @Override public Keycloak get() { - return null; + throw new IllegalStateException( + "'quarkus.keycloak.admin-client.server-url' must be set in order to use the Keycloak admin client as a CDI bean"); } }; } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNamespaceDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNamespaceDecorator.java index 0733592f0b624..3e04125891e8e 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNamespaceDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/AddNamespaceDecorator.java @@ -19,7 +19,9 @@ public AddNamespaceDecorator(String namespace) { @Override public void andThenVisit(ObjectMetaBuilder builder, ObjectMeta resourceMeta) { - builder.withNamespace(namespace); + if (!builder.hasNamespace()) { + builder.withNamespace(namespace); + } } @Override diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java index 193643df5c3ad..b368a2e42700a 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/Constants.java @@ -41,7 +41,7 @@ public final class Constants { static final String QUARKUS = "quarkus"; static final String QUARKUS_ANNOTATIONS_COMMIT_ID = "app.quarkus.io/commit-id"; - static final String QUARKUS_ANNOTATIONS_VCS_URL = "app.quarkus.io/vcs-url"; + static final String QUARKUS_ANNOTATIONS_VCS_URL = "app.quarkus.io/vcs-uri"; static final String QUARKUS_ANNOTATIONS_BUILD_TIMESTAMP = "app.quarkus.io/build-timestamp"; public static final String HTTP_PORT = "http"; diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java index 21ce61640fe93..05018bb854db1 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KnativeProcessor.java @@ -36,6 +36,7 @@ import io.dekorate.kubernetes.decorator.AddLabelDecorator; import io.dekorate.kubernetes.decorator.AddServiceAccountResourceDecorator; import io.dekorate.kubernetes.decorator.ApplicationContainerDecorator; +import io.dekorate.kubernetes.decorator.ApplyImagePullPolicyDecorator; import io.dekorate.project.Project; import io.quarkus.container.spi.BaseImageInfoBuildItem; import io.quarkus.container.spi.ContainerImageInfoBuildItem; @@ -156,6 +157,7 @@ public List createDecorators(ApplicationInfoBuildItem applic image.ifPresent(i -> { result.add(new DecoratorBuildItem(KNATIVE, new ApplyContainerImageDecorator(name, i.getImage()))); }); + result.add(new DecoratorBuildItem(KNATIVE, new ApplyImagePullPolicyDecorator(name, config.getImagePullPolicy()))); config.getContainerName().ifPresent(containerName -> result .add(new DecoratorBuildItem(KNATIVE, new ChangeContainerNameDecorator(containerName)))); diff --git a/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyOverrideMetadata.java b/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyOverrideMetadata.java new file mode 100644 index 0000000000000..f052a335696cc --- /dev/null +++ b/extensions/netty/deployment/src/main/java/io/quarkus/netty/deployment/NettyOverrideMetadata.java @@ -0,0 +1,21 @@ +package io.quarkus.netty.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.ExcludeConfigBuildItem; + +public class NettyOverrideMetadata { + + static final String NETTY_CODEC_JAR_MATCH_REGEX = "io\\.netty\\.netty-codec"; + static final String NETTY_CODEC_REFLECT_CONFIG_MATCH_REGEX = "/META-INF/native-image/io\\.netty/netty-codec/generated/handlers/reflect-config\\.json"; + static final String NETTY_HANDLER_JAR_MATCH_REGEX = "io\\.netty\\.netty-handler"; + static final String NETTY_HANDLER_REFLECT_CONFIG_MATCH_REGEX = "/META-INF/native-image/io\\.netty/netty-handler/generated/handlers/reflect-config\\.json"; + + @BuildStep + void excludeNettyDirectives(BuildProducer nativeImageExclusions) { + nativeImageExclusions + .produce(new ExcludeConfigBuildItem(NETTY_CODEC_JAR_MATCH_REGEX, NETTY_CODEC_REFLECT_CONFIG_MATCH_REGEX)); + nativeImageExclusions + .produce(new ExcludeConfigBuildItem(NETTY_HANDLER_JAR_MATCH_REGEX, NETTY_HANDLER_REFLECT_CONFIG_MATCH_REGEX)); + } +} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index f3fcf81a68dd6..433dbe70c1b88 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -14,7 +14,7 @@ public class OidcCommonConfig { * The base URL of the OpenID Connect (OIDC) server, for example, `https://host:port/auth`. * OIDC discovery endpoint will be called by default by appending a '.well-known/openid-configuration' path to this URL. * Note if you work with Keycloak OIDC server, make sure the base URL is in the following format: - * `https://host:port/auth/realms/{realm}` where `{realm}` has to be replaced by the name of the Keycloak realm. + * `https://host:port/realms/{realm}` where `{realm}` has to be replaced by the name of the Keycloak realm. */ @ConfigItem public Optional authServerUrl = Optional.empty(); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index c7c1674c8f21d..a9e4afbb5bbb5 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -266,7 +266,7 @@ private Map prepareConfiguration( String clientAuthServerBaseUrl = hostURL != null ? hostURL : internalURL; String clientAuthServerUrl = realmsURL(clientAuthServerBaseUrl, realmName); - boolean createDefaultRealm = realmReps.isEmpty() && capturedDevServicesConfiguration.createRealm; + boolean createDefaultRealm = (realmReps == null || realmReps.isEmpty()) && capturedDevServicesConfiguration.createRealm; String oidcClientId = getOidcClientId(createDefaultRealm); String oidcClientSecret = getOidcClientSecret(createDefaultRealm); @@ -283,9 +283,11 @@ private Map prepareConfiguration( createDefaultRealm(client, adminToken, clientAuthServerBaseUrl, users, oidcClientId, oidcClientSecret, errors); realmNames.add(realmName); } else { - for (RealmRepresentation realmRep : realmReps) { - createRealm(client, adminToken, clientAuthServerBaseUrl, realmRep, errors); - realmNames.add(realmRep.getRealm()); + if (realmReps != null) { + for (RealmRepresentation realmRep : realmReps) { + createRealm(client, adminToken, clientAuthServerBaseUrl, realmRep, errors); + realmNames.add(realmRep.getRealm()); + } } } } finally { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index cb5d345f75f97..c0cc270afa591 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -1098,7 +1098,7 @@ private Uni buildLogoutRedirectUriUni(RoutingContext context, TenantConfig @Override public Void apply(Void t) { String logoutUri = buildLogoutRedirectUri(configContext, idToken, context); - LOG.debugf("Logout uri: %s"); + LOG.debugf("Logout uri: %s", logoutUri); throw new AuthenticationRedirectException(logoutUri); } }); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java index 014d4513ce0b4..4e1e2190307a0 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java @@ -50,8 +50,8 @@ private static SecretKey createPkceSecretKey(OidcTenantConfig config) { if (pkceSecret == null) { throw new RuntimeException("Secret key for encrypting PKCE code verifier is missing"); } - if (pkceSecret.length() < 32) { - throw new RuntimeException("Secret key for encrypting PKCE code verifier must be at least 32 characters long"); + if (pkceSecret.length() != 32) { + throw new RuntimeException("Secret key for encrypting PKCE code verifier must be 32 characters long"); } return KeyUtils.createSecretKeyFromSecret(pkceSecret); } @@ -65,8 +65,8 @@ private static SecretKey createTokenEncSecretKey(OidcTenantConfig config) { if (encSecret == null) { throw new RuntimeException("Secret key for encrypting tokens is missing"); } - if (encSecret.length() < 32) { - throw new RuntimeException("Secret key for encrypting tokens must be at least 32 characters long"); + if (encSecret.length() != 32) { + throw new RuntimeException("Secret key for encrypting tokens must be 32 characters long"); } return KeyUtils.createSecretKeyFromSecret(encSecret); } diff --git a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java index 70bfb1a803373..87ddafc401046 100644 --- a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java +++ b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java @@ -54,6 +54,8 @@ public void close() { private Map> filters; + private final String lineSeparator = System.getProperty("line.separator"); + public CommonPanacheQueryImpl(EntityManager em, String query, String orderBy, Object paramsArrayOrMap) { this.em = em; this.query = query; @@ -82,7 +84,7 @@ public CommonPanacheQueryImpl project(Class type) { throw new PanacheQueryException("Unable to perform a projection on a named query"); } - String lowerCasedTrimmedQuery = query.trim().toLowerCase(); + String lowerCasedTrimmedQuery = query.trim().replace(lineSeparator, " ").toLowerCase(); if (lowerCasedTrimmedQuery.startsWith("select new ")) { throw new PanacheQueryException("Unable to perform a projection on a 'select new' query: " + query); } @@ -93,7 +95,7 @@ public CommonPanacheQueryImpl project(Class type) { // New query: SELECT new org.acme.ProjectionClass(e.field1, e.field2) from EntityClass e if (lowerCasedTrimmedQuery.startsWith("select ")) { int endSelect = lowerCasedTrimmedQuery.indexOf(" from "); - String trimmedQuery = query.trim(); + String trimmedQuery = query.trim().replace(lineSeparator, " "); // 7 is the length of "select " String selectClause = trimmedQuery.substring(7, endSelect).trim(); String from = trimmedQuery.substring(endSelect); diff --git a/extensions/reactive-mysql-client/deployment/pom.xml b/extensions/reactive-mysql-client/deployment/pom.xml index 3ea61350604b3..19c3517b65487 100644 --- a/extensions/reactive-mysql-client/deployment/pom.xml +++ b/extensions/reactive-mysql-client/deployment/pom.xml @@ -60,6 +60,10 @@ io.quarkus quarkus-devservices-mysql + + io.quarkus + quarkus-devservices-mariadb + io.quarkus 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 b763f424b52d3..2354842e7865c 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 @@ -84,8 +84,9 @@ ServiceStartBuildItem build(BuildProducer feature, } @BuildStep - DevServicesDatasourceConfigurationHandlerBuildItem devDbHandler() { - return DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.MYSQL); + List devDbHandler() { + return List.of(DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.MYSQL), + DevServicesDatasourceConfigurationHandlerBuildItem.reactive(DatabaseKind.MARIADB)); } @BuildStep diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java index 77ca63a9de075..d10e1f670b8fd 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java @@ -25,7 +25,7 @@ public class RedisClientConfig { * * @see Redis scheme on www.iana.org */ - @ConfigItem(defaultValueDocumentation = "redis://localhost:6379", name = RedisConfig.HOSTS_CONFIG_NAME) + @ConfigItem(name = RedisConfig.HOSTS_CONFIG_NAME) public Optional> hosts; /** diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java index 9550a98db6542..04367f7dfb73a 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/datasource/AbstractGeoCommands.java @@ -12,6 +12,7 @@ import java.util.OptionalDouble; import java.util.OptionalLong; import java.util.Set; +import java.util.regex.Pattern; import io.quarkus.redis.datasource.codecs.Codec; import io.quarkus.redis.datasource.codecs.Codecs; @@ -34,6 +35,8 @@ class AbstractGeoCommands extends AbstractRedisCommands { protected final Codec keyCodec; protected final Codec valueCodec; + private static final Pattern NOISE_REMOVER_PATTERN = Pattern.compile("[^a-zA-Z0-9\\.]"); + AbstractGeoCommands(RedisCommandExecutor redis, Class k, Class v) { super(redis, new Marshaller(k, v)); this.typeOfValue = v; @@ -233,7 +236,7 @@ Double decodeDistance(Response r) { if (r == null) { return null; } - return r.toDouble(); + return parseDouble(r); } List decodeGeoPositions(Response response) { @@ -241,7 +244,7 @@ List decodeGeoPositions(Response response) { if (nested == null) { return null; } else { - return GeoPosition.of(nested.get(0).toDouble(), nested.get(1).toDouble()); + return GeoPosition.of(parseDouble(nested.get(0)), parseDouble(nested.get(1))); } }); } @@ -261,33 +264,33 @@ List> decodeAsListOfGeoValues(Response r, boolean withDistance, bool V member = marshaller.decode(typeOfValue, response.get(0)); if (withCoordinates && withDistance && withHash) { - double dist = response.get(1).toDouble(); + double dist = parseDouble(response.get(1)); long hash = response.get(2).toLong(); - double longitude = response.get(3).get(0).toDouble(); - double latitude = response.get(3).get(1).toDouble(); + double longitude = parseDouble(response.get(3).get(0)); + double latitude = parseDouble(response.get(3).get(1)); list.add(new GeoValue<>(member, OptionalDouble.of(dist), OptionalLong.of(hash), OptionalDouble.of(longitude), OptionalDouble.of(latitude))); } else if (withCoordinates && withDistance) { - double dist = response.get(1).toDouble(); - double longitude = response.get(2).get(0).toDouble(); - double latitude = response.get(2).get(1).toDouble(); + double dist = parseDouble(response.get(1)); + double longitude = parseDouble(response.get(2).get(0)); + double latitude = parseDouble(response.get(2).get(1)); list.add(new GeoValue<>(member, OptionalDouble.of(dist), OptionalLong.empty(), OptionalDouble.of(longitude), OptionalDouble.of(latitude))); } else if (withCoordinates && withHash) { long hash = response.get(1).toLong(); - double longitude = response.get(2).get(0).toDouble(); - double latitude = response.get(2).get(1).toDouble(); + double longitude = parseDouble(response.get(2).get(0)); + double latitude = parseDouble(response.get(2).get(1)); list.add(new GeoValue<>(member, OptionalDouble.empty(), OptionalLong.of(hash), OptionalDouble.of(longitude), OptionalDouble.of(latitude))); } else if (withCoordinates) { // Only coordinates - double longitude = response.get(1).get(0).toDouble(); - double latitude = response.get(1).get(1).toDouble(); + double longitude = parseDouble(response.get(1).get(0)); + double latitude = parseDouble(response.get(1).get(1)); list.add(new GeoValue<>(member, OptionalDouble.empty(), OptionalLong.empty(), OptionalDouble.of(longitude), OptionalDouble.of(latitude))); } else if (withDistance && !withHash) { // Only distance - double dist = response.get(1).toDouble(); + double dist = parseDouble(response.get(1)); list.add(new GeoValue<>(member, OptionalDouble.of(dist), OptionalLong.empty(), OptionalDouble.empty(), OptionalDouble.empty())); } else if (!withDistance) { @@ -297,7 +300,7 @@ List> decodeAsListOfGeoValues(Response r, boolean withDistance, bool OptionalDouble.empty())); } else { // Distance and Hash - double dist = response.get(1).toDouble(); + double dist = parseDouble(response.get(1)); long hash = response.get(2).toLong(); list.add(new GeoValue<>(member, OptionalDouble.of(dist), OptionalLong.of(hash), OptionalDouble.empty(), OptionalDouble.empty())); @@ -305,4 +308,15 @@ List> decodeAsListOfGeoValues(Response r, boolean withDistance, bool } return list; } + + private static double parseDouble(Response response) { + double dist; + try { + dist = response.toDouble(); + } catch (NumberFormatException e) { + String s = NOISE_REMOVER_PATTERN.matcher(response.toString()).replaceAll(""); + dist = Double.parseDouble(s); + } + return dist; + } } diff --git a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java index 1d51c132fb2b1..753b343714968 100644 --- a/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java +++ b/extensions/redis-client/runtime/src/test/java/io/quarkus/redis/datasource/SearchCommandsTest.java @@ -455,7 +455,7 @@ void testAggregation() { .groupBy(new AggregateArgs.GroupBy().addProperty("@day").addProperty("@country").addReduceFunction("count", "num_visits")) .sortBy(new AggregateArgs.SortBy().ascending("@day").descending("@country"))); - assertThat(result.count()).isEqualTo(3); + assertThat(result.count()).isGreaterThanOrEqualTo(3); assertThat(result.documents()).allSatisfy(d -> { assertThat(d.property("day").asInteger()).isPositive(); assertThat(d.property("country").asString()).isNotNull(); diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java index 7a8a444f88f68..a78f85e87933d 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java @@ -91,10 +91,11 @@ public void boot(ShutdownContextBuildItem shutdown, executorBuildItem.getExecutorProxy(), resteasyVertxConfig); // failure handler for auth failures that occurred before the handler defined right above started processing the request + // we add the failure handler right before QuarkusErrorHandler + // so that user can define failure handlers that precede exception mappers final Handler failureHandler = recorder.vertxFailureHandler(vertx.getVertx(), executorBuildItem.getExecutorProxy(), resteasyVertxConfig); - filterBuildItemBuildProducer.produce(new FilterBuildItem(failureHandler, - VertxHttpRecorder.AFTER_DEFAULT_ROUTE_ORDER_MARK + REST_ROUTE_ORDER_OFFSET, true)); + filterBuildItemBuildProducer.produce(FilterBuildItem.ofAuthenticationFailureHandler(failureHandler)); // Exact match for resources matched to the root path routes.produce( diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java index ac93652557433..44dbffff92614 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java @@ -61,9 +61,6 @@ public void testAuthCompletionExMapper() { .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); } - /** - * Use failure handler as when proactive security is enabled, JAX-RS exception mappers won't do. - */ @ApplicationScoped public static final class CustomAuthCompletionExceptionHandler { diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionMapperTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionMapperTest.java new file mode 100644 index 0000000000000..f5372c7166fb1 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionMapperTest.java @@ -0,0 +1,86 @@ +package io.quarkus.resteasy.test.security; + +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; + +import java.util.function.Supplier; + +import javax.annotation.Priority; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +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.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class ProactiveAuthCompletionExceptionMapperTest { + + private static final String AUTHENTICATION_COMPLETION_EX = "AuthenticationCompletionException"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class) + .addAsResource(new StringAsset("quarkus.http.auth.form.enabled=true\n"), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testAuthCompletionExMapper() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + RestAssured + .given() + .filter(new CookieFilter()) + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "a d m i n") + .cookie("quarkus-redirect-location", "https://quarkus.io/guides") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(401) + .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); + } + + @Path("/hello") + public static class HelloResource { + + @GET + public String hello() { + return "Hello"; + } + + } + + @Priority(Priorities.USER) + @Provider + public static class CustomAuthCompletionExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(AuthenticationCompletionException e) { + return Response.status(UNAUTHORIZED).entity(AUTHENTICATION_COMPLETION_EX).build(); + } + } +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.java new file mode 100644 index 0000000000000..34f90cd676b0e --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.java @@ -0,0 +1,92 @@ +package io.quarkus.resteasy.test.security; + +import static io.quarkus.resteasy.test.security.ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.CustomForbiddenFailureHandler.CUSTOM_FORBIDDEN_EXCEPTION_HANDLER; +import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static org.hamcrest.Matchers.equalTo; + +import java.util.function.Supplier; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +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.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest { + + private static final String PROPERTIES = "quarkus.http.auth.basic=true\n" + + "quarkus.http.auth.policy.user-policy.roles-allowed=user\n" + + "quarkus.http.auth.permission.roles.paths=/secured\n" + + "quarkus.http.auth.permission.roles.policy=user-policy"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class) + .addAsResource(new StringAsset(PROPERTIES), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testDeniedAccessAdminResource() { + RestAssured.given() + .auth().basic("a d m i n", "a d m i n") + .when().get("/secured") + .then() + .statusCode(403) + .body(equalTo(CUSTOM_FORBIDDEN_EXCEPTION_HANDLER)); + } + + @Path("/secured") + public static class SecuredResource { + + @GET + public String get() { + throw new IllegalStateException(); + } + + } + + @ApplicationScoped + public static final class CustomForbiddenFailureHandler { + + public static final String CUSTOM_FORBIDDEN_EXCEPTION_HANDLER = CustomForbiddenFailureHandler.class.getName(); + + public void init(@Observes Router router) { + router.route().failureHandler(new Handler() { + @Override + public void handle(RoutingContext event) { + if (event.failure() instanceof ForbiddenException) { + event.response().setStatusCode(FORBIDDEN.getStatusCode()).end(CUSTOM_FORBIDDEN_EXCEPTION_HANDLER); + } else { + event.next(); + } + } + }); + } + + } + +} diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 39cfc80d38003..86edc4adff49d 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -1161,8 +1161,31 @@ private void handleSubResourceMethod(List ownerContext.constructor.writeInstanceField(forMethodTargetDesc, ownerContext.constructor.getThis(), constructorTarget); - ResultHandle subInstance = ownerMethod.newInstance(subConstructorDescriptor, - ownerMethod.readInstanceField(forMethodTargetDesc, ownerMethod.getThis())); + Supplier methodParamAnnotationsField = ownerContext.getLazyJavaMethodParamAnnotationsField( + methodIndex); + Supplier methodGenericParametersField = ownerContext.getLazyJavaMethodGenericParametersField( + methodIndex); + + AssignableResultHandle client = createRestClientField(name, ownerContext.classCreator, ownerMethod); + AssignableResultHandle webTarget = ownerMethod.createVariable(WebTarget.class); + ownerMethod.assign(webTarget, ownerMethod.readInstanceField(forMethodTargetDesc, ownerMethod.getThis())); + // Setup Path param from current method + for (int i = 0; i < method.getParameters().length; i++) { + MethodParameter param = method.getParameters()[i]; + if (param.parameterType == ParameterType.PATH) { + ResultHandle paramValue = ownerMethod.getMethodParam(i); + // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); + addPathParam(ownerMethod, webTarget, param.name, paramValue, + param.type, + client, + ownerMethod.readStaticField(methodGenericParametersField.get()), + ownerMethod.readStaticField(methodParamAnnotationsField.get()), + i); + } + } + + // Continue creating the subresource instance with the web target updated + ResultHandle subInstance = ownerMethod.newInstance(subConstructorDescriptor, webTarget); List subParamFields = new ArrayList<>(); @@ -1179,23 +1202,24 @@ private void handleSubResourceMethod(List ownerParameter.paramIndex)); } - FieldDescriptor clientField = createRestClientField(name, ownerContext.classCreator, ownerMethod, - subContext.classCreator, subInstance); - - Supplier methodParamAnnotationsField = ownerContext.getLazyJavaMethodParamAnnotationsField( - methodIndex); - Supplier methodGenericParametersField = ownerContext.getLazyJavaMethodGenericParametersField( - methodIndex); - // method parameters are rewritten to sub client fields (directly, public fields): + FieldDescriptor clientField = subContext.classCreator.getFieldCreator("client", RestClientBase.class) + .setModifiers(Modifier.PUBLIC) + .getFieldDescriptor(); + ownerMethod.writeInstanceField(clientField, subInstance, client); + // method parameters (except path parameters) are rewritten to sub client fields (directly, public fields): for (int i = 0; i < method.getParameters().length; i++) { - FieldDescriptor paramField = subContext.classCreator.getFieldCreator("param" + i, - method.getParameters()[i].type) - .setModifiers(Modifier.PUBLIC) - .getFieldDescriptor(); - ownerMethod.writeInstanceField(paramField, subInstance, ownerMethod.getMethodParam(i)); - subParamFields.add(new SubResourceParameter(method.getParameters()[i], method.getParameters()[i].type, - jandexMethod.parameterType(i), paramField, methodParamAnnotationsField, methodGenericParametersField, - i)); + MethodParameter param = method.getParameters()[i]; + if (param.parameterType != ParameterType.PATH) { + FieldDescriptor paramField = subContext.classCreator.getFieldCreator("param" + i, param.type) + .setModifiers(Modifier.PUBLIC) + .getFieldDescriptor(); + ownerMethod.writeInstanceField(paramField, subInstance, ownerMethod.getMethodParam(i)); + subParamFields.add(new SubResourceParameter(method.getParameters()[i], param.type, + jandexMethod.parameterType(i), paramField, methodParamAnnotationsField, + methodGenericParametersField, + i)); + } + } int subMethodIndex = 0; @@ -1555,22 +1579,19 @@ private void appendPath(MethodCreator constructor, String pathPart, AssignableRe * Create the `client` field into the `c` class that represents a RestClientBase instance. * The RestClientBase instance is coming from either a root client or a sub client (clients generated from root clients). */ - private FieldDescriptor createRestClientField(String name, ClassCreator c, MethodCreator methodCreator, ClassCreator sub, - ResultHandle subInstance) { - FieldDescriptor clientField = sub.getFieldCreator("client", RestClientBase.class) - .setModifiers(Modifier.PUBLIC) - .getFieldDescriptor(); + private AssignableResultHandle createRestClientField(String name, ClassCreator c, MethodCreator methodCreator) { + AssignableResultHandle client = methodCreator.createVariable(RestClientBase.class); if (c.getSuperClass().contains(RestClientBase.class.getSimpleName())) { // We're in a root client, so we can set the client field with: sub.client = (RestClientBase) this - methodCreator.writeInstanceField(clientField, subInstance, methodCreator.getThis()); + methodCreator.assign(client, methodCreator.getThis()); } else { FieldDescriptor subClientField = FieldDescriptor.of(name, "client", RestClientBase.class); - // We're in a sub sub resource, so we need to get the client from the field: subSub.client = sub.client - methodCreator.writeInstanceField(clientField, subInstance, - methodCreator.readInstanceField(subClientField, methodCreator.getThis())); + // We're in a sub-sub resource, so we need to get the client from the field: subSub.client = sub.client + methodCreator.assign(client, methodCreator.readInstanceField(subClientField, methodCreator.getThis())); } - return clientField; + + return client; } private void handleMultipartField(String formParamName, String partType, String partFilename, diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java index 0c3c5b8ad89e5..fa92296e337b1 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java @@ -187,7 +187,7 @@ protected void registerInterceptors( if (filterItem instanceof ContainerRequestFilterBuildItem) { ContainerRequestFilterBuildItem crfbi = (ContainerRequestFilterBuildItem) filterItem; interceptor.setNonBlockingRequired(crfbi.isNonBlockingRequired()); - interceptor.setReadBody(crfbi.isReadBody()); + interceptor.setWithFormRead(crfbi.isWithFormRead()); MethodInfo filterSourceMethod = crfbi.getFilterSourceMethod(); if (filterSourceMethod != null) { interceptor.metadata = Map.of(FILTER_SOURCE_METHOD_METADATA_KEY, filterSourceMethod); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ContainerRequestFilterBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ContainerRequestFilterBuildItem.java index 6ae524f8145e2..7f7a1faed4c92 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ContainerRequestFilterBuildItem.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ContainerRequestFilterBuildItem.java @@ -6,7 +6,7 @@ public final class ContainerRequestFilterBuildItem extends AbstractInterceptorBu private final boolean preMatching; private final boolean nonBlockingRequired; - private final boolean readBody; + private final boolean withFormRead; private final MethodInfo filterSourceMethod; @@ -14,7 +14,7 @@ protected ContainerRequestFilterBuildItem(Builder builder) { super(builder); this.preMatching = builder.preMatching; this.nonBlockingRequired = builder.nonBlockingRequired; - this.readBody = builder.readBody; + this.withFormRead = builder.withFormRead; this.filterSourceMethod = builder.filterSourceMethod; } @@ -22,7 +22,7 @@ public ContainerRequestFilterBuildItem(String className) { super(className); this.preMatching = false; this.nonBlockingRequired = false; - this.readBody = false; + this.withFormRead = false; this.filterSourceMethod = null; } @@ -34,8 +34,8 @@ public boolean isNonBlockingRequired() { return nonBlockingRequired; } - public boolean isReadBody() { - return readBody; + public boolean isWithFormRead() { + return withFormRead; } public MethodInfo getFilterSourceMethod() { @@ -45,7 +45,7 @@ public MethodInfo getFilterSourceMethod() { public static final class Builder extends AbstractInterceptorBuildItem.Builder { boolean preMatching = false; boolean nonBlockingRequired = false; - boolean readBody = false; + boolean withFormRead = false; MethodInfo filterSourceMethod = null; @@ -63,8 +63,8 @@ public Builder setNonBlockingRequired(boolean nonBlockingRequired) { return this; } - public Builder setReadBody(boolean readBody) { - this.readBody = readBody; + public Builder setWithFormRead(boolean withFormRead) { + this.withFormRead = withFormRead; return this; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 407faa9ee5491..92dfa45304f3e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -1204,7 +1204,10 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, RuntimeValue restInitialHandler = recorder.restInitialHandler(deployment); Handler handler = recorder.handler(restInitialHandler); Handler failureHandler = recorder.failureHandler(restInitialHandler); - filterBuildItemBuildProducer.produce(new FilterBuildItem(failureHandler, order, true)); + + // we add failure handler right before QuarkusErrorHandler + // so that user can define failure handlers that precede exception mappers + filterBuildItemBuildProducer.produce(FilterBuildItem.ofAuthenticationFailureHandler(failureHandler)); // Exact match for resources matched to the root path routes.produce(RouteBuildItem.builder() diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java index c535061abd937..42d5b78e2d8b9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java @@ -370,7 +370,7 @@ public void handleCustomAnnotatedMethods( .setPriority(generated.getPriority()) .setPreMatching(generated.isPreMatching()) .setNonBlockingRequired(generated.isNonBlocking()) - .setReadBody(generated.isReadBody()) + .setWithFormRead(generated.isWithFormRead()) .setFilterSourceMethod(generated.getFilterSourceMethod()); if (!generated.getNameBindingNames().isEmpty()) { builder.setNameBindingNames(generated.getNameBindingNames()); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ImpliedReadBodyRequestFilterTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ImpliedReadBodyRequestFilterTest.java new file mode 100644 index 0000000000000..605cb4677e18b --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ImpliedReadBodyRequestFilterTest.java @@ -0,0 +1,119 @@ +package io.quarkus.resteasy.reactive.server.test.customproviders; + +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.HttpHeaders; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.WithFormRead; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ImpliedReadBodyRequestFilterTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloResource.class); + } + }); + + @Test + public void testMethodWithBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello") + .then().body(Matchers.equalTo("hello Quarkus!!!!!!!")); + } + + @Test + public void testMethodWithUndeclaredBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/empty") + .then().body(Matchers.equalTo("hello !!!!!!!")); + } + + @Test + public void testMethodWithStringBody() { + // make sure that a form-reading filter doesn't prevent non-form request bodies from being deserialised + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/string") + .then().body(Matchers.equalTo("hello name=Quarkus!!!!!!!")); + RestAssured.with() + .body("Quarkus") + .post("/hello/string") + .then().body(Matchers.equalTo("hello Quarkus?")); + } + + @Test + public void testMethodWithoutBody() { + RestAssured.with() + .queryParam("name", "Quarkus") + .get("/hello") + .then().body(Matchers.equalTo("hello Quarkus!")); + } + + @Path("hello") + public static class HelloResource { + + @POST + public String helloPost(@RestForm String name, HttpHeaders headers) { + return "hello " + name + headers.getHeaderString("suffix"); + } + + @Path("empty") + @POST + public String helloEmptyPost(HttpHeaders headers) { + return "hello " + headers.getHeaderString("suffix"); + } + + @Path("string") + @POST + public String helloStringPost(String body, HttpHeaders headers) { + return "hello " + body + headers.getHeaderString("suffix"); + } + + @GET + public String helloGet(@RestQuery String name, HttpHeaders headers) { + return "hello " + name + headers.getHeaderString("suffix"); + } + } + + public static class Filters { + + @WithFormRead + @ServerRequestFilter + public void addSuffix(ResteasyReactiveContainerRequestContext containerRequestContext) { + ResteasyReactiveRequestContext rrContext = (ResteasyReactiveRequestContext) containerRequestContext + .getServerRequestContext(); + if (containerRequestContext.getMethod().equals("POST")) { + String nameFormParam = (String) rrContext.getFormParameter("name", true, false); + if (nameFormParam != null) { + containerRequestContext.getHeaders().putSingle("suffix", "!".repeat(nameFormParam.length())); + } else { + containerRequestContext.getHeaders().putSingle("suffix", "?"); + } + } else { + containerRequestContext.getHeaders().putSingle("suffix", "!"); + } + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ReadBodyRequestFilterTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ReadBodyRequestFilterTest.java index 86aca31051c54..952260f155988 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ReadBodyRequestFilterTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/ReadBodyRequestFilterTest.java @@ -11,6 +11,7 @@ import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestQuery; import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.WithFormRead; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; import org.jboss.shrinkwrap.api.ShrinkWrap; @@ -41,6 +42,27 @@ public void testMethodWithBody() { .then().body(Matchers.equalTo("hello Quarkus!!!!!!!")); } + @Test + public void testMethodWithUndeclaredBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/empty") + .then().body(Matchers.equalTo("hello !!!!!!!")); + } + + @Test + public void testMethodWithStringBody() { + // make sure that a form-reading filter doesn't prevent non-form request bodies from being deserialised + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/string") + .then().body(Matchers.equalTo("hello name=Quarkus!!!!!!!")); + RestAssured.with() + .body("Quarkus") + .post("/hello/string") + .then().body(Matchers.equalTo("hello Quarkus?")); + } + @Test public void testMethodWithoutBody() { RestAssured.with() @@ -57,6 +79,18 @@ public String helloPost(@RestForm String name, HttpHeaders headers) { return "hello " + name + headers.getHeaderString("suffix"); } + @Path("empty") + @POST + public String helloEmptyPost(HttpHeaders headers) { + return "hello " + headers.getHeaderString("suffix"); + } + + @Path("string") + @POST + public String helloStringPost(String body, HttpHeaders headers) { + return "hello " + body + headers.getHeaderString("suffix"); + } + @GET public String helloGet(@RestQuery String name, HttpHeaders headers) { return "hello " + name + headers.getHeaderString("suffix"); @@ -65,13 +99,18 @@ public String helloGet(@RestQuery String name, HttpHeaders headers) { public static class Filters { + @WithFormRead @ServerRequestFilter(readBody = true) public void addSuffix(ResteasyReactiveContainerRequestContext containerRequestContext) { ResteasyReactiveRequestContext rrContext = (ResteasyReactiveRequestContext) containerRequestContext .getServerRequestContext(); if (containerRequestContext.getMethod().equals("POST")) { String nameFormParam = (String) rrContext.getFormParameter("name", true, false); - containerRequestContext.getHeaders().putSingle("suffix", "!".repeat(nameFormParam.length())); + if (nameFormParam != null) { + containerRequestContext.getHeaders().putSingle("suffix", "!".repeat(nameFormParam.length())); + } else { + containerRequestContext.getHeaders().putSingle("suffix", "?"); + } } else { containerRequestContext.getHeaders().putSingle("suffix", "!"); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/WithFormBodyTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/WithFormBodyTest.java new file mode 100644 index 0000000000000..bc89943ad593a --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/WithFormBodyTest.java @@ -0,0 +1,64 @@ +package io.quarkus.resteasy.reactive.server.test.customproviders; + +import java.util.function.Supplier; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.server.WithFormRead; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class WithFormBodyTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloResource.class); + } + }); + + @Test + public void testMethodWithBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello") + .then().body(Matchers.equalTo("hello Quarkus")); + } + + @Test + public void testMethodWithUndeclaredBody() { + RestAssured.with() + .formParam("name", "Quarkus") + .post("/hello/empty") + .then().body(Matchers.equalTo("hello Quarkus")); + } + + @Path("hello") + public static class HelloResource { + + @POST + public String helloPost(@RestForm String name) { + return "hello " + name; + } + + @WithFormRead + @Path("empty") + @POST + public String helloEmptyPost(ServerRequestContext requestContext) { + return "hello " + ((ResteasyReactiveRequestContext) requestContext).getFormParameter("name", true, false); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartFormInputDevModeTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartFormInputDevModeTest.java index 83a02058088e2..6b33f16667d46 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartFormInputDevModeTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartFormInputDevModeTest.java @@ -74,7 +74,7 @@ private void doTest(String path) { .post("/multipart/" + path + "/2") .then() .statusCode(200) - .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + .body(equalTo("Alice - true - 50 - WORKING - true - true - true")); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputBodyHandlerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputBodyHandlerTest.java index a3e7cf8ed1ef4..f75e0a416573d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputBodyHandlerTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputBodyHandlerTest.java @@ -92,7 +92,7 @@ public void testSimple() { .post("/multipart/simple/2") .then() .statusCode(200) - .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + .body(equalTo("Alice - true - 50 - WORKING - true - true - true")); // ensure that the 3 uploaded files where created on disk Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java index a0d69d00d2c64..0316e038c7800 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartInputTest.java @@ -50,6 +50,9 @@ public JavaArchive get() { private final File HTML_FILE2 = new File("./src/test/resources/test2.html"); private final File XML_FILE = new File("./src/test/resources/test.html"); private final File TXT_FILE = new File("./src/test/resources/lorem.txt"); + private final String TXT = "lorem ipsum"; + private final String XML = ""; + private final String HTML = ""; @BeforeEach public void assertEmptyUploads() { @@ -68,15 +71,15 @@ public void testSimple() { .multiPart("active", "true") .multiPart("num", "25") .multiPart("status", "WORKING") - .multiPart("htmlFile", HTML_FILE, "text/html") - .multiPart("xmlFile", XML_FILE, "text/xml") - .multiPart("txtFile", TXT_FILE, "text/plain") + .multiPart("htmlFile", HTML, "text/html") + .multiPart("xmlFile", XML, "text/xml") + .multiPart("txtFile", TXT, "text/plain") .accept("text/plain") .when() .post("/multipart/simple/2") .then() .statusCode(200) - .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + .body(equalTo("Alice - true - 50 - WORKING - true - true - true")); // ensure that the 3 uploaded files where created on disk Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); @@ -110,15 +113,15 @@ public void testSimpleParam() { .multiPart("active", "true") .multiPart("num", "25") .multiPart("status", "WORKING") - .multiPart("htmlFile", HTML_FILE, "text/html") - .multiPart("xmlFile", XML_FILE, "text/xml") - .multiPart("txtFile", TXT_FILE, "text/plain") + .multiPart("htmlFile", HTML, "text/html") + .multiPart("xmlFile", XML, "text/xml") + .multiPart("txtFile", TXT, "text/plain") .accept("text/plain") .when() .post("/multipart/param/simple/2") .then() .statusCode(200) - .body(equalTo("Alice - true - 50 - WORKING - text/html - true - true")); + .body(equalTo("Alice - true - 50 - WORKING - true - true - true")); // ensure that the 3 uploaded files where created on disk Assertions.assertEquals(3, uploadDir.toFile().listFiles().length); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java index 3e8bb225da17f..62881e6efed67 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java @@ -15,6 +15,7 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; public class MultipartOutputUsingBlockingEndpointsTest extends AbstractMultipartTest { @@ -65,18 +66,20 @@ public void testRestResponse() { @Test public void testWithFormData() { - String response = RestAssured.get("/multipart/output/with-form-data") + ExtractableResponse extractable = RestAssured.get("/multipart/output/with-form-data") .then() - .log().all() .contentType(ContentType.MULTIPART) .statusCode(200) - .extract().asString(); + .extract(); - assertContainsValue(response, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); - assertContainsValue(response, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); - assertContainsValue(response, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); - assertContainsValue(response, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); - assertContainsValue(response, "values", MediaType.TEXT_PLAIN, "[one, two]"); + String body = extractable.asString(); + assertContainsValue(body, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); + assertContainsValue(body, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); + assertContainsValue(body, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); + assertContainsValue(body, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); + assertContainsValue(body, "values", MediaType.TEXT_PLAIN, "[one, two]"); + + assertThat(extractable.header("Content-Type")).contains("boundary="); } @Test diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java index 5548a439f98fa..a3958b01ae37a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/MultipartResource.java @@ -37,7 +37,7 @@ public String simple(@BeanParam FormData formData, Integer times) { } return formData.getName() + " - " + formData.active + " - " + times * formData.getNum() + " - " + formData.getStatus() + " - " - + formData.getHtmlPart().contentType() + " - " + Files.exists(formData.xmlPart) + " - " + + Files.exists(formData.getHtmlPart().filePath()) + " - " + Files.exists(formData.xmlPart) + " - " + formData.txtFile.exists(); } @@ -74,7 +74,7 @@ public String simple( } return name + " - " + active + " - " + times * num + " - " + status + " - " - + htmlPart.contentType() + " - " + Files.exists(xmlPart) + " - " + + Files.exists(htmlPart.filePath()) + " - " + Files.exists(xmlPart) + " - " + txtFile.exists(); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java index 946144889fbb4..49adcf5d43756 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java @@ -59,9 +59,6 @@ public void testAuthCompletionExMapper() { .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); } - /** - * Use failure handler as when proactive security is enabled, JAX-RS exception mappers won't do. - */ public static final class CustomAuthCompletionExceptionHandler { @Route(type = Route.HandlerType.FAILURE) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionMapperTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionMapperTest.java new file mode 100644 index 0000000000000..61f250e251ee3 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionMapperTest.java @@ -0,0 +1,72 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; + +import java.util.function.Supplier; + +import javax.ws.rs.core.Response; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +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.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class ProactiveAuthCompletionExceptionMapperTest { + + private static final String AUTHENTICATION_COMPLETION_EX = "AuthenticationCompletionException"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class, + CustomAuthCompletionExceptionMapper.class) + .addAsResource(new StringAsset("quarkus.http.auth.form.enabled=true\n"), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testAuthCompletionExMapper() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + RestAssured + .given() + .filter(new CookieFilter()) + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "a d m i n") + .cookie("quarkus-redirect-location", "https://quarkus.io/guides") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(401) + .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); + } + + public static final class CustomAuthCompletionExceptionMapper { + + @ServerExceptionMapper(value = AuthenticationCompletionException.class) + public Response unauthorized() { + return Response.status(UNAUTHORIZED).entity(AUTHENTICATION_COMPLETION_EX).build(); + } + + } + +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenExMapperTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenExMapperTest.java new file mode 100644 index 0000000000000..e16c8b2ee787f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenExMapperTest.java @@ -0,0 +1,79 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static org.hamcrest.Matchers.equalTo; + +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +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.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ProactiveAuthHttpPolicyForbiddenExMapperTest { + + private static final String PROPERTIES = "quarkus.http.auth.basic=true\n" + + "quarkus.http.auth.policy.user-policy.roles-allowed=user\n" + + "quarkus.http.auth.permission.roles.paths=/secured\n" + + "quarkus.http.auth.permission.roles.policy=user-policy"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class, CustomForbiddenExceptionMapper.class) + .addAsResource(new StringAsset(PROPERTIES), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testDeniedAccessAdminResource() { + RestAssured.given() + .auth().basic("a d m i n", "a d m i n") + .when().get("/secured") + .then() + .statusCode(403) + .body(equalTo(CustomForbiddenExceptionMapper.CUSTOM_FORBIDDEN_EXCEPTION_MAPPER)); + } + + @Path("/secured") + public static class SecuredResource { + + @GET + public String get() { + throw new IllegalStateException(); + } + + } + + public static final class CustomForbiddenExceptionMapper { + + public static final String CUSTOM_FORBIDDEN_EXCEPTION_MAPPER = CustomForbiddenExceptionMapper.class.getName(); + + @ServerExceptionMapper(value = ForbiddenException.class) + public Response forbidden() { + return Response.status(FORBIDDEN).entity(CUSTOM_FORBIDDEN_EXCEPTION_MAPPER).build(); + } + + } + +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java index 81678e9cddb4d..15ce1ca3a4b96 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java @@ -65,9 +65,6 @@ public String get() { } - /** - * Use failure handler as when proactive security is enabled, JAX-RS exception mappers won't do. - */ public static final class CustomForbiddenFailureHandler { public static final String CUSTOM_FORBIDDEN_EXCEPTION_MAPPER = CustomForbiddenFailureHandler.class.getName(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java index b8973cd1edce6..c520568020663 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/SeparatorQueryParamTest.java @@ -4,6 +4,7 @@ import java.util.List; +import javax.ws.rs.BeanParam; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -32,6 +33,15 @@ public void noQueryParams() { .header("x-size", "0"); } + @Test + public void noQueryParamsBean() { + get("/hello/bean") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello world")) + .header("x-size", "0"); + } + @Test public void singleQueryParam() { get("/hello?name=foo") @@ -41,6 +51,15 @@ public void singleQueryParam() { .header("x-size", "1"); } + @Test + public void singleQueryParamBean() { + get("/hello/bean?name=foo") + .then() + .statusCode(200) + .body(Matchers.equalTo("hello foo")) + .header("x-size", "1"); + } + @Test public void multipleQueryParams() { get("/hello?name=foo,bar&name=one,two,three&name=yolo") @@ -55,6 +74,16 @@ public static class HelloResource { @GET public RestResponse hello(@RestQuery("name") @Separator(",") List names) { + return toResponse(names); + } + + @GET + @Path("bean") + public RestResponse helloBean(@BeanParam Bean bean) { + return toResponse(bean.names); + } + + private RestResponse toResponse(List names) { int size = names.size(); String body = ""; if (names.isEmpty()) { @@ -66,4 +95,10 @@ public RestResponse hello(@RestQuery("name") @Separator(",") List names; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index c0a81b7d277bb..6cc91a1899e50 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -54,6 +54,7 @@ import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; +import io.quarkus.security.ForbiddenException; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; @@ -218,16 +219,10 @@ public Handler failureHandler(RuntimeValue r @Override public void handle(RoutingContext event) { - // this condition prevent exception mappers from handling auth failure exceptions when proactive - // security is enabled as for now, community decided that's expected behavior and only way for - // users to handle the exceptions is to define their own failure handler as in Reactive Routes - // more info here: https://github.com/quarkusio/quarkus/pull/28648#issuecomment-1287203946 - final boolean eventFailedByRESTEasyReactive = event - .get(QuarkusHttpUser.AUTH_FAILURE_HANDLER) instanceof FailingDefaultAuthFailureHandler; - - if (eventFailedByRESTEasyReactive && (event.failure() instanceof AuthenticationFailedException + if (event.failure() instanceof AuthenticationFailedException || event.failure() instanceof AuthenticationCompletionException - || event.failure() instanceof AuthenticationRedirectException)) { + || event.failure() instanceof AuthenticationRedirectException + || event.failure() instanceof ForbiddenException) { restInitialHandler.beginProcessing(event, event.failure()); } else { event.next(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java index d071a33698b4c..af2fa0ff05c17 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRuntimeRecorder.java @@ -35,6 +35,7 @@ public Supplier runtimeConfiguration(RuntimeValue[] classes = { JsonValuejectionEndpoint.class, TokenUtils.class, + AuthFailedExceptionMapper.class }; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(classes) + .addAsResource(new StringAsset("quarkus.http.auth.proactive=true\n"), "application.properties")); + + @Test + public void testExMapperCustomizedResponse() { + RestAssured + .given() + .auth().oauth2("absolute-nonsense") + .get("/endp/verifyInjectedIssuer").then() + .statusCode(401) + .body(Matchers.equalTo(CUSTOMIZED_RESPONSE)); + } + + public static class AuthFailedExceptionMapper { + + @ServerExceptionMapper(value = AuthenticationFailedException.class) + public Response unauthorized() { + return Response + .status(401) + .entity(CUSTOMIZED_RESPONSE).build(); + } + + } +} \ No newline at end of file diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java index 6803bd135f0a2..898774bf6d92e 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java @@ -1,9 +1,14 @@ package io.quarkus.smallrye.reactivemessaging.kafka.deployment; +import static io.quarkus.smallrye.reactivemessaging.kafka.HibernateOrmStateStore.HIBERNATE_ORM_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.HibernateReactiveStateStore.HIBERNATE_REACTIVE_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.RedisStateStore.REDIS_STATE_STORE; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Function; @@ -41,12 +46,16 @@ import io.quarkus.smallrye.reactivemessaging.kafka.ReactiveMessagingKafkaConfig; import io.quarkus.smallrye.reactivemessaging.kafka.RedisStateStore; import io.smallrye.mutiny.tuples.Functions.TriConsumer; +import io.smallrye.reactive.messaging.kafka.KafkaConnector; import io.vertx.kafka.client.consumer.impl.KafkaReadStreamImpl; public class SmallRyeReactiveMessagingKafkaProcessor { private static final Logger LOGGER = Logger.getLogger("io.quarkus.smallrye-reactive-messaging-kafka.deployment.processor"); + public static final String CHECKPOINT_STATE_STORE_MESSAGE = "Quarkus detected the use of `%s` for the" + + " Kafka checkpoint commit strategy but the extension has not been added. Consider adding '%s'."; + @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(Feature.SMALLRYE_REACTIVE_MESSAGING_KAFKA); @@ -74,25 +83,61 @@ public void ignoreDuplicateJmxRegistrationInDevAndTestModes(LaunchModeBuildItem } } + static boolean hasStateStoreConfig(String stateStoreName, Config config) { + Optional connectorStrategy = getConnectorProperty("checkpoint.state-store", config); + if (connectorStrategy.isPresent() && connectorStrategy.get().equals(stateStoreName)) { + return true; + } + List stateStores = getChannelProperties("checkpoint.state-store", config); + return stateStores.contains(stateStoreName); + } + + private static Optional getConnectorProperty(String keySuffix, Config config) { + return config.getOptionalValue("mp.messaging.connector." + KafkaConnector.CONNECTOR_NAME + "." + keySuffix, + String.class); + } + + private static List getChannelProperties(String keySuffix, Config config) { + List values = new ArrayList<>(); + for (String propertyName : config.getPropertyNames()) { + if (propertyName.startsWith("mp.messaging.incoming.") && propertyName.endsWith("." + keySuffix)) { + values.add(config.getValue(propertyName, String.class)); + } + } + return values; + } + @BuildStep public void checkpointRedis(BuildProducer additionalBean, Capabilities capabilities) { - if (capabilities.isPresent(Capability.REDIS_CLIENT)) { - additionalBean.produce(new AdditionalBeanBuildItem(RedisStateStore.Factory.class)); - additionalBean.produce(new AdditionalBeanBuildItem(DatabindProcessingStateCodec.Factory.class)); + if (hasStateStoreConfig(REDIS_STATE_STORE, ConfigProvider.getConfig())) { + if (capabilities.isPresent(Capability.REDIS_CLIENT)) { + additionalBean.produce(new AdditionalBeanBuildItem(RedisStateStore.Factory.class)); + additionalBean.produce(new AdditionalBeanBuildItem(DatabindProcessingStateCodec.Factory.class)); + } else { + LOGGER.warnf(CHECKPOINT_STATE_STORE_MESSAGE, REDIS_STATE_STORE, "quarkus-redis-client"); + } } } @BuildStep public void checkpointHibernateReactive(BuildProducer additionalBean, Capabilities capabilities) { - if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) { - additionalBean.produce(new AdditionalBeanBuildItem(HibernateReactiveStateStore.Factory.class)); + if (hasStateStoreConfig(HIBERNATE_REACTIVE_STATE_STORE, ConfigProvider.getConfig())) { + if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) { + additionalBean.produce(new AdditionalBeanBuildItem(HibernateReactiveStateStore.Factory.class)); + } else { + LOGGER.warnf(CHECKPOINT_STATE_STORE_MESSAGE, HIBERNATE_REACTIVE_STATE_STORE, "quarkus-hibernate-reactive"); + } } } @BuildStep public void checkpointHibernateOrm(BuildProducer additionalBean, Capabilities capabilities) { - if (capabilities.isPresent(Capability.HIBERNATE_ORM)) { - additionalBean.produce(new AdditionalBeanBuildItem(HibernateOrmStateStore.Factory.class)); + if (hasStateStoreConfig(HIBERNATE_ORM_STATE_STORE, ConfigProvider.getConfig())) { + if (capabilities.isPresent(Capability.HIBERNATE_ORM)) { + additionalBean.produce(new AdditionalBeanBuildItem(HibernateOrmStateStore.Factory.class)); + } else { + LOGGER.warnf(CHECKPOINT_STATE_STORE_MESSAGE, HIBERNATE_ORM_STATE_STORE, "quarkus-hibernate-orm"); + } } } diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/CheckpointStateStoreConfigTest.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/CheckpointStateStoreConfigTest.java new file mode 100644 index 0000000000000..03167b1e4edcb --- /dev/null +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/CheckpointStateStoreConfigTest.java @@ -0,0 +1,64 @@ +package io.quarkus.smallrye.reactivemessaging.kafka.deployment; + +import static io.quarkus.smallrye.reactivemessaging.kafka.HibernateOrmStateStore.HIBERNATE_ORM_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.HibernateReactiveStateStore.HIBERNATE_REACTIVE_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.RedisStateStore.REDIS_STATE_STORE; +import static io.quarkus.smallrye.reactivemessaging.kafka.deployment.SmallRyeReactiveMessagingKafkaProcessor.hasStateStoreConfig; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; + +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.common.MapBackedConfigSource; + +public class CheckpointStateStoreConfigTest { + + SmallRyeConfig config; + + @AfterEach + void tearDown() { + if (config != null) { + ConfigProviderResolver.instance().releaseConfig(config); + } + } + + private void createConfig(Map configMap) { + config = new SmallRyeConfigBuilder() + .withSources(new MapBackedConfigSource("test", configMap) { + }) + .build(); + } + + @Test + void testHasStateStoreConfigWithConnectorConfig() { + createConfig(Map.of("mp.messaging.connector.smallrye-kafka.checkpoint.state-store", HIBERNATE_ORM_STATE_STORE)); + assertTrue(hasStateStoreConfig(HIBERNATE_ORM_STATE_STORE, config)); + } + + @Test + void testHasStateStoreConfigWithChannelConfig() { + createConfig(Map.of("mp.messaging.incoming.my-channel.checkpoint.state-store", HIBERNATE_REACTIVE_STATE_STORE)); + assertTrue(hasStateStoreConfig(HIBERNATE_REACTIVE_STATE_STORE, config)); + } + + @Test + void testHasStateStoreConfigWithInvalidChannelConfig() { + createConfig(Map.of( + "mp.messaging.outgoing.my-channel.checkpoint.state-store", HIBERNATE_REACTIVE_STATE_STORE, + "mp.messaging.incoming.my-channel.state-store", HIBERNATE_ORM_STATE_STORE)); + assertFalse(hasStateStoreConfig(HIBERNATE_REACTIVE_STATE_STORE, config)); + assertFalse(hasStateStoreConfig(HIBERNATE_ORM_STATE_STORE, config)); + } + + @Test + void testHasStateStoreConfigEmptyConfig() { + createConfig(Map.of()); + assertFalse(hasStateStoreConfig(REDIS_STATE_STORE, config)); + } +} diff --git a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateOrmStateStore.java b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateOrmStateStore.java index b4f8637ea0a76..dacadc1645387 100644 --- a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateOrmStateStore.java +++ b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateOrmStateStore.java @@ -29,7 +29,7 @@ public class HibernateOrmStateStore implements CheckpointStateStore { - public static final String QUARKUS_HIBERNATE_ORM = "quarkus-hibernate-orm"; + public static final String HIBERNATE_ORM_STATE_STORE = "quarkus-hibernate-orm"; private final String consumerGroupId; private final SessionFactory sf; private final Class stateType; @@ -42,7 +42,7 @@ public HibernateOrmStateStore(String consumerGroupId, SessionFactory sf, } @ApplicationScoped - @Identifier(QUARKUS_HIBERNATE_ORM) + @Identifier(HIBERNATE_ORM_STATE_STORE) public static class Factory implements CheckpointStateStore.Factory { @Inject @@ -57,7 +57,7 @@ public CheckpointStateStore create(KafkaConnectorIncomingConfiguration config, V throw new IllegalArgumentException("State type needs to extend `CheckpointEntity`"); } String persistenceUnit = config.config().getOptionalValue(KafkaCommitHandler.Strategy.CHECKPOINT + "." + - QUARKUS_HIBERNATE_ORM + ".persistence-unit", String.class) + HIBERNATE_ORM_STATE_STORE + ".persistence-unit", String.class) .orElse(null); SessionFactory sf = persistenceUnit != null ? sessionFactories.select(new PersistenceUnit.PersistenceUnitLiteral(persistenceUnit)).get() diff --git a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateReactiveStateStore.java b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateReactiveStateStore.java index 94fed9eb48e27..811b29bb10115 100644 --- a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateReactiveStateStore.java +++ b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/HibernateReactiveStateStore.java @@ -25,6 +25,7 @@ public class HibernateReactiveStateStore implements CheckpointStateStore { + public static final String HIBERNATE_REACTIVE_STATE_STORE = "quarkus-hibernate-reactive"; private final String consumerGroupId; private final Mutiny.SessionFactory sf; private final Class stateType; @@ -37,7 +38,7 @@ public HibernateReactiveStateStore(String consumerGroupId, Mutiny.SessionFactory } @ApplicationScoped - @Identifier("quarkus-hibernate-reactive") + @Identifier(HIBERNATE_REACTIVE_STATE_STORE) public static class Factory implements CheckpointStateStore.Factory { @Inject diff --git a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/RedisStateStore.java b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/RedisStateStore.java index d73cbcbfc4a1b..b606a2e14a67b 100644 --- a/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/RedisStateStore.java +++ b/extensions/smallrye-reactive-messaging-kafka/runtime/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/RedisStateStore.java @@ -33,7 +33,7 @@ public class RedisStateStore implements CheckpointStateStore { - public static final String REDIS_CHECKPOINT_NAME = "quarkus-redis"; + public static final String REDIS_STATE_STORE = "quarkus-redis"; private final ReactiveRedisDataSource redis; private final String consumerGroupId; @@ -47,7 +47,7 @@ public RedisStateStore(ReactiveRedisDataSource redis, String consumerGroupId, Pr } @ApplicationScoped - @Identifier(REDIS_CHECKPOINT_NAME) + @Identifier(REDIS_STATE_STORE) public static class Factory implements CheckpointStateStore.Factory { @Inject @@ -62,7 +62,7 @@ public CheckpointStateStore create(KafkaConnectorIncomingConfiguration config, V KafkaConsumer consumer, Class stateType) { String consumerGroupId = (String) consumer.configuration().get(ConsumerConfig.GROUP_ID_CONFIG); String clientName = config.config().getOptionalValue(KafkaCommitHandler.Strategy.CHECKPOINT + "." + - REDIS_CHECKPOINT_NAME + ".client-name", String.class) + REDIS_STATE_STORE + ".client-name", String.class) .orElse(null); ReactiveRedisDataSource rds = clientName != null ? redisDataSource.select(RedisClientName.Literal.of(clientName)).get() diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java index 5e3253279e06c..ab7c6f4fd9883 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java @@ -15,6 +15,7 @@ public final class FilterBuildItem extends MultiBuildItem { public static final int CORS = 300; public static final int AUTHENTICATION = 200; public static final int AUTHORIZATION = 100; + private static final int AUTH_FAILURE_HANDLER = Integer.MIN_VALUE + 1; private final Handler handler; private final int priority; @@ -35,18 +36,22 @@ public FilterBuildItem(Handler handler, int priority) { } /** - * Creates a new instance of {@link FilterBuildItem}. + * Creates a new instance of {@link FilterBuildItem} with an authentication failure handler. * - * @param handler the handler, if {@code null} the filter won't be used. - * @param priority the priority, higher priority gets invoked first. Priority is only used to sort filters, user - * routes are called afterwards. Must be positive. - * @param isFailureHandler whether an HTTP request or failure should be routed to a handler. + * @param authFailureHandler authentication failure handler */ - public FilterBuildItem(Handler handler, int priority, boolean isFailureHandler) { - this.handler = handler; - checkPriority(priority); - this.priority = priority; - this.isFailureHandler = isFailureHandler; + private FilterBuildItem(Handler authFailureHandler) { + this.handler = authFailureHandler; + this.isFailureHandler = true; + this.priority = AUTH_FAILURE_HANDLER; + } + + /** + * Creates a new instance of {@link FilterBuildItem} with an authentication failure handler. + * The handler will be added as next to last, right before {@link io.quarkus.vertx.http.runtime.QuarkusErrorHandler}. + */ + public static FilterBuildItem ofAuthenticationFailureHandler(Handler authFailureHandler) { + return new FilterBuildItem(authFailureHandler); } private void checkPriority(int priority) { @@ -71,7 +76,16 @@ public boolean isFailureHandler() { * @return a filter object wrapping the handler and priority. */ public Filter toFilter() { - return new Filters.SimpleFilter(handler, priority, isFailureHandler); + if (isFailureHandler && priority == AUTH_FAILURE_HANDLER) { + // create filter for penultimate auth failure handler + final Filters.SimpleFilter filter = new Filters.SimpleFilter(); + filter.setPriority(AUTH_FAILURE_HANDLER); + filter.setFailureHandler(true); + filter.setHandler(handler); + return filter; + } else { + return new Filters.SimpleFilter(handler, priority, isFailureHandler); + } } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java index bea0ee1578301..c1852e10dd84d 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java @@ -49,6 +49,60 @@ void corsNotMatchingOrigin() { .header("Access-Control-Allow-Credentials", "false"); } + @Test + void corsSameOriginRequest() { + String origin = "http://localhost:8081"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin); + } + + @Test + void corsInvalidSameOriginRequest1() { + String origin = "http"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest2() { + String origin = "http://local"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest3() { + String origin = "http://localhost"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest4() { + String origin = "http://localhost:9999"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest5() { + String origin = "https://localhost:8483"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + @Test @DisplayName("Returns false 'Access-Control-Allow-Credentials' header on matching origin '*'") void corsMatchingOriginWithWildcard() { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BodyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BodyConfig.java index 144033f305561..1c8723f7f33e4 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BodyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BodyConfig.java @@ -55,4 +55,10 @@ public class BodyConfig { */ @ConfigItem public boolean preallocateBodyBuffer; + + /** + * HTTP multipart request related settings + */ + @ConfigItem + public MultiPartConfig multipart; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/MultiPartConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/MultiPartConfig.java new file mode 100644 index 0000000000000..a0e262b28d6e5 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/MultiPartConfig.java @@ -0,0 +1,23 @@ +package io.quarkus.vertx.http.runtime; + +import java.util.List; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConvertWith; +import io.quarkus.runtime.configuration.TrimmedStringConverter; + +/** + * A {@link ConfigGroup} for the settings related to HTTP multipart request handling. + */ +@ConfigGroup +public class MultiPartConfig { + + /** + * A list of {@code ContentType} to indicate whether a given multipart field should be handled as a file part. + */ + @ConfigItem + @ConvertWith(TrimmedStringConverter.class) + public Optional> fileContentTypes; +} 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 c5b476789e188..d1b853176547b 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 @@ -1,5 +1,6 @@ package io.quarkus.vertx.http.runtime.cors; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -80,7 +81,7 @@ public static List parseAllowedOriginsRegex(Optional> allo * If any regular expression origins are configured, try to match on them. * Regular expressions must begin and end with '/' * - * @param allowedOrigins the configured regex origins. + * @param allowOriginsRegex the configured regex origins. * @param origin the specified origin * @return true if any configured regular expressions match the specified origin, false otherwise */ @@ -179,7 +180,7 @@ public void handle(RoutingContext event) { } boolean allowsOrigin = isConfiguredWithWildcard(corsConfig.origins) || corsConfig.origins.get().contains(origin) - || isOriginAllowedByRegex(allowedOriginsRegex, origin); + || isOriginAllowedByRegex(allowedOriginsRegex, origin) || isSameOrigin(request, origin); if (allowsOrigin) { response.headers().set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); @@ -213,4 +214,80 @@ public void handle(RoutingContext event) { } } } + + static boolean isSameOrigin(HttpServerRequest request, String origin) { + //fast path check, when everything is the same + if (origin.startsWith(request.scheme())) { + if (!substringMatch(origin, request.scheme().length(), "://", false)) { + return false; + } + if (substringMatch(origin, request.scheme().length() + 3, request.host(), true)) { + //they are a simple match + return true; + } + return isSameOriginSlowPath(request, origin); + } else { + return false; + } + } + + static boolean isSameOriginSlowPath(HttpServerRequest request, String origin) { + String absUriString = request.absoluteURI(); + //we already know the scheme is correct, as the fast path will reject that + URI baseUri = URI.create(absUriString); + URI originUri = URI.create(origin); + if (!originUri.getPath().isEmpty()) { + //origin should not contain a path component + //just reject it in this case + return false; + } + if (!baseUri.getHost().equals(originUri.getHost())) { + return false; + } + if (baseUri.getPort() == originUri.getPort()) { + return true; + } + if (baseUri.getPort() != -1 && originUri.getPort() != -1) { + //ports are explictly set + return false; + } + if (baseUri.getScheme().equals("http")) { + if (baseUri.getPort() == 80 || baseUri.getPort() == -1) { + if (originUri.getPort() == 80 || originUri.getPort() == -1) { + //port is either unset or 80 + return true; + } + } + } else if (baseUri.getScheme().equals("https")) { + if (baseUri.getPort() == 443 || baseUri.getPort() == -1) { + if (originUri.getPort() == 443 || originUri.getPort() == -1) { + //port is either unset or 443 + return true; + } + } + } + return false; + } + + static boolean substringMatch(String str, int pos, String substring, boolean requireFull) { + int length = str.length(); + int subLength = substring.length(); + int strPos = pos; + int subPos = 0; + if (pos + subLength > length) { + //too long, avoid checking in the loop + return false; + } + for (;;) { + if (subPos == subLength) { + //if we are at the end return the correct value, depending on if we are also at the end of the origin + return !requireFull || strPos == length; + } + if (str.charAt(strPos) != substring.charAt(subPos)) { + return false; + } + strPos++; + subPos++; + } + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 6267661eb99e1..ffc0fff14ceee 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -176,9 +176,7 @@ public Uni apply(SecurityIdentity securityIdentity) public void accept(SecurityIdentity identity, Throwable throwable, Boolean aBoolean) { if (identity != null) { //when the result is evaluated we set the user, even if it is evaluated lazily - if (identity != null) { - event.setUser(new QuarkusHttpUser(identity)); - } + event.setUser(new QuarkusHttpUser(identity)); } else if (throwable != null) { //handle the auth failure //this can be customised diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java index 5686242b7eaef..e2348e9efcf7b 100644 --- a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java +++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java @@ -2,7 +2,9 @@ import static io.quarkus.vertx.http.runtime.cors.CORSFilter.isConfiguredWithWildcard; import static io.quarkus.vertx.http.runtime.cors.CORSFilter.isOriginAllowedByRegex; +import static io.quarkus.vertx.http.runtime.cors.CORSFilter.isSameOrigin; import static io.quarkus.vertx.http.runtime.cors.CORSFilter.parseAllowedOriginsRegex; +import static io.quarkus.vertx.http.runtime.cors.CORSFilter.substringMatch; import java.util.Arrays; import java.util.Collections; @@ -12,6 +14,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.vertx.core.http.HttpServerRequest; public class CORSFilterTest { @@ -37,4 +42,46 @@ public void isOriginAllowedByRegexTest() { Assertions.assertEquals(regexList.size(), 1); Assertions.assertTrue(isOriginAllowedByRegex(regexList, "https://abc-123.app.mydomain.com")); } + + @Test + public void sameOriginTest() { + var request = Mockito.mock(HttpServerRequest.class); + Mockito.when(request.scheme()).thenReturn("http"); + Mockito.when(request.host()).thenReturn("localhost"); + Mockito.when(request.absoluteURI()).thenReturn("http://localhost"); + Assertions.assertTrue(isSameOrigin(request, "http://localhost")); + Assertions.assertTrue(isSameOrigin(request, "http://localhost:80")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:8080")); + Assertions.assertFalse(isSameOrigin(request, "https://localhost")); + Mockito.when(request.host()).thenReturn("localhost:8080"); + Mockito.when(request.absoluteURI()).thenReturn("http://localhost:8080"); + Assertions.assertFalse(isSameOrigin(request, "http://localhost")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:80")); + Assertions.assertTrue(isSameOrigin(request, "http://localhost:8080")); + Assertions.assertFalse(isSameOrigin(request, "https://localhost:8080")); + Mockito.when(request.scheme()).thenReturn("https"); + Mockito.when(request.host()).thenReturn("localhost"); + Mockito.when(request.absoluteURI()).thenReturn("http://localhost"); + Assertions.assertFalse(isSameOrigin(request, "http://localhost")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:443")); + Assertions.assertFalse(isSameOrigin(request, "https://localhost:8080")); + Assertions.assertTrue(isSameOrigin(request, "https://localhost")); + Mockito.when(request.host()).thenReturn("localhost:8443"); + Mockito.when(request.absoluteURI()).thenReturn("https://localhost:8443"); + Assertions.assertFalse(isSameOrigin(request, "http://localhost")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:80")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:8443")); + Assertions.assertTrue(isSameOrigin(request, "https://localhost:8443")); + + } + + @Test + public void testSubstringMatches() { + Assertions.assertTrue(substringMatch("localhost", 0, "local", false)); + Assertions.assertFalse(substringMatch("localhost", 0, "local", true)); + Assertions.assertFalse(substringMatch("localhost", 1, "local", false)); + Assertions.assertTrue(substringMatch("localhost", 5, "host", false)); + Assertions.assertTrue(substringMatch("localhost", 5, "host", true)); + + } } diff --git a/extensions/websockets/client/runtime/src/main/java/io/quarkus/websockets/client/runtime/WebsocketCoreRecorder.java b/extensions/websockets/client/runtime/src/main/java/io/quarkus/websockets/client/runtime/WebsocketCoreRecorder.java index ca321461c596d..670656f1a0a5a 100644 --- a/extensions/websockets/client/runtime/src/main/java/io/quarkus/websockets/client/runtime/WebsocketCoreRecorder.java +++ b/extensions/websockets/client/runtime/src/main/java/io/quarkus/websockets/client/runtime/WebsocketCoreRecorder.java @@ -169,12 +169,12 @@ public T call(C context, UndertowSession session) throws Exception { boolean required = !requestContext.isActive(); if (required) { requestContext.activate(); - Principal p = session.getUserPrincipal(); - if (p instanceof WebSocketPrincipal) { - var current = getCurrentIdentityAssociation(); - if (current != null) { - current.setIdentity(((WebSocketPrincipal) p).getSecurityIdentity()); - } + } + Principal p = session.getUserPrincipal(); + if (p instanceof WebSocketPrincipal) { + var current = getCurrentIdentityAssociation(); + if (current != null) { + current.setIdentity(((WebSocketPrincipal) p).getSecurityIdentity()); } } try { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java index 952895860541d..b58e17cbcdfb4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java @@ -27,6 +27,7 @@ import javax.enterprise.context.spi.Contextual; import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.CreationException; import javax.enterprise.inject.IllegalProductException; import javax.enterprise.inject.literal.InjectLiteral; import javax.enterprise.inject.spi.InterceptionType; @@ -63,6 +64,8 @@ import io.quarkus.gizmo.FieldCreator; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.FunctionCreator; +import io.quarkus.gizmo.Gizmo; +import io.quarkus.gizmo.Gizmo.StringBuilderGenerator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -946,7 +949,25 @@ protected void implementCreate(ClassOutput classOutput, ClassCreator beanCreator injectionPointToProviderSupplierField, reflectionRegistration, targetPackage, isApplicationClass, create); } else if (bean.isSynthetic()) { - bean.getCreatorConsumer().accept(create); + if (bean.getScope().isNormal()) { + // Normal scoped synthetic beans should never return null + MethodCreator createSynthetic = beanCreator + .getMethodCreator("createSynthetic", providerType.descriptorName(), CreationalContext.class) + .setModifiers(ACC_PRIVATE); + bean.getCreatorConsumer().accept(createSynthetic); + ResultHandle ret = create.invokeVirtualMethod(createSynthetic.getMethodDescriptor(), create.getThis(), + create.getMethodParam(0)); + BytecodeCreator nullBeanInstance = create.ifNull(ret).trueBranch(); + StringBuilderGenerator message = Gizmo.newStringBuilder(nullBeanInstance); + message.append("Null contextual instance was produced by a normal scoped synthetic bean: "); + message.append(Gizmo.toString(nullBeanInstance, nullBeanInstance.getThis())); + ResultHandle e = nullBeanInstance.newInstance( + MethodDescriptor.ofConstructor(CreationException.class, String.class), message.callToString()); + nullBeanInstance.throwException(e); + create.returnValue(ret); + } else { + bean.getCreatorConsumer().accept(create); + } } // Bridge method needed diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/NormalScopedSyntheticBeanProducedNullTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/NormalScopedSyntheticBeanProducedNullTest.java new file mode 100644 index 0000000000000..7814f10472cc3 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/NormalScopedSyntheticBeanProducedNullTest.java @@ -0,0 +1,55 @@ +package io.quarkus.arc.test.buildextension.beans; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.CreationException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.processor.BeanRegistrar; +import io.quarkus.arc.test.ArcTestContainer; + +public class NormalScopedSyntheticBeanProducedNullTest { + + public static volatile boolean beanDestroyerInvoked = false; + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder().beanRegistrars(new TestRegistrar()).build(); + + @Test + public void testCreationException() { + CreationException e = assertThrows(CreationException.class, () -> { + Arc.container().instance(CharSequence.class).get().length(); + }); + assertTrue(e.getMessage().contains("Null contextual instance was produced by a normal scoped synthetic bean"), + e.getMessage()); + } + + static class TestRegistrar implements BeanRegistrar { + + @Override + public void register(RegistrationContext context) { + context.configure(CharSequence.class).types(CharSequence.class).unremovable().scope(ApplicationScoped.class) + .creator(CharSequenceCreator.class).done(); + } + + } + + public static class CharSequenceCreator implements BeanCreator { + + @Override + public CharSequence create(CreationalContext creationalContext, Map params) { + return null; + } + + } + +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java index a30aad09d2f53..9e2587ee65a3b 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/WorkspaceLoader.java @@ -153,13 +153,11 @@ private LocalProject loadAndCacheProject(Path pomFile) throws BootstrapMavenExce } private Model rawModel(Path pomFile) throws BootstrapMavenException { - final Model rawModel = rawModelCache.getOrDefault(pomFile.getParent(), + Model rawModel = rawModelCache.getOrDefault(pomFile.getParent(), modelProvider == null ? null : modelProvider.apply(pomFile.getParent())); - return rawModel == null ? loadAndCacheRawModel(pomFile) : rawModel; - } - - private Model loadAndCacheRawModel(Path pomFile) throws BootstrapMavenException { - final Model rawModel = readModel(pomFile); + if (rawModel == null) { + rawModel = readModel(pomFile); + } rawModelCache.put(pomFile.getParent(), rawModel); return rawModel; } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java index 7fb5137508642..6d7951ed1f80d 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/scanning/ClientEndpointIndexer.java @@ -11,6 +11,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import javax.ws.rs.core.MediaType; @@ -115,7 +116,8 @@ protected ResourceMethod createResourceMethod(MethodInfo info, ClassInfo actualE } @Override - protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, int i) { + protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, int i, + Set fileFormNames) { ClassInfo beanParamClassInfo = index.getClassByName(paramType.name()); methodParameters[i] = parseClientBeanParam(beanParamClassInfo, index); @@ -127,15 +129,18 @@ private MethodParameter parseClientBeanParam(ClassInfo beanParamClassInfo, Index return new ClientBeanParamInfo(items, beanParamClassInfo.name().toString()); } + @Override protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, Map existingConverters, AdditionalReaders additionalReaders, Map injectableBeans, boolean hasRuntimeConverters) { throw new RuntimeException("Injectable beans not supported in client"); } + @Override protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, boolean encoded, Type paramType, ClientIndexedParam parameterResult, String name, String defaultValue, ParameterType type, - String elementType, boolean single, String signature) { + String elementType, boolean single, String signature, + Set fileFormNames) { DeclaredTypes declaredTypes = getDeclaredTypes(paramType, currentClassInfo, actualEndpointInfo); String mimePart = getPartMime(parameterResult.getAnns()); String partFileName = getPartFileName(parameterResult.getAnns()); diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index f0c129876ac7e..4a96c60416859 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -570,7 +570,9 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf basicResourceClassInfo.getConsumes()); boolean suspended = false; boolean sse = false; - boolean formParamRequired = false; + boolean formParamRequired = getAnnotationStore().getAnnotation(currentMethodInfo, + ResteasyReactiveDotNames.WITH_FORM_READ) != null; + Set fileFormNames = new HashSet<>(); Type bodyParamType = null; TypeArgMapper typeArgMapper = new TypeArgMapper(currentMethodInfo.declaringClass(), index); for (int i = 0; i < methodParameters.length; ++i) { @@ -604,12 +606,12 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf } methodParameters[i] = createMethodParameter(currentClassInfo, actualEndpointInfo, encoded, paramType, parameterResult, name, defaultValue, type, elementType, single, - AsmUtil.getSignature(paramType, typeArgMapper)); + AsmUtil.getSignature(paramType, typeArgMapper), fileFormNames); if (type == ParameterType.BEAN || type == ParameterType.MULTI_PART_FORM) { // transform the bean param - formParamRequired |= handleBeanParam(actualEndpointInfo, paramType, methodParameters, i); + formParamRequired |= handleBeanParam(actualEndpointInfo, paramType, methodParameters, i, fileFormNames); } else if (type == ParameterType.FORM) { formParamRequired = true; } @@ -731,6 +733,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf .setSse(sse) .setStreamElementType(streamElementType) .setFormParamRequired(formParamRequired) + .setFileFormNames(fileFormNames) .setParameters(methodParameters) .setSimpleReturnType( toClassName(currentMethodInfo.returnType(), currentClassInfo, actualEndpointInfo, index)) @@ -885,7 +888,7 @@ private String determineReturnType(Type returnType, TypeArgMapper typeArgMapper, } protected abstract boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, - int i); + int i, Set fileFormNames); protected void handleAdditionalMethodProcessing(METHOD method, ClassInfo currentClassInfo, MethodInfo info, AnnotationStore annotationStore) { @@ -901,7 +904,8 @@ protected abstract InjectableBean scanInjectableBean(ClassInfo currentClassInfo, protected abstract MethodParameter createMethodParameter(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, boolean encoded, Type paramType, PARAM parameterResult, String name, String defaultValue, - ParameterType type, String elementType, boolean single, String signature); + ParameterType type, String elementType, boolean single, String signature, + Set fileFormNames); private String[] applyDefaultProduces(String[] produces, Type nonAsyncReturnType, DotName httpMethod) { diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java index 8a7efd8526c8b..ab185a22ab89f 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java @@ -261,6 +261,9 @@ public final class ResteasyReactiveDotNames { public static final DotName RESTEASY_REACTIVE_CONTAINER_REQUEST_CONTEXT = DotName .createSimple("org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext"); + public static final DotName WITH_FORM_READ = DotName + .createSimple("org.jboss.resteasy.reactive.server.WithFormRead"); + public static final DotName OBJECT = DotName.createSimple(Object.class.getName()); public static final DotName CONTINUATION = DotName.createSimple("kotlin.coroutines.Continuation"); diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java index 86e2629e80c46..bee058ff3d280 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/Separator.java @@ -11,7 +11,7 @@ * parameter (using the value of the annotation) and populate the list with those values. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.PARAMETER }) +@Target({ ElementType.PARAMETER, ElementType.FIELD }) public @interface Separator { String value(); diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/BeanParamInfo.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/BeanParamInfo.java index e9dcd4d9103c0..0040b7f9f304c 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/BeanParamInfo.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/BeanParamInfo.java @@ -1,9 +1,13 @@ package org.jboss.resteasy.reactive.common.model; +import java.util.HashSet; +import java.util.Set; + public class BeanParamInfo implements InjectableBean { private boolean isFormParamRequired; private boolean isInjectionRequired; private int fieldExtractorsCount; + private Set fileFormNames = new HashSet<>(); @Override public boolean isFormParamRequired() { @@ -36,4 +40,14 @@ public int getFieldExtractorsCount() { public void setFieldExtractorsCount(int fieldExtractorsCount) { this.fieldExtractorsCount = fieldExtractorsCount; } + + @Override + public Set getFileFormNames() { + return fileFormNames; + } + + @Override + public void setFileFormNames(Set fileFormNames) { + this.fileFormNames = fileFormNames; + } } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/InjectableBean.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/InjectableBean.java index aea871820d09c..e659e6bf50608 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/InjectableBean.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/InjectableBean.java @@ -1,5 +1,7 @@ package org.jboss.resteasy.reactive.common.model; +import java.util.Set; + /** * Class that represents information about injectable beans as we scan them, such as * resource endpoint beans, or BeanParam classes. @@ -25,4 +27,8 @@ public interface InjectableBean { int getFieldExtractorsCount(); void setFieldExtractorsCount(int fieldExtractorsCount); + + Set getFileFormNames(); + + void setFileFormNames(Set fileFormNames); } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceInterceptor.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceInterceptor.java index 1092d32cc6dca..146b956b24fee 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceInterceptor.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceInterceptor.java @@ -16,7 +16,7 @@ public class ResourceInterceptor private BeanFactory factory; private int priority = Priorities.USER; // default priority as defined by spec private boolean nonBlockingRequired; // whether or not @NonBlocking was specified on the class - private boolean readBody; // whether or not 'readBody' was set true for this filter + private boolean withFormRead; // whether or not '@WithFormRead' was set on this filter /** * The class names of the {@code @NameBinding} annotations that the method is annotated with. @@ -76,12 +76,12 @@ public void setNonBlockingRequired(boolean nonBlockingRequired) { this.nonBlockingRequired = nonBlockingRequired; } - public boolean isReadBody() { - return readBody; + public boolean isWithFormRead() { + return withFormRead; } - public void setReadBody(boolean readBody) { - this.readBody = readBody; + public void setWithFormRead(boolean withFormRead) { + this.withFormRead = withFormRead; } // spec says that writer interceptors are sorted in ascending order diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java index 7725306fefc1b..54463c2a191fa 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceMethod.java @@ -68,6 +68,8 @@ public class ResourceMethod { private boolean isFormParamRequired; + private Set fileFormNames; + private List subResourceMethods; public ResourceMethod() { @@ -224,6 +226,15 @@ public ResourceMethod setFormParamRequired(boolean isFormParamRequired) { return this; } + public Set getFileFormNames() { + return fileFormNames; + } + + public ResourceMethod setFileFormNames(Set fileFormNames) { + this.fileFormNames = fileFormNames; + return this; + } + public ResourceMethod setStreamElementType(String streamElementType) { this.streamElementType = streamElementType; return this; diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java index caf772ebb5e66..cae5bc7227def 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java @@ -95,6 +95,13 @@ public class ServerEndpointIndexer extends EndpointIndexer { + + private static final DotName FILE_DOT_NAME = DotName.createSimple(File.class.getName()); + private static final DotName PATH_DOT_NAME = DotName.createSimple(Path.class.getName()); + private static final DotName FILEUPLOAD_DOT_NAME = DotName.createSimple(FileUpload.class.getName()); + + private static final Set SUPPORTED_MULTIPART_FILE_TYPES = Set.of(FILE_DOT_NAME, PATH_DOT_NAME, + FILEUPLOAD_DOT_NAME); protected final EndpointInvokerFactory endpointInvokerFactory; protected final List methodScanners; protected final FieldInjectionIndexerExtension fieldInjectionHandler; @@ -176,7 +183,8 @@ protected ServerResourceMethod createResourceMethod(MethodInfo methodInfo, Class } @Override - protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, int i) { + protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, int i, + Set fileFormNames) { ClassInfo beanParamClassInfo = index.getClassByName(paramType.name()); InjectableBean injectableBean = scanInjectableBean(beanParamClassInfo, actualEndpointInfo, @@ -186,7 +194,7 @@ protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, + "Annotations like `@QueryParam` should be used in fields, not in methods.", beanParamClassInfo.name())); } - + fileFormNames.addAll(injectableBean.getFileFormNames()); return injectableBean.isFormParamRequired(); } @@ -233,6 +241,7 @@ private void validateMethodPath(ServerResourceMethod method, ClassInfo currentCl } } + @Override protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, Map existingConverters, AdditionalReaders additionalReaders, Map injectableBeans, boolean hasRuntimeConverters) { @@ -280,6 +289,24 @@ protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInf } else if (result.getType() == ParameterType.FORM) { // direct form param requirement currentInjectableBean.setFormParamRequired(true); + + if (SUPPORTED_MULTIPART_FILE_TYPES.contains(field.type().name())) { + String name = field.name(); + AnnotationInstance restForm = field.annotation(ResteasyReactiveDotNames.REST_FORM_PARAM); + AnnotationInstance formParam = field.annotation(ResteasyReactiveDotNames.FORM_PARAM); + if (restForm != null) { + AnnotationValue value = restForm.value(); + if (value != null) { + name = value.asString(); + } + } else if (formParam != null) { + AnnotationValue value = formParam.value(); + if (value != null) { + name = value.asString(); + } + } + currentInjectableBean.getFileFormNames().add(name); + } } } // the TCK expects that fields annotated with @BeanParam are handled last @@ -309,15 +336,22 @@ protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInf return currentInjectableBean; } + @Override protected MethodParameter createMethodParameter(ClassInfo currentClassInfo, ClassInfo actualEndpointInfo, boolean encoded, Type paramType, ServerIndexedParameter parameterResult, String name, String defaultValue, ParameterType type, - String elementType, boolean single, String signature) { + String elementType, boolean single, String signature, + Set fileFormNames) { ParameterConverterSupplier converter = parameterResult.getConverter(); DeclaredTypes declaredTypes = getDeclaredTypes(paramType, currentClassInfo, actualEndpointInfo); String mimeType = getPartMime(parameterResult.getAnns()); String separator = getSeparator(parameterResult.getAnns()); + String declaredType = declaredTypes.getDeclaredType(); + + if (SUPPORTED_MULTIPART_FILE_TYPES.contains(DotName.createSimple(declaredType))) { + fileFormNames.add(name); + } return new ServerMethodParameter(name, - elementType, declaredTypes.getDeclaredType(), declaredTypes.getDeclaredUnresolvedType(), + elementType, declaredType, declaredTypes.getDeclaredUnresolvedType(), type, single, signature, converter, defaultValue, parameterResult.isObtainedAsCollection(), parameterResult.isOptional(), encoded, parameterResult.getCustomParameterExtractor(), mimeType, separator); diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java index e40a932562258..60ddd874a0b23 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java @@ -273,7 +273,8 @@ public static Map generateGlobalMapper(MethodInfo targetMethod, // generate a constructor that takes the Instance as an argument in order to avoid missing bean issues if the target has been conditionally disabled // the body can freely read the instance value because if the target has been conditionally disabled, the generated class will not be instantiated ctor = cc.getMethodCreator("", void.class, Instance.class).setSignature( - String.format("(Ljavax/enterprise/inject/Instance;)V", + String.format("(L%s;)V", + Instance.class.getName().replace('.', '/'), targetClass.name().toString().replace('.', '/'))); } else { // generate a constructor that takes the target class as an argument - this class is a CDI bean so Arc will be able to inject into the generated class diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/CustomFilterGenerator.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/CustomFilterGenerator.java index bb08d63067ee3..2c99a7eaac714 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/CustomFilterGenerator.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/CustomFilterGenerator.java @@ -522,7 +522,9 @@ private FieldDescriptor generateConstructorAndDelegateField(ClassCreator cc, Cla if (checkForOptionalBean) { // generate a constructor that takes the Instance as an argument ctor = cc.getMethodCreator("", void.class, Instance.class).setSignature( - String.format("(Ljavax/enterprise/inject/Instance;)V", declaringClassName.replace('.', '/'))); + String.format("(L%s;)V", + Instance.class.getName().replace('.', '/'), + declaringClassName.replace('.', '/'))); } else { // generate a constructor that takes the target class as an argument - this class is a CDI bean so Arc will be able to inject into the generated class ctor = cc.getMethodCreator("", void.class, declaringClassName); diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/FilterGeneration.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/FilterGeneration.java index 9d07114902225..7b6508ecc7713 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/FilterGeneration.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/filters/FilterGeneration.java @@ -19,6 +19,7 @@ import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.server.processor.util.GeneratedClass; import org.jboss.resteasy.reactive.server.processor.util.GeneratedClassOutput; +import org.jboss.resteasy.reactive.server.processor.util.ResteasyReactiveServerDotNames; public class FilterGeneration { public static List generate(IndexView index, Set unwrappableTypes, @@ -37,6 +38,7 @@ public static List generate(IndexView index, Set unwra boolean preMatching = false; boolean nonBlockingRequired = false; boolean readBody = false; + boolean withFormRead = methodInfo.hasAnnotation(ResteasyReactiveServerDotNames.WITH_FORM_READ); Set nameBindingNames = new HashSet<>(); AnnotationValue priorityValue = instance.value("priority"); @@ -56,10 +58,17 @@ public static List generate(IndexView index, Set unwra readBody = readBodyValue.asBoolean(); } - if (readBody && preMatching) { - throw new IllegalStateException( - "Setting both 'readBody' and 'preMatching' to 'true' on '@ServerRequestFilter' is invalid. Offending method is '" - + methodInfo.name() + "' of class '" + methodInfo.declaringClass().name() + "'"); + if (preMatching) { + if (readBody) { + throw new IllegalStateException( + "Setting both 'readBody' and 'preMatching' to 'true' on '@ServerRequestFilter' is invalid. Offending method is '" + + methodInfo.name() + "' of class '" + methodInfo.declaringClass().name() + "'"); + } + if (withFormRead) { + throw new IllegalStateException( + "Setting both '@WithFormRead' and 'preMatching' to 'true' on '@ServerRequestFilter' is invalid. Offending method is '" + + methodInfo.name() + "' of class '" + methodInfo.declaringClass().name() + "'"); + } } List annotations = methodInfo.annotations(); @@ -78,7 +87,7 @@ public static List generate(IndexView index, Set unwra } ret.add(new GeneratedFilter(output.getOutput(), generatedClassName, methodInfo.declaringClass().name().toString(), - true, priority, preMatching, nonBlockingRequired, nameBindingNames, readBody, methodInfo)); + true, priority, preMatching, nonBlockingRequired, nameBindingNames, withFormRead || readBody, methodInfo)); } for (AnnotationInstance instance : index .getAnnotations(SERVER_RESPONSE_FILTER)) { @@ -128,14 +137,14 @@ public static class GeneratedFilter { final boolean preMatching; final boolean nonBlocking; final Set nameBindingNames; - final boolean readBody; + final boolean withFormRead; final MethodInfo filterSourceMethod; public GeneratedFilter(List generatedClasses, String generatedClassName, String declaringClassName, boolean requestFilter, Integer priority, boolean preMatching, boolean nonBlocking, - Set nameBindingNames, boolean readBody, MethodInfo filterSourceMethod) { + Set nameBindingNames, boolean withFormRead, MethodInfo filterSourceMethod) { this.generatedClasses = generatedClasses; this.generatedClassName = generatedClassName; this.declaringClassName = declaringClassName; @@ -144,7 +153,7 @@ public GeneratedFilter(List generatedClasses, String generatedCl this.preMatching = preMatching; this.nonBlocking = nonBlocking; this.nameBindingNames = nameBindingNames; - this.readBody = readBody; + this.withFormRead = withFormRead; this.filterSourceMethod = filterSourceMethod; } @@ -180,8 +189,8 @@ public Set getNameBindingNames() { return nameBindingNames; } - public boolean isReadBody() { - return readBody; + public boolean isWithFormRead() { + return withFormRead; } public MethodInfo getFilterSourceMethod() { diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/DotNames.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/DotNames.java deleted file mode 100644 index d1fdb5c0ed9ba..0000000000000 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/multipart/DotNames.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.jboss.resteasy.reactive.server.processor.generation.multipart; - -import java.io.File; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.file.Path; - -import org.jboss.jandex.DotName; -import org.jboss.resteasy.reactive.multipart.FileUpload; - -final class DotNames { - - static final String POPULATE_METHOD_NAME = "populate"; - static final DotName OBJECT_NAME = DotName.createSimple(Object.class.getName()); - static final DotName STRING_NAME = DotName.createSimple(String.class.getName()); - static final DotName BYTE_NAME = DotName.createSimple(byte.class.getName()); - static final DotName INPUT_STREAM_NAME = DotName.createSimple(InputStream.class.getName()); - static final DotName INPUT_STREAM_READER_NAME = DotName.createSimple(InputStreamReader.class.getName()); - static final DotName FIELD_UPLOAD_NAME = DotName.createSimple(FileUpload.class.getName()); - static final DotName PATH_NAME = DotName.createSimple(Path.class.getName()); - static final DotName FILE_NAME = DotName.createSimple(File.class.getName()); - - private DotNames() { - } -} diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/util/ResteasyReactiveServerDotNames.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/util/ResteasyReactiveServerDotNames.java index 4fbe147aae4f3..bf6706327c8fb 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/util/ResteasyReactiveServerDotNames.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/util/ResteasyReactiveServerDotNames.java @@ -6,6 +6,7 @@ import org.jboss.resteasy.reactive.server.ServerRequestFilter; import org.jboss.resteasy.reactive.server.ServerResponseFilter; import org.jboss.resteasy.reactive.server.SimpleResourceInfo; +import org.jboss.resteasy.reactive.server.WithFormRead; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; @@ -25,5 +26,6 @@ public class ResteasyReactiveServerDotNames { public static final DotName QUARKUS_REST_CONTAINER_REQUEST_CONTEXT = DotName .createSimple(ResteasyReactiveContainerRequestContext.class.getName()); public static final DotName SIMPLIFIED_RESOURCE_INFO = DotName.createSimple(SimpleResourceInfo.class.getName()); - + public static final DotName WITH_FORM_READ = DotName + .createSimple(WithFormRead.class.getName()); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java index bb8a6c2379d24..8e60ebb805a6d 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerRequestFilter.java @@ -113,6 +113,9 @@ * Resource Methods that the filter applies to, it will be executed in normal fashion. * * Also note that this setting and {@link ServerRequestFilter#preMatching()} cannot be both set to true. + * + * @deprecated use {@link WithFormRead} on your filter to force reading the form values before your filter is invoked. */ + @Deprecated boolean readBody() default false; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/WithFormRead.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/WithFormRead.java new file mode 100644 index 0000000000000..3eef6995eee2c --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/WithFormRead.java @@ -0,0 +1,18 @@ +package org.jboss.resteasy.reactive.server; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Forces the form body to be read and parsed before filters and the endpoint are invoked. This is only + * useful if your endpoint does not contain any declared form parameter, which would otherwise force + * the form body being read anyway. + * You can place this annotation on request filters as well as endpoints. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface WithFormRead { + +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java index 4829909c40610..39e499454e13d 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormEncodedDataDefinition.java @@ -5,6 +5,7 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Set; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.HttpHeaders; @@ -37,7 +38,7 @@ public FormEncodedDataDefinition() { } @Override - public FormDataParser create(final ResteasyReactiveRequestContext exchange) { + public FormDataParser create(final ResteasyReactiveRequestContext exchange, Set fileFormNames) { String mimeType = exchange.serverRequest().getRequestHeader(HttpHeaders.CONTENT_TYPE); if (forceCreation || (mimeType != null && mimeType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED))) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java index 7fa20a50e5584..8d382ed75a4b5 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/FormParserFactory.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Supplier; @@ -28,11 +29,12 @@ public class FormParserFactory { * Creates a form data parser for this request. * * @param exchange The exchange + * @param fileFormNames * @return A form data parser, or null if there is no parser registered for the request content type */ - public FormDataParser createParser(final ResteasyReactiveRequestContext exchange) { + public FormDataParser createParser(final ResteasyReactiveRequestContext exchange, Set fileFormNames) { for (int i = 0; i < parserDefinitions.length; ++i) { - FormDataParser parser = parserDefinitions[i].create(exchange); + FormDataParser parser = parserDefinitions[i].create(exchange, fileFormNames); if (parser != null) { return parser; } @@ -42,7 +44,7 @@ public FormDataParser createParser(final ResteasyReactiveRequestContext exchange public interface ParserDefinition { - FormDataParser create(final ResteasyReactiveRequestContext exchange); + FormDataParser create(final ResteasyReactiveRequestContext exchange, Set fileFormNames); T setDefaultCharset(String charset); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java index 216da2f1965f6..9d3465317a90d 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultiPartParserDefinition.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Supplier; @@ -53,6 +54,7 @@ public class MultiPartParserDefinition implements FormParserFactory.ParserDefini private long maxAttributeSize = 2048; private long maxEntitySize = -1; + private List fileContentTypes; public MultiPartParserDefinition(Supplier executorSupplier) { this.executorSupplier = executorSupplier; @@ -65,7 +67,7 @@ public MultiPartParserDefinition(Supplier executorSupplier, final Path } @Override - public FormDataParser create(final ResteasyReactiveRequestContext exchange) { + public FormDataParser create(final ResteasyReactiveRequestContext exchange, Set fileFormNames) { String mimeType = exchange.serverRequest().getRequestHeader(HttpHeaders.CONTENT_TYPE); if (mimeType != null && mimeType.startsWith(MULTIPART_FORM_DATA)) { String boundary = HeaderUtil.extractQuotedValueFromHeader(mimeType, "boundary"); @@ -76,7 +78,7 @@ public FormDataParser create(final ResteasyReactiveRequestContext exchange) { return null; } final MultiPartUploadHandler parser = new MultiPartUploadHandler(exchange, boundary, maxIndividualFileSize, - fileSizeThreshold, defaultCharset, mimeType, maxAttributeSize, maxEntitySize); + fileSizeThreshold, defaultCharset, mimeType, maxAttributeSize, maxEntitySize, fileFormNames); exchange.registerCompletionCallback(new CompletionCallback() { @Override public void onComplete(Throwable throwable) { @@ -152,6 +154,15 @@ public MultiPartParserDefinition setMaxEntitySize(long maxEntitySize) { return this; } + public List getFileContentTypes() { + return fileContentTypes; + } + + public MultiPartParserDefinition setFileContentTypes(List fileContentTypes) { + this.fileContentTypes = fileContentTypes; + return this; + } + private final class MultiPartUploadHandler implements FormDataParser, MultipartParser.PartHandler { private final ResteasyReactiveRequestContext exchange; @@ -161,6 +172,7 @@ private final class MultiPartUploadHandler implements FormDataParser, MultipartP private final long fileSizeThreshold; private final long maxAttributeSize; private final long maxEntitySize; + private final Set fileFormNames; private String defaultEncoding; private final ByteArrayOutputStream contentBytes = new ByteArrayOutputStream(); @@ -175,13 +187,15 @@ private final class MultiPartUploadHandler implements FormDataParser, MultipartP private MultiPartUploadHandler(final ResteasyReactiveRequestContext exchange, final String boundary, final long maxIndividualFileSize, final long fileSizeThreshold, final String defaultEncoding, - String contentType, long maxAttributeSize, long maxEntitySize) { + String contentType, long maxAttributeSize, long maxEntitySize, + Set fileFormNames) { this.exchange = exchange; this.maxIndividualFileSize = maxIndividualFileSize; this.defaultEncoding = defaultEncoding; this.fileSizeThreshold = fileSizeThreshold; this.maxAttributeSize = maxAttributeSize; this.maxEntitySize = maxEntitySize; + this.fileFormNames = fileFormNames; int maxParameters = 1000; this.data = new FormData(maxParameters); String charset = defaultEncoding; @@ -236,7 +250,9 @@ public void beginPart(final CaseInsensitiveMap headers) { if (disposition.startsWith("form-data")) { currentName = HeaderUtil.extractQuotedValueFromHeader(disposition, "name"); fileName = HeaderUtil.extractQuotedValueFromHeaderWithEncoding(disposition, "filename"); - if (fileName != null && fileSizeThreshold == 0) { + String contentType = headers.getFirst(HttpHeaders.CONTENT_TYPE); + if (((fileName != null) || isFileContentType(contentType) || fileFormNames.contains(currentName)) + && fileSizeThreshold == 0) { try { if (tempFileLocation != null) { Files.createDirectories(tempFileLocation); @@ -254,6 +270,14 @@ public void beginPart(final CaseInsensitiveMap headers) { } } + private boolean isFileContentType(String contentType) { + if (contentType == null || fileContentTypes == null) { + return false; + } + + return fileContentTypes.contains(contentType); + } + @Override public void data(final ByteBuffer buffer) throws IOException { this.currentFileSize += buffer.remaining(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java index a7915b23b9349..39b21a7a58bff 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartMessageBodyWriter.java @@ -1,5 +1,7 @@ package org.jboss.resteasy.reactive.server.core.multipart; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; + import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -136,7 +138,7 @@ private void writeHeaders(String partName, Object partValue, PartItem part, Char throws IOException { part.getHeaders().put(HttpHeaders.CONTENT_DISPOSITION, List.of("form-data; name=\"" + partName + "\"" + getFileNameIfFile(partValue, part.getFilename()))); - part.getHeaders().put(HttpHeaders.CONTENT_TYPE, List.of(part.getMediaType())); + part.getHeaders().put(CONTENT_TYPE, List.of(part.getMediaType())); for (Map.Entry> entry : part.getHeaders().entrySet()) { writeLine(outputStream, entry.getKey() + ": " + entry.getValue().stream().map(String::valueOf) .collect(Collectors.joining("; ")), charset); @@ -206,8 +208,15 @@ private String generateBoundary() { private void appendBoundaryIntoMediaType(ResteasyReactiveRequestContext requestContext, String boundary, MediaType mediaType) { - requestContext.setResponseContentType(new MediaType(mediaType.getType(), mediaType.getSubtype(), - Collections.singletonMap(BOUNDARY_PARAM, boundary))); + MediaType mediaTypeWithBoundary = new MediaType(mediaType.getType(), mediaType.getSubtype(), + Collections.singletonMap(BOUNDARY_PARAM, boundary)); + requestContext.setResponseContentType(mediaTypeWithBoundary); + + // this is a total hack, but it's needed to make RestResponse work properly + requestContext.serverResponse().setResponseHeader(CONTENT_TYPE, mediaTypeWithBoundary.toString()); + if (requestContext.getResponse().isCreated()) { + requestContext.getResponse().get().getHeaders().remove(CONTENT_TYPE); + } } private boolean isNotEmpty(String str) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java index 5506f976f1d92..0c44ebb2b0941 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/multipart/MultipartSupport.java @@ -283,9 +283,9 @@ public static DefaultFileUpload getFileUpload(String formName, ResteasyReactiveR public static List getFileUploads(String formName, ResteasyReactiveRequestContext context) { List result = new ArrayList<>(); - FormData fileUploads = context.getFormData(); - if (fileUploads != null) { - Collection fileUploadsForName = fileUploads.get(formName); + FormData formData = context.getFormData(); + if (formData != null) { + Collection fileUploadsForName = formData.get(formName); if (fileUploadsForName != null) { for (FormData.FormValue fileUpload : fileUploadsForName) { if (fileUpload.isFileItem()) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java index e2966a4152ae9..41731c72a8cf9 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java @@ -187,7 +187,7 @@ public BeanFactory.BeanInstance apply(Class aClass) { .entrySet()) { preMatchHandlers .add(new ResourceRequestFilterHandler(entry.getValue(), true, entry.getKey().isNonBlockingRequired(), - entry.getKey().isReadBody())); + entry.getKey().isWithFormRead())); } } for (int i = 0; i < info.getGlobalHandlerCustomizers().size(); i++) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java index 719a87bbc61e5..89775a73b4620 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeInterceptorDeployment.java @@ -98,7 +98,7 @@ public RuntimeInterceptorDeployment(DeploymentInfo info, ConfigurationImpl confi .entrySet()) { globalRequestInterceptorHandlers .add(new ResourceRequestFilterHandler(entry.getValue(), false, entry.getKey().isNonBlockingRequired(), - entry.getKey().isReadBody())); + entry.getKey().isWithFormRead())); } InterceptorHandler globalInterceptorHandler = null; @@ -330,7 +330,7 @@ public List setupRequestFilterHandler() { .entrySet()) { handlers.add( new ResourceRequestFilterHandler(entry.getValue(), false, entry.getKey().isNonBlockingRequired(), - entry.getKey().isNonBlockingRequired())); + entry.getKey().isWithFormRead())); } } return handlers; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 1b93682704e40..08494b3d68ce1 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -242,7 +242,7 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, //spec doesn't seem to test this, but RESTEasy does not run request filters for both root and sub resources (which makes sense) //so only run request filters for methods that are leaf resources - i.e. have a HTTP method annotation so we ensure only one will run - boolean hasReadBodyRequestFilters = false; + boolean hasWithFormReadRequestFilters = false; if (method.getHttpMethod() != null) { List containerRequestFilterHandlers = interceptorDeployment .setupRequestFilterHandler(); @@ -257,13 +257,15 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, } else { handlers.add(handler); } - if (handler.isReadBody()) { - hasReadBodyRequestFilters = true; - } } } else { handlers.addAll(containerRequestFilterHandlers); } + for (ResourceRequestFilterHandler handler : containerRequestFilterHandlers) { + if (handler.isWithFormRead()) { + hasWithFormReadRequestFilters = true; + } + } } // some parameters need the body to be read @@ -280,28 +282,28 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, } } // form params can be everywhere (field, beanparam, param) - boolean checkReadBodyRequestFilters = false; - if (method.isFormParamRequired()) { + boolean checkWithFormReadRequestFilters = false; + if (method.isFormParamRequired() || hasWithFormReadRequestFilters) { // read the body as multipart in one go - handlers.add(new FormBodyHandler(bodyParameter != null, executorSupplier)); - checkReadBodyRequestFilters = true; + handlers.add(new FormBodyHandler(bodyParameter != null, executorSupplier, method.getFileFormNames())); + checkWithFormReadRequestFilters = true; } else if (bodyParameter != null) { if (!defaultBlocking) { if (!method.isBlocking()) { // allow the body to be read by chunks handlers.add(new InputHandler(resteasyReactiveConfig.getInputBufferSize(), executorSupplier)); - checkReadBodyRequestFilters = true; + checkWithFormReadRequestFilters = true; } } } - if (checkReadBodyRequestFilters && hasReadBodyRequestFilters) { + if (checkWithFormReadRequestFilters && hasWithFormReadRequestFilters) { // we need to remove the corresponding filters from the handlers list and add them to its end in the same order List readBodyRequestFilters = new ArrayList<>(1); for (int i = handlers.size() - 2; i >= 0; i--) { var serverRestHandler = handlers.get(i); if (serverRestHandler instanceof ResourceRequestFilterHandler) { ResourceRequestFilterHandler resourceRequestFilterHandler = (ResourceRequestFilterHandler) serverRestHandler; - if (resourceRequestFilterHandler.isReadBody()) { + if (resourceRequestFilterHandler.isWithFormRead()) { readBodyRequestFilters.add(handlers.remove(i)); } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java index b7f12b58df269..42c7ada9b433e 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FormBodyHandler.java @@ -8,6 +8,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Supplier; @@ -20,17 +21,18 @@ import org.jboss.resteasy.reactive.server.core.multipart.MultiPartParserDefinition; import org.jboss.resteasy.reactive.server.spi.GenericRuntimeConfigurableServerRestHandler; import org.jboss.resteasy.reactive.server.spi.RuntimeConfiguration; -import org.jboss.resteasy.reactive.server.spi.ServerHttpRequest; public class FormBodyHandler implements GenericRuntimeConfigurableServerRestHandler { private final boolean alsoSetInputStream; private final Supplier executorSupplier; + private final Set fileFormNames; private volatile FormParserFactory formParserFactory; - public FormBodyHandler(boolean alsoSetInputStream, Supplier executorSupplier) { + public FormBodyHandler(boolean alsoSetInputStream, Supplier executorSupplier, Set fileFormNames) { this.alsoSetInputStream = alsoSetInputStream; this.executorSupplier = executorSupplier; + this.fileFormNames = fileFormNames; } @Override @@ -46,6 +48,7 @@ public void configure(RuntimeConfiguration configuration) { .setMaxAttributeSize(configuration.limits().maxFormAttributeSize()) .setMaxEntitySize(configuration.limits().maxBodySize().orElse(-1L)) .setDeleteUploadsOnEnd(configuration.body().deleteUploadedFilesOnEnd()) + .setFileContentTypes(configuration.body().multiPart().fileContentTypes()) .setDefaultCharset(configuration.body().defaultCharset().name()) .setTempFileLocation(Path.of(configuration.body().uploadsDirectory()))) @@ -74,14 +77,12 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti requestContext.setFormData(existingParsedForm); return; } - ServerHttpRequest serverHttpRequest = requestContext.serverRequest(); + FormDataParser factory = formParserFactory.createParser(requestContext, fileFormNames); + if (factory == null) { + return; + } if (BlockingOperationSupport.isBlockingAllowed()) { //blocking IO approach - - FormDataParser factory = formParserFactory.createParser(requestContext); - if (factory == null) { - return; - } CapturingInputStream cis = null; if (alsoSetInputStream) { // the TCK allows the body to be read as a form param and also as a body param @@ -95,10 +96,6 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti requestContext.setInputStream(new ByteArrayInputStream(cis.baos.toByteArray())); } } else if (alsoSetInputStream) { - FormDataParser factory = formParserFactory.createParser(requestContext); - if (factory == null) { - return; - } requestContext.suspend(); executorSupplier.get().execute(new Runnable() { @Override @@ -115,10 +112,6 @@ public void run() { } }); } else { - FormDataParser factory = formParserFactory.createParser(requestContext); - if (factory == null) { - return; - } //parse will auto resume factory.parse(); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/MediaTypeMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/MediaTypeMapper.java index 480d342920cbb..383b95b3931b6 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/MediaTypeMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/MediaTypeMapper.java @@ -28,6 +28,9 @@ */ public class MediaTypeMapper implements ServerRestHandler { + private static final MediaType[] DEFAULT_MEDIA_TYPES = new MediaType[] { MediaType.WILDCARD_TYPE }; + private static final List DEFAULT_MEDIA_TYPES_LIST = List.of(DEFAULT_MEDIA_TYPES); + final Map resourcesByConsumes; final List consumesTypes; @@ -35,20 +38,17 @@ public MediaTypeMapper(List runtimeResources) { resourcesByConsumes = new HashMap<>(); consumesTypes = new ArrayList<>(); for (RuntimeResource runtimeResource : runtimeResources) { - MediaType consumesMT = runtimeResource.getConsumes().isEmpty() ? MediaType.WILDCARD_TYPE - : runtimeResource.getConsumes().get(0); - if (!resourcesByConsumes.containsKey(consumesMT)) { - consumesTypes.add(consumesMT); - resourcesByConsumes.put(consumesMT, new Holder()); - } - MediaType[] produces = runtimeResource.getProduces() != null - ? runtimeResource.getProduces().getSortedOriginalMediaTypes() - : null; - if (produces == null) { - produces = new MediaType[] { MediaType.WILDCARD_TYPE }; + List consumesMediaTypes = getConsumesMediaTypes(runtimeResource); + for (MediaType consumedMediaType : consumesMediaTypes) { + if (!resourcesByConsumes.containsKey(consumedMediaType)) { + consumesTypes.add(consumedMediaType); + resourcesByConsumes.put(consumedMediaType, new Holder()); + } } - for (MediaType producesMT : produces) { - resourcesByConsumes.get(consumesMT).setResource(runtimeResource, producesMT); + for (MediaType producesMT : getProducesMediaTypes(runtimeResource)) { + for (MediaType consumedMediaType : consumesMediaTypes) { + resourcesByConsumes.get(consumedMediaType).setResource(runtimeResource, producesMT); + } } } for (Holder holder : resourcesByConsumes.values()) { @@ -116,6 +116,17 @@ public MediaType selectMediaType(ResteasyReactiveRequestContext requestContext, return selected; } + private MediaType[] getProducesMediaTypes(RuntimeResource runtimeResource) { + return runtimeResource.getProduces() == null + ? DEFAULT_MEDIA_TYPES + : runtimeResource.getProduces().getSortedOriginalMediaTypes(); + } + + private List getConsumesMediaTypes(RuntimeResource runtimeResource) { + return runtimeResource.getConsumes().isEmpty() ? DEFAULT_MEDIA_TYPES_LIST + : runtimeResource.getConsumes(); + } + private static final class Holder { private final Map mtWithoutParamsToResource = new HashMap<>(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceRequestFilterHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceRequestFilterHandler.java index 7fbdca4e10962..6e19c70d8ae4a 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceRequestFilterHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceRequestFilterHandler.java @@ -11,14 +11,14 @@ public class ResourceRequestFilterHandler implements ServerRestHandler { private final ContainerRequestFilter filter; private final boolean preMatch; private final boolean nonBlockingRequired; - private final boolean readBody; + private final boolean withFormRead; public ResourceRequestFilterHandler(ContainerRequestFilter filter, boolean preMatch, boolean nonBlockingRequired, - boolean readBody) { + boolean withFormRead) { this.filter = filter; this.preMatch = preMatch; this.nonBlockingRequired = nonBlockingRequired; - this.readBody = readBody; + this.withFormRead = withFormRead; } public ContainerRequestFilter getFilter() { @@ -33,8 +33,8 @@ public boolean isNonBlockingRequired() { return nonBlockingRequired; } - public boolean isReadBody() { - return readBody; + public boolean isWithFormRead() { + return withFormRead; } @Override diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java index 6fe9c4a290ab1..958c6e55b7f8f 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/DefaultRuntimeConfiguration.java @@ -2,6 +2,7 @@ import java.nio.charset.Charset; import java.time.Duration; +import java.util.List; import java.util.Optional; public class DefaultRuntimeConfiguration implements RuntimeConfiguration { @@ -10,9 +11,16 @@ public class DefaultRuntimeConfiguration implements RuntimeConfiguration { private final Limits limits; public DefaultRuntimeConfiguration(Duration readTimeout, boolean deleteUploadedFilesOnEnd, String uploadsDirectory, - Charset defaultCharset, Optional maxBodySize, long maxFormAttributeSize) { + List fileContentTypes, Charset defaultCharset, Optional maxBodySize, long maxFormAttributeSize) { this.readTimeout = readTimeout; body = new Body() { + Body.MultiPart multiPart = new Body.MultiPart() { + @Override + public List fileContentTypes() { + return fileContentTypes; + } + }; + @Override public boolean deleteUploadedFilesOnEnd() { return deleteUploadedFilesOnEnd; @@ -27,6 +35,11 @@ public String uploadsDirectory() { public Charset defaultCharset() { return defaultCharset; } + + @Override + public MultiPart multiPart() { + return multiPart; + } }; limits = new Limits() { @Override diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java index 5bdd06534a537..6ccfdcee9dd50 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/RuntimeConfiguration.java @@ -2,6 +2,7 @@ import java.nio.charset.Charset; import java.time.Duration; +import java.util.List; import java.util.Optional; public interface RuntimeConfiguration { @@ -19,6 +20,12 @@ interface Body { String uploadsDirectory(); Charset defaultCharset(); + + MultiPart multiPart(); + + interface MultiPart { + List fileContentTypes(); + } } interface Limits { diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java index 92d770aeb910a..b996daf921ddc 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/framework/ResteasyReactiveUnitTest.java @@ -121,6 +121,7 @@ public boolean isBlockingAllowed() { static Vertx vertx; static ExecutorService executor; boolean deleteUploadedFilesOnEnd = true; + List fileContentTypes; Path uploadPath; private List> scanCustomizers = new ArrayList<>(); @@ -149,6 +150,11 @@ public ResteasyReactiveUnitTest setDeleteUploadedFilesOnEnd(boolean deleteUpload return this; } + public ResteasyReactiveUnitTest setFileContentTypes(List fileContentTypes) { + this.fileContentTypes = fileContentTypes; + return this; + } + public ResteasyReactiveUnitTest setUploadPath(Path uploadPath) { this.uploadPath = uploadPath; return this; @@ -391,7 +397,7 @@ public Thread newThread(Runnable r) { DefaultRuntimeConfiguration runtimeConfiguration = new DefaultRuntimeConfiguration(Duration.ofMinutes(1), deleteUploadedFilesOnEnd, uploadPath != null ? uploadPath.toAbsolutePath().toString() : System.getProperty("java.io.tmpdir"), - defaultCharset, Optional.empty(), maxFormAttributeSize); + fileContentTypes, defaultCharset, Optional.empty(), maxFormAttributeSize); ResteasyReactiveDeploymentManager.RunnableApplication application = prepared.createApplication(runtimeConfiguration, new VertxRequestContextFactory(), executor); fieldInjectionSupport.runtimeInit(testClassLoader, application.getDeployment()); diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartFileContentTypeTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartFileContentTypeTest.java new file mode 100644 index 0000000000000..93c4771f78959 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartFileContentTypeTest.java @@ -0,0 +1,72 @@ +package org.jboss.resteasy.reactive.server.vertx.test.multipart; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.function.Supplier; + +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; +import org.jboss.resteasy.reactive.server.vertx.test.multipart.other.OtherPackageFormDataBase; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.restassured.RestAssured; + +public class MultipartFileContentTypeTest extends AbstractMultipartTest { + + private static final Path uploadDir = Paths.get("file-uploads"); + + @RegisterExtension + static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() + .setDeleteUploadedFilesOnEnd(false) + .setUploadPath(uploadDir) + .setFileContentTypes(List.of(MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_SVG_XML)) + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(FormDataBase.class, OtherPackageFormDataBase.class, FormData.class, Status.class, + OtherFormData.class, FormDataSameFileName.class, + OtherFormDataBase.class, + MultipartResource.class, OtherMultipartResource.class); + } + + }); + + private final File FILE = new File("./src/test/resources/test.html"); + + @BeforeEach + public void assertEmptyUploads() { + Assertions.assertTrue(isDirectoryEmpty(uploadDir)); + } + + @AfterEach + public void clearDirectory() { + clearDirectory(uploadDir); + } + + @Test + public void testFilePartWithExpectedContentType() throws IOException { + RestAssured.given() + .multiPart("octetStream", null, Files.readAllBytes(FILE.toPath()), MediaType.APPLICATION_OCTET_STREAM) + .multiPart("svgXml", null, Files.readAllBytes(FILE.toPath()), MediaType.APPLICATION_SVG_XML) + .accept("text/plain") + .when() + .post("/multipart/optional") + .then() + .statusCode(200); + + // ensure that the 2 uploaded files where created on disk + Assertions.assertEquals(2, uploadDir.toFile().listFiles().length); + } +} diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java index 14c35bfdf92a7..fc27042f39599 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/multipart/MultipartOutputUsingBlockingEndpointsTest.java @@ -14,6 +14,7 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; public class MultipartOutputUsingBlockingEndpointsTest extends AbstractMultipartTest { @@ -46,18 +47,20 @@ public void testSimple() { @Test public void testWithFormData() { - String response = RestAssured.get("/multipart/output/with-form-data") + ExtractableResponse extractable = RestAssured.get("/multipart/output/with-form-data") .then() - .log().all() .contentType(ContentType.MULTIPART) .statusCode(200) - .extract().asString(); + .extract(); - assertContainsValue(response, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); - assertContainsValue(response, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); - assertContainsValue(response, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); - assertContainsValue(response, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); - assertContainsValue(response, "values", MediaType.TEXT_PLAIN, "[one, two]"); + String body = extractable.asString(); + assertContainsValue(body, "name", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_NAME); + assertContainsValue(body, "custom-surname", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_SURNAME); + assertContainsValue(body, "custom-status", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_STATUS); + assertContainsValue(body, "active", MediaType.TEXT_PLAIN, MultipartOutputResource.RESPONSE_ACTIVE); + assertContainsValue(body, "values", MediaType.TEXT_PLAIN, "[one, two]"); + + assertThat(extractable.header("Content-Type")).contains("boundary="); } @Test diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/DefaultMediaTypeTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/DefaultMediaTypeTest.java index 0dcc843024768..f930b447f4c70 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/DefaultMediaTypeTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/DefaultMediaTypeTest.java @@ -216,4 +216,28 @@ public void postInteger() throws Exception { String responseContent = response.readEntity(String.class); LOG.debug(String.format("Response: %s", responseContent)); } + + @Test + @DisplayName("Post Multi Media Type Consumer") + public void testConsumesMultiMediaType() { + WebTarget target = client.target(generateURL("/postMultiMediaTypeConsumer")); + Response response = target.request().post(Entity.entity("payload", "application/soap+xml")); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), + response.getStatus()); + Assertions.assertEquals("postMultiMediaTypeConsumer", response.readEntity(String.class)); + + response = target.request().post(Entity.entity("payload", MediaType.TEXT_XML)); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), + response.getStatus()); + Assertions.assertEquals("postMultiMediaTypeConsumer", response.readEntity(String.class)); + + response = target.request().post(Entity.entity("payload", "any/media-type")); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), + response.getStatus()); + Assertions.assertEquals("any/media-type", response.readEntity(String.class)); + + response = target.request().post(Entity.entity("payload", "unexpected/media-type")); + Assertions.assertEquals(Response.Status.UNSUPPORTED_MEDIA_TYPE.getStatusCode(), + response.getStatus()); + } } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/resource/DefaultMediaTypeResource.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/resource/DefaultMediaTypeResource.java index 993cb9f9a6eff..ba8f67206e065 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/resource/DefaultMediaTypeResource.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/resource/basic/resource/DefaultMediaTypeResource.java @@ -3,9 +3,11 @@ import java.util.Date; import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -75,4 +77,18 @@ public Response postIntegerProduce(String source) throws Exception { public Response postInteger(String source) throws Exception { return Response.ok().entity(5).build(); } + + @Path("postMultiMediaTypeConsumer") + @Consumes({ "application/soap+xml", MediaType.TEXT_XML }) + @POST + public Response postMultiMediaTypeConsumer() { + return Response.ok("postMultiMediaTypeConsumer").build(); + } + + @Path("postMultiMediaTypeConsumer") + @Consumes({ "any/media-type" }) + @POST + public Response postMultiMediaTypeConsumerAnyContentType(@HeaderParam(HttpHeaders.CONTENT_TYPE) String contentType) { + return Response.ok(contentType).build(); + } } diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml index 03a05295ef8ad..b42ba3bdf2f71 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/build.yml @@ -45,4 +45,7 @@ jobs: cache: 'maven' - name: Build with Maven - run: mvn -B formatter:validate clean install --file pom.xml + run: mvn -B clean verify -Dno-format + + - name: Build with Maven (Native) + run: mvn -B verify -Dnative -Dquarkus.native.container-build -Dnative.surefire.skip diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml index 9bd97b523e449..32bbddd82a3b5 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: - name: Maven release ${{steps.metadata.outputs.current-version}} run: | git checkout -b release - mvn -B release:prepare -Prelease -DpreparationGoals="clean install" -DreleaseVersion=${{steps.metadata.outputs.current-version}} -DdevelopmentVersion=${{steps.metadata.outputs.next-version}} + mvn -B release:prepare -Prelease -DreleaseVersion=${{steps.metadata.outputs.current-version}} -DdevelopmentVersion=${{steps.metadata.outputs.next-version}} if ! git diff --quiet docs/modules/ROOT/pages/includes/attributes.adoc; then git add docs/modules/ROOT/pages/includes/attributes.adoc git commit -m "Update stable version for documentation" @@ -68,19 +68,11 @@ jobs: git commit -m "Update stable version for documentation" # Move the tag after inclusion of documentation adjustments git tag -f ${{steps.metadata.outputs.current-version}} + # Push modified tag + git push origin refs/tags/${{steps.metadata.outputs.current-version}} -f fi # Go back to base branch git checkout ${{github.base_ref}} - - name: Push changes to ${{github.base_ref}} - uses: ad-m/github-push-action@v0.6.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: ${{github.base_ref}} - - - name: Push tags - uses: ad-m/github-push-action@v0.6.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - tags: true - branch: ${{github.base_ref}} + - name: Push changes to ${{github.base_ref}} branch + run: git push diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/README.tpl.qute.md b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/README.tpl.qute.md index 1ee0f8fef47f3..d5bf52926285a 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/README.tpl.qute.md +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/README.tpl.qute.md @@ -1,4 +1,13 @@ -# {project.artifact-id} Project +{#if readme.include-default-content} +{#if project.name} +# {project.name} +{#else} +# {project.artifact-id} +{/if} +{#if project.description} + +> {project.description} +{/if} This project uses Quarkus, the Supersonic Subatomic Java Framework. @@ -60,3 +69,4 @@ If you want to learn more about building native executables, please consult {bui ## Provided Code {/if} +{/if} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/codestart.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/codestart.yml index 180f54758c227..2822b07c03a85 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/codestart.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/codestart.yml @@ -25,5 +25,7 @@ language: artifact-id: quarkus-project version: 1.0.0-SNAPSHOT package-name: org.acme + readme: + include-default-content: true dependencies: - io.quarkus:quarkus-arc diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/github-action/base/.github/workflows/ci.tpl.qute.yml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/github-action/base/.github/workflows/ci.tpl.qute.yml index 59956fe437203..a5fe09ec507b8 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/github-action/base/.github/workflows/ci.tpl.qute.yml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/tooling/github-action/base/.github/workflows/ci.tpl.qute.yml @@ -1,6 +1,6 @@ -## This is basic continuous integration build for your Quarkus application. +## A basic GitHub Actions workflow for your Quarkus application. -name: Quarkus Codestart CI +name: CI build on: push: @@ -12,11 +12,17 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK {java.version} - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: {java.version} + distribution: temurin + {#if buildtool.cli == 'gradle'} + cache: gradle + {#else} + cache: maven + {/if} - name: Build {#if buildtool.cli == 'gradle'} uses: eskatos/gradle-command-action@v1 diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java index 44bd3c7a8c5f2..e7976251f7915 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/CreateExtension.java @@ -55,12 +55,12 @@ public enum LayoutType { public static final String DEFAULT_QUARKIVERSE_PARENT_GROUP_ID = "io.quarkiverse"; public static final String DEFAULT_QUARKIVERSE_PARENT_ARTIFACT_ID = "quarkiverse-parent"; - public static final String DEFAULT_QUARKIVERSE_PARENT_VERSION = "10"; + public static final String DEFAULT_QUARKIVERSE_PARENT_VERSION = "12"; public static final String DEFAULT_QUARKIVERSE_NAMESPACE_ID = "quarkus-"; public static final String DEFAULT_QUARKIVERSE_GUIDE_URL = "https://quarkiverse.github.io/quarkiverse-docs/%s/dev/"; private static final String DEFAULT_SUREFIRE_PLUGIN_VERSION = "3.0.0-M7"; - private static final String DEFAULT_COMPILER_PLUGIN_VERSION = "3.8.1"; + private static final String DEFAULT_COMPILER_PLUGIN_VERSION = "3.10.1"; private final QuarkusExtensionCodestartProjectInputBuilder builder = QuarkusExtensionCodestartProjectInput.builder(); private final Path baseDir; diff --git a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/SnapshotTesting.java b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/SnapshotTesting.java index bea70ac28e81c..d0a4578536456 100644 --- a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/SnapshotTesting.java +++ b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/SnapshotTesting.java @@ -174,7 +174,7 @@ public static AbstractPathAssert assertThatMatchSnapshot(Path fileToCheck, St final String snapshotNotFoundDescription = "corresponding snapshot file not found for " + snapshotIdentifier + " (Use -Dsnap to create it automatically)"; - final String description = "Snapshot is not matching (use -Dsnap to udpate it automatically): " + final String description = "Snapshot is not matching (use -Dsnap to update it automatically): " + snapshotIdentifier; if (isUTF8File(fileToCheck)) { assertThat(snapshotFile).as(snapshotNotFoundDescription).isRegularFile(); @@ -264,7 +264,7 @@ public static ListAssert assertThatDirectoryTreeMatchSnapshots(String sn .collect(toList()); return assertThat(tree) - .as("Snapshot is not matching (use -Dsnap to udpate it automatically):" + snapshotName) + .as("Snapshot is not matching (use -Dsnap to update it automatically):" + snapshotName) .containsExactlyInAnyOrderElementsOf(content); }); } diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/README.md b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/README.md index 6142e7fc97079..e088c8db5b9c5 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/README.md +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateDefault/README.md @@ -1,4 +1,4 @@ -# test-codestart Project +# test-codestart This project uses Quarkus, the Supersonic Subatomic Java Framework. diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml index f26a18ff19293..b91dae4af7558 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleNoWrapperGithubAction/.github_workflows_ci.yml @@ -1,6 +1,6 @@ -## This is basic continuous integration build for your Quarkus application. +## A basic GitHub Actions workflow for your Quarkus application. -name: Quarkus Codestart CI +name: CI build on: push: @@ -12,11 +12,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: 11 + distribution: temurin + cache: gradle - name: Build uses: eskatos/gradle-command-action@v1 with: diff --git a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml index 522d6dc1151da..214af798d2fbe 100644 --- a/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml +++ b/independent-projects/tools/devtools-testing/src/test/resources/__snapshots__/QuarkusCodestartGenerationTest/generateGradleWrapperGithubAction/.github_workflows_ci.yml @@ -1,6 +1,6 @@ -## This is basic continuous integration build for your Quarkus application. +## A basic GitHub Actions workflow for your Quarkus application. -name: Quarkus Codestart CI +name: CI build on: push: @@ -12,10 +12,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: 11 + distribution: temurin + cache: maven - name: Build run: ./gradlew build diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index c5bdaf4912b75..81a7a1f5ab424 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -42,7 +42,7 @@ - 3.8.1 + 3.10.1 1.6.0 2.12.13 4.4.0 diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java index 363fd589281d4..8b74ce145ad59 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java @@ -45,6 +45,7 @@ @Path("test") public class TestEndpoint { + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); // fake unused injection point to force ArC to not remove this otherwise I can't mock it in the tests @Inject MockablePersonRepository mockablePersonRepository; @@ -1188,6 +1189,13 @@ public String testProjection() { person = Person.find("name = ?1", "2").project(PersonName.class).firstResult(); Assertions.assertEquals("2", person.name); + person = Person.find(String.format( + "select uniqueName, name%sfrom io.quarkus.it.panache.Person%swhere name = ?1", + LINE_SEPARATOR, LINE_SEPARATOR), "2") + .project(PersonName.class) + .firstResult(); + Assertions.assertEquals("2", person.name); + person = Person.find("name = :name", Parameters.with("name", "2")).project(PersonName.class).firstResult(); Assertions.assertEquals("2", person.name); diff --git a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java index 7352e9f637d16..c75e6cf594091 100644 --- a/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java +++ b/integration-tests/infinispan-client/src/main/java/io/quarkus/it/infinispan/client/TestServlet.java @@ -4,16 +4,19 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.stream.Collectors; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -25,11 +28,14 @@ import org.infinispan.counter.api.CounterConfiguration; import org.infinispan.counter.api.CounterManager; import org.infinispan.counter.api.CounterType; +import org.infinispan.counter.api.Storage; import org.infinispan.counter.api.StrongCounter; +import org.infinispan.counter.api.WeakCounter; import org.infinispan.query.dsl.Query; import org.infinispan.query.dsl.QueryFactory; import io.quarkus.infinispan.client.Remote; +import io.smallrye.common.annotation.Blocking; @Path("/test") public class TestServlet { @@ -111,16 +117,38 @@ public String ickleQueryAuthorSurname(@PathParam("id") String name) { .collect(Collectors.joining(",", "[", "]")); } + @Path("counter/{id}") + @POST + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public boolean defineCounter(@PathParam("id") String id, @QueryParam("type") String type, + @QueryParam("storage") String storage) { + cacheSetup.ensureStarted(); + CounterConfiguration configuration = counterManager.getConfiguration(id); + if (configuration == null) { + configuration = CounterConfiguration.builder(CounterType.valueOf(type)).storage(Storage.valueOf(storage)).build(); + return counterManager.defineCounter(id, configuration); + } + return true; + } + @Path("incr/{id}") @GET @Produces(MediaType.TEXT_PLAIN) + @Blocking public CompletionStage incrementCounter(@PathParam("id") String id) { cacheSetup.ensureStarted(); CounterConfiguration configuration = counterManager.getConfiguration(id); if (configuration == null) { - configuration = CounterConfiguration.builder(CounterType.BOUNDED_STRONG).build(); - counterManager.defineCounter(id, configuration); + return CompletableFuture.completedFuture(0L); } + + if (configuration.type() == CounterType.WEAK) { + WeakCounter weakCounter = counterManager.getWeakCounter(id); + weakCounter.sync().increment(); + return CompletableFuture.completedFuture(weakCounter.getValue()); + } + StrongCounter strongCounter = counterManager.getStrongCounter(id); return strongCounter.incrementAndGet(); } diff --git a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java index fac14fdf755d7..eaddbd5bafb1c 100644 --- a/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java +++ b/integration-tests/infinispan-client/src/test/java/io/quarkus/it/infinispan/client/InfinispanClientFunctionalityTest.java @@ -4,6 +4,8 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import org.infinispan.counter.api.CounterType; +import org.infinispan.counter.api.Storage; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; @@ -33,8 +35,30 @@ public void testIckleQuery() { @Test public void testCounterIncrement() { - String initialValue = RestAssured.when().get("test/incr/somevalue").body().print(); - String nextValue = RestAssured.when().get("test/incr/somevalue").body().print(); + RestAssured.given() + .queryParam("type", CounterType.BOUNDED_STRONG) + .queryParam("storage", Storage.VOLATILE) + .post("test/counter/strong-1").body().print(); + + RestAssured.given() + .queryParam("type", CounterType.WEAK) + .queryParam("storage", Storage.VOLATILE) + .post("test/counter/weak-1").body().print(); + + RestAssured.given() + .queryParam("type", CounterType.UNBOUNDED_STRONG) + .queryParam("storage", Storage.PERSISTENT) + .post("test/counter/strong-2").body().print(); + + assertCounterIncrement("strong-1"); + assertCounterIncrement("weak-1"); + assertCounterIncrement("strong-2"); + } + + private void assertCounterIncrement(String counterName) { + String initialValue = RestAssured.given() + .get("test/incr/" + counterName).body().print(); + String nextValue = RestAssured.when().get("test/incr/" + counterName).body().print(); assertEquals(Integer.parseInt(initialValue) + 1, Integer.parseInt(nextValue)); } diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingApplication.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingApplication.kt new file mode 100644 index 0000000000000..e7bd494373b0f --- /dev/null +++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingApplication.kt @@ -0,0 +1,11 @@ +package io.quarkus.it.kotser + +import io.quarkus.runtime.Quarkus.run +import io.quarkus.runtime.annotations.QuarkusMain + +@QuarkusMain +class GreetingApplication + +fun main(args: Array) { + run(*args) +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithExtendedPropertiesTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithExtendedPropertiesTest.java new file mode 100644 index 0000000000000..33cd3dce48266 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KnativeWithExtendedPropertiesTest.java @@ -0,0 +1,75 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.knative.serving.v1.Service; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KnativeWithExtendedPropertiesTest { + + private static final String APP_NAME = "knative-with-extended-properties"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .overrideConfigKey("quarkus.kubernetes.deployment-target", "knative") + .overrideConfigKey("quarkus.knative.revision-auto-scaling.container-concurrency", "5") + .overrideConfigKey("quarkus.knative.min-scale", "5") + .overrideConfigKey("quarkus.knative.max-scale", "10") + .overrideConfigKey("quarkus.knative.image-pull-policy", "Never") + .setLogFileName("k8s.log") + .setForcedDependencies( + Collections.singletonList(new AppArtifact("io.quarkus", "quarkus-kubernetes", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("knative.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("knative.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("knative.yml")); + assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Service.class, s -> { + assertThat(s.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + }); + + assertThat(s.getSpec()).satisfies(serviceSpec -> { + assertThat(serviceSpec.getTemplate()).satisfies(template -> { + assertThat(template.getMetadata()).satisfies(m -> { + assertThat(m.getAnnotations()).contains(entry("autoscaling.knative.dev/minScale", "5")); + assertThat(m.getAnnotations()).contains(entry("autoscaling.knative.dev/maxScale", "10")); + }); + assertThat(template.getSpec()).satisfies(revisionSpec -> { + assertThat(revisionSpec.getContainerConcurrency()).isEqualTo(5); + + assertThat(revisionSpec.getContainers().get(0)).satisfies(c -> { + assertThat(c.getImagePullPolicy()).isEqualTo("Never"); + }); + }); + }); + }); + }); + } +} \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCustomRouteResourceTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCustomRouteResourceTest.java new file mode 100644 index 0000000000000..93e7595de9b74 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithCustomRouteResourceTest.java @@ -0,0 +1,81 @@ +package io.quarkus.it.kubernetes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.openshift.api.model.Route; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.Version; +import io.quarkus.kubernetes.spi.CustomProjectRootBuildItem; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestBuildStep; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class OpenshiftWithCustomRouteResourceTest { + + private static final String APP_NAME = "openshift-with-custom-route-resource"; + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar.addClasses(GreetingResource.class)) + .setApplicationName(APP_NAME) + .setApplicationVersion("0.1-SNAPSHOT") + .withConfigurationResource(APP_NAME + ".properties") + .addCustomResourceEntry(Path.of("src", "main", "kubernetes", "openshift.yml"), + "manifests/" + APP_NAME + "/openshift.yml") + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-openshift", Version.getVersion()))) + .addBuildChainCustomizerEntries( + new QuarkusProdModeTest.BuildChainCustomizerEntry(CustomProjectRootBuildItemProducerProdMode.class, + Collections.singletonList(CustomProjectRootBuildItem.class), Collections.emptyList())); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("openshift.yml")) + .satisfies(p -> assertThat(p.toFile().listFiles()).hasSize(2)); + List openshiftList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("openshift.yml")); + assertThat(openshiftList).filteredOn(i -> "Route".equals(i.getKind())).singleElement().satisfies(i -> { + assertThat(i).isInstanceOfSatisfying(Route.class, r -> { + assertThat(r.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo(APP_NAME); + assertThat(m.getLabels()).contains(entry("foo", "bar")); + assertThat(m.getAnnotations()).contains(entry("bar", "baz")); + assertThat(m.getAnnotations()).contains(entry("kubernetes.io/tls-acme", "true")); + }); + assertThat(r.getSpec().getPort().getTargetPort().getStrVal()).isEqualTo("http"); + assertThat(r.getSpec().getTo().getKind()).isEqualTo("Service"); + assertThat(r.getSpec().getTo().getName()).isEqualTo(APP_NAME); + }); + }); + } + + public static class CustomProjectRootBuildItemProducerProdMode extends ProdModeTestBuildStep { + + public CustomProjectRootBuildItemProducerProdMode(Map testContext) { + super(testContext); + } + + @Override + public void execute(BuildContext context) { + context.produce(new CustomProjectRootBuildItem( + (Path) getTestContext().get(QuarkusProdModeTest.BUILD_CONTEXT_CUSTOM_SOURCES_PATH_KEY))); + } + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndExistingResourcesTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndExistingResourcesTest.java index dd32c26359737..43e6f8ba9639e 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndExistingResourcesTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/WithKubernetesClientAndExistingResourcesTest.java @@ -30,7 +30,8 @@ public class WithKubernetesClientAndExistingResourcesTest { .withConfigurationResource("kubernetes-with-" + APPLICATION_NAME + ".properties") .addCustomResourceEntry(Path.of("src", "main", "kubernetes", "kubernetes.yml"), "manifests/kubernetes-with-" + APPLICATION_NAME + "/kubernetes.yml") - .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes-client", Version.getVersion()))) + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-kubernetes-client", Version.getVersion()), + Dependency.of("io.quarkus", "quarkus-kubernetes", Version.getVersion()))) .addBuildChainCustomizerEntries( new QuarkusProdModeTest.BuildChainCustomizerEntry( KubernetesWithCustomResourcesTest.CustomProjectRootBuildItemProducerProdMode.class, @@ -58,12 +59,17 @@ public void assertGeneratedResources() throws IOException { } }); - assertThat(kubernetesList).filteredOn(h -> "ServiceAccount".equals(h.getKind())).singleElement().satisfies(h -> { - assertThat(h.getMetadata().getName()).isEqualTo(APPLICATION_NAME); - }); + assertThat(kubernetesList).filteredOn(h -> "ServiceAccount".equals(h.getKind())).singleElement() + .satisfies(h -> assertThat(h.getMetadata().getName()).isEqualTo(APPLICATION_NAME)); + + assertThat(kubernetesList).filteredOn(h -> "RoleBinding".equals(h.getKind())).singleElement() + .satisfies(h -> assertThat(h.getMetadata().getName()).isEqualTo(APPLICATION_NAME + "-view")); - assertThat(kubernetesList).filteredOn(h -> "RoleBinding".equals(h.getKind())).singleElement().satisfies(h -> { - assertThat(h.getMetadata().getName()).isEqualTo(APPLICATION_NAME + "-view"); + // check that if quarkus.kubernetes.namespace is set, "manually" set namespaces are not overwritten + assertThat(kubernetesList).filteredOn(h -> "ConfigMap".equals(h.getKind())).singleElement().satisfies(h -> { + final var metadata = h.getMetadata(); + assertThat(metadata.getName()).isEqualTo("foo"); + assertThat(metadata.getNamespace()).isEqualTo("foo"); }); } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-existing-resources.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-existing-resources.properties index e69de29bb2d1d..2dac144175834 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-existing-resources.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-client-existing-resources.properties @@ -0,0 +1 @@ +quarkus.kubernetes.namespace=bar \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/kubernetes-with-client-existing-resources/kubernetes.yml b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/kubernetes-with-client-existing-resources/kubernetes.yml index 50b0ff7a15371..ac0f2031d2cf3 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/kubernetes-with-client-existing-resources/kubernetes.yml +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/kubernetes-with-client-existing-resources/kubernetes.yml @@ -30,4 +30,11 @@ spec: targetPort: 27017 selector: name: my-service - type: ClusterIP \ No newline at end of file + type: ClusterIP +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: foo + namespace: foo +--- \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/openshift-with-custom-route-resource/openshift.yml b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/openshift-with-custom-route-resource/openshift.yml new file mode 100644 index 0000000000000..d715a98a7353c --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/manifests/openshift-with-custom-route-resource/openshift.yml @@ -0,0 +1,8 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: openshift-with-custom-route-resource +spec: + tls: + termination: edge + insecureEdgeTerminationPolicy: Redirect \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-custom-route-resource.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-custom-route-resource.properties new file mode 100644 index 0000000000000..a7300637a33bf --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/openshift-with-custom-route-resource.properties @@ -0,0 +1,6 @@ +quarkus.kubernetes.deployment-target=openshift +quarkus.openshift.labels.foo=bar +quarkus.openshift.annotations.bar=baz +quarkus.openshift.group=grp + +quarkus.openshift.route.annotations."kubernetes.io/tls-acme"=true \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml index 3398446768dad..6a6a9d86e68d7 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_pom.xml @@ -5,7 +5,7 @@ io.quarkiverse quarkiverse-parent - 10 + 12 io.quarkiverse.my-quarkiverse-ext quarkus-my-quarkiverse-ext-parent @@ -18,7 +18,7 @@ docs - 3.8.1 + 3.10.1 11 UTF-8 UTF-8 diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml index 960fb8cc5e9b1..c4e836f7ba286 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateStandaloneExtension/my-org-my-own-ext_pom.xml @@ -12,7 +12,7 @@ runtime - 3.8.1 + 3.10.1 ${surefire-plugin.version} 11 UTF-8 diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 9b574aa049a34..1639daed9ddd4 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -99,6 +99,7 @@ private void doTestCodeFlowEncryptedIdToken(String tenant) throws IOException { } } + @Test public void testCodeFlowFormPostAndBackChannelLogout() throws IOException { defineCodeFlowLogoutStub(); try (final WebClient webClient = createWebClient()) { diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingApplication.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingApplication.kt new file mode 100644 index 0000000000000..1b18c62d2a3d4 --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingApplication.kt @@ -0,0 +1,15 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.runtime.Quarkus.run +import io.quarkus.runtime.annotations.QuarkusMain + +@QuarkusMain +class GreetingApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + run(*args) + } + } +} diff --git a/jakarta/transform.sh b/jakarta/transform.sh index a381dc8986160..3f5412f1ce3c3 100755 --- a/jakarta/transform.sh +++ b/jakarta/transform.sh @@ -301,6 +301,9 @@ sed -i 's@org.jboss.narayana.rts:narayana-lra@org.jboss.narayana.rts:narayana-lr sed -i 's@org.jboss.narayana.rts:lra-client@org.jboss.narayana.rts:lra-client-jakarta@g' extensions/narayana-lra/runtime/pom.xml sed -i 's@META-INF/services/javax.ws.rs.client.ClientBuilder@META-INF/services/jakarta.ws.rs.client.ClientBuilder@g' extensions/narayana-lra/runtime/pom.xml +# Disable REST Client TCK timeout tests that are not working +sed -i 's@ @ \n org.eclipse.microprofile.rest.client.tck.timeout.TimeoutBuilderIndependentOfMPConfigTest\n org.eclipse.microprofile.rest.client.tck.timeout.TimeoutTest\n org.eclipse.microprofile.rest.client.tck.timeout.TimeoutViaMPConfigTest\n org.eclipse.microprofile.rest.client.tck.timeout.TimeoutViaMPConfigWithConfigKeyTest\n @' tcks/microprofile-rest-client/pom.xml + find integration-tests/gradle -name build.gradle | xargs sed -i 's/javax.enterprise.context.ApplicationScoped/jakarta.enterprise.context.ApplicationScoped/g' find integration-tests/gradle -name build.gradle | xargs sed -i 's/javax.ws.rs.Path/jakarta.ws.rs.Path/g' diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java index c63013d4a910f..bb0cf06ecffcd 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java @@ -30,7 +30,7 @@ * {@link @QuarkusTest} so the test class structure must take this into account. */ @Target(ElementType.TYPE) -@ExtendWith({ DisabledOnIntegrationTestCondition.class, QuarkusTestExtension.class, QuarkusIntegrationTestExtension.class }) +@ExtendWith({ DisabledOnIntegrationTestCondition.class, QuarkusIntegrationTestExtension.class }) @Retention(RetentionPolicy.RUNTIME) public @interface QuarkusIntegrationTest {