diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index a5cde63603c09..e512e56a78585 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -17,6 +17,6 @@ io.quarkus.develocity quarkus-project-develocity-extension - 1.1.6 + 1.1.7 diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 8ec95913d728c..153d34aace15b 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -16,7 +16,7 @@ 2.0.2 2.0.3 - 1.78.1 + 1.79 1.0.2.5 1.0.19 9.0.5 @@ -94,7 +94,7 @@ 23.1.2 1.8.0 - 2.18.0 + 2.18.1 1.0.0.Final 3.17.0 1.17.1 diff --git a/build-parent/pom.xml b/build-parent/pom.xml index f4ebcb2afe700..038b375e7e169 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -83,7 +83,7 @@ 2.2.0 - docker.io/postgres:14 + docker.io/postgres:17 docker.io/mariadb:10.11 icr.io/db2_community/db2:11.5.9.0 mcr.microsoft.com/mssql/server:2022-latest diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java index 196a709a76e00..9db9cca9af3b8 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java @@ -267,7 +267,7 @@ public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String public TemplateHtmlBuilder(String title, String subTitle, String details, List actions, String redirect, List config) { - this(true, null, title, subTitle, details, actions, null, Collections.emptyList()); + this(true, null, title, subTitle, details, actions, redirect, config); } public TemplateHtmlBuilder(boolean showStack, String baseUrl, String title, String subTitle, String details, diff --git a/docs/src/main/asciidoc/_attributes.adoc b/docs/src/main/asciidoc/_attributes.adoc index c92e26a18e931..02a72d2c99e8d 100644 --- a/docs/src/main/asciidoc/_attributes.adoc +++ b/docs/src/main/asciidoc/_attributes.adoc @@ -41,7 +41,7 @@ :quarkus-blob-url: ${quarkus-base-url}/blob/main :quarkus-tree-url: ${quarkus-base-url}/tree/main :quarkus-issues-url: ${quarkus-base-url}/issues -:quarkus-images-url: https://github.com/quarkusio/quarkus-images/tree +:quarkus-images-url: https://github.com/quarkusio/quarkus-images :quarkus-chat-url: https://quarkusio.zulipchat.com :quarkus-mailing-list-subscription-email: quarkus-dev+subscribe@googlegroups.com :quarkus-mailing-list-index: https://groups.google.com/d/forum/quarkus-dev diff --git a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc index 664b7c28d395b..0ba65a1796a92 100644 --- a/docs/src/main/asciidoc/deploying-to-google-cloud.adoc +++ b/docs/src/main/asciidoc/deploying-to-google-cloud.adoc @@ -60,14 +60,14 @@ Then, you will need to create a `src/main/appengine/app.yaml` file, let's keep i [source, yaml] ---- -runtime: java11 +runtime: java21 ---- This will create a default service for your App Engine application. [NOTE] ==== -You can also use the new Java 17 runtime by defining `runtime: java17` instead. +You can also use another Java runtime supported by App Engine, for example, for Java 17, use `runtime: java17` instead. ==== App Engine Standard does not support the default Quarkus' specific packaging layout, therefore, you must set up your application to be packaged as an uber-jar via your `application.properties` file: @@ -114,7 +114,7 @@ First, add the plugin to your `pom.xml`: com.google.cloud.tools appengine-maven-plugin - 2.4.4 + 2.7.0 GCLOUD_CONFIG <1> gettingstarted diff --git a/docs/src/main/asciidoc/dev-ui.adoc b/docs/src/main/asciidoc/dev-ui.adoc index 75c4d41b0531a..dedfcef592f0d 100644 --- a/docs/src/main/asciidoc/dev-ui.adoc +++ b/docs/src/main/asciidoc/dev-ui.adoc @@ -747,7 +747,7 @@ import 'qui-ide-link'; [${sourceClassNameFull}]; + lineNumber='${sourceLineNumber}'>[${sourceClassNameFull}]; ---- https://github.com/quarkusio/quarkus/blob/582f1f78806d2268885faea7aa8f5a4d2b3f5b98/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js#L315[Example code] diff --git a/docs/src/main/asciidoc/getting-started-reactive.adoc b/docs/src/main/asciidoc/getting-started-reactive.adoc index c6ee1eef7ba2b..f5b67fa360de3 100644 --- a/docs/src/main/asciidoc/getting-started-reactive.adoc +++ b/docs/src/main/asciidoc/getting-started-reactive.adoc @@ -184,6 +184,10 @@ Create the `src/main/java/org/acme/hibernate/orm/panache/FruitResource.java` fil ---- package org.acme.hibernate.orm.panache; +import java.util.List; + +import io.quarkus.panache.common.Sort; +import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.Path; diff --git a/docs/src/main/asciidoc/images/oidc-facebook-1.png b/docs/src/main/asciidoc/images/oidc-facebook-1.png index 4a5b67ab09763..ffc582fbaa727 100644 Binary files a/docs/src/main/asciidoc/images/oidc-facebook-1.png and b/docs/src/main/asciidoc/images/oidc-facebook-1.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-2.png b/docs/src/main/asciidoc/images/oidc-facebook-2.png index e3836f96d54c2..5caf09ee2c670 100644 Binary files a/docs/src/main/asciidoc/images/oidc-facebook-2.png and b/docs/src/main/asciidoc/images/oidc-facebook-2.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-3.png b/docs/src/main/asciidoc/images/oidc-facebook-3.png index ee57845e060f5..4200cee4fc9b8 100644 Binary files a/docs/src/main/asciidoc/images/oidc-facebook-3.png and b/docs/src/main/asciidoc/images/oidc-facebook-3.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-4.png b/docs/src/main/asciidoc/images/oidc-facebook-4.png index 08199d4e7deea..c17510732206e 100644 Binary files a/docs/src/main/asciidoc/images/oidc-facebook-4.png and b/docs/src/main/asciidoc/images/oidc-facebook-4.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-5.png b/docs/src/main/asciidoc/images/oidc-facebook-5.png index 82bf435d7a3a3..db3e29fcf2532 100644 Binary files a/docs/src/main/asciidoc/images/oidc-facebook-5.png and b/docs/src/main/asciidoc/images/oidc-facebook-5.png differ diff --git a/docs/src/main/asciidoc/images/oidc-facebook-6.png b/docs/src/main/asciidoc/images/oidc-facebook-6.png index 92ef83325e99a..189af2445a6dc 100644 Binary files a/docs/src/main/asciidoc/images/oidc-facebook-6.png and b/docs/src/main/asciidoc/images/oidc-facebook-6.png differ diff --git a/docs/src/main/asciidoc/mailer-reference.adoc b/docs/src/main/asciidoc/mailer-reference.adoc index 14d523190fb68..45ceed5cf83d9 100644 --- a/docs/src/main/asciidoc/mailer-reference.adoc +++ b/docs/src/main/asciidoc/mailer-reference.adoc @@ -66,9 +66,9 @@ To send a simple email, proceed as follows: [source, java] ---- // Imperative API: -mailer.send(Mail.withText("to@acme.org", "A simple email from quarkus", "This is my body.")); +mailer.send(Mail.withText("to@acme.org", "A simple email from quarkus", "This is my body.").setFrom("from@acme.org")); // Reactive API: -Uni stage = reactiveMailer.send(Mail.withText("to@acme.org", "A reactive email from quarkus", "This is my body.")); +Uni stage = reactiveMailer.send(Mail.withText("to@acme.org", "A reactive email from quarkus", "This is my body.").setFrom("from@acme.org")); ---- For example, you can use the `Mailer` in an HTTP endpoint as follows: @@ -78,14 +78,14 @@ For example, you can use the `Mailer` in an HTTP endpoint as follows: @GET @Path("/imperative") public void sendASimpleEmail() { - mailer.send(Mail.withText("to@acme.org", "A simple email from quarkus", "This is my body")); + mailer.send(Mail.withText("to@acme.org", "A simple email from quarkus", "This is my body").setFrom("from@acme.org")); } @GET @Path("/reactive") public Uni sendASimpleEmailAsync() { return reactiveMailer.send( - Mail.withText("to@acme.org", "A reactive email from quarkus", "This is my body")); + Mail.withText("to@acme.org", "A reactive email from quarkus", "This is my body").setFrom("from@acme.org")); } ---- @@ -96,6 +96,20 @@ You can create new `io.quarkus.mailer.Mail` instances from the constructor or fr `Mail.withHtml` helper methods. The `Mail` instance lets you add recipients (to, cc, or bcc), set the subject, headers, sender (from) address... +Most of these properties are optional, but the sender address is required. It can either be set on an individual `Mail` instances using + +[source,java] +---- +.setFrom("from@acme.org"); +---- + +or a global default can be configured, using + +[source,properties] +---- +quarkus.mailer.from=from@acme.org +---- + You can also send several `Mail` objects in one call: [source, java] diff --git a/docs/src/main/asciidoc/mongodb-panache.adoc b/docs/src/main/asciidoc/mongodb-panache.adoc index bef7cf9c63556..853f6e27b7dfa 100644 --- a/docs/src/main/asciidoc/mongodb-panache.adoc +++ b/docs/src/main/asciidoc/mongodb-panache.adoc @@ -707,7 +707,7 @@ TIP: Using `@BsonProperty` is not needed to define custom column mappings, as th TIP: You can have your projection class extends from another class. In this case, the parent class also needs to have use `@ProjectionFor` annotation. -TIP: If you run Java 17+, records are a good fit for projection classes. +TIP: Records are a good fit for projection classes. == Query debugging @@ -724,6 +724,7 @@ quarkus.log.category."io.quarkus.mongodb.panache.common.runtime".level=DEBUG MongoDB with Panache uses the link:{mongodb-doc-root-url}/fundamentals/data-formats/document-data-format-pojo/[PojoCodecProvider], with link:{mongodb-doc-root-url}/fundamentals/data-formats/document-data-format-pojo/#configure-the-driver-for-pojos[automatic POJO support], to automatically convert your object to a BSON document. +This codec also supports Java records so you can use them for your entities or an attribute of your entities. In case you encounter the `org.bson.codecs.configuration.CodecConfigurationException` exception, it means the codec is not able to automatically convert your object. diff --git a/docs/src/main/asciidoc/mongodb.adoc b/docs/src/main/asciidoc/mongodb.adoc index 59d936c5544c0..3885605ace972 100644 --- a/docs/src/main/asciidoc/mongodb.adoc +++ b/docs/src/main/asciidoc/mongodb.adoc @@ -570,7 +570,8 @@ public class CodecFruitService { == The POJO Codec The link:https://www.mongodb.com/docs/drivers/java/sync/current/fundamentals/data-formats/document-data-format-pojo/[POJO Codec] provides a set of annotations that enable the customization of -the way a POJO is mapped to a MongoDB collection and this codec is initialized automatically by Quarkus +the way a POJO is mapped to a MongoDB collection and this codec is initialized automatically by Quarkus. +This codec also supports Java records so you can use them for your POJOs or an attribute of your POJOs. One of these annotations is the `@BsonDiscriminator` annotation that allows to storage multiple Java types in a single MongoDB collection by adding a discriminator field inside the document. It can be useful when working with abstract types or interfaces. diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index fa652b907ac08..329345aa1a95a 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -1275,7 +1275,7 @@ public interface ProtectedResourceService { Additionally, `AccessTokenRequestReactiveFilter` can support a complex application that needs to exchange the tokens before propagating them. -If you work with link:https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange[Keycloak] or another OIDC provider that supports a link:https://tools.ietf.org/html/rfc8693[Token Exchange] token grant, then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: +If you work with link:https://www.keycloak.org/securing-apps/token-exchange[Keycloak] or another OIDC provider that supports a link:https://tools.ietf.org/html/rfc8693[Token Exchange] token grant, then you can configure `AccessTokenRequestReactiveFilter` to exchange the token like this: [source,properties] ---- @@ -1369,7 +1369,7 @@ Alternatively, `AccessTokenRequestFilter` can be registered automatically with a ==== Exchange token before propagation -If the current access token needs to be exchanged before propagation and you work with link:https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange[Keycloak] or other OpenID Connect Provider which supports a link:https://tools.ietf.org/html/rfc8693[Token Exchange] token grant, then you can configure `AccessTokenRequestFilter` like this: +If the current access token needs to be exchanged before propagation and you work with link:https://www.keycloak.org/securing-apps/token-exchange[Keycloak] or other OpenID Connect Provider which supports a link:https://tools.ietf.org/html/rfc8693[Token Exchange] token grant, then you can configure `AccessTokenRequestFilter` like this: [source,properties] ---- diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index f24cbd8555521..36b044bd2f4b4 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -165,11 +165,11 @@ Facebook you will not be let you test your application on `localhost` like most you will need to run it over HTTPS and make it publicly accessible, so for development purposes you may want to use a service such as https://ngrok.com. -In order to set up OIDC for Facebook start by https://developers.facebook.com/apps/create/[Creating an application], select `None` as an app type, and press `Next`: +In order to set up OIDC for Facebook start by https://developers.facebook.com/apps/create/[Creating an application], select `Other` as an app type, and click `Next`. image::oidc-facebook-1.png[role="thumb"] -Now enter an application name, and contact email, and press `Create app`: +Now choose your application type. For this guide choose `Consumer` and click `Next` until you reach the screen below. Now enter an application name, and contact email, and press `Create app`: image::oidc-facebook-2.png[role="thumb"] @@ -177,11 +177,12 @@ On the app page, click `Set up` on the `Facebook login` product: image::oidc-facebook-3.png[role="thumb"] -Quick the `Quickstarts` page and click on `Facebook login > Settings` on the left menu: +On the `Quickstart` page click on `Facebook login > Settings` on the left menu: image::oidc-facebook-4.png[role="thumb"] -Enter your `Redirect URIs` (set to `/_renarde/security/oidc-success`) and press `Save changes`: +First click on `Get Advanced Access` to switch `public_profile` to advanced access. +Then enter your `Redirect URIs` (set to `/facebook`) and press `Save changes`: image::oidc-facebook-5.png[role="thumb"] diff --git a/extensions/amazon-lambda/deployment/pom.xml b/extensions/amazon-lambda/deployment/pom.xml index 24060850709a8..083bdd130e531 100644 --- a/extensions/amazon-lambda/deployment/pom.xml +++ b/extensions/amazon-lambda/deployment/pom.xml @@ -29,6 +29,11 @@ rest-assured test + + org.assertj + assertj-core + test + io.quarkus diff --git a/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java b/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java index 33d9a16615482..d7b631b826e24 100644 --- a/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java +++ b/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java @@ -3,13 +3,13 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import jakarta.inject.Named; @@ -29,6 +29,7 @@ import io.quarkus.amazon.lambda.runtime.LambdaBuildTimeConfig; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.processor.DotNames; import io.quarkus.builder.BuildException; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; @@ -58,6 +59,18 @@ public final class AmazonLambdaProcessor { private static final DotName NAMED = DotName.createSimple(Named.class.getName()); private static final Logger log = Logger.getLogger(AmazonLambdaProcessor.class); + private static final Predicate INCLUDE_HANDLER_PREDICATE = new Predicate<>() { + + @Override + public boolean test(ClassInfo classInfo) { + if (classInfo.isAbstract() || classInfo.hasAnnotation(DotNames.DECORATOR)) { + return false; + } + + return true; + } + }; + @BuildStep FeatureBuildItem feature() { return new FeatureBuildItem(Feature.AMAZON_LAMBDA); @@ -75,11 +88,13 @@ List discover(CombinedIndexBuildItem combinedIndexBuildIt BuildProducer reflectiveHierarchy, BuildProducer reflectiveClassBuildItemBuildProducer) throws BuildException { - Collection allKnownImplementors = combinedIndexBuildItem.getIndex().getAllKnownImplementors(REQUEST_HANDLER); + List allKnownImplementors = new ArrayList<>( + combinedIndexBuildItem.getIndex().getAllKnownImplementors(REQUEST_HANDLER) + .stream().filter(INCLUDE_HANDLER_PREDICATE).toList()); allKnownImplementors.addAll(combinedIndexBuildItem.getIndex() - .getAllKnownImplementors(REQUEST_STREAM_HANDLER)); + .getAllKnownImplementors(REQUEST_STREAM_HANDLER).stream().filter(INCLUDE_HANDLER_PREDICATE).toList()); allKnownImplementors.addAll(combinedIndexBuildItem.getIndex() - .getAllKnownSubclasses(SKILL_STREAM_HANDLER)); + .getAllKnownSubclasses(SKILL_STREAM_HANDLER).stream().filter(INCLUDE_HANDLER_PREDICATE).toList()); if (allKnownImplementors.size() > 0 && providedLambda.isPresent()) { throw new BuildException( diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaWithDecoratorTest.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaWithDecoratorTest.java new file mode 100644 index 0000000000000..e1c286131eb58 --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaWithDecoratorTest.java @@ -0,0 +1,83 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.containsString; + +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import jakarta.annotation.Priority; +import jakarta.decorator.Decorator; +import jakarta.decorator.Delegate; +import jakarta.enterprise.inject.Any; +import jakarta.inject.Inject; + +import org.jboss.logging.Logger; +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 com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +import io.quarkus.amazon.lambda.deployment.testing.model.InputPerson; +import io.quarkus.test.QuarkusUnitTest; + +class LambdaWithDecoratorTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap + .create(JavaArchive.class) + .addClasses(LambdaWithDecorator.class, RequestHandlerDecorator.class, InputPerson.class)) + .setLogRecordPredicate(record -> record.getLevel().intValue() == Level.INFO.intValue() + && record.getMessage().contains("handling request with id")) + .assertLogRecords(records -> assertThat(records) + .extracting(LogRecord::getMessage) + .isNotEmpty()); + + @Test + public void testLambdaWithDecorator() throws Exception { + // you test your lambdas by invoking on http://localhost:8081 + // this works in dev mode too + + InputPerson in = new InputPerson("Stu"); + given() + .contentType("application/json") + .accept("application/json") + .body(in) + .when() + .post() + .then() + .statusCode(200) + .body(containsString("Hey Stu")); + } + + public static class LambdaWithDecorator implements RequestHandler { + + @Override + public String handleRequest(InputPerson input, Context context) { + return "Hey " + input.getName(); + } + } + + @Priority(10) + @Decorator + public static class RequestHandlerDecorator implements RequestHandler { + + @Inject + Logger logger; + + @Inject + @Any + @Delegate + RequestHandler delegate; + + @Override + public O handleRequest(I i, Context context) { + logger.info("handling request with id " + context.getAwsRequestId()); + return delegate.handleRequest(i, context); + } + } +} diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-beans.js b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-beans.js index b7f0fa17190f1..b4a54b0f54423 100644 --- a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-beans.js +++ b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-beans.js @@ -130,8 +130,7 @@ export class QwcArcBeans extends LitElement { ${bean.nonDefaultQualifiers.map(qualifier => html`${this._qualifierRenderer(qualifier)}` )} - ${bean.providerType.name} + ${bean.providerType.name} `; } @@ -213,4 +212,4 @@ export class QwcArcBeans extends LitElement { }); } } -customElements.define('qwc-arc-beans', QwcArcBeans); \ No newline at end of file +customElements.define('qwc-arc-beans', QwcArcBeans); diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-observers.js b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-observers.js index 4b2ae091e50d9..9baa1c606b2e3 100644 --- a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-observers.js +++ b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-observers.js @@ -90,8 +90,7 @@ export class QwcArcObservers extends LitElement { } _sourceRenderer(bean){ - return html`${bean.declaringClass.name}#${bean.methodName}()`; + return html`${bean.declaringClass.name}#${bean.methodName}()`; } _typeRenderer(bean){ @@ -144,4 +143,4 @@ export class QwcArcObservers extends LitElement { return s.replaceAll('_', ' '); } } -customElements.define('qwc-arc-observers', QwcArcObservers); \ No newline at end of file +customElements.define('qwc-arc-observers', QwcArcObservers); diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-removed-components.js b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-removed-components.js index 4532b052a728b..28a1afe244677 100644 --- a/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-removed-components.js +++ b/extensions/arc/deployment/src/main/resources/dev-ui/qwc-arc-removed-components.js @@ -143,8 +143,7 @@ export class QwcArcRemovedComponents extends LitElement { ${bean.nonDefaultQualifiers.map(qualifier => html`${this._simpleNameRenderer(qualifier)}` )} - ${bean.providerType.name} + ${bean.providerType.name} `; } @@ -204,4 +203,4 @@ export class QwcArcRemovedComponents extends LitElement { }); } } -customElements.define('qwc-arc-removed-components', QwcArcRemovedComponents); \ No newline at end of file +customElements.define('qwc-arc-removed-components', QwcArcRemovedComponents); diff --git a/extensions/grpc/deployment/src/main/resources/dev-ui/qwc-grpc-services.js b/extensions/grpc/deployment/src/main/resources/dev-ui/qwc-grpc-services.js index 1675b06f73056..945e8520174e7 100644 --- a/extensions/grpc/deployment/src/main/resources/dev-ui/qwc-grpc-services.js +++ b/extensions/grpc/deployment/src/main/resources/dev-ui/qwc-grpc-services.js @@ -153,8 +153,7 @@ export class QwcGrpcServices extends observeState(QwcHotReloadElement) { } _serviceClassRenderer(service){ - return html`${service.serviceClass}`; + return html`${service.serviceClass}`; } _methodsRenderer(service){ @@ -368,4 +367,4 @@ export class QwcGrpcServices extends observeState(QwcHotReloadElement) { return JSON.stringify(JSON.parse(content), null, 2); } } -customElements.define('qwc-grpc-services', QwcGrpcServices); \ No newline at end of file +customElements.define('qwc-grpc-services', QwcGrpcServices); diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java index e5dee80db7fe3..eda5b00cd66d3 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcRequestContextProperties.java @@ -19,22 +19,57 @@ public OidcRequestContextProperties(Map properties) { this.properties = properties; } + /** + * Get property value + * + * @param name property name + * @return property value + */ public T get(String name) { @SuppressWarnings("unchecked") T value = (T) properties.get(name); return value; } + /** + * Get property value as String + * + * @param name property name + * @return property value as String + */ public String getString(String name) { return (String) get(name); } + /** + * Get typed property value + * + * @param name property name + * @param type property type + * @return typed property value + */ public T get(String name, Class type) { return type.cast(get(name)); } + /** + * Get an unmodifiable view of the current context properties. + * + * @return all properties + */ public Map getAll() { return Collections.unmodifiableMap(properties); } + /** + * Set the property + * + * @param name property name + * @param value property value + * @return this OidcRequestContextProperties instance + */ + public OidcRequestContextProperties put(String name, Object value) { + properties.put(name, value); + return this; + } } diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/package-info.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/package-info.java index 0340f0bbc54c6..0deec3af70317 100644 --- a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/package-info.java +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/package-info.java @@ -56,8 +56,10 @@ * otherwise it will be the name of your entity. *

*

- * The Mongo PojoCodec is used to serialize your entity to Bson Document, you can find more information on it's - * documentation page: https://mongodb.github.io/mongo-java-driver/3.10/bson/pojos/ + * The Mongo PojoCodec is used to serialize your entity to a Bson Document, you can find more information on its + * documentation page: + * https://www.mongodb.com/docs/drivers/java/sync/current/fundamentals/data-formats/document-data-format-pojo. + * This codec also supports Java records. * You can use the MongoDB annotations to control the mapping to the database : @BsonId, * @BsonProperty("fieldName"), @BsonIgnore. *

diff --git a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java index 12bb568c727d1..a2589605d5e6a 100644 --- a/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java +++ b/extensions/quartz/runtime/src/main/java/io/quarkus/quartz/runtime/QuartzRuntimeConfig.java @@ -42,8 +42,11 @@ public class QuartzRuntimeConfig { public int batchTriggerAcquisitionMaxCount; /** * The size of scheduler thread pool. This will initialize the number of worker threads in the pool. + *

+ * It's important to bear in mind that Quartz threads are not used to execute scheduled methods, instead the regular Quarkus + * thread pool is used by default. See also {@code quarkus.quartz.run-blocking-scheduled-method-on-quartz-thread}. */ - @ConfigItem(defaultValue = "25") + @ConfigItem(defaultValue = "10") public int threadCount; /** diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java index 73d843f80d00a..fa1fdb002bf0a 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java @@ -156,6 +156,9 @@ * Map.Entry entry = (Map.iterator) var3.next(); * String field = (String) entry.getKey(); * JsonNode jsonNode = (JsonNode) entry.getValue(); + * if (jsonNode.isNull()) { + * continue; + * } * switch (field) { * case "content": * dataItem.setContent(context.readTreeAsValue(jsonNode, this.valueTypes[0])); @@ -240,10 +243,13 @@ private boolean deserializeObject(ClassInfo classInfo, ResultHandle objHandle, C .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getKey", Object.class), mapEntry); ResultHandle fieldValue = loopCreator.checkCast(loopCreator .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getValue", Object.class), mapEntry), JsonNode.class); - Switch.StringSwitch strSwitch = loopCreator.stringSwitch(fieldName); + + loopCreator.ifTrue(loopCreator.invokeVirtualMethod(ofMethod(JsonNode.class, "isNull", boolean.class), fieldValue)) + .trueBranch().continueScope(loopCreator); Set deserializedFields = new HashSet<>(); ResultHandle deserializationContext = deserialize.getMethodParam(1); + Switch.StringSwitch strSwitch = loopCreator.stringSwitch(fieldName); return deserializeFields(classCreator, classInfo, deserializationContext, objHandle, fieldValue, deserializedFields, strSwitch, parseTypeParameters(classInfo, classCreator)); } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java index 6ca5109cd4e21..457977f1f4064 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java @@ -190,13 +190,13 @@ protected boolean createSerializationMethod(ClassInfo classInfo, ClassCreator cl private boolean serializeObject(ClassInfo classInfo, ClassCreator classCreator, String beanClassName, MethodCreator serialize) { - Set serializedFields = new HashSet<>(); SerializationContext ctx = new SerializationContext(serialize, beanClassName); // jsonGenerator.writeStartObject(); MethodDescriptor writeStartObject = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writeStartObject", "void"); serialize.invokeVirtualMethod(writeStartObject, ctx.jsonGenerator); + Set serializedFields = new HashSet<>(); boolean valid = serializeObjectData(classInfo, classCreator, serialize, ctx, serializedFields); // jsonGenerator.writeEndObject(); @@ -222,7 +222,7 @@ private boolean serializeFields(ClassInfo classInfo, ClassCreator classCreator, SerializationContext ctx, Set serializedFields) { for (FieldInfo fieldInfo : classFields(classInfo)) { FieldSpecs fieldSpecs = fieldSpecsFromField(classInfo, fieldInfo); - if (fieldSpecs != null && serializedFields.add(fieldSpecs.fieldName)) { + if (fieldSpecs != null && serializedFields.add(fieldSpecs.jsonName)) { if (fieldSpecs.hasUnknownAnnotation()) { return false; } @@ -236,7 +236,7 @@ private boolean serializeMethods(ClassInfo classInfo, ClassCreator classCreator, SerializationContext ctx, Set serializedFields) { for (MethodInfo methodInfo : classMethods(classInfo)) { FieldSpecs fieldSpecs = fieldSpecsFromMethod(methodInfo); - if (fieldSpecs != null && serializedFields.add(fieldSpecs.fieldName)) { + if (fieldSpecs != null && serializedFields.add(fieldSpecs.jsonName)) { if (fieldSpecs.hasUnknownAnnotation()) { return false; } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 05f12b8e9ebf4..855c43625c09e 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -116,6 +116,13 @@ public Dog echoDog(Dog dog) { return dog; } + @POST + @Path("/record-echo") + @Consumes(MediaType.APPLICATION_JSON) + public StateRecord echoDog(StateRecord stateRecord) { + return stateRecord; + } + @EnableSecureSerialization @GET @Path("/abstract-cat") diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index 8aa2a009e8418..d2f22569f9a7a 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.function.Supplier; @@ -35,7 +36,7 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class) + NestedInterface.class, StateRecord.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n"), "application.properties"); @@ -675,6 +676,24 @@ public void testEcho() { .body("veterinarian.title", Matchers.nullValue()); } + @Test + public void testEchoWithNullString() { + RestAssured + .with() + .body("{\"publicName\":null,\"veterinarian\":{\"name\":\"Dolittle\"},\"age\":5,\"vaccinated\":true}") + .contentType("application/json; charset=utf-8") + .post("/simple/dog-echo") + .then() + .statusCode(200) + .contentType("application/json") + .body("publicName", Matchers.nullValue()) + .body("privateName", Matchers.nullValue()) + .body("age", Matchers.is(5)) + .body("vaccinated", Matchers.is(true)) + .body("veterinarian.name", Matchers.is("Dolittle")) + .body("veterinarian.title", Matchers.nullValue()); + } + @Test public void testEchoWithMissingPrimitive() { RestAssured @@ -691,4 +710,27 @@ public void testEchoWithMissingPrimitive() { .body("veterinarian.name", Matchers.is("Dolittle")) .body("veterinarian.title", Matchers.nullValue()); } + + @Test + public void testRecordEcho() { + String response = RestAssured + .with() + .body("{\"code\":\"AL\",\"is_enabled\":true,\"name\":\"Alabama\"}") + .contentType("application/json; charset=utf-8") + .post("/simple/record-echo") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("Alabama")) + .body("code", Matchers.is("AL")) + .body("is_enabled", Matchers.is(true)) + .extract() + .asString(); + + int first = response.indexOf("is_enabled"); + int last = response.lastIndexOf("is_enabled"); + // assert that the "is_enabled" field is present only once in the response + assertTrue(first >= 0); + assertEquals(first, last); + } } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java index 0254e48598fe8..65dec05aa59a4 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonWithReflectionFreeSerializersTest.java @@ -25,7 +25,7 @@ public JavaArchive get() { AbstractPet.class, Dog.class, Cat.class, Veterinarian.class, AbstractNamedPet.class, AbstractUnsecuredPet.class, UnsecuredPet.class, SecuredPersonInterface.class, Frog.class, Pond.class, FrogBodyParts.class, FrogBodyParts.BodyPart.class, ContainerDTO.class, - NestedInterface.class) + NestedInterface.class, StateRecord.class) .addAsResource(new StringAsset("admin-expression=admin\n" + "user-expression=user\n" + "birth-date-roles=alice,bob\n" + diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/StateRecord.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/StateRecord.java new file mode 100644 index 0000000000000..1e7aed0af9943 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/StateRecord.java @@ -0,0 +1,8 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record StateRecord(String name, String code, @JsonProperty("is_enabled") boolean isEnabled) { +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index d9fe9f478b498..67adec4015f7f 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -245,6 +245,8 @@ public class ResteasyReactiveProcessor { private static final int SECURITY_EXCEPTION_MAPPERS_PRIORITY = Priorities.USER + 1; private static final String[] EMPTY_STRING_ARRAY = new String[0]; + private static final DotName QUARKUS_TEST_MOCK = DotName.createSimple("io.quarkus.test.Mock"); + @BuildStep public FeatureBuildItem buildSetup() { return new FeatureBuildItem(Feature.REST); @@ -697,21 +699,23 @@ public Supplier apply(ClassInfo classInfo) { generatedClassBuildItemBuildProducer, applicationClassPredicate, reflectiveClassBuildItemBuildProducer)); serverEndpointIndexer = serverEndpointIndexerBuilder.build(); - Map> allMethods = new HashMap<>(); - for (ClassInfo i : scannedResources.values()) { - Optional endpoints = serverEndpointIndexer.createEndpoints(i, true); + Map> allServerMethods = new HashMap<>(); + for (ClassInfo ci : scannedResources.values()) { + Optional endpoints = serverEndpointIndexer.createEndpoints(ci, true); if (endpoints.isPresent()) { - if (singletonClasses.contains(i.name().toString())) { - endpoints.get().setFactory(new SingletonBeanFactory<>(i.name().toString())); + if (singletonClasses.contains(ci.name().toString())) { + endpoints.get().setFactory(new SingletonBeanFactory<>(ci.name().toString())); } resourceClasses.add(endpoints.get()); - for (ResourceMethod rm : endpoints.get().getMethods()) { - addResourceMethodByPath(allMethods, endpoints.get().getPath(), i, rm); + if (!ignoreResourceForDuplicateDetection(ci)) { + for (ResourceMethod rm : endpoints.get().getMethods()) { + addResourceMethodByPath(allServerMethods, endpoints.get().getPath(), ci, rm); + } } } } - checkForDuplicateEndpoint(config, allMethods); + checkForDuplicateEndpoint(config, allServerMethods); //now index possible sub resources. These are all classes that have method annotations //that are not annotated @Path @@ -782,6 +786,14 @@ public Supplier apply(ClassInfo classInfo) { handleDateFormatReflection(reflectiveClassBuildItemBuildProducer, index); } + // TODO: this is really just a hackish way of allowing the use of @Mock so we might need something better + private boolean ignoreResourceForDuplicateDetection(ClassInfo ci) { + if (ci.hasAnnotation(QUARKUS_TEST_MOCK)) { + return true; + } + return false; + } + private boolean filtersAccessResourceMethod(ResourceInterceptors resourceInterceptors) { AtomicBoolean ab = new AtomicBoolean(false); ResourceInterceptors.FiltersVisitor visitor = new ResourceInterceptors.FiltersVisitor() { diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/duplicate/DuplicateResourceAndClientTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/duplicate/DuplicateResourceAndClientTest.java new file mode 100644 index 0000000000000..69fdd14033620 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/duplicate/DuplicateResourceAndClientTest.java @@ -0,0 +1,59 @@ +package io.quarkus.resteasy.reactive.server.test.duplicate; + +import static io.restassured.RestAssured.when; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +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.Mock; +import io.quarkus.test.QuarkusUnitTest; + +public class DuplicateResourceAndClientTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Client.class, Resource.class)); + + @Test + public void dummy() { + when() + .get("/hello") + .then() + .statusCode(200); + } + + @Path("/hello") + public interface Client { + + @GET + @Produces(MediaType.TEXT_PLAIN) + String hello(); + } + + @Mock + public static class ClientMock implements Client { + + @Override + public String hello() { + return ""; + } + } + + @Path("/hello") + public static class Resource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "hello"; + } + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RepeatedPermissionsAllowedTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RepeatedPermissionsAllowedTest.java new file mode 100644 index 0000000000000..19e41ddfa6652 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/RepeatedPermissionsAllowedTest.java @@ -0,0 +1,113 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.PermissionsAllowed; +import io.quarkus.security.StringPermission; +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.json.JsonObject; + +public class RepeatedPermissionsAllowedTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestIdentityProvider.class, TestIdentityController.class, HelloResource.class) + .addAsResource( + new StringAsset( + "quarkus.log.category.\"io.quarkus.vertx.http.runtime.QuarkusErrorHandler\".level=OFF" + + System.lineSeparator()), + "application.properties")); + + @BeforeAll + public static void setupUsers() { + TestIdentityController.resetRoles() + .add("user", "user", new StringPermission("read")) + .add("admin", "admin", new StringPermission("read"), new StringPermission("write")); + } + + @Test + public void testRepeatedPermissionsAllowedOnClass() { + // anonymous user + RestAssured.given() + .body("{%$$#!#@") // assures checks are eager + .post("/hello") + .then() + .statusCode(401); + // authenticated user, insufficient rights + RestAssured.given() + .auth().preemptive().basic("user", "user") + .body("{%$$#!#@") // assures checks are eager + .post("/hello") + .then() + .statusCode(403); + // authorized user, invalid payload + RestAssured.given() + .auth().preemptive().basic("admin", "admin") + .body("{%$$#!#@") // assures checks are eager + .post("/hello") + .then() + .statusCode(500); + } + + @Test + public void testRepeatedPermissionsAllowedOnInterface() { + // anonymous user + RestAssured.given() + .body("{%$$#!#@") // assures checks are eager + .post("/hello-interface") + .then() + .statusCode(401); + // authenticated user, insufficient rights + RestAssured.given() + .auth().preemptive().basic("user", "user") + .body("{%$$#!#@") // assures checks are eager + .post("/hello-interface") + .then() + .statusCode(403); + // authorized user, invalid payload + RestAssured.given() + .auth().preemptive().basic("admin", "admin") + .body("{%$$#!#@") // assures checks are eager + .post("/hello-interface") + .then() + .statusCode(500); + } + + @Path("/hello") + public static class HelloResource { + + @PermissionsAllowed(value = "write") + @PermissionsAllowed(value = "read") + @POST + public String sayHello(JsonObject entity) { + return "ignored"; + } + } + + @Path("/hello-interface") + public interface HelloInterface { + + @PermissionsAllowed(value = "write") + @PermissionsAllowed(value = "read") + @POST + String sayHello(JsonObject entity); + } + + public static class HelloInterfaceImpl implements HelloInterface { + + @Override + public String sayHello(JsonObject entity) { + return "ignored"; + } + } +} diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/SecurityTransformerUtils.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/SecurityTransformerUtils.java index c85c497a3db9b..39ab0bf50c575 100644 --- a/extensions/security/spi/src/main/java/io/quarkus/security/spi/SecurityTransformerUtils.java +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/SecurityTransformerUtils.java @@ -23,6 +23,7 @@ public final class SecurityTransformerUtils { public static final DotName DENY_ALL = DotName.createSimple(DenyAll.class.getName()); private static final Set SECURITY_ANNOTATIONS = Set.of(DotName.createSimple(RolesAllowed.class.getName()), DotName.createSimple(PermissionsAllowed.class.getName()), + DotName.createSimple(PermissionsAllowed.List.class.getName()), DotName.createSimple(Authenticated.class.getName()), DotName.createSimple(DenyAll.class.getName()), DotName.createSimple(PermitAll.class.getName())); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java index ffee04566e460..368c57ec0a632 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java @@ -8,8 +8,6 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; import io.quarkus.deployment.IsDevelopment; @@ -126,7 +124,6 @@ public static void openBrowser(HttpRootPathBuildItem rp, NonApplicationRootPathB } StringBuilder sb = new StringBuilder("http://"); - Config c = ConfigProvider.getConfig(); sb.append(host); sb.append(":"); sb.append(port); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHandlerInitializer.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHandlerInitializer.java index ad8200b875184..429afe2aa5d6e 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHandlerInitializer.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHandlerInitializer.java @@ -19,6 +19,17 @@ public void register(@Observes Router router) { + "|" + rc.request().remoteAddress().toString() + "|" + rc.request().uri() + "|" + rc.request().absoluteURI())); + router.route("/trusted-proxy").handler(rc -> rc.response() + .end(rc.request().scheme() + "|" + rc.request().getHeader(HttpHeaders.HOST) + "|" + + rc.request().remoteAddress().toString() + + "|" + rc.request().getHeader("X-Forwarded-Trusted-Proxy"))); + router.route("/path-trusted-proxy").handler(rc -> rc.response() + .end(rc.request().scheme() + + "|" + rc.request().getHeader(HttpHeaders.HOST) + + "|" + rc.request().remoteAddress().toString() + + "|" + rc.request().uri() + + "|" + rc.request().absoluteURI() + + "|" + rc.request().getHeader("X-Forwarded-Trusted-Proxy"))); } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHeaderTest.java index 9fc8a8b841d73..bbf1e0af0d4f3 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHeaderTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHeaderTest.java @@ -30,6 +30,41 @@ public void test() { .body(Matchers.equalTo("https|somehost|backend:4444")); } + @Test + public void testWithoutTrustedProxyHeader() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|null")); + } + + @Test + public void testThatTrustedProxyHeaderCannotBeForged() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .header("X-Forwarded-Trusted-Proxy", "true") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|null")); + + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .header("X-Forwarded-Trusted-Proxy", "hello") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|null")); + + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .header("X-Forwarded-Trusted-Proxy", "false") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|null")); + } + @Test public void testForwardedForWithSequenceOfProxies() { assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/proxy/TrustedForwarderProxyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/proxy/TrustedForwarderProxyTest.java index d267b617b99d2..206dd9b192e6d 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/proxy/TrustedForwarderProxyTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/proxy/TrustedForwarderProxyTest.java @@ -1,5 +1,7 @@ package io.quarkus.vertx.http.proxy; +import static org.assertj.core.api.Assertions.assertThat; + import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.Test; @@ -31,6 +33,51 @@ public void testHeadersAreUsed() { .body(Matchers.equalTo("http|somehost2|backend2:5555|/path|http://somehost2/path")); } + @Test + public void testHeadersAreUsedWithTrustedProxyHeader() { + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .get("/path-trusted-proxy") + .then() + .body(Matchers + .equalTo("http|somehost2|backend2:5555|/path-trusted-proxy|http://somehost2/path-trusted-proxy|null")); + } + + @Test + public void testWithoutTrustedProxyHeader() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|null")); + } + + @Test + public void testThatTrustedProxyHeaderCannotBeForged() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .header("X-Forwarded-Trusted-Proxy", "true") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|null")); + + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .header("X-Forwarded-Trusted-Proxy", "hello") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|null")); + + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .header("X-Forwarded-Trusted-Proxy", "false") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|null")); + } + /** * As described on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded, diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/proxy/TrustedProxyHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/proxy/TrustedProxyHeaderTest.java new file mode 100644 index 0000000000000..3193855e8de7e --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/proxy/TrustedProxyHeaderTest.java @@ -0,0 +1,95 @@ +package io.quarkus.vertx.http.proxy; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.ForwardedHandlerInitializer; +import io.restassured.RestAssured; + +/** + * Test the trusted-proxy header + */ +public class TrustedProxyHeaderTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ForwardedHandlerInitializer.class) + .addAsResource(new StringAsset(""" + quarkus.http.proxy.proxy-address-forwarding=true + quarkus.http.proxy.allow-forwarded=true + quarkus.http.proxy.enable-forwarded-host=true + quarkus.http.proxy.enable-forwarded-prefix=true + quarkus.http.proxy.allow-forwarded=true + quarkus.http.proxy.enable-trusted-proxy-header=true + quarkus.http.proxy.trusted-proxies=localhost + """), + "application.properties")); + + @Test + public void testHeadersAreUsed() { + RestAssured.given() + .header("Forwarded", "proto=http;for=backend2:5555;host=somehost2") + .get("/path-trusted-proxy") + .then() + .body(Matchers + .equalTo("http|somehost2|backend2:5555|/path-trusted-proxy|http://somehost2/path-trusted-proxy|true")); + } + + @Test + public void testTrustedProxyHeader() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|true")); + } + + @Test + public void testThatTrustedProxyHeaderCannotBeForged() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .header("X-Forwarded-Trusted-Proxy", "true") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|true")); + + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .header("X-Forwarded-Trusted-Proxy", "hello") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|true")); + + RestAssured.given() + .header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https") + .header("X-Forwarded-Trusted-Proxy", "false") + .get("/trusted-proxy") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|true")); + } + + /** + * As described on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded, + * the syntax should be case-insensitive. + *

