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;