+ * Kong, for example, uses `Proto` instead of `proto` and `For` instead of `for`. + */ + @Test + public void testHeadersAreUsedWhenUsingCasedCharacters() { + RestAssured.given() + .header("Forwarded", "Proto=http;For=backend2:5555;Host=somehost2") + .get("/path-trusted-proxy") + .then() + .body(Matchers + .equalTo("http|somehost2|backend2:5555|/path-trusted-proxy|http://somehost2/path-trusted-proxy|true")); + } +} diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js index e255967f68e0e..a9a34fa93ca8e 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js @@ -293,7 +293,7 @@ export class QwcContinuousTesting extends QwcHotReloadElement { _renderStacktrace(stackTraces){ return html`${stackTraces.map((stackTrace) => html`
${stackTrace.className}#${stackTrace.methodName}(${stackTrace.fileName}:${stackTrace.lineNumber})` + lineNumber='${stackTrace.lineNumber}'>${stackTrace.className}#${stackTrace.methodName}(${stackTrace.fileName}:${stackTrace.lineNumber})` )}`; } @@ -329,8 +329,8 @@ export class QwcContinuousTesting extends QwcHotReloadElement { _testRenderer(testLine){ let level = testLine.testExecutionResult.status.toLowerCase(); - return html` + return html` + ${testLine.testClass} `; @@ -463,4 +463,4 @@ export class QwcContinuousTesting extends QwcHotReloadElement { this._displayTags = !this._displayTags; } } -customElements.define('qwc-continuous-testing', QwcContinuousTesting); \ No newline at end of file +customElements.define('qwc-continuous-testing', QwcContinuousTesting); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js index 5a3b9e8fcb2bc..c364e755d3be4 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js @@ -332,7 +332,7 @@ export class QwcServerLog extends QwcAbstractLogElement { return html`[${sourceClassNameFull}]`; + lineNumber='${sourceLineNumber}'>[${sourceClassNameFull}]`; } } @@ -341,7 +341,7 @@ export class QwcServerLog extends QwcAbstractLogElement { return html`[${sourceClassNameFullShort}]`; + lineNumber='${sourceLineNumber}'>[${sourceClassNameFullShort}]`; } } @@ -350,7 +350,7 @@ export class QwcServerLog extends QwcAbstractLogElement { return html`[${sourceClassName}]`; + lineNumber='${sourceLineNumber}'>[${sourceClassName}]`; } } @@ -365,7 +365,7 @@ export class QwcServerLog extends QwcAbstractLogElement { return html`${sourceFileName}`; + lineNumber='${sourceLineNumber}'>${sourceFileName}`; } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java index 95be0431ee84e..a8ead2e5b6e35 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java @@ -40,6 +40,7 @@ class ForwardedParser { private static final AsciiString X_FORWARDED_PROTO = AsciiString.cached("X-Forwarded-Proto"); private static final AsciiString X_FORWARDED_PORT = AsciiString.cached("X-Forwarded-Port"); private static final AsciiString X_FORWARDED_FOR = AsciiString.cached("X-Forwarded-For"); + private static final AsciiString X_FORWARDED_TRUSTED_PROXY = AsciiString.cached("X-Forwarded-Trusted-Proxy"); private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?", Pattern.CASE_INSENSITIVE); private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?", Pattern.CASE_INSENSITIVE); @@ -128,7 +129,8 @@ private void calculate() { setHostAndPort(delegate.host(), port); uri = delegate.uri(); - if (trustedProxyCheck.isProxyAllowed()) { + boolean isProxyAllowed = trustedProxyCheck.isProxyAllowed(); + if (isProxyAllowed) { String forwarded = delegate.getHeader(FORWARDED); if (forwardingProxyOptions.allowForwarded && forwarded != null) { Matcher matcher = FORWARDED_PROTO_PATTERN.matcher(forwarded); @@ -193,6 +195,21 @@ private void calculate() { authority = HostAndPort.create(host, port >= 0 ? port : -1); host = host + (port >= 0 ? ":" + port : ""); delegate.headers().set(HOST_HEADER, host); + // TODO Add a test + if (forwardingProxyOptions.enableTrustedProxyHeader) { + // Verify that the header was not already set. + if (delegate.headers().contains(X_FORWARDED_TRUSTED_PROXY)) { + log.warn("The header " + X_FORWARDED_TRUSTED_PROXY + " was already set. Overwriting it."); + } + delegate.headers().set(X_FORWARDED_TRUSTED_PROXY, Boolean.toString(isProxyAllowed)); + } else { + // Verify that the header was not already set - to avoid forgery. + if (delegate.headers().contains(X_FORWARDED_TRUSTED_PROXY)) { + log.warn("The header " + X_FORWARDED_TRUSTED_PROXY + " was already set. Removing it."); + delegate.headers().remove(X_FORWARDED_TRUSTED_PROXY); + } + } + absoluteURI = scheme + "://" + host + uri; log.debug("Recalculated absoluteURI to " + absoluteURI); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java index e7c0cf032a6fb..23aafa044f1f7 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java @@ -15,14 +15,16 @@ public class ForwardingProxyOptions { final AsciiString forwardedHostHeader; final AsciiString forwardedPrefixHeader; public final TrustedProxyCheckBuilder trustedProxyCheckBuilder; + final boolean enableTrustedProxyHeader; public ForwardingProxyOptions(final boolean proxyAddressForwarding, - final boolean allowForwarded, - final boolean allowXForwarded, - final boolean enableForwardedHost, - final AsciiString forwardedHostHeader, - final boolean enableForwardedPrefix, - final AsciiString forwardedPrefixHeader, + boolean allowForwarded, + boolean allowXForwarded, + boolean enableForwardedHost, + boolean enableTrustedProxyHeader, + AsciiString forwardedHostHeader, + boolean enableForwardedPrefix, + AsciiString forwardedPrefixHeader, TrustedProxyCheckBuilder trustedProxyCheckBuilder) { this.proxyAddressForwarding = proxyAddressForwarding; this.allowForwarded = allowForwarded; @@ -32,15 +34,16 @@ public ForwardingProxyOptions(final boolean proxyAddressForwarding, this.forwardedHostHeader = forwardedHostHeader; this.forwardedPrefixHeader = forwardedPrefixHeader; this.trustedProxyCheckBuilder = trustedProxyCheckBuilder; + this.enableTrustedProxyHeader = enableTrustedProxyHeader; } public static ForwardingProxyOptions from(ProxyConfig proxy) { final boolean proxyAddressForwarding = proxy.proxyAddressForwarding; final boolean allowForwarded = proxy.allowForwarded; final boolean allowXForwarded = proxy.allowXForwarded.orElse(!allowForwarded); - final boolean enableForwardedHost = proxy.enableForwardedHost; final boolean enableForwardedPrefix = proxy.enableForwardedPrefix; + final boolean enableTrustedProxyHeader = proxy.enableTrustedProxyHeader; final AsciiString forwardedPrefixHeader = AsciiString.cached(proxy.forwardedPrefixHeader); final AsciiString forwardedHostHeader = AsciiString.cached(proxy.forwardedHostHeader); @@ -50,6 +53,7 @@ public static ForwardingProxyOptions from(ProxyConfig proxy) { || parts.isEmpty() ? null : TrustedProxyCheckBuilder.builder(parts); return new ForwardingProxyOptions(proxyAddressForwarding, allowForwarded, allowXForwarded, enableForwardedHost, - forwardedHostHeader, enableForwardedPrefix, forwardedPrefixHeader, proxyCheckBuilder); + enableTrustedProxyHeader, forwardedHostHeader, enableForwardedPrefix, forwardedPrefixHeader, + proxyCheckBuilder); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java index f4a2f557c796b..210fe6ddfb1ba 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java @@ -76,6 +76,18 @@ public class ProxyConfig { @ConfigItem(defaultValue = "X-Forwarded-Prefix") public String forwardedPrefixHeader; + /** + * Adds the header `X-Forwarded-Trusted-Proxy` if the request is forwarded by a trusted proxy. + * The value is `true` if the request is forwarded by a trusted proxy, otherwise `null`. + *

+ * The forwarded parser detects forgery attempts and if the incoming request contains this header, it will be removed + * from the request. + *

+ * The `X-Forwarded-Trusted-Proxy` header is a custom header, not part of the standard `Forwarded` header. + */ + @ConfigItem(defaultValue = "false") + public boolean enableTrustedProxyHeader; + /** * Configure the list of trusted proxy addresses. * Received `Forwarded`, `X-Forwarded` or `X-Forwarded-*` headers from any other proxy address will be ignored. diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/BasicConnectorContextTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/BasicConnectorContextTest.java new file mode 100644 index 0000000000000..fcddf32bf6ffa --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/BasicConnectorContextTest.java @@ -0,0 +1,89 @@ +package io.quarkus.websockets.next.test.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.BasicWebSocketConnector; +import io.quarkus.websockets.next.OnClose; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketClientConnection; + +public class BasicConnectorContextTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(ServerEndpoint.class); + }); + + @TestHTTPResource("/end") + URI uri; + + static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(2); + + static final Set THREADS = ConcurrentHashMap.newKeySet(); + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2); + + @Test + void testClient() throws InterruptedException { + BasicWebSocketConnector connector = BasicWebSocketConnector.create(); + connector + .executionModel(BasicWebSocketConnector.ExecutionModel.NON_BLOCKING) + .onTextMessage((c, m) -> { + String thread = Thread.currentThread().getName(); + THREADS.add(thread); + MESSAGE_LATCH.countDown(); + }) + .onClose((c, cr) -> { + CLOSED_LATCH.countDown(); + }) + .baseUri(uri); + WebSocketClientConnection conn1 = connector.connectAndAwait(); + WebSocketClientConnection conn2 = connector.connectAndAwait(); + assertTrue(MESSAGE_LATCH.await(10, TimeUnit.SECONDS)); + if (Runtime.getRuntime().availableProcessors() > 1) { + // Each client should be executed on a dedicated event loop thread + assertEquals(2, THREADS.size()); + } else { + // Single core - the event pool is shared + // Due to some CI weirdness it might happen that the system incorrectly reports single core + // Therefore, the assert checks if the number of threads used is >= 1 + assertTrue(THREADS.size() >= 1); + } + conn1.closeAndAwait(); + conn2.closeAndAwait(); + assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + } + + @WebSocket(path = "/end") + public static class ServerEndpoint { + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1); + + @OnOpen + String open() { + return "Hello!"; + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientContextTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientContextTest.java new file mode 100644 index 0000000000000..0ea1a055434e5 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/ClientContextTest.java @@ -0,0 +1,104 @@ +package io.quarkus.websockets.next.test.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnClose; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketClient; +import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.WebSocketConnector; +import io.smallrye.mutiny.Uni; + +public class ClientContextTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(ServerEndpoint.class, ClientEndpoint.class); + }); + + @Inject + WebSocketConnector connector; + + @TestHTTPResource("/") + URI uri; + + @Test + void testClient() throws InterruptedException { + connector.baseUri(uri); + WebSocketClientConnection conn1 = connector.connectAndAwait(); + WebSocketClientConnection conn2 = connector.connectAndAwait(); + assertTrue(ClientEndpoint.MESSAGE_LATCH.await(10, TimeUnit.SECONDS)); + if (Runtime.getRuntime().availableProcessors() > 1) { + // Each client should be executed on a dedicated event loop thread + assertEquals(2, ClientEndpoint.THREADS.size()); + } else { + // Single core - the event pool is shared + // Due to some CI weirdness it might happen that the system incorrectly reports single core + // Therefore, the assert checks if the number of threads used is >= 1 + assertTrue(ClientEndpoint.THREADS.size() >= 1); + } + conn1.closeAndAwait(); + conn2.closeAndAwait(); + assertTrue(ClientEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + } + + @WebSocket(path = "/end") + public static class ServerEndpoint { + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1); + + @OnOpen + String open() { + return "Hello!"; + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + + } + + @WebSocketClient(path = "/end") + public static class ClientEndpoint { + + static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(2); + + static final Set THREADS = ConcurrentHashMap.newKeySet(); + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2); + + @OnTextMessage + Uni onMessage(String message) { + String thread = Thread.currentThread().getName(); + THREADS.add(thread); + MESSAGE_LATCH.countDown(); + return Uni.createFrom().voidItem(); + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + + } + +} diff --git a/extensions/websockets-next/pom.xml b/extensions/websockets-next/pom.xml index 1e149d244734f..623739b4ed3c7 100644 --- a/extensions/websockets-next/pom.xml +++ b/extensions/websockets-next/pom.xml @@ -12,6 +12,8 @@ quarkus-websockets-next-parent Quarkus - WebSockets Next + Use a modern declarative API to define WebSocket server and client endpoints + pom deployment diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java index b9e3af7e7395f..bf6dd1044e0bf 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java @@ -6,6 +6,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -22,12 +23,16 @@ import io.quarkus.websockets.next.WebSocketClientException; import io.quarkus.websockets.next.WebSocketsClientRuntimeConfig; import io.smallrye.mutiny.Uni; +import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; import io.vertx.core.http.WebSocketConnectOptions; +import io.vertx.core.impl.ContextImpl; +import io.vertx.core.impl.VertxImpl; @Typed(BasicWebSocketConnector.class) @Dependent @@ -111,10 +116,10 @@ public Uni connect() { throw new WebSocketClientException("Endpoint URI not set!"); } - // Currently we create a new client for each connection + // A new client is created for each connection + // The client is created when the returned Uni is subscribed // The client is closed when the connection is closed - // TODO would it make sense to share clients? - WebSocketClient client = vertx.createWebSocketClient(populateClientOptions()); + AtomicReference client = new AtomicReference<>(); WebSocketConnectOptions connectOptions = newConnectOptions(baseUri); StringBuilder requestUri = new StringBuilder(); @@ -140,87 +145,110 @@ public Uni connect() { throw new WebSocketClientException(e); } - return Uni.createFrom().completionStage(() -> client.connect(connectOptions).toCompletionStage()) - .map(ws -> { - String clientId = BasicWebSocketConnector.class.getName(); - TrafficLogger trafficLogger = TrafficLogger.forClient(config); - WebSocketClientConnectionImpl connection = new WebSocketClientConnectionImpl(clientId, ws, - codecs, - pathParams, - serverEndpointUri, - headers, trafficLogger); - if (trafficLogger != null) { - trafficLogger.connectionOpened(connection); - } - connectionManager.add(BasicWebSocketConnectorImpl.class.getName(), connection); + Uni websocket = Uni.createFrom(). emitter(e -> { + // Create a new event loop context for each client, otherwise the current context is used + // We want to avoid a situation where if multiple clients/connections are created in a row, + // the same event loop is used and so writing/receiving messages is de-facto serialized + // Get rid of this workaround once https://github.com/eclipse-vertx/vert.x/issues/5366 is resolved + ContextImpl context = ((VertxImpl) vertx).createEventLoopContext(); + context.dispatch(new Handler() { + @Override + public void handle(Void event) { + WebSocketClient c = vertx.createWebSocketClient(populateClientOptions()); + client.setPlain(c); + c.connect(connectOptions, new Handler>() { + @Override + public void handle(AsyncResult r) { + if (r.succeeded()) { + e.complete(r.result()); + } else { + e.fail(r.cause()); + } + } + }); + } + }); + }); + return websocket.map(ws -> { + String clientId = BasicWebSocketConnector.class.getName(); + TrafficLogger trafficLogger = TrafficLogger.forClient(config); + WebSocketClientConnectionImpl connection = new WebSocketClientConnectionImpl(clientId, ws, + codecs, + pathParams, + serverEndpointUri, + headers, trafficLogger); + if (trafficLogger != null) { + trafficLogger.connectionOpened(connection); + } + connectionManager.add(BasicWebSocketConnectorImpl.class.getName(), connection); - if (openHandler != null) { - doExecute(connection, null, (c, ignored) -> openHandler.accept(c)); - } + if (openHandler != null) { + doExecute(connection, null, (c, ignored) -> openHandler.accept(c)); + } - if (textMessageHandler != null) { - ws.textMessageHandler(new Handler() { - @Override - public void handle(String message) { - if (trafficLogger != null) { - trafficLogger.textMessageReceived(connection, message); - } - doExecute(connection, message, textMessageHandler); - } - }); + if (textMessageHandler != null) { + ws.textMessageHandler(new Handler() { + @Override + public void handle(String message) { + if (trafficLogger != null) { + trafficLogger.textMessageReceived(connection, message); + } + doExecute(connection, message, textMessageHandler); } + }); + } - if (binaryMessageHandler != null) { - ws.binaryMessageHandler(new Handler() { + if (binaryMessageHandler != null) { + ws.binaryMessageHandler(new Handler() { - @Override - public void handle(Buffer message) { - if (trafficLogger != null) { - trafficLogger.binaryMessageReceived(connection, message); - } - doExecute(connection, message, binaryMessageHandler); - } - }); + @Override + public void handle(Buffer message) { + if (trafficLogger != null) { + trafficLogger.binaryMessageReceived(connection, message); + } + doExecute(connection, message, binaryMessageHandler); } + }); + } - if (pongMessageHandler != null) { - ws.pongHandler(new Handler() { + if (pongMessageHandler != null) { + ws.pongHandler(new Handler() { - @Override - public void handle(Buffer event) { - doExecute(connection, event, pongMessageHandler); - } - }); + @Override + public void handle(Buffer event) { + doExecute(connection, event, pongMessageHandler); } + }); + } - if (errorHandler != null) { - ws.exceptionHandler(new Handler() { + if (errorHandler != null) { + ws.exceptionHandler(new Handler() { - @Override - public void handle(Throwable event) { - doExecute(connection, event, errorHandler); - } - }); + @Override + public void handle(Throwable event) { + doExecute(connection, event, errorHandler); } + }); + } - ws.closeHandler(new Handler() { + ws.closeHandler(new Handler() { - @Override - public void handle(Void event) { - if (trafficLogger != null) { - trafficLogger.connectionClosed(connection); - } - if (closeHandler != null) { - doExecute(connection, new CloseReason(ws.closeStatusCode(), ws.closeReason()), closeHandler); - } - connectionManager.remove(BasicWebSocketConnectorImpl.class.getName(), connection); - client.close(); - } + @Override + public void handle(Void event) { + if (trafficLogger != null) { + trafficLogger.connectionClosed(connection); + } + if (closeHandler != null) { + doExecute(connection, new CloseReason(ws.closeStatusCode(), ws.closeReason()), closeHandler); + } + connectionManager.remove(BasicWebSocketConnectorImpl.class.getName(), connection); + client.get().close(); + } - }); + }); - return connection; - }); + return connection; + }); } private void doExecute(WebSocketClientConnectionImpl connection, MESSAGE message, diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java index 686f132c71038..185c47fbcd59c 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectorImpl.java @@ -7,6 +7,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Typed; @@ -23,9 +24,14 @@ import io.quarkus.websockets.next.runtime.WebSocketClientRecorder.ClientEndpoint; import io.quarkus.websockets.next.runtime.WebSocketClientRecorder.ClientEndpointsContext; import io.smallrye.mutiny.Uni; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.http.WebSocket; import io.vertx.core.http.WebSocketClient; import io.vertx.core.http.WebSocketConnectOptions; +import io.vertx.core.impl.ContextImpl; +import io.vertx.core.impl.VertxImpl; @Typed(WebSocketConnector.class) @Dependent @@ -46,10 +52,10 @@ public class WebSocketConnectorImpl extends WebSocketConnectorBase connect() { - // Currently we create a new client for each connection + // A new client is created for each connection + // The client is created when the returned Uni is subscribed // The client is closed when the connection is closed - // TODO would it make sense to share clients? - WebSocketClient client = vertx.createWebSocketClient(populateClientOptions()); + AtomicReference client = new AtomicReference<>(); StringBuilder serverEndpoint = new StringBuilder(); if (baseUri != null) { @@ -88,28 +94,51 @@ public Uni connect() { } subprotocols.forEach(connectOptions::addSubProtocol); - return Uni.createFrom().completionStage(() -> client.connect(connectOptions).toCompletionStage()) - .map(ws -> { - TrafficLogger trafficLogger = TrafficLogger.forClient(config); - WebSocketClientConnectionImpl connection = new WebSocketClientConnectionImpl(clientEndpoint.clientId, ws, - codecs, - pathParams, - serverEndpointUri, headers, trafficLogger); - if (trafficLogger != null) { - trafficLogger.connectionOpened(connection); - } - connectionManager.add(clientEndpoint.generatedEndpointClass, connection); - - Endpoints.initialize(vertx, Arc.container(), codecs, connection, ws, - clientEndpoint.generatedEndpointClass, config.autoPingInterval(), SecuritySupport.NOOP, - config.unhandledFailureStrategy(), trafficLogger, - () -> { - connectionManager.remove(clientEndpoint.generatedEndpointClass, connection); - client.close(); - }); - - return connection; - }); + Uni websocket = Uni.createFrom(). emitter(e -> { + // Create a new event loop context for each client, otherwise the current context is used + // We want to avoid a situation where if multiple clients/connections are created in a row, + // the same event loop is used and so writing/receiving messages is de-facto serialized + // Get rid of this workaround once https://github.com/eclipse-vertx/vert.x/issues/5366 is resolved + ContextImpl context = ((VertxImpl) vertx).createEventLoopContext(); + context.dispatch(new Handler() { + @Override + public void handle(Void event) { + WebSocketClient c = vertx.createWebSocketClient(populateClientOptions()); + client.setPlain(c); + c.connect(connectOptions, new Handler>() { + @Override + public void handle(AsyncResult r) { + if (r.succeeded()) { + e.complete(r.result()); + } else { + e.fail(r.cause()); + } + } + }); + } + }); + }); + return websocket.map(ws -> { + TrafficLogger trafficLogger = TrafficLogger.forClient(config); + WebSocketClientConnectionImpl connection = new WebSocketClientConnectionImpl(clientEndpoint.clientId, ws, + codecs, + pathParams, + serverEndpointUri, headers, trafficLogger); + if (trafficLogger != null) { + trafficLogger.connectionOpened(connection); + } + connectionManager.add(clientEndpoint.generatedEndpointClass, connection); + + Endpoints.initialize(vertx, Arc.container(), codecs, connection, ws, + clientEndpoint.generatedEndpointClass, config.autoPingInterval(), SecuritySupport.NOOP, + config.unhandledFailureStrategy(), trafficLogger, + () -> { + connectionManager.remove(clientEndpoint.generatedEndpointClass, connection); + client.get().close(); + }); + + return connection; + }); } String getEndpointClass(InjectionPoint injectionPoint) { diff --git a/independent-projects/extension-maven-plugin/pom.xml b/independent-projects/extension-maven-plugin/pom.xml index a4593cf828fc8..07c4e4ec89003 100644 --- a/independent-projects/extension-maven-plugin/pom.xml +++ b/independent-projects/extension-maven-plugin/pom.xml @@ -38,7 +38,7 @@ 11 11 3.9.9 - 2.18.0 + 2.18.1 1.5.2 5.10.5 diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/FragmentSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/FragmentSectionHelper.java index 0fb1a948b9fe1..dcb28af105470 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/FragmentSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/FragmentSectionHelper.java @@ -19,12 +19,15 @@ public class FragmentSectionHelper implements SectionHelper { private static final String ID = "id"; + // the generated id of the template that declares this fragment section + private final String generatedTemplateId; private final String identifier; private final Expression rendered; - FragmentSectionHelper(String identifier, Expression isVisible) { + FragmentSectionHelper(String identifier, Expression rendered, String generatedTemplateId) { this.identifier = identifier; - this.rendered = isVisible; + this.rendered = rendered; + this.generatedTemplateId = generatedTemplateId; } public String getIdentifier() { @@ -33,11 +36,7 @@ public String getIdentifier() { @Override public CompletionStage resolve(SectionResolutionContext context) { - if (rendered == null - // executed from an include section - || context.getParameters().containsKey(Template.Fragment.ATTRIBUTE) - // the attribute is set if executed separately via Template.Fragment - || context.resolutionContext().getAttribute(Fragment.ATTRIBUTE) != null) { + if (isAlwaysExecuted(context)) { return context.execute(); } return context.resolutionContext().evaluate(rendered).thenCompose(r -> { @@ -45,6 +44,17 @@ public CompletionStage resolve(SectionResolutionContext context) { }); } + private boolean isAlwaysExecuted(SectionResolutionContext context) { + if (rendered == null + // executed from an include section + || context.getParameters().containsKey(Fragment.ATTRIBUTE)) { + return true; + } + Object attribute = context.resolutionContext().getAttribute(Fragment.ATTRIBUTE); + // the attribute is set if executed separately via Template.Fragment + return attribute != null && attribute.equals(generatedTemplateId + identifier); + } + public static class Factory implements SectionHelperFactory { static final Pattern FRAGMENT_PATTERN = Pattern.compile("[a-zA-Z0-9_]+"); @@ -99,7 +109,7 @@ public FragmentSectionHelper initialize(SectionInitContext context) { .build(); } } - return new FragmentSectionHelper(id, context.getExpression(RENDERED)); + return new FragmentSectionHelper(id, context.getExpression(RENDERED), generatedId); } @Override diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java index 7d787d0efde55..0396b248d2393 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java @@ -391,7 +391,9 @@ public Template getOriginalTemplate() { @Override public TemplateInstance instance() { TemplateInstance instance = super.instance(); - instance.setAttribute(Fragment.ATTRIBUTE, true); + // when a fragment is executed separately we need a way to instruct FragmentSectionHelper to ignore the "renreded" parameter + // Fragment.ATTRIBUTE contains the generated id of the template that declares the fragment section and the fragment identifier + instance.setAttribute(Fragment.ATTRIBUTE, TemplateImpl.this.getGeneratedId() + FragmentImpl.this.getId()); return instance; } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/FragmentTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/FragmentTest.java index 020877d2ac5d5..2bf58e9605318 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/FragmentTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/FragmentTest.java @@ -77,4 +77,41 @@ public void testInvalidId() { expected.getMessage()); } + @Test + public void testNestedFragmentRendered() { + Engine engine = Engine.builder().addDefaults().build(); + Template alpha = engine.parse(""" + OK + {#fragment id=\"nested\" rendered=false} + NOK + {/} + {#fragment id=\"visible\"} + 01 + {/fragment} + """); + engine.putTemplate("alpha", alpha); + assertEquals("OK01", alpha.render().replaceAll("\\s", "")); + assertEquals("NOK", alpha.getFragment("nested").render().trim()); + + Template bravo = engine.parse(""" + {#include $nested} + {#fragment id=\"nested\" rendered=false} + OK + {/} + """); + assertEquals("OK", bravo.render().trim()); + assertEquals("OK", bravo.getFragment("nested").render().trim()); + + assertEquals("NOK", engine.parse("{#include alpha$nested /}").render().trim()); + Template charlie = engine.parse("{#include alpha /}"); + assertEquals("OK01", charlie.render().replaceAll("\\s", "")); + + Template delta = engine.parse(""" + {#fragment id=\"nested\" rendered=false} + {#include alpha /} + {/} + """); + assertEquals("OK01", delta.getFragment("nested").render().replaceAll("\\s", "")); + } + } diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/HasPriority.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/HasPriority.java index 4d4d4d2c4b2a8..932892a4d83aa 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/HasPriority.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/HasPriority.java @@ -14,6 +14,13 @@ class TreeMapComparator implements Comparator { public static final TreeMapComparator INSTANCE = new TreeMapComparator(); + public static final TreeMapComparator REVERSED = new TreeMapComparator() { + @Override + public int compare(HasPriority o1, HasPriority o2) { + return super.compare(o2, o1); + } + }; + @Override public int compare(HasPriority o1, HasPriority o2) { int res = o1.priority().compareTo(o2.priority()); 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 b4bdd4e8f09f0..d31dbe4a1caab 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 @@ -95,7 +95,6 @@ public void setRuntimeType(RuntimeType runtimeType) { this.runtimeType = runtimeType; } - // spec says that writer interceptors are sorted in ascending order @Override public int compareTo(ResourceInterceptor o) { return this.priority().compareTo(o.priority()); @@ -105,12 +104,8 @@ public int compareTo(ResourceInterceptor o) { public static class Reversed extends ResourceInterceptor { @Override - public Integer priority() { - Integer p = super.priority(); - if (p == null) { - return null; - } - return -p; + public int compareTo(ResourceInterceptor o) { + return o.priority().compareTo(this.priority()); } } } diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index bd77fb12d9287..c1ad03065add4 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -61,7 +61,7 @@ 4.5.9 5.5.0 1.0.0.Final - 2.18.0 + 2.18.1 2.7.0 3.0.2 3.0.4 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 7679f9e2d0c44..9511aca57fbcd 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 @@ -173,7 +173,8 @@ TreeMap, T> buildInterceptorMap( Map, T> globalInterceptorsMap, Map, T> nameInterceptorsMap, Map, T> methodSpecificInterceptorsMap, ResourceMethod method, boolean reversed) { - TreeMap, T> interceptorsToUse = new TreeMap<>(HasPriority.TreeMapComparator.INSTANCE); + TreeMap, T> interceptorsToUse = new TreeMap<>( + reversed ? HasPriority.TreeMapComparator.REVERSED : HasPriority.TreeMapComparator.INSTANCE); interceptorsToUse.putAll(globalInterceptorsMap); interceptorsToUse.putAll(methodSpecificInterceptorsMap); for (ResourceInterceptor nameInterceptor : nameInterceptorsMap.keySet()) { diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/simple/MultipleResponseFiltersWithPrioritiesTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/simple/MultipleResponseFiltersWithPrioritiesTest.java new file mode 100644 index 0000000000000..74586aa63371c --- /dev/null +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/simple/MultipleResponseFiltersWithPrioritiesTest.java @@ -0,0 +1,122 @@ +package org.jboss.resteasy.reactive.server.vertx.test.simple; + +import static io.restassured.RestAssured.*; +import static io.restassured.RestAssured.when; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; + +import org.jboss.resteasy.reactive.server.vertx.test.CookiesSetInFilterTest; +import org.jboss.resteasy.reactive.server.vertx.test.framework.ResteasyReactiveUnitTest; +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.restassured.http.Headers; + +public class MultipleResponseFiltersWithPrioritiesTest { + + @RegisterExtension + static ResteasyReactiveUnitTest test = new ResteasyReactiveUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(CookiesSetInFilterTest.TestResource.class, CookiesSetInFilterTest.Filters.class)); + + @Test + void requestDoesNotContainCookie() { + when().get("/test") + .then() + .statusCode(200) + .body(is("foo")); + } + + @Test + void test() { + Headers headers = get("/hello") + .then() + .statusCode(200) + .extract().headers(); + assertThat(headers.getValues("filter-response")).containsOnly("max-default-0-minPlus1-min"); + } + + @Path("hello") + public static class TestResource { + + @GET + public String get() { + return "hello"; + } + } + + @Provider + @Priority(Integer.MAX_VALUE) + public static class FilterMax implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + responseContext.getHeaders().putSingle("filter-response", "max"); + } + + } + + @Provider + public static class FilterDefault implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + String previousFilterHeaderValue = (String) responseContext.getHeaders().getFirst("filter-response"); + responseContext.getHeaders().putSingle("filter-response", previousFilterHeaderValue + "-default"); + } + + } + + @Provider + @Priority(0) + public static class Filter0 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + String previousFilterHeaderValue = (String) responseContext.getHeaders().getFirst("filter-response"); + responseContext.getHeaders().putSingle("filter-response", previousFilterHeaderValue + "-0"); + } + + } + + @Provider + @Priority(Integer.MIN_VALUE + 1) + public static class FilterMinPlus1 implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + String previousFilterHeaderValue = (String) responseContext.getHeaders().getFirst("filter-response"); + responseContext.getHeaders().putSingle("filter-response", previousFilterHeaderValue + "-minPlus1"); + } + + } + + @Provider + @Priority(Integer.MIN_VALUE) + public static class FilterMin implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + String previousFilterHeaderValue = (String) responseContext.getHeaders().getFirst("filter-response"); + responseContext.getHeaders().putSingle("filter-response", previousFilterHeaderValue + "-min"); + } + + } +} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/deploy-snapshots.tpl.qute.yml.disabled b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/deploy-snapshots.tpl.qute.yml.disabled deleted file mode 100644 index 83b24dde6d2e4..0000000000000 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/deploy-snapshots.tpl.qute.yml.disabled +++ /dev/null @@ -1,40 +0,0 @@ -# This workflow will build and deploy a snapshot of your artifact to Sonatype Snapshots repository -name: Deploy Snapshots - -concurrency: - group: ${{ github.ref }}-${{ github.workflow }} - cancel-in-progress: true -on: - workflow_dispatch: - push: - branches: [ main ] - -defaults: - run: - shell: bash - -jobs: - deploy-snapshot: - runs-on: ubuntu-latest - name: Deploy Snapshot artifacts - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: {java.version} - cache: 'maven' - server-id: 'ossrh' - server-username: MAVEN_USERNAME - server-password: MAVEN_PASSWORD - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg-passphrase: MAVEN_GPG_PASSPHRASE - - - name: Deploy Snapshot - run: | - mvn -B clean deploy -DperformRelease=true -Drelease - env: - MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} - MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 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 41437b50048d2..850175e64281d 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 @@ -87,7 +87,7 @@ 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 = "17"; + public static final String DEFAULT_QUARKIVERSE_PARENT_VERSION = "18"; public static final String DEFAULT_QUARKIVERSE_NAMESPACE_ID = "quarkus-"; public static final String DEFAULT_QUARKIVERSE_GUIDE_URL = "https://docs.quarkiverse.io/%s/dev/"; diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdatesRepository.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdatesRepository.java index 9ced7ecc63129..1c6b8d4ea397e 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdatesRepository.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdatesRepository.java @@ -4,7 +4,16 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -26,6 +35,8 @@ private QuarkusUpdatesRepository() { } private static final String QUARKUS_UPDATE_RECIPES_GA = "io.quarkus:quarkus-update-recipes"; + private static final Pattern VERSION_EXTRACTION_PATTERN = Pattern.compile("[.][^.]+$"); + public static final String DEFAULT_UPDATE_RECIPES_VERSION = "LATEST"; public static final String DEFAULT_MAVEN_REWRITE_PLUGIN_VERSION = "4.46.0"; @@ -46,7 +57,7 @@ public static FetchResult fetchRecipes(MessageWriter log, MavenArtifactResolver } List artifacts = new ArrayList<>(); - Map recipes = new HashMap<>(); + Map recipes = new LinkedHashMap<>(); String propRewritePluginVersion = null; for (String gav : gavs) { @@ -147,7 +158,7 @@ public String getRewritePluginVersion() { } static boolean shouldApplyRecipe(String recipeFileName, String currentVersion, String targetVersion) { - String recipeVersion = recipeFileName.replaceFirst("[.][^.]+$", ""); + String recipeVersion = VERSION_EXTRACTION_PATTERN.matcher(recipeFileName).replaceFirst(""); final DefaultArtifactVersion recipeAVersion = new DefaultArtifactVersion(recipeVersion); final DefaultArtifactVersion currentAVersion = new DefaultArtifactVersion(currentVersion); final DefaultArtifactVersion targetAVersion = new DefaultArtifactVersion(targetVersion); @@ -172,6 +183,7 @@ static Map fetchUpdateRecipes(ResourceLoader resourceLoader, Str .matches("^\\d\\H+.ya?ml$")) .filter(p -> shouldApplyRecipe(p.getFileName().toString(), versions[0], versions[1])) + .sorted(RecipeVersionComparator.INSTANCE) .map(p -> { try { return new String[] { p.toString(), @@ -231,4 +243,18 @@ static List applyStartsWith(String key, Map recipeDire .collect(Collectors.toList()); } + private static class RecipeVersionComparator implements Comparator { + + private static final RecipeVersionComparator INSTANCE = new RecipeVersionComparator(); + + @Override + public int compare(Path recipePath1, Path recipePath2) { + DefaultArtifactVersion recipeVersion1 = new DefaultArtifactVersion( + VERSION_EXTRACTION_PATTERN.matcher(recipePath1.getFileName().toString()).replaceFirst("")); + DefaultArtifactVersion recipeVersion2 = new DefaultArtifactVersion( + VERSION_EXTRACTION_PATTERN.matcher(recipePath2.getFileName().toString()).replaceFirst("")); + + return recipeVersion1.compareTo(recipeVersion2); + } + } } diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index e55be8216067a..8a71188735391 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -49,7 +49,7 @@ 3.26.3 - 2.18.0 + 2.18.1 4.1.0 5.10.5 1.27.1 diff --git a/integration-tests/flyway/src/main/java/io/quarkus/it/flyway/FlywayFunctionalityResource.java b/integration-tests/flyway/src/main/java/io/quarkus/it/flyway/FlywayFunctionalityResource.java index 5c72b0b721dad..648c69c9c748c 100644 --- a/integration-tests/flyway/src/main/java/io/quarkus/it/flyway/FlywayFunctionalityResource.java +++ b/integration-tests/flyway/src/main/java/io/quarkus/it/flyway/FlywayFunctionalityResource.java @@ -71,7 +71,7 @@ public String returnInitSql() { @GET @Path("init-sql-result") public Integer returnInitSqlResult() { - return (Integer) entityManager.createNativeQuery("SELECT f_my_constant()") + return (Integer) entityManager.createNativeQuery("SELECT TEST_SCHEMA.f_my_constant()") .getSingleResult(); } diff --git a/integration-tests/flyway/src/main/resources/application.properties b/integration-tests/flyway/src/main/resources/application.properties index 01118fbc3f0cc..141e7e7b3149a 100644 --- a/integration-tests/flyway/src/main/resources/application.properties +++ b/integration-tests/flyway/src/main/resources/application.properties @@ -22,7 +22,7 @@ quarkus.flyway.placeholders.foo=bar quarkus.flyway.placeholders.title=REPLACED quarkus.flyway.placeholder-prefix=#[ quarkus.flyway.placeholder-suffix=] -quarkus.flyway.init-sql=CREATE SCHEMA IF NOT EXISTS TEST_SCHEMA;CREATE OR REPLACE FUNCTION f_my_constant() RETURNS integer LANGUAGE plpgsql as $func$ BEGIN return 100; END $func$; +quarkus.flyway.init-sql=CREATE SCHEMA IF NOT EXISTS TEST_SCHEMA;CREATE OR REPLACE FUNCTION TEST_SCHEMA.f_my_constant() RETURNS integer LANGUAGE plpgsql as $func$ BEGIN return 100; END $func$; quarkus.hibernate-orm.database.generation=validate # second Agroal config @@ -33,4 +33,4 @@ quarkus.flyway.second-datasource.locations=db/location3 quarkus.flyway.second-datasource.sql-migration-prefix=V quarkus.flyway.second-datasource.migrate-at-start=true quarkus.flyway.second-datasource.placeholders.mambo=poa - +quarkus.flyway.second-datasource.init-sql=CREATE SCHEMA IF NOT EXISTS TEST_SCHEMA; diff --git a/integration-tests/flyway/src/main/resources/db/location3/V1.0.0__Quarkus.sql b/integration-tests/flyway/src/main/resources/db/location3/V1.0.0__Quarkus.sql index fb341850919bf..3d9d44eed5768 100644 --- a/integration-tests/flyway/src/main/resources/db/location3/V1.0.0__Quarkus.sql +++ b/integration-tests/flyway/src/main/resources/db/location3/V1.0.0__Quarkus.sql @@ -1,7 +1,8 @@ -CREATE TABLE multiple_flyway_test + +CREATE TABLE TEST_SCHEMA.multiple_flyway_test ( id INT, name VARCHAR(255) ); -INSERT INTO multiple_flyway_test(id, name) +INSERT INTO TEST_SCHEMA.multiple_flyway_test(id, name) VALUES (1, 'Multiple flyway datasources should work seamlessly in JVM and native mode'); \ No newline at end of file diff --git a/integration-tests/flyway/src/main/resources/db/location3/afterMigrate.sql b/integration-tests/flyway/src/main/resources/db/location3/afterMigrate.sql index aa8276f50e4b4..b74fc6059ca59 100644 --- a/integration-tests/flyway/src/main/resources/db/location3/afterMigrate.sql +++ b/integration-tests/flyway/src/main/resources/db/location3/afterMigrate.sql @@ -1 +1 @@ -select count(1) from multiple_flyway_test; +select count(1) from TEST_SCHEMA.multiple_flyway_test; diff --git a/integration-tests/flyway/src/test/java/io/quarkus/it/flyway/FlywayFunctionalityTest.java b/integration-tests/flyway/src/test/java/io/quarkus/it/flyway/FlywayFunctionalityTest.java index 59bd504667ea8..aa58144597cfd 100644 --- a/integration-tests/flyway/src/test/java/io/quarkus/it/flyway/FlywayFunctionalityTest.java +++ b/integration-tests/flyway/src/test/java/io/quarkus/it/flyway/FlywayFunctionalityTest.java @@ -46,10 +46,10 @@ public void testPlaceholdersPrefixSuffix() { } @Test - @DisplayName("Returns whether the init-sql is CREATE SCHEMA IF NOT EXISTS TEST_SCHEMA;CREATE OR REPLACE FUNCTION f_my_constant() RETURNS integer LANGUAGE plpgsql as $func$ BEGIN return 100; END $func$; or not") + @DisplayName("Returns whether the init-sql is CREATE SCHEMA IF NOT EXISTS TEST_SCHEMA;CREATE OR REPLACE FUNCTION TEST_SCHEMA.f_my_constant() RETURNS integer LANGUAGE plpgsql as $func$ BEGIN return 100; END $func$; or not") public void testReturnInitSql() { when().get("/flyway/init-sql").then().body(is( - "CREATE SCHEMA IF NOT EXISTS TEST_SCHEMA;CREATE OR REPLACE FUNCTION f_my_constant() RETURNS integer LANGUAGE plpgsql as $func$ BEGIN return 100; END $func$;")); + "CREATE SCHEMA IF NOT EXISTS TEST_SCHEMA;CREATE OR REPLACE FUNCTION TEST_SCHEMA.f_my_constant() RETURNS integer LANGUAGE plpgsql as $func$ BEGIN return 100; END $func$;")); } @Test diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/dir-tree.snapshot b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/dir-tree.snapshot index 5b7c91c9acf74..68b2bbd34478d 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/dir-tree.snapshot +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/dir-tree.snapshot @@ -6,7 +6,6 @@ quarkus-my-quarkiverse-ext/.github/dependabot.yml quarkus-my-quarkiverse-ext/.github/project.yml quarkus-my-quarkiverse-ext/.github/workflows/ quarkus-my-quarkiverse-ext/.github/workflows/build.yml -quarkus-my-quarkiverse-ext/.github/workflows/deploy-snapshots.yml.disabled quarkus-my-quarkiverse-ext/.github/workflows/pre-release.yml quarkus-my-quarkiverse-ext/.github/workflows/quarkus-snapshot.yaml quarkus-my-quarkiverse-ext/.github/workflows/release-perform.yml @@ -109,4 +108,4 @@ quarkus-my-quarkiverse-ext/runtime/src/main/java/io/quarkiverse/my/quarkiverse/e quarkus-my-quarkiverse-ext/runtime/src/main/java/io/quarkiverse/my/quarkiverse/ext/runtime/ quarkus-my-quarkiverse-ext/runtime/src/main/resources/ quarkus-my-quarkiverse-ext/runtime/src/main/resources/META-INF/ -quarkus-my-quarkiverse-ext/runtime/src/main/resources/META-INF/quarkus-extension.yaml \ No newline at end of file +quarkus-my-quarkiverse-ext/runtime/src/main/resources/META-INF/quarkus-extension.yaml 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 bcd729e0be9e0..5ba330047d481 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 - 17 + 18 io.quarkiverse.my-quarkiverse-ext quarkus-my-quarkiverse-ext-parent diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenRequestResponseFilter.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenRequestResponseFilter.java new file mode 100644 index 0000000000000..e05d38e209081 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenRequestResponseFilter.java @@ -0,0 +1,49 @@ +package io.quarkus.it.keycloak; + +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.arc.Unremovable; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcRequestFilter; +import io.quarkus.oidc.common.OidcResponseFilter; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.oidc.runtime.OidcUtils; + +@ApplicationScoped +@Unremovable +@OidcEndpoint(value = Type.TOKEN) +public class TokenRequestResponseFilter implements OidcRequestFilter, OidcResponseFilter { + private static final Logger LOG = Logger.getLogger(TokenRequestResponseFilter.class); + + private ConcurrentHashMap instants = new ConcurrentHashMap<>(); + + @Override + public void filter(OidcRequestContext rc) { + final Instant now = Instant.now(); + instants.put(rc.contextProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE), now); + rc.contextProperties().put("instant", now); + } + + @Override + public void filter(OidcResponseContext rc) { + Instant instant1 = instants.remove(rc.requestProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE)); + Instant instant2 = rc.requestProperties().get("instant"); + boolean instantsAreTheSame = instant1 == instant2; + if (rc.statusCode() == 200 + && instantsAreTheSame + && rc.responseHeaders().get("Content-Type").equals("application/json") + && OidcConstants.AUTHORIZATION_CODE.equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) + && "code-flow-user-info-github-cached-in-idtoken" + .equals(rc.requestProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE))) { + LOG.debug("Authorization code completed for tenant 'code-flow-user-info-github-cached-in-idtoken' in an instant: " + + instantsAreTheSame); + } + } + +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenResponseFilter.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenResponseFilter.java deleted file mode 100644 index 0a4dcb731fc7d..0000000000000 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenResponseFilter.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.quarkus.it.keycloak; - -import jakarta.enterprise.context.ApplicationScoped; - -import org.jboss.logging.Logger; - -import io.quarkus.arc.Unremovable; -import io.quarkus.oidc.common.OidcEndpoint; -import io.quarkus.oidc.common.OidcEndpoint.Type; -import io.quarkus.oidc.common.OidcResponseFilter; -import io.quarkus.oidc.common.runtime.OidcConstants; -import io.quarkus.oidc.runtime.OidcUtils; - -@ApplicationScoped -@Unremovable -@OidcEndpoint(value = Type.TOKEN) -public class TokenResponseFilter implements OidcResponseFilter { - private static final Logger LOG = Logger.getLogger(TokenResponseFilter.class); - - @Override - public void filter(OidcResponseContext rc) { - if (rc.statusCode() == 200 - && rc.responseHeaders().get("Content-Type").equals("application/json") - && OidcConstants.AUTHORIZATION_CODE.equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) - && "code-flow-user-info-github-cached-in-idtoken" - .equals(rc.requestProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE))) { - LOG.debug("Authorization code completed for tenant 'code-flow-user-info-github-cached-in-idtoken'"); - } - } - -} diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index edeeaceebf842..dab898294d1c5 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -244,8 +244,8 @@ quarkus.log.category."io.quarkus.oidc.runtime.OidcProviderClient".min-level=TRAC quarkus.log.category."io.quarkus.oidc.runtime.OidcProviderClient".level=TRACE quarkus.log.category."io.quarkus.it.keycloak.SignedUserInfoResponseFilter".min-level=TRACE quarkus.log.category."io.quarkus.it.keycloak.SignedUserInfoResponseFilter".level=TRACE -quarkus.log.category."io.quarkus.it.keycloak.TokenResponseFilter".min-level=TRACE -quarkus.log.category."io.quarkus.it.keycloak.TokenResponseFilter".level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.TokenRequestResponseFilter".min-level=TRACE +quarkus.log.category."io.quarkus.it.keycloak.TokenRequestResponseFilter".level=TRACE quarkus.log.file.enable=true quarkus.log.file.format=%C - %s%n 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 d1e91dcabfbfa..f3963d220e9db 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 @@ -466,7 +466,7 @@ public void run() throws Throwable { } else if (line.contains("Response contains signed UserInfo")) { signedUserInfoResponseFilterMessageDetected = true; } else if (line.contains( - "Authorization code completed for tenant 'code-flow-user-info-github-cached-in-idtoken'")) { + "Authorization code completed for tenant 'code-flow-user-info-github-cached-in-idtoken' in an instant: true")) { codeFlowCompletedResponseFilterMessageDetected = true; } if (lineConfirmingVerificationDetected diff --git a/integration-tests/rest-client-reactive-stork/pom.xml b/integration-tests/rest-client-reactive-stork/pom.xml index 3bf27a3e6dc9b..d2de2b01f1013 100644 --- a/integration-tests/rest-client-reactive-stork/pom.xml +++ b/integration-tests/rest-client-reactive-stork/pom.xml @@ -44,11 +44,6 @@ io.smallrye.stork stork-service-discovery-static-list - - io.smallrye.stork - stork-configuration-generator - provided - @@ -121,6 +116,17 @@ + + maven-compiler-plugin + + + + io.smallrye.stork + stork-configuration-generator + + + + io.quarkus quarkus-maven-plugin diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/SharedResource.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/SharedResource.java new file mode 100644 index 0000000000000..ff77182a0f830 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/SharedResource.java @@ -0,0 +1,44 @@ +package io.quarkus.it.extension.testresources; + +import static java.util.Objects.requireNonNull; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class SharedResource implements QuarkusTestResourceLifecycleManager { + private String argument; + + @Override + public void init(Map initArgs) { + this.argument = requireNonNull(initArgs.get("resource.arg")); + } + + @Override + public Map start() { + System.err.println(getClass().getSimpleName() + " start with arg '" + argument + "'"); + return Map.of(); + } + + @Override + public void stop() { + System.err.println(getClass().getSimpleName() + " stop"); + } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields(argument, + new TestInjector.AnnotatedAndMatchesType(SharedResourceAnnotation.class, String.class)); + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface SharedResourceAnnotation { + } +} diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/SomeResource1.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/SomeResource1.java new file mode 100644 index 0000000000000..7c0d4c8b6c7b6 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/SomeResource1.java @@ -0,0 +1,35 @@ +package io.quarkus.it.extension.testresources; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class SomeResource1 implements QuarkusTestResourceLifecycleManager { + @Override + public Map start() { + System.err.println(getClass().getSimpleName() + " start"); + return Map.of(); + } + + @Override + public void stop() { + System.err.println(getClass().getSimpleName() + " stop"); + } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields(getClass().getSimpleName(), + new TestInjector.AnnotatedAndMatchesType(Resource1Annotation.class, String.class)); + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Resource1Annotation { + } +} diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/SomeResource2.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/SomeResource2.java new file mode 100644 index 0000000000000..66cfb038e9b1b --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/SomeResource2.java @@ -0,0 +1,35 @@ +package io.quarkus.it.extension.testresources; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class SomeResource2 implements QuarkusTestResourceLifecycleManager { + @Override + public Map start() { + System.err.println(getClass().getSimpleName() + " start"); + return Map.of(); + } + + @Override + public void stop() { + System.err.println(getClass().getSimpleName() + " stop"); + } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields(getClass().getSimpleName(), + new TestInjector.AnnotatedAndMatchesType(Resource2Annotation.class, String.class)); + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Resource2Annotation { + } +} diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/WithResourcesPoliciesFirstTest.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/WithResourcesPoliciesFirstTest.java new file mode 100644 index 0000000000000..477fef0c94af4 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/WithResourcesPoliciesFirstTest.java @@ -0,0 +1,31 @@ +package io.quarkus.it.extension.testresources; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.common.TestResourceScope; +import io.quarkus.test.common.WithTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@WithTestResource(value = SomeResource1.class, scope = TestResourceScope.MATCHING_RESOURCES) +@WithTestResource(value = SharedResource.class, scope = TestResourceScope.MATCHING_RESOURCES, initArgs = { + @ResourceArg(name = "resource.arg", value = "test-one") }) +public class WithResourcesPoliciesFirstTest { + @SomeResource1.Resource1Annotation + String resource1; + @SomeResource2.Resource2Annotation + String resource2; + @SharedResource.SharedResourceAnnotation + String sharedResource; + + @Test + public void checkOnlyResource1started() { + assertThat(Arrays.asList(resource1, resource2, sharedResource)).isEqualTo( + Arrays.asList("SomeResource1", null, "test-one")); + } +} diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/WithResourcesPoliciesSecondTest.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/WithResourcesPoliciesSecondTest.java new file mode 100644 index 0000000000000..2e4215154fba4 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/testresources/WithResourcesPoliciesSecondTest.java @@ -0,0 +1,31 @@ +package io.quarkus.it.extension.testresources; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.common.TestResourceScope; +import io.quarkus.test.common.WithTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@WithTestResource(value = SomeResource2.class, scope = TestResourceScope.MATCHING_RESOURCES) +@WithTestResource(value = SharedResource.class, scope = TestResourceScope.MATCHING_RESOURCES, initArgs = { + @ResourceArg(name = "resource.arg", value = "test-two") }) +public class WithResourcesPoliciesSecondTest { + @SomeResource1.Resource1Annotation + String resource1; + @SomeResource2.Resource2Annotation + String resource2; + @SharedResource.SharedResourceAnnotation + String sharedResource; + + @Test + public void checkOnlyResource1started() { + assertThat(Arrays.asList(resource1, resource2, sharedResource)).isEqualTo( + Arrays.asList(null, "SomeResource2", "test-two")); + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java index 9a40ff79e4ef1..8521d7407200a 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java @@ -108,7 +108,7 @@ public TestResourceManager(Class testClass, this.testResourceComparisonInfo = new HashSet<>(); for (TestResourceClassEntry uniqueEntry : uniqueEntries) { testResourceComparisonInfo.add(new TestResourceComparisonInfo( - uniqueEntry.testResourceLifecycleManagerClass().getName(), uniqueEntry.getScope())); + uniqueEntry.testResourceLifecycleManagerClass().getName(), uniqueEntry.getScope(), uniqueEntry.args)); } Set remainingUniqueEntries = initParallelTestResources(uniqueEntries); @@ -326,7 +326,12 @@ public static Set testResourceCo } Set result = new HashSet<>(uniqueEntries.size()); for (TestResourceClassEntry entry : uniqueEntries) { - result.add(new TestResourceComparisonInfo(entry.testResourceLifecycleManagerClass().getName(), entry.getScope())); + Map args = new HashMap<>(entry.args); + if (entry.configAnnotation != null) { + args.put("configAnnotation", entry.configAnnotation.annotationType().getName()); + } + result.add(new TestResourceComparisonInfo(entry.testResourceLifecycleManagerClass().getName(), entry.getScope(), + args)); } return result; } @@ -439,15 +444,15 @@ private static void addTestResourceEntry(QuarkusTestResource quarkusTestResource private static Collection findTestResourceInstancesOfClass(Class testClass, IndexView index) { // collect all test supertypes for matching per-test targets - Set testClasses = new HashSet<>(); + Set currentTestClassHierarchy = new HashSet<>(); Class current = testClass; while (current != Object.class) { - testClasses.add(current.getName()); + currentTestClassHierarchy.add(current.getName()); current = current.getSuperclass(); } current = testClass.getEnclosingClass(); while (current != null) { - testClasses.add(current.getName()); + currentTestClassHierarchy.add(current.getName()); current = current.getEnclosingClass(); } @@ -455,7 +460,7 @@ private static Collection findTestResourceInstancesOfClass(C for (DotName testResourceClasses : List.of(WITH_TEST_RESOURCE, QUARKUS_TEST_RESOURCE)) { for (AnnotationInstance annotation : index.getAnnotations(testResourceClasses)) { - if (keepTestResourceAnnotation(annotation, annotation.target().asClass(), testClasses)) { + if (keepTestResourceAnnotation(annotation, annotation.target().asClass(), currentTestClassHierarchy)) { testResourceAnnotations.add(annotation); } } @@ -466,7 +471,8 @@ private static Collection findTestResourceInstancesOfClass(C for (AnnotationInstance annotation : index.getAnnotations(testResourceListClasses)) { for (AnnotationInstance nestedAnnotation : annotation.value().asNestedArray()) { // keep the list target - if (keepTestResourceAnnotation(nestedAnnotation, annotation.target().asClass(), testClasses)) { + if (keepTestResourceAnnotation(nestedAnnotation, annotation.target().asClass(), + currentTestClassHierarchy)) { testResourceAnnotations.add(nestedAnnotation); } } @@ -477,21 +483,22 @@ private static Collection findTestResourceInstancesOfClass(C } private static boolean keepTestResourceAnnotation(AnnotationInstance annotation, ClassInfo targetClass, - Set testClasses) { + Set currentTestClassHierarchy) { if (targetClass.isAnnotation()) { // meta-annotations have already been handled in collectMetaAnnotations return false; } if (restrictToAnnotatedClass(annotation)) { - return testClasses.contains(targetClass.name().toString('.')); + return currentTestClassHierarchy.contains(targetClass.name().toString('.')); } return true; } private static boolean restrictToAnnotatedClass(AnnotationInstance annotation) { - return TestResourceClassEntryHandler.determineScope(annotation) == RESTRICTED_TO_CLASS; + return TestResourceClassEntryHandler.determineScope(annotation) == RESTRICTED_TO_CLASS + || TestResourceClassEntryHandler.determineScope(annotation) == MATCHING_RESOURCES; } /** @@ -516,7 +523,7 @@ public static boolean testResourcesRequireReload(Set return false; } - if (hasRestrictedToClassScope(existing) || hasRestrictedToClassScope(next)) { + if (anyResourceRestrictedToClass(existing) || anyResourceRestrictedToClass(next)) { return true; } @@ -538,8 +545,8 @@ public static boolean testResourcesRequireReload(Set return false; } - private static boolean hasRestrictedToClassScope(Set existing) { - for (TestResourceComparisonInfo info : existing) { + private static boolean anyResourceRestrictedToClass(Set testResources) { + for (TestResourceComparisonInfo info : testResources) { if (info.scope == RESTRICTED_TO_CLASS) { return true; } @@ -603,7 +610,8 @@ public TestResourceScope getScope() { } } - public record TestResourceComparisonInfo(String testResourceLifecycleManagerClass, TestResourceScope scope) { + public record TestResourceComparisonInfo(String testResourceLifecycleManagerClass, TestResourceScope scope, + Map args) { } diff --git a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerReloadTest.java b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerReloadTest.java index a4748969f9120..782f0d9ad2076 100644 --- a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerReloadTest.java +++ b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerReloadTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collections; +import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; @@ -22,66 +23,87 @@ public void emptyResources() { @Test public void differentCount() { assertTrue(testResourcesRequireReload(Collections.emptySet(), - Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS)))); + Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS, Map.of())))); - assertTrue(testResourcesRequireReload(Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS)), + assertTrue(testResourcesRequireReload(Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS, Map.of())), Collections.emptySet())); } @Test public void sameSingleRestrictedToClassResource() { assertTrue(testResourcesRequireReload( - Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS)), - Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS)))); + Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS, Map.of())), + Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS, Map.of())))); } @Test public void sameSingleMatchingResource() { assertFalse(testResourcesRequireReload( - Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES)), - Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES)))); + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of())), + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of())))); + } + + @Test + public void sameSingleMatchingResourceWithArgs() { + assertFalse(testResourcesRequireReload( + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of("a", "b"))), + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of("a", "b"))))); + } + + @Test + public void sameSingleResourceDifferentArgs() { + assertTrue(testResourcesRequireReload( + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of())), + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of("a", "b"))))); + } + + @Test + public void sameSingleResourceDifferentArgValues() { + assertTrue(testResourcesRequireReload( + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of("a", "b"))), + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of("a", "c"))))); } @Test public void differentSingleMatchingResource() { assertTrue(testResourcesRequireReload( - Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES)), - Set.of(new TestResourceComparisonInfo("test2", MATCHING_RESOURCES)))); + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of())), + Set.of(new TestResourceComparisonInfo("test2", MATCHING_RESOURCES, Map.of())))); } @Test public void sameMultipleMatchingResource() { assertFalse(testResourcesRequireReload( Set.of( - new TestResourceComparisonInfo("test", MATCHING_RESOURCES), - new TestResourceComparisonInfo("test2", MATCHING_RESOURCES), - new TestResourceComparisonInfo("test3", GLOBAL)), - Set.of(new TestResourceComparisonInfo("test3", GLOBAL), - new TestResourceComparisonInfo("test2", MATCHING_RESOURCES), - new TestResourceComparisonInfo("test", MATCHING_RESOURCES)))); + new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of()), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCES, Map.of()), + new TestResourceComparisonInfo("test3", GLOBAL, Map.of())), + Set.of(new TestResourceComparisonInfo("test3", GLOBAL, Map.of()), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCES, Map.of()), + new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of())))); } @Test public void differentMultipleMatchingResource() { assertTrue(testResourcesRequireReload( Set.of( - new TestResourceComparisonInfo("test", MATCHING_RESOURCES), - new TestResourceComparisonInfo("test2", MATCHING_RESOURCES), - new TestResourceComparisonInfo("test3", GLOBAL)), - Set.of(new TestResourceComparisonInfo("test3", GLOBAL), - new TestResourceComparisonInfo("test2", MATCHING_RESOURCES), - new TestResourceComparisonInfo("TEST", MATCHING_RESOURCES)))); + new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of()), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCES, Map.of()), + new TestResourceComparisonInfo("test3", GLOBAL, Map.of())), + Set.of(new TestResourceComparisonInfo("test3", GLOBAL, Map.of()), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCES, Map.of()), + new TestResourceComparisonInfo("TEST", MATCHING_RESOURCES, Map.of())))); } @Test public void differentGlobalMultipleMatchingResource() { assertTrue(testResourcesRequireReload( Set.of( - new TestResourceComparisonInfo("test", MATCHING_RESOURCES), - new TestResourceComparisonInfo("test2", MATCHING_RESOURCES), - new TestResourceComparisonInfo("test4", GLOBAL)), - Set.of(new TestResourceComparisonInfo("test3", GLOBAL), - new TestResourceComparisonInfo("test2", MATCHING_RESOURCES), - new TestResourceComparisonInfo("TEST", MATCHING_RESOURCES)))); + new TestResourceComparisonInfo("test", MATCHING_RESOURCES, Map.of()), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCES, Map.of()), + new TestResourceComparisonInfo("test4", GLOBAL, Map.of())), + Set.of(new TestResourceComparisonInfo("test3", GLOBAL, Map.of()), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCES, Map.of()), + new TestResourceComparisonInfo("TEST", MATCHING_RESOURCES, Map.of())))); } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractTestWithCallbacksExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractTestWithCallbacksExtension.java index c48fdc5624a9e..0299c686e134d 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractTestWithCallbacksExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractTestWithCallbacksExtension.java @@ -100,7 +100,7 @@ protected void invokeAfterAllCallbacks(Class clazz, Object testContext) throw invokeCallbacks(afterAllCallbacks, "afterAll", clazz, testContext); } - protected void populateCallbacks(ClassLoader classLoader) throws ClassNotFoundException { + protected static void clearCallbacks() { beforeClassCallbacks = new ArrayList<>(); afterConstructCallbacks = new ArrayList<>(); beforeEachCallbacks = new ArrayList<>(); @@ -108,6 +108,10 @@ protected void populateCallbacks(ClassLoader classLoader) throws ClassNotFoundEx afterTestCallbacks = new ArrayList<>(); afterEachCallbacks = new ArrayList<>(); afterAllCallbacks = new ArrayList<>(); + } + + protected void populateCallbacks(ClassLoader classLoader) throws ClassNotFoundException { + clearCallbacks(); ServiceLoader quarkusTestBeforeClassLoader = ServiceLoader .load(Class.forName(QuarkusTestBeforeClassCallback.class.getName(), false, classLoader), classLoader); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestExtensionState.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestExtensionState.java index 0983096593d22..5c51ae0d062f4 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestExtensionState.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestExtensionState.java @@ -8,11 +8,11 @@ public class IntegrationTestExtensionState extends QuarkusTestExtensionState { - private Map sysPropRestore; + private final Map sysPropRestore; public IntegrationTestExtensionState(TestResourceManager testResourceManager, Closeable resource, - Map sysPropRestore) { - super(testResourceManager, resource); + Runnable clearCallbacks, Map sysPropRestore) { + super(testResourceManager, resource, clearCallbacks); this.sysPropRestore = sysPropRestore; } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java index c2c6d43c99ac2..1dd9e846a844f 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java @@ -13,12 +13,10 @@ import static io.quarkus.test.junit.IntegrationTestUtil.handleDevServices; import static io.quarkus.test.junit.IntegrationTestUtil.readQuarkusArtifactProperties; import static io.quarkus.test.junit.IntegrationTestUtil.startLauncher; -import static io.quarkus.test.junit.TestResourceUtil.testResourcesRequireReload; import static io.quarkus.test.junit.TestResourceUtil.TestResourceManagerReflections.copyEntriesFromProfile; import java.io.Closeable; import java.io.File; -import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Path; import java.time.Duration; @@ -108,7 +106,6 @@ public void beforeTestExecution(ExtensionContext context) throws Exception { } else { throwBootFailureException(); - return; } } @@ -305,7 +302,7 @@ public void close() throws Throwable { Closeable resource = new IntegrationTestExtensionStateResource(launcher, devServicesLaunchResult.getCuratedApplication()); IntegrationTestExtensionState state = new IntegrationTestExtensionState(testResourceManager, resource, - sysPropRestore); + AbstractTestWithCallbacksExtension::clearCallbacks, sysPropRestore); testHttpEndpointProviders = TestHttpEndpointProvider.load(); return state; @@ -467,7 +464,7 @@ public IntegrationTestExtensionStateResource(ArtifactLauncher launcher, } @Override - public void close() throws IOException { + public void close() { if (launcher != null) { try { launcher.close(); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 33e6a6175196d..8afb99cf10bb4 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -3,7 +3,6 @@ import static io.quarkus.test.junit.IntegrationTestUtil.activateLogging; import java.io.Closeable; -import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.reflect.Constructor; @@ -272,7 +271,7 @@ public Thread newThread(Runnable r) { Closeable shutdownTask = new Closeable() { @Override - public void close() throws IOException { + public void close() { TracingHandler.quarkusStopping(); try { runningQuarkusApplication.close(); @@ -295,8 +294,7 @@ public void close() throws IOException { } } }; - ExtensionState state = new ExtensionState(testResourceManager, shutdownTask); - return state; + return new ExtensionState(testResourceManager, shutdownTask, AbstractTestWithCallbacksExtension::clearCallbacks); } catch (Throwable e) { if (!InitialConfigurator.DELAYED_HANDLER.isActivated()) { activateLogging(); @@ -888,7 +886,7 @@ public void interceptAfterEachMethod(Invocation invocation, ReflectiveInvo @Override public void interceptAfterAllMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - if (isNativeOrIntegrationTest(extensionContext.getRequiredTestClass())) { + if (runningQuarkusApplication == null || isNativeOrIntegrationTest(extensionContext.getRequiredTestClass())) { invocation.proceed(); return; } @@ -1176,12 +1174,12 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con public static class ExtensionState extends QuarkusTestExtensionState { - public ExtensionState(Closeable testResourceManager, Closeable resource) { - super(testResourceManager, resource); + public ExtensionState(Closeable testResourceManager, Closeable resource, Runnable clearCallbacks) { + super(testResourceManager, resource, clearCallbacks); } @Override - protected void doClose() throws IOException { + protected void doClose() { ClassLoader old = Thread.currentThread().getContextClassLoader(); if (runningQuarkusApplication != null) { Thread.currentThread().setContextClassLoader(runningQuarkusApplication.getClassLoader()); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtensionState.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtensionState.java index 6024cb50f400c..121bf20220c0d 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtensionState.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtensionState.java @@ -15,11 +15,13 @@ public class QuarkusTestExtensionState implements ExtensionContext.Store.Closeab protected final Closeable testResourceManager; protected final Closeable resource; private final Thread shutdownHook; + private final Runnable clearCallbacks; private Throwable testErrorCause; - public QuarkusTestExtensionState(Closeable testResourceManager, Closeable resource) { + public QuarkusTestExtensionState(Closeable testResourceManager, Closeable resource, Runnable clearCallbacks) { this.testResourceManager = testResourceManager; this.resource = resource; + this.clearCallbacks = clearCallbacks; this.shutdownHook = new Thread(new Runnable() { @Override public void run() { @@ -40,6 +42,7 @@ public Throwable getTestErrorCause() { public void close() throws IOException { if (closed.compareAndSet(false, true)) { doClose(); + clearCallbacks.run(); try { Runtime.getRuntime().removeShutdownHook(shutdownHook); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceUtil.java index 975699c137ab8..eb1fea1f4ff62 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceUtil.java @@ -192,8 +192,10 @@ static Set testResourceCompariso if (originalTestResourceScope != null) { testResourceScope = TestResourceScope.valueOf(originalTestResourceScope.toString()); } + Object originalArgs = entry.getClass().getMethod("args").invoke(entry); + Map args = (Map) originalArgs; result.add(new TestResourceManager.TestResourceComparisonInfo(testResourceLifecycleManagerClass, - testResourceScope)); + testResourceScope, args)); } return result;