From fcff3c0dcd1fe7604b2d3e35abb6a56661488dd7 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 13 Nov 2024 13:26:29 +0100 Subject: [PATCH 01/39] WebSockets Next: clarify connector API docs/javadoc - also add tests for programmatic lookup of connectors - related to #44465 (cherry picked from commit c59a6dcfbf4957b09d4c29d3d642ace28dd5f8c9) --- .../asciidoc/websockets-next-reference.adoc | 62 +++++++++- .../ClientEndpointProgrammaticTest.java | 114 ++++++++++++++++++ ...nnectorProgrammaticInjectionPointTest.java | 40 ++++++ .../next/BasicWebSocketConnector.java | 27 ++++- .../next/OpenClientConnections.java | 4 +- .../websockets/next/OpenConnections.java | 4 +- .../next/WebSocketClientConnection.java | 2 +- .../websockets/next/WebSocketConnection.java | 2 +- .../websockets/next/WebSocketConnector.java | 28 ++++- 9 files changed, 271 insertions(+), 12 deletions(-) create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index e90a96fe2284c..23d223caef1ca 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -4,7 +4,7 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// [id="websockets-next-reference-guide"] -= WebSockets Next extension reference guide += WebSockets Next reference guide :extension-status: preview include::_attributes.adoc[] :numbered: @@ -78,7 +78,7 @@ implementation("io.quarkus:quarkus-websockets-next") == Endpoints -Both the server and client APIs allow you to define _endpoints_ that are used to consume and send messages. +Both the <> and <> define _endpoints_ that are used to consume and send messages. The endpoints are implemented as CDI beans and support injection. Endpoints declare <> annotated with `@OnTextMessage`, `@OnBinaryMessage`, `@OnPong`, `@OnOpen`, `@OnClose` and `@OnError`. These methods are used to handle various WebSocket events. @@ -559,6 +559,7 @@ This means that if an endpoint receives events `A` and `B` (in this particular o However, in some situations it is preferable to process events concurrently, i.e. with no ordering guarantees but also with no concurrency limits. For this cases, the `InboundProcessingMode#CONCURRENT` should be used. +[[server-api]] == Server API === HTTP server configuration @@ -900,14 +901,15 @@ public class CustomTenantResolver implements TenantResolver { ---- For more information on Hibernate multitenancy, refer to the https://quarkus.io/guides/hibernate-orm#multitenancy[hibernate documentation]. +[[client-api]] == Client API [[client-connectors]] === Client connectors -The `io.quarkus.websockets.next.WebSocketConnector` is used to configure and create new connections for client endpoints. -A CDI bean that implements this interface is provided and can be injected in other beans. -The actual type argument is used to determine the client endpoint. +A connector can be used to configure and open a new client connection backed by a client endpoint that is used to consume and send messages. +Quarkus provides a CDI bean with bean type `io.quarkus.websockets.next.WebSocketConnector` and default qualifer that can be injected in other beans. +The actual type argument of an injection point is used to determine the client endpoint. The type is validated during build - if it does not represent a client endpoint the build fails. Let’s consider the following client endpoint: @@ -955,6 +957,31 @@ public class MyBean { NOTE: If an application attempts to inject a connector for a missing endpoint, an error is thrown. +Connectors are not thread-safe and should not be used concurrently. +Connectors should also not be reused. +If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`: + +[source, java] +---- +import jakarta.enterprise.inject.Instance; + +@Singleton +public class MyBean { + + @Inject + Instance> connector; + + void connect() { + var connection1 = connector.get().baseUri(uri) + .addHeader("Foo", "alpha") + .connectAndAwait(); + var connection2 = connector.get().baseUri(uri) + .addHeader("Foo", "bravo") + .connectAndAwait(); + } +} +---- + ==== Basic connector In the case where the application developer does not need the combination of the client endpoint and the connector, a _basic connector_ can be used. @@ -991,6 +1018,31 @@ The basic connector is closer to a low-level API and is reserved for advanced us However, unlike others low-level WebSocket clients, it is still a CDI bean and can be injected in other beans. It also provides a way to configure the execution model of the callbacks, ensuring optimal integration with the rest of Quarkus. +Connectors are not thread-safe and should not be used concurrently. +Connectors should also not be reused. +If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`: + +[source, java] +---- +import jakarta.enterprise.inject.Instance; + +@Singleton +public class MyBean { + + @Inject + Instance connector; + + void connect() { + var connection1 = connector.get().baseUri(uri) + .addHeader("Foo", "alpha") + .connectAndAwait(); + var connection2 = connector.get().baseUri(uri) + .addHeader("Foo", "bravo") + .connectAndAwait(); + } +} +---- + [[ws-client-connection]] === WebSocket client connection diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java new file mode 100644 index 0000000000000..b885c5c82f0ff --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java @@ -0,0 +1,114 @@ +package io.quarkus.websockets.next.test.client.programmatic; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.inject.Instance; +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.HandshakeRequest; +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; + +public class ClientEndpointProgrammaticTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(ServerEndpoint.class, ClientEndpoint.class); + }); + + @Inject + Instance> connector; + + @TestHTTPResource("/") + URI uri; + + @Test + void testClient() throws InterruptedException { + WebSocketClientConnection connection1 = connector + .get() + .baseUri(uri) + .addHeader("Foo", "Lu") + .connectAndAwait(); + connection1.sendTextAndAwait("Hi!"); + + WebSocketClientConnection connection2 = connector + .get() + .baseUri(uri) + .addHeader("Foo", "Ma") + .connectAndAwait(); + connection2.sendTextAndAwait("Hi!"); + + assertTrue(ClientEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hello Lu!")); + assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hi!")); + assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hello Ma!")); + assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hi!"), ClientEndpoint.MESSAGES.toString()); + + connection1.closeAndAwait(); + connection2.closeAndAwait(); + assertTrue(ClientEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + } + + @WebSocket(path = "/endpoint") + public static class ServerEndpoint { + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2); + + @OnOpen + String open(HandshakeRequest handshakeRequest) { + return "Hello " + handshakeRequest.header("Foo") + "!"; + } + + @OnTextMessage + String echo(String message) { + return message; + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + + } + + @WebSocketClient(path = "/endpoint") + public static class ClientEndpoint { + + static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(4); + + static final List MESSAGES = new CopyOnWriteArrayList<>(); + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2); + + @OnTextMessage + void onMessage(String message, HandshakeRequest handshakeRequest) { + MESSAGES.add(handshakeRequest.header("Foo") + ":" + message); + MESSAGE_LATCH.countDown(); + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java new file mode 100644 index 0000000000000..80a4a39226b82 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java @@ -0,0 +1,40 @@ +package io.quarkus.websockets.next.test.client.programmatic; + +import static org.junit.jupiter.api.Assertions.fail; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Unremovable; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.websockets.next.WebSocketClientException; +import io.quarkus.websockets.next.WebSocketConnector; + +public class InvalidConnectorProgrammaticInjectionPointTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Service.class); + }) + .setExpectedException(WebSocketClientException.class, true); + + @Test + void testInvalidInjectionPoint() { + fail(); + } + + @Unremovable + @Singleton + public static class Service { + + @Inject + Instance> invalid; + + } + +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java index 98ab5ac1596e2..f261b513dce2a 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java @@ -5,6 +5,9 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; + import io.quarkus.arc.Arc; import io.smallrye.common.annotation.CheckReturnValue; import io.smallrye.common.annotation.Experimental; @@ -12,10 +15,30 @@ import io.vertx.core.buffer.Buffer; /** - * This basic connector can be used to configure and open new client connections. Unlike with {@link WebSocketConnector} a - * client endpoint class is not needed. + * A basic connector can be used to configure and open a new client connection. Unlike with {@link WebSocketConnector} a + * client endpoint is not used to consume and send messages. + *

+ * Quarkus provides a CDI bean with bean type {@code BasicWebSocketConnector} and qualifier {@link Default}. *

* This construct is not thread-safe and should not be used concurrently. + *

+ * Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor + * instance programmatically using {@link Instance#get()}: + *

+ * import jakarta.enterprise.inject.Instance;
+ *
+ * @Inject
+ * Instance<BasicWebSocketConnector> connector;
+ *
+ * void connect() {
+ *      var connection1 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "alpha")
+ *                  .connectAndAwait();
+ *      var connection2 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "bravo")
+ *                  .connectAndAwait();
+ * }
+ * 
* * @see WebSocketClientConnection */ diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java index e4270cc8b54ae..0f99844f49ecd 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java @@ -4,12 +4,14 @@ import java.util.Optional; import java.util.stream.Stream; +import jakarta.enterprise.inject.Default; + import io.smallrye.common.annotation.Experimental; /** * Provides convenient access to all open client connections. *

- * Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface. + * Quarkus provides a CDI bean with bean type {@link OpenClientConnections} and qualifier {@link Default}. */ @Experimental("This API is experimental and may change in the future") public interface OpenClientConnections extends Iterable { diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java index c8a5c797289c7..6f5f59c2cdbf0 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java @@ -4,12 +4,14 @@ import java.util.Optional; import java.util.stream.Stream; +import jakarta.enterprise.inject.Default; + import io.smallrye.common.annotation.Experimental; /** * Provides convenient access to all open connections. *

- * Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface. + * Quarkus provides a CDI bean with bean type {@link OpenConnections} and qualifier {@link Default}. */ @Experimental("This API is experimental and may change in the future") public interface OpenConnections extends Iterable { diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java index e33f95bea1e54..d6c11f3f5bccd 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java @@ -5,7 +5,7 @@ /** * This interface represents a client connection to a WebSocket endpoint. *

- * Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocketClient} + * Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocketClient} * endpoint and used to interact with the connected server. */ @Experimental("This API is experimental and may change in the future") diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java index c5deaa339b216..ea82ba5942b4d 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java @@ -8,7 +8,7 @@ /** * This interface represents a connection from a client to a specific {@link WebSocket} endpoint on the server. *

- * Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocket} + * Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocket} * endpoint and used to interact with the connected client, or all clients connected to the endpoint respectively * (broadcasting). *

diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java index 06f91ddf3e919..9a170e4f1431d 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java @@ -3,14 +3,40 @@ import java.net.URI; import java.net.URLEncoder; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; + import io.smallrye.common.annotation.CheckReturnValue; import io.smallrye.common.annotation.Experimental; import io.smallrye.mutiny.Uni; /** - * This connector can be used to configure and open new client connections using a client endpoint class. + * A connector can be used to configure and open a new client connection backed by a client endpoint that is used to + * consume and send messages. + *

+ * Quarkus provides a CDI bean with bean type {@code WebSocketConnector} and qualifier {@link Default}. The actual type + * argument of an injection point is used to determine the client endpoint. The type is validated during build + * and if it does not represent a client endpoint then the build fails. *

* This construct is not thread-safe and should not be used concurrently. + *

+ * Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor + * instance programmatically using {@link Instance#get()}: + *

+ * import jakarta.enterprise.inject.Instance;
+ *
+ * @Inject
+ * Instance<WebSocketConnector<MyEndpoint>> connector;
+ *
+ * void connect() {
+ *      var connection1 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "alpha")
+ *                  .connectAndAwait();
+ *      var connection2 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "bravo")
+ *                  .connectAndAwait();
+ * }
+ * 
* * @param The client endpoint class * @see WebSocketClient From 88e7c3266eba5ba2c9913efb808c84d42dc7eead Mon Sep 17 00:00:00 2001 From: brunobat Date: Wed, 13 Nov 2024 15:21:16 +0000 Subject: [PATCH 02/39] Improve test reliability (cherry picked from commit e579daceb5e801929c378fe8801bff7110953854) --- .../common/exporter/InMemoryMetricExporter.java | 7 ++++++- .../vertx/exporter/AbstractExporterTest.java | 8 ++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java index 56e548f9167c5..1abb8874bede2 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java @@ -131,7 +131,12 @@ public void assertCountPointsAtLeast(final String name, final String target, fin .untilAsserted(() -> { List metricData = getFinishedMetricItems(name, target); Assertions.assertTrue(1 <= metricData.size()); - Assertions.assertTrue(countPoints <= metricData.get(0).getData().getPoints().size()); + Assertions.assertTrue(countPoints <= metricData.stream() + .reduce((first, second) -> second) // get the last received + .orElse(null) + .getData() + .getPoints() + .size()); }); } diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java index 1ba0b74775661..980bebaed537b 100644 --- a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java @@ -132,17 +132,21 @@ private Metric getMetric(final String metricName) { .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> { List reqs = metrics.getMetricRequests(); - assertThat(reqs).hasSizeGreaterThan(1); + Optional metric = getMetric(metricName, reqs); + assertThat(metric).isPresent(); }); final List metricRequests = metrics.getMetricRequests(); + return getMetric(metricName, metricRequests).get(); + } + private Optional getMetric(String metricName, List metricRequests) { return metricRequests.stream() .flatMap(reqs -> reqs.getResourceMetricsList().stream()) .flatMap(resourceMetrics -> resourceMetrics.getScopeMetricsList().stream()) .flatMap(libraryMetrics -> libraryMetrics.getMetricsList().stream()) .filter(metric -> metric.getName().equals(metricName)) - .findFirst().get(); + .findFirst(); } private void verifyLogs() { From edb7d3384d5231f8339126887ea337af45924559 Mon Sep 17 00:00:00 2001 From: Cristian Burlacu Date: Tue, 12 Nov 2024 21:59:29 +0100 Subject: [PATCH 03/39] fix: support for short and uncommon field names like set, get, and is. (cherry picked from commit 7bbb0a83d9ca27f97adea79c7ac57c5316df2d4c) --- .../processor/JacksonCodeGenerator.java | 7 ++- .../FieldNameSetGetPrefixResourceTest.java | 52 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FieldNameSetGetPrefixResourceTest.java diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java index d407c7e6ec443..b822619d7bc9f 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java @@ -161,7 +161,7 @@ protected enum FieldKind { MAP(true), TYPE_VARIABLE(true); - private boolean generic; + private final boolean generic; FieldKind(boolean generic) { this.generic = generic; @@ -281,7 +281,7 @@ private Type fieldType() { if (isPublicField()) { return fieldInfo.type(); } - if (methodInfo.name().startsWith("set")) { + if (methodInfo.parametersCount() == 1 && methodInfo.name().startsWith("set")) { return methodInfo.parameterType(0); } return methodInfo.returnType(); @@ -304,6 +304,9 @@ private String fieldName() { private String fieldNameFromMethod(MethodInfo methodInfo) { String methodName = methodInfo.name(); + if (methodName.equals("get") || methodName.equals("set") || methodName.equals("is")) { + return methodName; + } if (methodName.startsWith("is")) { return methodName.substring(2, 3).toLowerCase() + methodName.substring(3); } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FieldNameSetGetPrefixResourceTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FieldNameSetGetPrefixResourceTest.java new file mode 100644 index 0000000000000..6a483942bc139 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FieldNameSetGetPrefixResourceTest.java @@ -0,0 +1,52 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.common.annotation.NonBlocking; + +// Ensures uncommon field names like "set", "get", and "is" are generated correctly. +class FieldNameSetGetPrefixResourceTest { + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class, Resource.UncommonBody.class).addAsResource( + new StringAsset( + "quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true\n"), + "application.properties")); + + @Test + void testFieldNameSetGetIsPrefix() { + RestAssured.get("/field-name-prefixes") + .then() + .statusCode(200) + .contentType("application/json") + .body("id", Matchers.equalTo("id")) + .body("set", Matchers.is(true)) + .body("get", Matchers.is(true)) + .body("is", Matchers.is(false)) + .body("setText", Matchers.equalTo("setText")); + } + + @NonBlocking + @Path("/field-name-prefixes") + private static class Resource { + @GET + public UncommonBody get() { + return new UncommonBody("id", true, true, false, "setText"); + } + + private record UncommonBody(String id, boolean set, boolean get, boolean is, String setText) { + } + } + +} From 140d941012f2818d8e90aea01f620da9e0e6e8bf Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Wed, 13 Nov 2024 15:19:10 -0500 Subject: [PATCH 04/39] Bump smallrye-open-api from 4.0.2 to 4.0.3 Signed-off-by: Michael Edgar (cherry picked from commit 5c34e9e5b2631592bfe8db63e28cb807a3a76b72) --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 186b103e3e18c..495dca31a1f52 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -51,7 +51,7 @@ 3.10.1 4.1.0 4.0.0 - 4.0.2 + 4.0.3 2.11.0 6.6.1 4.6.0 From 221ed8cd757f548d3c0613022153068b10ed5197 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Wed, 13 Nov 2024 17:21:43 +0100 Subject: [PATCH 05/39] Update to Quartz 2.5.0 and drop Jakarta transformation workaround (cherry picked from commit 64e9ad7966df2e161510d06e9ec4273165b1a3d3) --- bom/application/pom.xml | 2 +- extensions/quartz/deployment/pom.xml | 5 -- .../quartz/deployment/JakartaEnablement.java | 89 ------------------- extensions/quartz/runtime/pom.xml | 14 --- 4 files changed, 1 insertion(+), 109 deletions(-) delete mode 100644 extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/JakartaEnablement.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 495dca31a1f52..f2d309cc61482 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -115,7 +115,7 @@ 4.4.16 4.1.5 9.2.1 - 2.3.2 + 2.5.0 2.3.230 42.7.4 diff --git a/extensions/quartz/deployment/pom.xml b/extensions/quartz/deployment/pom.xml index b67c16e1a8706..c206dc6570a62 100644 --- a/extensions/quartz/deployment/pom.xml +++ b/extensions/quartz/deployment/pom.xml @@ -34,11 +34,6 @@ io.quarkus quarkus-quartz - - org.eclipse.transformer - org.eclipse.transformer - 0.5.0 - io.quarkus diff --git a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/JakartaEnablement.java b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/JakartaEnablement.java deleted file mode 100644 index 7bb45dfb9afbc..0000000000000 --- a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/JakartaEnablement.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.quarkus.quartz.deployment; - -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.eclipse.transformer.action.ActionContext; -import org.eclipse.transformer.action.ByteData; -import org.eclipse.transformer.action.impl.ActionContextImpl; -import org.eclipse.transformer.action.impl.ByteDataImpl; -import org.eclipse.transformer.action.impl.ClassActionImpl; -import org.eclipse.transformer.action.impl.SelectionRuleImpl; -import org.eclipse.transformer.action.impl.SignatureRuleImpl; -import org.eclipse.transformer.util.FileUtils; -import org.objectweb.asm.ClassReader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.quarkus.bootstrap.classloading.QuarkusClassLoader; -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; - -/** - * Quartz is compiled using references to classes in the javax packages; - * we need to transform these to fix compatibility with jakarta packages. - * We do this by leveraging the Eclipse Transformer project during Augmentation, so - * that end users don't need to bother. - */ -public class JakartaEnablement { - - private static final List CLASSES_NEEDING_TRANSFORMATION = List.of( - "org.quartz.ExecuteInJTATransaction", - "org.quartz.ee.servlet.QuartzInitializerServlet", - "org.quartz.ee.servlet.QuartzInitializerListener", - "org.quartz.ee.jta.JTAJobRunShell", - "org.quartz.ee.jta.UserTransactionHelper", - "org.quartz.ee.jta.UserTransactionHelper$UserTransactionWithContext", - "org.quartz.xml.XMLSchedulingDataProcessor", - "org.quartz.impl.jdbcjobstore.JTANonClusteredSemaphore"); - - @BuildStep - void transformToJakarta(BuildProducer transformers) { - if (QuarkusClassLoader.isClassPresentAtRuntime("jakarta.transaction.Transaction")) { - JakartaTransformer tr = new JakartaTransformer(); - for (String classname : CLASSES_NEEDING_TRANSFORMATION) { - final BytecodeTransformerBuildItem item = new BytecodeTransformerBuildItem.Builder() - .setCacheable(true) - .setContinueOnFailure(false) - .setClassToTransform(classname) - .setClassReaderOptions(ClassReader.SKIP_DEBUG) - .setInputTransformer(tr::transform) - .build(); - transformers.produce(item); - } - } - } - - private static class JakartaTransformer { - - private final Logger logger; - private final ActionContext ctx; - // We need to prevent the Eclipse Transformer to adjust the "javax" packages. - // Thus why we split the strings. - private static final Map renames = Map.of("javax" + ".transaction", "jakarta.transaction", - "javax" + ".servlet", "jakarta.servlet", - "javax" + ".xml.bind", "jakarta.xml.bind"); - - JakartaTransformer() { - logger = LoggerFactory.getLogger("JakartaTransformer"); - //N.B. we enable only this single transformation of package renames, not the full set of capabilities of Eclipse Transformer; - //this might need tailoring if the same idea gets applied to a different context. - ctx = new ActionContextImpl(logger, - new SelectionRuleImpl(logger, Collections.emptyMap(), Collections.emptyMap()), - new SignatureRuleImpl(logger, renames, null, null, null, null, null, Collections.emptyMap())); - } - - byte[] transform(final String name, final byte[] bytes) { - logger.debug("Jakarta EE compatibility enhancer for Quarkus: transforming " + name); - final ClassActionImpl classTransformer = new ClassActionImpl(ctx); - final ByteBuffer input = ByteBuffer.wrap(bytes); - final ByteData inputData = new ByteDataImpl(name, input, FileUtils.DEFAULT_CHARSET); - final ByteData outputData = classTransformer.apply(inputData); - return outputData.buffer().array(); - } - } - -} diff --git a/extensions/quartz/runtime/pom.xml b/extensions/quartz/runtime/pom.xml index afb86369ef3b5..76b0fd2e9aaaf 100644 --- a/extensions/quartz/runtime/pom.xml +++ b/extensions/quartz/runtime/pom.xml @@ -30,20 +30,6 @@ org.quartz-scheduler quartz - - - com.zaxxer - HikariCP - - - com.zaxxer - HikariCP-java7 - - - com.mchange - c3p0 - - jakarta.transaction From ae54d1c6125fa71bf49a04c0bcb4248fb91393ce Mon Sep 17 00:00:00 2001 From: Inaki Villar Date: Wed, 13 Nov 2024 14:30:17 -0800 Subject: [PATCH 06/39] QuarkusBuildTask uses getBuildForkOptions (cherry picked from commit dbedbdc5157f5d0d46641f3ef80b016811e78ac1) --- .../gradle/tasks/QuarkusBuildTask.java | 2 +- .../tasks/QuarkusPluginExtensionView.java | 4 ++++ .../build.gradle.kts | 23 +++++++++++++++++++ .../gradle.properties | 2 ++ .../settings.gradle | 16 +++++++++++++ .../src/main/java/org/acme/EntryPoint.java | 9 ++++++++ ...ionsAreIncludedInQuarkusBuildTaskTest.java | 16 +++++++++++++ 7 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/build.gradle.kts create mode 100644 integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/gradle.properties create mode 100644 integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/settings.gradle create mode 100644 integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/src/main/java/org/acme/EntryPoint.java create mode 100644 integration-tests/gradle/src/test/java/io/quarkus/gradle/BuildForkOptionsAreIncludedInQuarkusBuildTaskTest.java diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java index f54010f9aba90..a67d05456d118 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java @@ -272,7 +272,7 @@ void generateBuild() { .collect(Collectors.joining("\n ", "\n ", ""))); } - WorkQueue workQueue = workQueue(quarkusProperties, getExtensionView().getCodeGenForkOptions().get()); + WorkQueue workQueue = workQueue(quarkusProperties, getExtensionView().getBuildForkOptions().get()); workQueue.submit(BuildWorker.class, params -> { params.getBuildSystemProperties() diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java index f4a8e87aba3d9..9dc97a6c53ea7 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java @@ -64,6 +64,7 @@ public QuarkusPluginExtensionView(Project project, QuarkusPluginExtension extens getCleanupBuildOutput().set(extension.getCleanupBuildOutput()); getFinalName().set(extension.getFinalName()); getCodeGenForkOptions().set(getProviderFactory().provider(() -> extension.codeGenForkOptions)); + getBuildForkOptions().set(getProviderFactory().provider(() -> extension.buildForkOptions)); getIgnoredEntries().set(extension.ignoredEntriesProperty()); getMainResources().setFrom(project.getExtensions().getByType(SourceSetContainer.class).getByName(MAIN_SOURCE_SET_NAME) .getResources().getSourceDirectories()); @@ -127,6 +128,9 @@ private Provider> getQuarkusRelevantProjectProperties(Projec @Nested public abstract ListProperty> getCodeGenForkOptions(); + @Nested + public abstract ListProperty> getBuildForkOptions(); + @Input @Optional public abstract Property getJarEnabled(); diff --git a/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/build.gradle.kts b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/build.gradle.kts new file mode 100644 index 0000000000000..8d6264bd678f9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + java + id("io.quarkus") +} + +repositories { + mavenCentral() + mavenLocal() +} + +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +dependencies { + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) +} + +quarkus { + buildForkOptions { + println("message!") + } +} diff --git a/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/gradle.properties b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/gradle.properties new file mode 100644 index 0000000000000..ec2b6ef199c2c --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/settings.gradle b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/settings.gradle new file mode 100644 index 0000000000000..73c92a8563f3a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} +rootProject.name='code-with-quarkus' diff --git a/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/src/main/java/org/acme/EntryPoint.java b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/src/main/java/org/acme/EntryPoint.java new file mode 100644 index 0000000000000..ed5d9a989519d --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/src/main/java/org/acme/EntryPoint.java @@ -0,0 +1,9 @@ +package org.acme; + + + +public class EntryPoint { + public static void main(String[] args) { + + } +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/BuildForkOptionsAreIncludedInQuarkusBuildTaskTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/BuildForkOptionsAreIncludedInQuarkusBuildTaskTest.java new file mode 100644 index 0000000000000..dcddf7cdabcdb --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/BuildForkOptionsAreIncludedInQuarkusBuildTaskTest.java @@ -0,0 +1,16 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class BuildForkOptionsAreIncludedInQuarkusBuildTaskTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testBuildForkOptionsAreProcessed() throws Exception { + var projectDir = getProjectDir("basic-java-application-with-fork-options"); + var buildResult = runGradleWrapper(projectDir, "clean", "quarkusBuild"); + assertThat(BuildResult.isSuccessful(buildResult.getTasks().get(":quarkusGenerateCode"))).isTrue(); + assertThat(buildResult.getOutput().contains("message!")).isTrue(); + } +} From f86b772a9e84cc5d1c90de4f92c4a762037dbafb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 20:43:34 +0000 Subject: [PATCH 07/39] Bump com.gradle.develocity from 3.18.1 to 3.18.2 in /devtools/gradle Bumps com.gradle.develocity from 3.18.1 to 3.18.2. --- updated-dependencies: - dependency-name: com.gradle.develocity dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] (cherry picked from commit 81113515bd2955c7d47d58333150433f1cd6c9a8) --- devtools/gradle/settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/gradle/settings.gradle.kts b/devtools/gradle/settings.gradle.kts index b7ac646dd3f37..4d008c259a339 100644 --- a/devtools/gradle/settings.gradle.kts +++ b/devtools/gradle/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.gradle.develocity") version "3.18.1" + id("com.gradle.develocity") version "3.18.2" } develocity { From 05e0633f7f86278862728073d60e1385013cfd5b Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 14 Nov 2024 09:07:17 +0200 Subject: [PATCH 08/39] Register Kotlin's empty list and map for reflection Fixes: #44472 (cherry picked from commit 15232bf4d40e64df242adf2f8c263e6bb2bac6bc) --- .../quarkus/kotlin/deployment/KotlinProcessor.java | 2 ++ .../kotlin/io/quarkus/it/kotser/GreetingResource.kt | 12 ++++++++++++ .../test/kotlin/io/quarkus/it/kotser/ResourceTest.kt | 10 ++++++++++ 3 files changed, 24 insertions(+) diff --git a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java index 340f4de9888ac..aff88dfbd80b2 100644 --- a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java +++ b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java @@ -61,6 +61,8 @@ void registerKotlinReflection(final BuildProducer refl .build()); reflectiveClass.produce(ReflectiveClassBuildItem.builder("kotlin.KotlinVersion$Companion[]").constructors(false) .build()); + reflectiveClass.produce( + ReflectiveClassBuildItem.builder("kotlin.collections.EmptyList", "kotlin.collections.EmptyMap").build()); nativeResourcePatterns.produce(builder().includePatterns( "META-INF/.*.kotlin_module$", diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt index f03c9a4729413..a1d0804d563c0 100644 --- a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt +++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt @@ -73,5 +73,17 @@ class GreetingResource { return Response.ok().entity(javaMethod.invoke(this)).build() } + @GET + @Path("emptyList") + fun emptyList(): List { + return emptyList() + } + + @GET + @Path("emptyMap") + fun emptyMap(): Map { + return emptyMap() + } + fun reflect() = "hello, world" } diff --git a/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt b/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt index df95d3b5ea7e3..35d43c86e164f 100644 --- a/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt +++ b/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt @@ -124,4 +124,14 @@ open class ResourceTest { body(CoreMatchers.equalTo("hello, world")) } } + + @Test + fun testEmptyList() { + When { get("/emptyList") } Then { statusCode(200) } + } + + @Test + fun testEmptyMap() { + When { get("/emptyList") } Then { statusCode(200) } + } } From ec2339841321ed4f252d62412c81cbdc6e818c2d Mon Sep 17 00:00:00 2001 From: Inaki Villar Date: Fri, 8 Nov 2024 15:24:12 -0800 Subject: [PATCH 09/39] declaring explicitly the build service in the QuarkusBuildTask (cherry picked from commit b6324cd1b535df6dd3fbaa9c5c9ac314aa4d1187) --- .../java/io/quarkus/gradle/QuarkusPlugin.java | 33 ++-- .../gradle/tasks/QuarkusBuildTask.java | 6 +- .../build.gradle.kts | 15 ++ .../gradle.properties | 4 + .../modA/build.gradle.kts | 16 ++ .../modB/.dockerignore | 5 + .../modB/build.gradle.kts | 15 ++ .../main/java/org/acme/GreetingResource.java | 16 ++ .../resources/META-INF/resources/index.html | 182 ++++++++++++++++++ .../src/main/resources/application.properties | 0 .../org/acme/NativeGreetingResourceIT.java | 9 + .../java/org/acme/GreetingResourceTest.java | 21 ++ .../settings.gradle.kts | 23 +++ .../QuarkusAppliedToMultipleModulesTest.java | 18 ++ 14 files changed, 345 insertions(+), 18 deletions(-) create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/build.gradle.kts create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/gradle.properties create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modA/build.gradle.kts create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/.dockerignore create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/build.gradle.kts create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/java/org/acme/GreetingResource.java create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/META-INF/resources/index.html create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/application.properties create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/native-test/java/org/acme/NativeGreetingResourceIT.java create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/test/java/org/acme/GreetingResourceTest.java create mode 100644 integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/settings.gradle.kts create mode 100644 integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusAppliedToMultipleModulesTest.java diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index 15d58e6c94f71..8e6b0a97d9d01 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -139,10 +139,6 @@ public void apply(Project project) { // Apply the `java` plugin project.getPluginManager().apply(JavaPlugin.class); - project.getGradle().getSharedServices().registerIfAbsent("forcedPropertiesService", ForcedPropertieBuildService.class, - spec -> { - }); - registerModel(); // register extension @@ -156,7 +152,11 @@ public void apply(Project project) { private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { TaskContainer tasks = project.getTasks(); - + String forcedPropertiesService = String.format("forcedPropertiesService-%s", project.getName()); + Provider serviceProvider = project.getGradle().getSharedServices().registerIfAbsent( + forcedPropertiesService, ForcedPropertieBuildService.class, + spec -> { + }); final String devRuntimeConfigName = ApplicationDeploymentClasspathBuilder .getBaseRuntimeConfigName(LaunchMode.DEVELOPMENT); final Configuration devRuntimeDependencies = project.getConfigurations().maybeCreate(devRuntimeConfigName); @@ -236,14 +236,14 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { }); tasks.register(QUARKUS_SHOW_EFFECTIVE_CONFIG_TASK_NAME, QuarkusShowEffectiveConfig.class, task -> { - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.setDescription("Show effective Quarkus build configuration."); }); TaskProvider quarkusBuildDependencies = tasks.register(QUARKUS_BUILD_DEP_TASK_NAME, QuarkusBuildDependencies.class, task -> { - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); task.getApplicationModel() @@ -256,7 +256,7 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { TaskProvider quarkusBuildCacheableAppParts = tasks.register( QUARKUS_BUILD_APP_PARTS_TASK_NAME, QuarkusBuildCacheableAppParts.class, task -> { - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.dependsOn(quarkusGenerateCode); task.getOutputs().doNotCacheIf( "Not adding uber-jars, native binaries and mutable-jar package type to Gradle " + @@ -272,7 +272,7 @@ public boolean isSatisfiedBy(Task t) { }); TaskProvider quarkusBuild = tasks.register(QUARKUS_BUILD_TASK_NAME, QuarkusBuild.class, build -> { - configureQuarkusBuildTask(project, quarkusExt, build, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, build, quarkusBuildAppModelTask, serviceProvider); build.dependsOn(quarkusBuildDependencies, quarkusBuildCacheableAppParts); build.getOutputs().doNotCacheIf( "Only collects and combines the outputs of " + QUARKUS_BUILD_APP_PARTS_TASK_NAME + " and " @@ -296,7 +296,7 @@ public boolean isSatisfiedBy(Task t) { tasks.register(IMAGE_BUILD_TASK_NAME, ImageBuild.class, task -> { task.dependsOn(quarkusRequiredExtension); - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getBuilderName().set(quarkusRequiredExtension.flatMap(ImageCheckRequirementsTask::getOutputFile)); task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); task.getApplicationModel() @@ -306,7 +306,7 @@ public boolean isSatisfiedBy(Task t) { tasks.register(IMAGE_PUSH_TASK_NAME, ImagePush.class, task -> { task.dependsOn(quarkusRequiredExtension); - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getBuilderName().set(quarkusRequiredExtension.flatMap(ImageCheckRequirementsTask::getOutputFile)); task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); task.getApplicationModel() @@ -315,7 +315,7 @@ public boolean isSatisfiedBy(Task t) { }); tasks.register(DEPLOY_TASK_NAME, Deploy.class, task -> { - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); task.getApplicationModel() .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); @@ -326,7 +326,7 @@ public boolean isSatisfiedBy(Task t) { quarkusExt); TaskProvider quarkusRun = tasks.register(QUARKUS_RUN_TASK_NAME, QuarkusRun.class, build -> { - configureQuarkusBuildTask(project, quarkusExt, build, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, build, quarkusBuildAppModelTask, serviceProvider); build.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); build.getApplicationModel() .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); @@ -527,10 +527,13 @@ private static void configureApplicationModelTask(Project project, QuarkusApplic task.getApplicationModel().set(project.getLayout().getBuildDirectory().file(quarkusModelFile)); } - private static void configureQuarkusBuildTask(Project project, QuarkusPluginExtension quarkusExt, QuarkusBuildTask task, - TaskProvider quarkusGenerateAppModelTask) { + private static void configureQuarkusBuildTask(Project project, QuarkusBuildTask task, + TaskProvider quarkusGenerateAppModelTask, + Provider serviceProvider) { task.getApplicationModel().set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); SourceSet mainSourceSet = getSourceSet(project, SourceSet.MAIN_SOURCE_SET_NAME); + task.getAdditionalForcedProperties().set(serviceProvider); + task.usesService(serviceProvider); task.setCompileClasspath(mainSourceSet.getCompileClasspath().plus(mainSourceSet.getRuntimeClasspath()) .plus(mainSourceSet.getAnnotationProcessorPath()) .plus(mainSourceSet.getResources())); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java index a67d05456d118..f5f75c86681ed 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java @@ -17,10 +17,10 @@ import org.gradle.api.file.RegularFileProperty; import org.gradle.api.logging.LogLevel; import org.gradle.api.provider.Property; -import org.gradle.api.services.ServiceReference; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitivity; @@ -48,8 +48,8 @@ public abstract class QuarkusBuildTask extends QuarkusTask { static final String NATIVE_SOURCES = "native-sources"; private final QuarkusPluginExtensionView extensionView; - @ServiceReference("forcedPropertiesService") - abstract Property getAdditionalForcedProperties(); + @Internal + public abstract Property getAdditionalForcedProperties(); QuarkusBuildTask(String description, boolean compatible) { super(description, compatible); diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/build.gradle.kts b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/build.gradle.kts new file mode 100644 index 0000000000000..c6260a98129f5 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/build.gradle.kts @@ -0,0 +1,15 @@ +allprojects { + + group = "org.acme" + version = "1.0.0-SNAPSHOT" + + repositories { + mavenLocal { + content { + includeGroupByRegex("io.quarkus.*") + includeGroup("org.hibernate.orm") + } + } + mavenCentral() + } +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/gradle.properties b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/gradle.properties new file mode 100644 index 0000000000000..0b9b349582ac3 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/gradle.properties @@ -0,0 +1,4 @@ +# Gradle properties + +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modA/build.gradle.kts b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modA/build.gradle.kts new file mode 100644 index 0000000000000..f9b4541bc516f --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modA/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `java-library` + id("io.quarkus") + id("com.github.ben-manes.versions") version "0.51.0" + +} +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +dependencies { + implementation(enforcedPlatform("$quarkusPlatformGroupId:$quarkusPlatformArtifactId:$quarkusPlatformVersion")) + api("io.quarkus:quarkus-resteasy") + api("io.quarkus:quarkus-resteasy-jackson") + api("io.quarkus:quarkus-arc") +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/.dockerignore b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/.dockerignore new file mode 100644 index 0000000000000..4361d2fb38ddc --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/.dockerignore @@ -0,0 +1,5 @@ +* +!build/*-runner +!build/*-runner.jar +!build/lib/* +!build/quarkus-app/* \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/build.gradle.kts b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/build.gradle.kts new file mode 100644 index 0000000000000..7db659d8359d7 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` + id("io.quarkus") + id("com.github.ben-manes.versions") version "0.51.0" +} +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +val javaVersion = "17" + +dependencies { + implementation(enforcedPlatform("$quarkusPlatformGroupId:$quarkusPlatformArtifactId:$quarkusPlatformVersion")) + implementation(project(":modA")) +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/java/org/acme/GreetingResource.java b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/java/org/acme/GreetingResource.java new file mode 100644 index 0000000000000..e5f5b6ca5694b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/java/org/acme/GreetingResource.java @@ -0,0 +1,16 @@ +package org.acme; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo bar"; + } +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/META-INF/resources/index.html b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000000..abf53e21ca41b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,182 @@ + + + + + code-with-quarkus - 1.0.0-SNAPSHOT + + + + + + +
+
+

Congratulations, you have created a new Quarkus cloud application.

+ +

What is this page?

+ +

This page is served by Quarkus. The source is in + src/main/resources/META-INF/resources/index.html.

+ +

What are your next steps?

+ +

If not already done, run the application in dev mode using: ./gradlew quarkusDev. +

+
    +
  • Your static assets are located in src/main/resources/META-INF/resources.
  • +
  • Configure your application in src/main/resources/application.properties.
  • +
  • Quarkus now ships with a Dev UI (available in dev mode only)
  • +
  • Play with the getting started example code located in src/main/java:
  • +
+
+

RESTEasy JAX-RS example

+

REST is easy peasy with this Hello World RESTEasy resource.

+

@Path: /hello-resteasy

+

Related guide section...

+
+
+

RESTEasy JSON serialisation using Jackson

+

This example demonstrate RESTEasy JSON serialisation by letting you list, add and remove quark types from a list. Quarked!

+

@Path: /resteasy-jackson/quarks/

+

Related guide section...

+
+ +
+
+
+

Application

+
    +
  • GroupId: org.acme
  • +
  • ArtifactId: code-with-quarkus
  • +
  • Version: 1.0.0-SNAPSHOT
  • +
  • Quarkus Version: 1.12.2.Final
  • +
+
+
+

Do you like Quarkus?

+
    +
  • Go give it a star on GitHub.
  • +
+
+
+

Selected extensions guides

+ +
+ +
+
+ + \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/application.properties new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/native-test/java/org/acme/NativeGreetingResourceIT.java b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/native-test/java/org/acme/NativeGreetingResourceIT.java new file mode 100644 index 0000000000000..909dc2e828c69 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/native-test/java/org/acme/NativeGreetingResourceIT.java @@ -0,0 +1,9 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class NativeGreetingResourceIT extends GreetingResourceTest { + + // Execute the same tests but in native mode. +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/test/java/org/acme/GreetingResourceTest.java b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/test/java/org/acme/GreetingResourceTest.java new file mode 100644 index 0000000000000..c832dcf849e72 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/test/java/org/acme/GreetingResourceTest.java @@ -0,0 +1,21 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +public class GreetingResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/hello") + .then() + .statusCode(200) + .body(is("foo bar")); + } + +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/settings.gradle.kts b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/settings.gradle.kts new file mode 100644 index 0000000000000..b93cd3eb8bc81 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + val quarkusPluginVersion: String by settings + repositories { + mavenLocal { + content { + includeGroupByRegex("io.quarkus.*") + includeGroup("org.hibernate.orm") + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id("io.quarkus") version quarkusPluginVersion + } +} + +rootProject.name="code-with-quarkus" + +include( + "modA", + "modB" +) diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusAppliedToMultipleModulesTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusAppliedToMultipleModulesTest.java new file mode 100644 index 0000000000000..849ed8abd92b7 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusAppliedToMultipleModulesTest.java @@ -0,0 +1,18 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +public class QuarkusAppliedToMultipleModulesTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testBasicMultiModuleBuild() throws Exception { + final File projectDir = getProjectDir("quarkus-plugin-in-multiple-modules"); + final BuildResult build = runGradleWrapper(projectDir, "clean", "quarkusBuild"); + assertThat(BuildResult.isSuccessful(build.getTasks().get(":modA:quarkusBuild"))).isTrue(); + assertThat(BuildResult.isSuccessful(build.getTasks().get(":modB:quarkusBuild"))).isTrue(); + } +} From 6e3ad3d73ecddb48da6c1af3781ce6c457488596 Mon Sep 17 00:00:00 2001 From: Jose Date: Thu, 14 Nov 2024 11:39:50 +0100 Subject: [PATCH 10/39] Support title and type fields when generating HAL links Before these changes, we were only considering the "href" field for the HAL links. Example: ```json { "_links": { "subject": { "href": "/subject" } } } ``` After these changes, users that populate also the title and/or the type like: ```java @GET @Path("/with-rest-link-with-all-fields") @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) public HalEntityWrapper get() { var entity = // ... return new HalEntityWrapper<>(entity, Link.fromUri(URI.create("/path/to/100")) .rel("all") .title("The title link") // the link title .type(MediaType.APPLICATION_JSON) // the link type .build()); } ``` Or using the annotation like: ```java @GET @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) @RestLink(entityType = TestRecordWithRestLinkId.class, title = "The with rest link title", type = MediaType.APPLICATION_JSON) @InjectRestLinks public TestRecordWithRestLinkId get() { return // ... } ``` Then, the links will have the title and/or type fields populated: ```json { "_links": { "subject": { "href": "/subject", "title": "The with rest link title", "type": "application/json" } } } ``` (cherry picked from commit 679c1ed522eae8cd304d4639d7fac5ed8a6edcad) --- .../src/main/java/io/quarkus/hal/HalLink.java | 14 +++++++++++- .../quarkus/hal/HalLinkJacksonSerializer.java | 8 +++++++ .../quarkus/hal/HalLinkJsonbSerializer.java | 8 +++++++ .../main/java/io/quarkus/hal/HalWrapper.java | 4 +++- .../links/runtime/hal/ResteasyHalService.java | 2 +- .../deployment/LinksContainerFactory.java | 4 +++- .../deployment/AbstractHalLinksTest.java | 17 ++++++++++++++ .../links/deployment/TestResource.java | 19 +++++++++++++++- .../resteasy/reactive/links/RestLink.java | 14 ++++++++++++ .../reactive/links/runtime/LinkInfo.java | 16 +++++++++++++- .../links/runtime/RestLinksProviderImpl.java | 22 ++++++++++++++----- .../hal/ResteasyReactiveHalService.java | 2 +- 12 files changed, 117 insertions(+), 13 deletions(-) diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java index accf86b916e7e..dd9ed6a3bfe60 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java @@ -3,12 +3,24 @@ public class HalLink { private final String href; + private final String title; + private final String type; - public HalLink(String href) { + public HalLink(String href, String title, String type) { this.href = href; + this.title = title; + this.type = type; } public String getHref() { return href; } + + public String getTitle() { + return title; + } + + public String getType() { + return type; + } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java index 01873dba72aa5..7bcf0ab6cc227 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java @@ -12,6 +12,14 @@ public class HalLinkJacksonSerializer extends JsonSerializer { public void serialize(HalLink value, JsonGenerator generator, SerializerProvider serializers) throws IOException { generator.writeStartObject(); generator.writeObjectField("href", value.getHref()); + if (value.getTitle() != null) { + generator.writeObjectField("title", value.getTitle()); + } + + if (value.getType() != null) { + generator.writeObjectField("type", value.getType()); + } + generator.writeEndObject(); } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java index 60fd49d43e2a5..44fb1354b24d0 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java @@ -10,6 +10,14 @@ public class HalLinkJsonbSerializer implements JsonbSerializer { public void serialize(HalLink value, JsonGenerator generator, SerializationContext context) { generator.writeStartObject(); generator.write("href", value.getHref()); + if (value.getTitle() != null) { + generator.write("title", value.getTitle()); + } + + if (value.getType() != null) { + generator.write("type", value.getType()); + } + generator.writeEnd(); } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java index 16338be4d3aea..1e3fbc97deb20 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java @@ -24,7 +24,9 @@ public Map getLinks() { @SuppressWarnings("unused") public void addLinks(Link... links) { for (Link link : links) { - this.links.put(link.getRel(), new HalLink(link.getUri().toString())); + this.links.put(link.getRel(), new HalLink(link.getUri().toString(), + link.getTitle(), + link.getType())); } } } diff --git a/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java index 6da3709d42b2f..73e5ae4beabc0 100644 --- a/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java +++ b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java @@ -29,7 +29,7 @@ protected Map getInstanceLinks(Object entity) { private Map linksToMap(RESTServiceDiscovery serviceDiscovery) { Map links = new HashMap<>(serviceDiscovery.size()); for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) { - links.put(atomLink.getRel(), new HalLink(atomLink.getHref())); + links.put(atomLink.getRel(), new HalLink(atomLink.getHref(), atomLink.getTitle(), atomLink.getType())); } return links; } diff --git a/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java b/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java index 2a991448e49df..7c19b66d00eae 100644 --- a/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java +++ b/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java @@ -62,6 +62,8 @@ private LinkInfo getLinkInfo(ResourceMethod resourceMethod, MethodInfo resourceM AnnotationInstance restLinkAnnotation, String resourceClassPath, IndexView index) { Type returnType = getNonAsyncReturnType(resourceMethodInfo.returnType()); String rel = getAnnotationValue(restLinkAnnotation, "rel", deductRel(resourceMethod, returnType, index)); + String title = getAnnotationValue(restLinkAnnotation, "title", null); + String type = getAnnotationValue(restLinkAnnotation, "type", null); String entityType = getAnnotationValue(restLinkAnnotation, "entityType", deductEntityType(returnType)); String path = UriBuilder.fromPath(resourceClassPath).path(resourceMethod.getPath()).toTemplate(); while (path.endsWith("/")) { @@ -69,7 +71,7 @@ private LinkInfo getLinkInfo(ResourceMethod resourceMethod, MethodInfo resourceM } Set pathParameters = getPathParameters(path); - return new LinkInfo(rel, entityType, path, pathParameters); + return new LinkInfo(rel, title, type, entityType, path, pathParameters); } /** diff --git a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java index 99f71ebaac2b0..4bb1d51d03d86 100644 --- a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java +++ b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java @@ -3,6 +3,9 @@ import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + import org.jboss.resteasy.reactive.common.util.RestMediaType; import org.junit.jupiter.api.Test; @@ -89,5 +92,19 @@ void shouldGetHalLinksForRestLinkId() { .thenReturn(); assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/records/with-rest-link-id/100"); + assertThat(response.body().jsonPath().getString("_links.self.title")).isEqualTo("The with rest link title"); + assertThat(response.body().jsonPath().getString("_links.self.type")).isEqualTo(MediaType.APPLICATION_JSON); + } + + @Test + void shouldIncludeAllFieldsFromLink() { + Response response = given() + .header(HttpHeaders.ACCEPT, RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-rest-link-with-all-fields") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.all.href")).endsWith("/records/with-rest-link-id/100"); + assertThat(response.body().jsonPath().getString("_links.all.title")).isEqualTo("The title link"); + assertThat(response.body().jsonPath().getString("_links.all.type")).isEqualTo(MediaType.APPLICATION_JSON); } } diff --git a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java index 50255cd5f39cb..42750f5090a57 100644 --- a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java +++ b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.reactive.links.deployment; +import java.net.URI; import java.time.Duration; import java.util.Arrays; import java.util.LinkedList; @@ -11,10 +12,12 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Link; import jakarta.ws.rs.core.MediaType; import org.jboss.resteasy.reactive.common.util.RestMediaType; +import io.quarkus.hal.HalEntityWrapper; import io.quarkus.resteasy.reactive.links.InjectRestLinks; import io.quarkus.resteasy.reactive.links.RestLink; import io.quarkus.resteasy.reactive.links.RestLinkType; @@ -172,7 +175,7 @@ public TestRecordWithPersistenceId getWithPersistenceId(@PathParam("id") int id) @GET @Path("/with-rest-link-id/{id}") @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) - @RestLink(entityType = TestRecordWithRestLinkId.class) + @RestLink(entityType = TestRecordWithRestLinkId.class, title = "The with rest link title", type = MediaType.APPLICATION_JSON) @InjectRestLinks public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) { return REST_LINK_ID_RECORDS.stream() @@ -181,4 +184,18 @@ public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) { .orElseThrow(NotFoundException::new); } + @GET + @Path("/with-rest-link-with-all-fields") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + public HalEntityWrapper getAllFieldsFromLink() { + + var entity = new TestRecordWithIdAndPersistenceIdAndRestLinkId(1, 10, 100, "one"); + return new HalEntityWrapper<>(entity, + Link.fromUri(URI.create("/records/with-rest-link-id/100")) + .rel("all") + .title("The title link") + .type(MediaType.APPLICATION_JSON) + .build()); + } + } diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java index cfa5316efb180..d083abcf59e61 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java @@ -25,6 +25,20 @@ */ String rel() default ""; + /** + * Intended for labelling the link with a human-readable identifier. + * + * @return the link title. + */ + String title() default ""; + + /** + * Hint to indicate the media type expected when dereferencing the target resource. + * + * @return the link expected media type. + */ + String type() default ""; + /** * Declares a link for the given type of resources. * If not set, it will default to the returning type of the annotated method. diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java index 45851358c6e3c..fda5c0f2e7dea 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java @@ -6,14 +6,20 @@ public final class LinkInfo { private final String rel; + private final String title; + + private final String type; + private final String entityType; private final String path; private final Set pathParameters; - public LinkInfo(String rel, String entityType, String path, Set pathParameters) { + public LinkInfo(String rel, String title, String type, String entityType, String path, Set pathParameters) { this.rel = rel; + this.title = title; + this.type = type; this.entityType = entityType; this.path = path; this.pathParameters = pathParameters; @@ -23,6 +29,14 @@ public String getRel() { return rel; } + public String getTitle() { + return title; + } + + public String getType() { + return type; + } + public String getEntityType() { return entityType; } diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java index f29ccc56c1710..787b4bb90254f 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java @@ -37,9 +37,7 @@ public Collection getTypeLinks(Class elementType) { List links = new ArrayList<>(linkInfoList.size()); for (LinkInfo linkInfo : linkInfoList) { if (linkInfo.getPathParameters().size() == 0) { - links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) - .rel(linkInfo.getRel()) - .build()); + links.add(linkBuilderFor(linkInfo).build()); } } return links; @@ -52,13 +50,25 @@ public Collection getInstanceLinks(T instance) { List linkInfoList = linksContainer.getForClass(instance.getClass()); List links = new ArrayList<>(linkInfoList.size()); for (LinkInfo linkInfo : linkInfoList) { - links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) - .rel(linkInfo.getRel()) - .build(getPathParameterValues(linkInfo, instance))); + links.add(linkBuilderFor(linkInfo).build(getPathParameterValues(linkInfo, instance))); } return links; } + private Link.Builder linkBuilderFor(LinkInfo linkInfo) { + Link.Builder builder = Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) + .rel(linkInfo.getRel()); + if (linkInfo.getTitle() != null) { + builder.title(linkInfo.getTitle()); + } + + if (linkInfo.getType() != null) { + builder.type(linkInfo.getType()); + } + + return builder; + } + private Object[] getPathParameterValues(LinkInfo linkInfo, Object instance) { List values = new ArrayList<>(linkInfo.getPathParameters().size()); for (String name : linkInfo.getPathParameters()) { diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java index c7a58fa36e1d0..a3d66cc7f172f 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java @@ -34,7 +34,7 @@ protected Map getInstanceLinks(Object entity) { private Map linksToMap(Collection refLinks) { Map links = new HashMap<>(); for (Link link : refLinks) { - links.put(link.getRel(), new HalLink(link.getUri().toString())); + links.put(link.getRel(), new HalLink(link.getUri().toString(), link.getTitle(), link.getType())); } return links; From 55edef10dac0ca7ca43367df08cf2637b331751f Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 14 Nov 2024 12:06:04 +0100 Subject: [PATCH 11/39] WebSockets Next: support arrays as valid return types of callbacks - for binary messages a custom codec is always needed (cherry picked from commit d90b5f90079a9392ee622015baa92310b1edb2b9) --- .../next/deployment/WebSocketProcessor.java | 2 +- .../next/test/codec/ArrayBinaryCodecTest.java | 80 +++++++++++++++++++ .../next/test/codec/ArrayTextCodecTest.java | 58 ++++++++++++++ .../codec/ByteArrayBinaryMessageTest.java | 55 +++++++++++++ 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayBinaryCodecTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayTextCodecTest.java create mode 100644 extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ByteArrayBinaryMessageTest.java diff --git a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java index f32cb7327b77a..0b6eddb255515 100644 --- a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java +++ b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java @@ -1594,10 +1594,10 @@ static boolean hasBlockingSignature(MethodInfo method) { if (KotlinUtils.isKotlinSuspendMethod(method)) { return false; } - switch (method.returnType().kind()) { case VOID: case CLASS: + case ARRAY: return true; case PARAMETERIZED_TYPE: // Uni, Multi -> non-blocking diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayBinaryCodecTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayBinaryCodecTest.java new file mode 100644 index 0000000000000..82dd8aa42d889 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayBinaryCodecTest.java @@ -0,0 +1,80 @@ +package io.quarkus.websockets.next.test.codec; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Type; +import java.net.URI; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +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.BinaryMessageCodec; +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; + +public class ArrayBinaryCodecTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Item.class, Endpoint.class, WSClient.class, ItemArrayBinaryCodec.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("end") + URI testUri; + + @Test + public void testCodec() { + try (WSClient client = new WSClient(vertx)) { + client.connect(testUri); + client.sendAndAwait(Buffer.buffer("Foo")); + client.waitForMessages(1); + assertEquals("Foo", client.getMessages().get(0).toString()); + } + } + + @Singleton + public static class ItemArrayBinaryCodec implements BinaryMessageCodec { + + @Override + public boolean supports(Type type) { + return (type instanceof GenericArrayType) || (type instanceof Class && ((Class) type).isArray()); + } + + @Override + public Buffer encode(Item[] value) { + return Buffer.buffer(value[0].getName()); + } + + @Override + public Item[] decode(Type type, Buffer value) { + throw new UnsupportedOperationException(); + } + + } + + @WebSocket(path = "/end") + public static class Endpoint { + + @OnBinaryMessage + Item[] process(String name) { + Item item = new Item(); + item.setName(name); + item.setCount(1); + return new Item[] { item }; + } + + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayTextCodecTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayTextCodecTest.java new file mode 100644 index 0000000000000..9bd6d0bc1bc1d --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayTextCodecTest.java @@ -0,0 +1,58 @@ +package io.quarkus.websockets.next.test.codec; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; + +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.OnOpen; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class ArrayTextCodecTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Item.class, Endpont.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("end") + URI testUri; + + @Test + public void testCodec() throws Exception { + try (WSClient client = new WSClient(vertx)) { + client.connect(testUri); + client.waitForMessages(1); + assertEquals(new JsonArray().add(new JsonObject().put("name", "Foo").put("count", 1)).toString(), + client.getMessages().get(0).toString()); + } + } + + @WebSocket(path = "/end") + public static class Endpont { + + // The default JsonTextMessageCodec is used + @OnOpen + Item[] open() { + Item item = new Item(); + item.setName("Foo"); + item.setCount(1); + return new Item[] { item }; + } + + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ByteArrayBinaryMessageTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ByteArrayBinaryMessageTest.java new file mode 100644 index 0000000000000..988db21af3eb3 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ByteArrayBinaryMessageTest.java @@ -0,0 +1,55 @@ +package io.quarkus.websockets.next.test.codec; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; + +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.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; + +public class ByteArrayBinaryMessageTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Endpont.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("end") + URI testUri; + + @Test + public void testCodec() throws Exception { + try (WSClient client = new WSClient(vertx)) { + client.connect(testUri); + client.send(Buffer.buffer("43")); + client.waitForMessages(1); + assertEquals("43", client.getMessages().get(0).toString()); + } + } + + @WebSocket(path = "/end") + public static class Endpont { + + // This is an equivalent to Sender#sendBinary(byte[]) + // byte[] is encoded with Buffer#buffer(byte[]), codec is not needed + @OnBinaryMessage + byte[] echo(Buffer message) { + return message.getBytes(); + } + + } +} From 72cdf51a2f5bcedf7b68f7128dea4ed96d80cfb3 Mon Sep 17 00:00:00 2001 From: brunobat Date: Thu, 14 Nov 2024 09:58:56 +0000 Subject: [PATCH 12/39] Improve test reliability (cherry picked from commit 73b1e5cd7b0444c6e22bad91ff656252a3171523) --- .../deployment/common/exporter/InMemoryMetricExporter.java | 5 ----- .../deployment/metrics/HttpServerMetricsTest.java | 5 ++++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java index 1abb8874bede2..104062b53127c 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java @@ -106,11 +106,6 @@ private static boolean isPathFound(String path, Attributes attributes) { return value.toString().equals(path); } - public void assertCount(final int count) { - Awaitility.await().atMost(5, SECONDS) - .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems().size())); - } - public void assertCount(final String name, final String target, final int count) { Awaitility.await().atMost(5, SECONDS) .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems(name, target).size())); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/HttpServerMetricsTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/HttpServerMetricsTest.java index 31da620a25891..dd2839e33024f 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/HttpServerMetricsTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/HttpServerMetricsTest.java @@ -70,7 +70,10 @@ void collectsHttpRouteFromEndAttributes() { .statusCode(INTERNAL_SERVER_ERROR.getStatusCode()); metricExporter.assertCountPointsAtLeast("http.server.request.duration", null, 2); - MetricData metric = metricExporter.getFinishedMetricItems("http.server.request.duration", null).get(0); + MetricData metric = metricExporter + .getFinishedMetricItems("http.server.request.duration", null).stream() + .reduce((first, second) -> second) // get the last received + .orElse(null); assertThat(metric) .hasName("http.server.request.duration") From b0d38ec9cb732b27b5a72b8472439bc8759efc28 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Fri, 9 Aug 2024 12:51:53 +0100 Subject: [PATCH 13/39] Add explanation/concept for extension maturity matrix Co-Authored-By: Max Rydahl Andersen Co-Authored-By: Guillaume Smet (cherry picked from commit 8180289a5e5ed089c197dfa4f5b393cba6a85fba) --- .../asciidoc/extension-maturity-matrix.adoc | 178 +++++++++++++++++ .../src/main/asciidoc/extension-metadata.adoc | 2 +- .../images/extension-maturity_matrix.svg | 186 ++++++++++++++++++ .../src/main/asciidoc/writing-extensions.adoc | 4 + 4 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 docs/src/main/asciidoc/extension-maturity-matrix.adoc create mode 100644 docs/src/main/asciidoc/images/extension-maturity_matrix.svg diff --git a/docs/src/main/asciidoc/extension-maturity-matrix.adoc b/docs/src/main/asciidoc/extension-maturity-matrix.adoc new file mode 100644 index 0000000000000..43dfed1e028e9 --- /dev/null +++ b/docs/src/main/asciidoc/extension-maturity-matrix.adoc @@ -0,0 +1,178 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// +[id="extension-maturity-matrix"] += A maturity matrix for Quarkus extensions +include::_attributes.adoc[] +:diataxis-type: concept +:categories: writing-extensions +:topics: extensions +:summary: Quarkus extensions can do a lot, or a little. This guide explains some of the capabilities extension authors might want to include. +//// +The document header ends at the first blank line. Do not remove the blank line between the header and the abstract summary. +//// + +What makes a good Quarkus extension? What capabilities is a Quarkus extension expected to provide? Of course, it depends on the extension you are building. But, we found a set of attributes common to many extensions. This document explains what they are. + +image::extension-maturity-matrix.svg[A maturity matrix] + +This isn't defining an exact order, even within a single row. Different extensions have different goals, and different developers will have different views on what capabilities are most important. You may wish to (for example) prioritise a fantastic programming model over enhancing your extension's Dev UI tile. That's fine! + +Also, not every step will apply to every extension. For example, you don't need a Dev Service if your extension doesn't depend on external services. + +It's completely OK to publish a first version of an extension that doesn't handle everything. In fact, it's OK if your extension _never_ gets to the more advanced features. This is a suggested pathway, not a minimum feature set. + +Also note that this list only includes the technical features of your extension. +You might also want to think about how you share your extension, and how it presents itself to the world. +The link:https://hub.quarkiverse.io/checklistfornewprojects/[new extension checklist] on the Quarkiverse Hub has a useful list of ways extensions can participate in the ecosystem. +It's also a good idea to spend some time on the metadata in the xref:extension-metadata#quarkus-extension-yaml[`quarkus-extension.yaml` file], which is used by Quarkus tooling. + +Here are some pointers on how to achieve those capabilities. + +== Run modes + +Quarkus applications can be run as a normal jar-based JVM application, +or live-coded in dev mode, or compiled to a native binary. +Each environment places different demands on framework extensions. + +=== Works in JVM mode + +For most extensions, this is the minimum expectation. +When wrapping an existing library, this is usually trivial to achieve; if an extension is providing net-new capability, it might be a bit more work. Quarkus provides tools for xref:writing-extensions.adoc#testing-extensions[unit testing and integration testing] extensions. + +=== Works in dev mode + +In some cases, extra work may be needed to ensure any wrapped libraries can tolerate +dev mode, since the classloading is different and hot reloading can break some assumptions. Extensions may also wish to add some +xref:writing-extensions.adoc#integrating-with-development-mode[special handling for dev mode]. +To add automated tests which validate dev mode, you can xref:writing-extensions.adoc#testing-hot-reload[add tests which extend the `QuarkusDevModeTest`]. + +=== Works as a native application + +For many libraries, native mode support is the primary motivation for creating an extension. See xref:writing-extensions.adoc#native-executable-support[the guide on native executable support] for more discussion about some of the adaptations that might be needed. + +== Developer Joy + +Developer Joy is an important Quarkus principle. +Here are some extension capabilities that contribute to joyful development. + +=== Configuration support + +Extensions should support Quarkus's unified configuration, by xref:writing-extensions.adoc#configuration[integrating with the Quarkus configuration model]. +The Writing Extensions guide has more guidance on xref:writing-extensions.adoc#how-to-expose-configuration[the Quarkus configuration philosophy]. + +=== CDI Beans + +Quarkus extensions should aim to xref:writing-extensions.adoc#expose-your-components-via-cdi[expose components via CDI], so that they can be consumed in a frictionless way by user applications. +Having everything injectable as CDI beans also helps testing, especially xref:getting-started-testing#mock-support[mocking]. + +=== Dev Service + +Dev Services are generally relevant for extensions that "connect" to something, such as databases for datasources, a keycloak instance for security, an Apache Kafka instance for messaging, etc. + +To provide a Dev Service, use the `DevServicesResultBuildItem` build item. See the xref:extension-writing-dev-service.adoc[Dev Services how-to] for more information. + +=== Basic Dev UI + +Every extension gets a tile in the Dev UI. The default tile pulls information from the xref:extension-metadata.adoc[extension metadata], which is another reason to spend a bit of time getting the metadata right. + +Extensions also use Dev UI hooks to present extra information to users. For example, the tile could include a link to an external console, or an internal page which presents simple text metrics. See the xref:dev-ui.adoc[Dev UI for extension developers] guide. + + +=== Rich Dev UI + +Some extensions provide extremely sophisticated Dev UIs. +For example, they might allow users to interact with the running application, xref:dev-ui.adoc#hot-reload[respond to reloads], visualise application metrics, or xref:dev-ui.adoc#add-a-log-to-the-footer[stream an application-specific log]. +The xref:dev-ui.adoc[Dev UI for extension developers] guide also explains these more advanced options. + +=== Joyful programming model + +Quarkus's build-time philosophy means extensions can tidy up API boilerplate and make programming models more concise and expressive. +A good starting point is usually to use + xref:writing-extensions.adoc#scanning-deployments-using-jandex[Jandex] to scan user code for annotations and other markers. +Although providing new, joyful, ways to do things is good, +it's important to not break the normal patterns that users may be familiar with. + +For some inspiration in this area, have a look at xref:logging#simplified-logging[simplified logging], xref:hibernate-orm-panache.adoc[simplified Hibernate ORM with Panache], the xref:rest-client.adoc#query-parameters[`@RestQuery` annotation], or the way Quarkus allows test containers to be used xref:getting-started-dev-services.adoc[without any configuration]. + +=== Codestart application template + +Codestarts are templates which can be used to generate applications for users. +Extensions can xref:extension-codestart.adoc[provide their own codestart templates]. + +== Supersonic subatomic performance + +Extensions should use build-time application knowledge to eliminate wasteful runtime code paths. We call this supersonic subatomic performance. +Because Quarkus moves work to the build stage, Quarkus applications should have fast startup, high throughput, and low memory requirements. Performance tuning is a large subject, but extensions should use build-time application knowledge to eliminate wasteful runtime code paths at runtime. + +=== Static initialization + +Do as much initialization as much as possible statically. +This avoid runtime overhead. + +=== Replace reflection with generated bytecode + +Many Java libraries make heavy use of reflection to delay decisions to run-time. Quarkus aims to improve performance by moving logic to build time, reducing unnecessary dynamism. +Extensions should aim to replace reflection with build-time code. +This is enabled by + xref:writing-extensions.adoc#scanning-deployments-using-jandex[Jandex], an "offline reflection" library. It may also be necessary to do some bytecode transformation of existing libraries. + +For a case study of how to eliminate reflection and what the performance benefits turned out to be, see https://quarkus.io/blog/quarkus-metaprogramming/[reflectionless Jackson serialization] + +=== Virtual thread support + +Not every library is suitable for using with virtual threads, out of the box. +xref:virtual-threads#why-not["Why not virtual threads everywhere?"] explains why. + +To get your library working properly with virtual threads, you should make sure the library is not pinning the carrier thread. + Quarkus has xref:virtual-threads.adoc#testing-virtual-thread-applications[test helpers to do these checks in an automated way]. + For dispatching work, you should use the xref:virtual-threads.adoc#inject-the-virtual-thread-executor[virtual executor managed by Quarkus]. The link:https://quarkus.io/extensions/io.quarkus/quarkus-websockets-next/[WebSockets-next extension] uses the virtual dispatcher and smart dispatch, and is a good example to follow. + +=== Hot path performance optimization + +Although Quarkus offers some unique opportunities for extension performance, extension developers shouldn't forget https://www.linkedin.com/pulse/how-optimize-software-performance-efficiency-subcodevs/[the basics of performance optimization]. + +=== Non-blocking internals + +Quarkus's reactive core is a key contributor to its excellent throughput and scalability. Extensions should consider adopting this model for their own internal operations. + +=== Add Mutiny-based APIs + +For maximum scalability, go beyond the reactive core and enable fully reactive programming, using Mutiny. Most projects that support a reactive programming model offer two distinct extensions, a `-reactive` and a plain one. +See, for example, https://quarkus.io/extensions/io.quarkiverse.quarkus-elasticsearch/quarkus-elasticsearch/[ElasticSearch] and https://quarkus.io/extensions/io.quarkiverse.quarkus-elasticsearch-reactive/quarkus-elasticsearch-reactive/[ElasticSearch Reactive] extensions. + +== Operations + +Developer joy is important, but so are observability, maintainability, and other operational considerations. +Many of these characteristics come by default with the Quarkus framework or https://quarkus.io/extensions/io.quarkus/quarkus-opentelemetry/[observability-focussed extensions]. But extensions can do more. + +=== Logging + +Quarkus uses JBoss Logging as its logging engine, and xref:logging[supports several logging APIs]. (This is normal Java logging, not OpenTelemetry logging.) + +Avoid using errors and warnings for conditions that will not affect normal operation. These outputs can cause false alarms in user monitoring systems. + + +=== Define health endpoints + +Extensions may wish to xref:writing-extensions#extension-defined-endpoints[define library-specific endpoints] for health criteria which are specific to that extension. To add a new endpoint, extensions should produce a `NonApplicationRootPathBuildItem`. + +=== Tracing context + +You should test that OpenTelemetry output for applications using your extension have properly-defined spans. You may need to do extra work to ensure spans are created with the right tracing ID. +For example, extensions which have reactive internals should support xref:duplicated-context.adoc[duplicated contexts] for correct context propagation. + +=== Advanced Kubernetes and containers integration + +Quarkus is designed to be a Kubernetes-native runtime. +Extensions can continue this philosophy by adding library-specific integration points with Kubernetes. +Being Kubernetes-native implies being container-native. At a minimum, extensions should always work well in containers, but extensions may also have opportunities to integrate with the lower levels of the container stack. + + +== References + +- xref:writing-extensions.adoc[Writing your own extension] guide +- xref:building-my-first-extension.adoc[Building your first extension] +- link:https://hub.quarkiverse.io.adoc[The Quarkiverse Hub documentation] \ No newline at end of file diff --git a/docs/src/main/asciidoc/extension-metadata.adoc b/docs/src/main/asciidoc/extension-metadata.adoc index 2fc7a7efe4d02..2bd3af6873a81 100644 --- a/docs/src/main/asciidoc/extension-metadata.adoc +++ b/docs/src/main/asciidoc/extension-metadata.adoc @@ -11,7 +11,7 @@ include::_attributes.adoc[] Quarkus extensions are distributed as Maven JAR artifacts that application and other libraries may depend on. When a Quarkus application project is built, tested or edited using the Quarkus dev tools, Quarkus extension JAR artifacts will be identified on the application classpath by the presence of the Quarkus extension metadata files in them. This document describes the purpose of each Quarkus extension metadata file and its content. -IMPORTANT: Two of the metadata files have the same name but different extensions, `quarkus-extension.yaml` and `quarkus-extension.properties`. It is easy to mix them up, be careful. You will usually edit the YAML file and track it in your SCM. While you _can_ manually manage the properties file, Quarkus will generated it at build if you don't. +IMPORTANT: Two of the metadata files have the same name but different extensions, `quarkus-extension.yaml` and `quarkus-extension.properties`. It is easy to mix them up, be careful. You will usually edit the YAML file and track it in your SCM. While you _can_ manually manage the properties file, Quarkus will generate it at build if you don't. [[quarkus-extension-yaml]] == META-INF/quarkus-extension.yaml diff --git a/docs/src/main/asciidoc/images/extension-maturity_matrix.svg b/docs/src/main/asciidoc/images/extension-maturity_matrix.svg new file mode 100644 index 0000000000000..964d9f7d22e99 --- /dev/null +++ b/docs/src/main/asciidoc/images/extension-maturity_matrix.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + Run modes + Works in dev mode + Works on JVM + Works in native + + + + + + + + + + + + + + + + + + + + + Developer joy + Unified config + CDI bean + Dev Service + Basic Dev UI + Rich Dev UI + Joyful programming model + Codestart template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Static init + Replace reflection + Virtual thread support + Non-blocking internals + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Mutiny-based API + + + + + + + + + + + + + + + Operations + Logging + Define health endpoints + Tracing context + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Implemented| Not yet done/not in scope + \ No newline at end of file diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index f62c27c51d03a..5e6f84dcf8340 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -331,6 +331,10 @@ Too flexible to benefit from the build time boot promoted by Quarkus. Most extension we have seen do not make use of these extreme flexibility capabilities. The way to port a CDI extension to Quarkus is to rewrite it as a Quarkus extension which will define the various beans at build time (deployment time in extension parlance). +=== Levels of capability + +Quarkus extensions can do lots of things. The xref:extension-maturity-matrix.adoc[extension maturity matrix] lays out a path through the various capabilities, with a suggested implementation order. + == Technical aspect [[bootstrap-three-phases]] From 9ad311daf6d2d4314fb2d19d8bf21b89c26c7b43 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Thu, 14 Nov 2024 12:08:15 +0000 Subject: [PATCH 14/39] Log in smallrye-jwt and oauth2 extensions when no bearer access token is available (cherry picked from commit 7e3f237d4334ef6fbecbd10cb728bcbd9049c973) --- .../security/oauth2/runtime/auth/OAuth2AuthMechanism.java | 8 ++++++-- .../smallrye/jwt/runtime/auth/JWTAuthMechanism.java | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java index ef644ce1007b8..2742c4e1b1307 100644 --- a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java +++ b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java @@ -5,6 +5,8 @@ import jakarta.enterprise.context.ApplicationScoped; +import org.jboss.logging.Logger; + import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.quarkus.security.credential.TokenCredential; @@ -23,7 +25,7 @@ */ @ApplicationScoped public class OAuth2AuthMechanism implements HttpAuthenticationMechanism { - + private static final Logger LOG = Logger.getLogger(OAuth2AuthMechanism.class); private static final String BEARER_PREFIX = "Bearer "; protected static final ChallengeData CHALLENGE_DATA = new ChallengeData( @@ -46,7 +48,9 @@ public Uni authenticate(RoutingContext context, String authHeader = context.request().headers().get("Authorization"); if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) { - // No suitable bearer token has been found in this request, + // No suitable bearer token has been found in this request + LOG.debug("Bearer access token is not available"); + return Uni.createFrom().nullItem(); } diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java index 0ac20289bbaa7..79b6dcabfda21 100644 --- a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java @@ -9,6 +9,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.jboss.logging.Logger; + import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; @@ -34,6 +36,7 @@ */ @ApplicationScoped public class JWTAuthMechanism implements HttpAuthenticationMechanism { + private static final Logger LOG = Logger.getLogger(JWTAuthMechanism.class); private static final String ERROR_MSG = "SmallRye JWT requires a safe (isolated) Vert.x sub-context for propagation " + "of the '" + TokenCredential.class.getName() + "', but the current context hasn't been flagged as such."; protected static final String COOKIE_HEADER = "Cookie"; @@ -86,6 +89,8 @@ public void run() { return identityProviderManager .authenticate(HttpSecurityUtils.setRoutingContextAttribute( new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken)), context)); + } else { + LOG.debug("Bearer access token is not available"); } return Uni.createFrom().optional(Optional.empty()); } From 2ede8888a704ff3e1959bb146ef80281e7b809ef Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Wed, 13 Nov 2024 09:51:34 -0300 Subject: [PATCH 15/39] Use `QUARKUS_FLYWAY_ACTIVE` instead of `QUARKUS_FLYWAY_ENABLED` env in Kubernetes resources Because the original property is a build-time configuration property (cherry picked from commit ef974032b8df45c29e9338ebf5ff7bcfdce68284) --- .../io/quarkus/flyway/deployment/FlywayProcessor.java | 4 ++-- .../it/kubernetes/KubernetesWithFlywayInitBase.java | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java index e55818785a9af..bf5a400c5610d 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java @@ -298,8 +298,8 @@ public ServiceStartBuildItem startActions(FlywayRecorder recorder, public InitTaskBuildItem configureInitTask(ApplicationInfoBuildItem app) { return InitTaskBuildItem.create() .withName(app.getName() + "-flyway-init") - .withTaskEnvVars(Map.of("QUARKUS_INIT_AND_EXIT", "true", "QUARKUS_FLYWAY_ENABLED", "true")) - .withAppEnvVars(Map.of("QUARKUS_FLYWAY_ENABLED", "false")) + .withTaskEnvVars(Map.of("QUARKUS_INIT_AND_EXIT", "true", "QUARKUS_FLYWAY_ACTIVE", "true")) + .withAppEnvVars(Map.of("QUARKUS_FLYWAY_ACTIVE", "false")) .withSharedEnvironment(true) .withSharedFilesystem(true); } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java index 565d098e0fa6e..d9c682e6d5c30 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java @@ -29,9 +29,7 @@ public void assertGeneratedResources(Path kubernetesDir, String name, String tas && name.equals(d.getMetadata().getName())) .map(d -> (Deployment) d).findAny(); - assertTrue(deployment.isPresent()); - assertThat(deployment).satisfies(j -> j.isPresent()); - assertThat(deployment.get()).satisfies(d -> { + assertThat(deployment).isPresent().get().satisfies(d -> { assertThat(d.getMetadata()).satisfies(m -> { assertThat(m.getName()).isEqualTo(name); }); @@ -56,9 +54,7 @@ public void assertGeneratedResources(Path kubernetesDir, String name, String tas .filter(j -> "Job".equals(j.getKind()) && jobName.equals(j.getMetadata().getName())) .map(j -> (Job) j) .findAny(); - assertTrue(job.isPresent()); - - assertThat(job.get()).satisfies(j -> { + assertThat(job).isPresent().get().satisfies(j -> { assertThat(j.getSpec()).satisfies(jobSpec -> { assertThat(jobSpec.getCompletionMode()).isEqualTo("NonIndexed"); assertThat(jobSpec.getTemplate()).satisfies(t -> { @@ -69,7 +65,7 @@ public void assertGeneratedResources(Path kubernetesDir, String name, String tas assertThat(podSpec.getRestartPolicy()).isEqualTo("OnFailure"); assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { assertThat(container.getName()).isEqualTo(jobName); - assertThat(container.getEnv()).filteredOn(env -> "QUARKUS_FLYWAY_ENABLED".equals(env.getName())) + assertThat(container.getEnv()).filteredOn(env -> "QUARKUS_FLYWAY_ACTIVE".equals(env.getName())) .singleElement().satisfies(env -> { assertThat(env.getValue()).isEqualTo("true"); }); From a77aba479f1c0118d01767af9e7a7ecb14314a2e Mon Sep 17 00:00:00 2001 From: Thomas Canava Date: Wed, 13 Nov 2024 20:55:11 +0100 Subject: [PATCH 16/39] Fix nullpointer on null code ws-next (cherry picked from commit 106984727116f19651e51b013156d76bee1a3d14) --- .../next/runtime/BasicWebSocketConnectorImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 bf6dd1044e0bf..45dffc6c30849 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 @@ -239,7 +239,11 @@ public void handle(Void event) { trafficLogger.connectionClosed(connection); } if (closeHandler != null) { - doExecute(connection, new CloseReason(ws.closeStatusCode(), ws.closeReason()), closeHandler); + CloseReason reason = CloseReason.INTERNAL_SERVER_ERROR; + if (ws.closeStatusCode() != null) { + reason = new CloseReason(ws.closeStatusCode(), ws.closeReason()); + } + doExecute(connection, reason, closeHandler); } connectionManager.remove(BasicWebSocketConnectorImpl.class.getName(), connection); client.get().close(); From ee8558c395b8a851d87827b160bda6dee63f77ce Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Thu, 14 Nov 2024 15:13:50 +0100 Subject: [PATCH 17/39] SmallRye Fault Tolerance: upgrade to 6.6.2 (cherry picked from commit ef3a1c7e95e00c8e23c42d8fb07bc0180060584d) --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index f2d309cc61482..173d6630f1147 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -53,7 +53,7 @@ 4.0.0 4.0.3 2.11.0 - 6.6.1 + 6.6.2 4.6.0 2.1.2 1.0.13 From 78683cdabb86a77d37088300c9b6af16bad16169 Mon Sep 17 00:00:00 2001 From: Christian Ivanov Date: Thu, 14 Nov 2024 13:29:20 +0100 Subject: [PATCH 18/39] fixed Timestamp not being set for otel log signals (cherry picked from commit 3c1905096acc443e9ea93f267f40e6e5f01c5b50) --- .../opentelemetry/runtime/logs/OpenTelemetryLogHandler.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java index de673ab9c77db..9c4606ea68e99 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java @@ -10,6 +10,7 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.time.Instant; import java.util.Map; import java.util.Optional; import java.util.logging.Handler; @@ -43,6 +44,7 @@ public void publish(LogRecord record) { final LogRecordBuilder logRecordBuilder = openTelemetry.getLogsBridge() .loggerBuilder(INSTRUMENTATION_NAME) .build().logRecordBuilder() + .setTimestamp(Instant.now()) .setObservedTimestamp(record.getInstant()); if (record.getLevel() != null) { From 8aa98a36d96678cb50007af4b83dbcdd06fea881 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Wed, 13 Nov 2024 23:32:26 +0100 Subject: [PATCH 19/39] * Support configuring quarkus.bootstrap.effective-model-builder as a POM property; * suport resolving JAR artifacts with classifiers from the classes dir; * support the JAR and Surefire plugin configurations in the pluginManagement when initializing artifact source sets. (cherry picked from commit 405455aab58a1d4afaefceb200cd38637ca69714) --- .../main/java/io/quarkus/maven/DevMojo.java | 3 +- .../maven/QuarkusBootstrapProvider.java | 5 +- .../resolver/maven/BootstrapMavenContext.java | 5 +- .../maven/BootstrapMavenContextConfig.java | 18 +++ .../maven/workspace/LocalProject.java | 142 ++++++++++-------- .../maven/workspace/LocalWorkspace.java | 14 +- .../java/io/quarkus/maven/it/DevMojoIT.java | 8 + .../app/pom.xml | 36 +++++ .../app/src/main/java/org/acme/App.java | 19 +++ .../library/pom.xml | 14 ++ .../main/java/org/acme/BigBeanProducer.java | 15 ++ .../main/java/org/acme/shared/BigBean.java | 7 + .../java/org/acme/shared/BigBeanProducer.java | 11 ++ .../src/main/resources/META-INF/beans.xml | 0 .../multimodule-filtered-classifier/pom.xml | 86 +++++++++++ 15 files changed, 304 insertions(+), 79 deletions(-) create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/pom.xml create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/src/main/java/org/acme/App.java create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/pom.xml create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/BigBeanProducer.java create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBean.java create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBeanProducer.java create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/resources/META-INF/beans.xml create mode 100644 integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/pom.xml diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index f605744e82af5..b5a95a94b2d9e 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -1375,7 +1375,8 @@ private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootst .setPreferPomsFromWorkspace(true) // it's important to set the base directory instead of the POM // which maybe manipulated by a plugin and stored outside the base directory - .setCurrentProject(project.getBasedir().toString()); + .setCurrentProject(project.getBasedir().toString()) + .setEffectiveModelBuilder(BootstrapMavenContextConfig.getEffectiveModelBuilderProperty(projectProperties)); // There are a couple of reasons we don't want to use the original Maven session: // 1) a reload could be triggered by a change in a pom.xml, in which case the Maven session might not be in sync any more with the effective POM; diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java index 8b3a2d7a4bb44..a2b09b2e7a06c 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java @@ -40,6 +40,7 @@ import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContextConfig; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.EffectiveModelResolver; import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; @@ -195,7 +196,9 @@ private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, Launch .setPreferPomsFromWorkspace(true) .setProjectModelProvider(getProjectMap(mojo.mavenSession())::get) // pass the repositories since Maven extensions could manipulate repository configs - .setRemoteRepositories(mojo.remoteRepositories())); + .setRemoteRepositories(mojo.remoteRepositories()) + .setEffectiveModelBuilder(BootstrapMavenContextConfig + .getEffectiveModelBuilderProperty(mojo.mavenProject().getProperties()))); } // PROD packaging mode with workspace discovery disabled return MavenArtifactResolver.builder() diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java index 3eb1af3732511..7dc987859495a 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java @@ -97,7 +97,7 @@ public class BootstrapMavenContext { private static final String SETTINGS_XML = "settings.xml"; private static final String SETTINGS_SECURITY = "settings.security"; - private static final String EFFECTIVE_MODEL_BUILDER_PROP = "quarkus.bootstrap.effective-model-builder"; + static final String EFFECTIVE_MODEL_BUILDER_PROP = "quarkus.bootstrap.effective-model-builder"; private static final String WARN_ON_FAILING_WS_MODULES_PROP = "quarkus.bootstrap.warn-on-failing-workspace-modules"; private static final String MAVEN_RESOLVER_TRANSPORT_KEY = "maven.resolver.transport"; @@ -1080,8 +1080,7 @@ public boolean isPreferPomsFromWorkspace() { public boolean isEffectiveModelBuilder() { if (effectiveModelBuilder == null) { - final String s = PropertyUtils.getProperty(EFFECTIVE_MODEL_BUILDER_PROP); - effectiveModelBuilder = s == null ? false : Boolean.parseBoolean(s); + effectiveModelBuilder = Boolean.getBoolean(EFFECTIVE_MODEL_BUILDER_PROP); } return effectiveModelBuilder; } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContextConfig.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContextConfig.java index 6dc0d5ebb242b..47a80712c78b3 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContextConfig.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContextConfig.java @@ -6,6 +6,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Properties; import java.util.function.Function; import org.apache.maven.model.Model; @@ -20,6 +21,23 @@ public class BootstrapMavenContextConfig> { + /** + * Resolves the effective value of the {@code effective-model-builder} option by looking for the + * {@code quarkus.bootstrap.effective-model-builder} property among the system properties and, + * if not set, in the properties argument. + *

+ * If the property is found, the method will return the result of {@link java.lang.Boolean#parseBoolean}. + * If the property is not set, the method will return false. + * + * @param props primary source of properties + * @return whether effective model builder should be enabled + */ + public static boolean getEffectiveModelBuilderProperty(Properties props) { + final String value = System.getProperty(BootstrapMavenContext.EFFECTIVE_MODEL_BUILDER_PROP); + return value == null ? Boolean.parseBoolean(props.getProperty(BootstrapMavenContext.EFFECTIVE_MODEL_BUILDER_PROP)) + : Boolean.parseBoolean(value); + } + protected String localRepo; protected String[] localRepoTail; protected Boolean localRepoTailIgnoreAvailability; diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java index f97744c845d07..ca502db7afa0a 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java @@ -366,71 +366,19 @@ public WorkspaceModule toWorkspaceModule(BootstrapMavenContext ctx) { .setBuildDir(getOutputDir()); final Model model = modelBuildingResult == null ? getRawModel() : modelBuildingResult.getEffectiveModel(); - if (!ArtifactCoords.TYPE_POM.equals(model.getPackaging())) { - final Build build = model.getBuild(); - boolean addDefaultSourceSet = true; - if (build != null && !build.getPlugins().isEmpty()) { - for (Plugin plugin : build.getPlugins()) { - if (plugin.getArtifactId().equals("maven-jar-plugin")) { - if (plugin.getExecutions().isEmpty()) { - final DefaultArtifactSources src = processJarPluginExecutionConfig(plugin.getConfiguration(), - false); - if (src != null) { - addDefaultSourceSet = false; - moduleBuilder.addArtifactSources(src); - } - } else { - for (PluginExecution e : plugin.getExecutions()) { - DefaultArtifactSources src = null; - if (e.getGoals().contains(ArtifactCoords.TYPE_JAR)) { - src = processJarPluginExecutionConfig(e.getConfiguration(), false); - addDefaultSourceSet &= !(src != null && e.getId().equals("default-jar")); - } else if (e.getGoals().contains("test-jar")) { - src = processJarPluginExecutionConfig(e.getConfiguration(), true); - } - if (src != null) { - moduleBuilder.addArtifactSources(src); - } - } - } - } else if (plugin.getArtifactId().equals("maven-surefire-plugin") && plugin.getConfiguration() != null) { - Object config = plugin.getConfiguration(); - if (!(config instanceof Xpp3Dom)) { - continue; - } - Xpp3Dom dom = (Xpp3Dom) config; - final Xpp3Dom depExcludes = dom.getChild("classpathDependencyExcludes"); - if (depExcludes != null) { - final Xpp3Dom[] excludes = depExcludes.getChildren("classpathDependencyExclude"); - if (excludes != null) { - final List list = new ArrayList<>(excludes.length); - for (Xpp3Dom exclude : excludes) { - list.add(exclude.getValue()); - } - moduleBuilder.setTestClasspathDependencyExclusions(list); - } - } - final Xpp3Dom additionalElements = dom.getChild("additionalClasspathElements"); - if (additionalElements != null) { - final Xpp3Dom[] elements = additionalElements.getChildren("additionalClasspathElement"); - if (elements != null) { - final List list = new ArrayList<>(elements.length); - for (Xpp3Dom element : elements) { - for (String s : element.getValue().split(",")) { - list.add(stripProjectBasedirPrefix(s, PROJECT_BASEDIR)); - } - } - moduleBuilder.setAdditionalTestClasspathElements(list); - } - } - } - } - } - + if (!ArtifactCoords.TYPE_POM.equals(getPackaging())) { + final List plugins = model.getBuild() == null ? List.of() : model.getBuild().getPlugins(); + boolean addDefaultSourceSet = addSourceSetsFromPlugins(plugins, moduleBuilder); if (addDefaultSourceSet) { - moduleBuilder.addArtifactSources(new DefaultArtifactSources(ArtifactSources.MAIN, - List.of(new DefaultSourceDir(getSourcesSourcesDir(), getClassesDir(), getGeneratedSourcesDir())), - collectMainResources(null))); + var pluginManagement = model.getBuild() == null ? null : model.getBuild().getPluginManagement(); + if (pluginManagement != null) { + addDefaultSourceSet = addSourceSetsFromPlugins(pluginManagement.getPlugins(), moduleBuilder); + } + if (addDefaultSourceSet) { + moduleBuilder.addArtifactSources(new DefaultArtifactSources(ArtifactSources.MAIN, + List.of(new DefaultSourceDir(getSourcesSourcesDir(), getClassesDir(), getGeneratedSourcesDir())), + collectMainResources(null))); + } } if (!moduleBuilder.hasTestSources()) { // FIXME: do tests have generated sources? @@ -454,6 +402,70 @@ public WorkspaceModule toWorkspaceModule(BootstrapMavenContext ctx) { return this.module = moduleBuilder.build(); } + private boolean addSourceSetsFromPlugins(List plugins, WorkspaceModule.Mutable moduleBuilder) { + boolean addDefaultSourceSet = true; + int processedPlugins = 0; + for (int i = 0; i < plugins.size() && processedPlugins < 2; ++i) { + var plugin = plugins.get(i); + if (plugin.getArtifactId().equals("maven-jar-plugin")) { + ++processedPlugins; + if (plugin.getExecutions().isEmpty()) { + final DefaultArtifactSources src = processJarPluginExecutionConfig(plugin.getConfiguration(), + false); + if (src != null) { + addDefaultSourceSet = false; + moduleBuilder.addArtifactSources(src); + } + } else { + for (PluginExecution e : plugin.getExecutions()) { + DefaultArtifactSources src = null; + if (e.getGoals().contains(ArtifactCoords.TYPE_JAR)) { + src = processJarPluginExecutionConfig(e.getConfiguration(), false); + addDefaultSourceSet &= !(src != null && e.getId().equals("default-jar")); + } else if (e.getGoals().contains("test-jar")) { + src = processJarPluginExecutionConfig(e.getConfiguration(), true); + } + if (src != null) { + moduleBuilder.addArtifactSources(src); + } + } + } + } else if (plugin.getArtifactId().equals("maven-surefire-plugin") && plugin.getConfiguration() != null) { + ++processedPlugins; + Object config = plugin.getConfiguration(); + if (!(config instanceof Xpp3Dom)) { + continue; + } + Xpp3Dom dom = (Xpp3Dom) config; + final Xpp3Dom depExcludes = dom.getChild("classpathDependencyExcludes"); + if (depExcludes != null) { + final Xpp3Dom[] excludes = depExcludes.getChildren("classpathDependencyExclude"); + if (excludes != null) { + final List list = new ArrayList<>(excludes.length); + for (Xpp3Dom exclude : excludes) { + list.add(exclude.getValue()); + } + moduleBuilder.setTestClasspathDependencyExclusions(list); + } + } + final Xpp3Dom additionalElements = dom.getChild("additionalClasspathElements"); + if (additionalElements != null) { + final Xpp3Dom[] elements = additionalElements.getChildren("additionalClasspathElement"); + if (elements != null) { + final List list = new ArrayList<>(elements.length); + for (Xpp3Dom element : elements) { + for (String s : element.getValue().split(",")) { + list.add(stripProjectBasedirPrefix(s, PROJECT_BASEDIR)); + } + } + moduleBuilder.setAdditionalTestClasspathElements(list); + } + } + } + } + return addDefaultSourceSet; + } + private List toArtifactDependencies(List rawModelDeps, BootstrapMavenContext ctx) { if (rawModelDeps.isEmpty()) { @@ -509,7 +521,7 @@ private DefaultArtifactSources processJarPluginExecutionConfig(Object config, bo new DefaultSourceDir(new DirectoryPathTree(test ? getTestSourcesSourcesDir() : getSourcesSourcesDir()), new DirectoryPathTree(test ? getTestClassesDir() : getClassesDir(), filter), // FIXME: wrong for tests - new DirectoryPathTree(test ? getGeneratedSourcesDir() : getGeneratedSourcesDir(), filter), + new DirectoryPathTree(getGeneratedSourcesDir(), filter), Map.of())); final Collection resources = test ? collectTestResources(filter) : collectMainResources(filter); return new DefaultArtifactSources(classifier, sources, resources); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java index e68e4affef2e8..a570d9f80455f 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java @@ -124,16 +124,12 @@ public File findArtifact(Artifact artifact) { return path.toFile(); } - if (!artifact.getClassifier().isEmpty()) { - if ("tests".equals(artifact.getClassifier())) { - //special classifier used for test jars - path = lp.getTestClassesDir(); - if (Files.exists(path)) { - return path.toFile(); - } + if ("tests".equals(artifact.getClassifier())) { + //special classifier used for test jars + path = lp.getTestClassesDir(); + if (Files.exists(path)) { + return path.toFile(); } - // otherwise, this artifact hasn't been built yet - return null; } if (ArtifactCoords.TYPE_JAR.equals(artifact.getExtension())) { diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java index f9a1957dd172a..3f0873bc20084 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java @@ -1622,4 +1622,12 @@ public void testThatAptInAnnotationProcessorsWorks() throws MavenInvocationExcep assertThat(entityMetamodelClassFile).exists(); assertThat(entityQueryClassFile).doesNotExist(); } + + @Test + void testMultimoduleFilteredClassifier() + throws MavenInvocationException, IOException { + testDir = initProject("projects/multimodule-filtered-classifier"); + run(true); + assertThat(devModeClient.getHttpResponse("/")).isEqualTo("Big"); + } } diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/pom.xml new file mode 100644 index 0000000000000..4b0d7b99b3c82 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/pom.xml @@ -0,0 +1,36 @@ + + + + acme-parent + org.acme + 1.0.0-SNAPSHOT + + 4.0.0 + + acme-app + + + org.acme + \${project.version} + acme-lib + shared + + + + + + \${quarkus.platform.group-id} + quarkus-maven-plugin + + + + build + + + + + + + \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/src/main/java/org/acme/App.java b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/src/main/java/org/acme/App.java new file mode 100644 index 0000000000000..6f9e51cb54958 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/src/main/java/org/acme/App.java @@ -0,0 +1,19 @@ +package org.acme; + +import org.acme.shared.BigBean; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/") +public class App { + + @Inject + BigBean bean; + + @GET + public String get() { + return bean.getName(); + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/pom.xml new file mode 100644 index 0000000000000..dddbb8780fc0d --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/pom.xml @@ -0,0 +1,14 @@ + + + + acme-parent + org.acme + 1.0.0-SNAPSHOT + + 4.0.0 + + acme-lib + + \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/BigBeanProducer.java b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/BigBeanProducer.java new file mode 100644 index 0000000000000..993e92be768f1 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/BigBeanProducer.java @@ -0,0 +1,15 @@ +package org.acme; + +import jakarta.enterprise.inject.Produces; +import org.acme.shared.BigBean; + +/** + * The purpose of this class is to create a conflict with the shared BigBeanProducer + */ +public class BigBeanProducer { + + @Produces + public BigBean getName() { + return new BigBean(); + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBean.java b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBean.java new file mode 100644 index 0000000000000..e74b855a968a6 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBean.java @@ -0,0 +1,7 @@ +package org.acme.shared; + +public class BigBean { + public String getName() { + return "Big"; + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBeanProducer.java b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBeanProducer.java new file mode 100644 index 0000000000000..eabf45b6f1524 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBeanProducer.java @@ -0,0 +1,11 @@ +package org.acme.shared; + +import jakarta.enterprise.inject.Produces; + +public class BigBeanProducer { + + @Produces + public BigBean getBigBean() { + return new BigBean(); + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/resources/META-INF/beans.xml b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/pom.xml new file mode 100644 index 0000000000000..09ebd99118eda --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + org.acme + acme-parent + pom + 1.0.0-SNAPSHOT + + library + app + + + + @project.version@ + ${compiler-plugin.version} + ${version.surefire.plugin} + ${maven.compiler.source} + ${maven.compiler.target} + UTF-8 + UTF-8 + quarkus-bom + io.quarkus + true + + + + + \${quarkus.platform.group-id} + \${quarkus.platform.artifact-id} + \${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-rest + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + package + + jar + + + shared + + **/shared/* + META-INF/* + + + + + + + \${quarkus.platform.group-id} + quarkus-maven-plugin + \${quarkus.platform.version} + true + + + maven-compiler-plugin + \${compiler-plugin.version} + + + + + + \${quarkus.platform.group-id} + quarkus-maven-plugin + + + + From 66030ae7e74253e8da26c3fc5a01a967f6e7ea07 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Fri, 15 Nov 2024 14:03:37 +0000 Subject: [PATCH 20/39] Correct image file name (cherry picked from commit 1e8cad07ca089aea8d7d1a7a444befb534632966) --- ...xtension-maturity_matrix.svg => extension-maturity-matrix.svg} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/src/main/asciidoc/images/{extension-maturity_matrix.svg => extension-maturity-matrix.svg} (100%) diff --git a/docs/src/main/asciidoc/images/extension-maturity_matrix.svg b/docs/src/main/asciidoc/images/extension-maturity-matrix.svg similarity index 100% rename from docs/src/main/asciidoc/images/extension-maturity_matrix.svg rename to docs/src/main/asciidoc/images/extension-maturity-matrix.svg From ba6bbe7de98a67658d54479d6462094c5b1b287a Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Fri, 15 Nov 2024 11:12:32 +0100 Subject: [PATCH 21/39] Support Serde detection for Instance injection of channels Resolves #44500 (cherry picked from commit c858117389252c5e1d7c0bf65eda4ccc8e924e14) --- .../kafka/deployment/DotNames.java | 3 +++ ...allRyeReactiveMessagingKafkaProcessor.java | 23 ++++++++++-------- .../deployment/DefaultSerdeConfigTest.java | 24 +++++++++++++++++++ .../pulsar/deployment/DotNames.java | 3 +++ .../PulsarSchemaDiscoveryProcessor.java | 23 ++++++++++-------- .../deployment/DefaultSchemaConfigTest.java | 24 +++++++++++++++++++ 6 files changed, 80 insertions(+), 20 deletions(-) diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java index 5dd492dbbcb1f..8d3b71041bdca 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java @@ -4,6 +4,9 @@ final class DotNames { // @formatter:off + static final DotName INSTANCE = DotName.createSimple(jakarta.enterprise.inject.Instance.class.getName()); + static final DotName INJECTABLE_INSTANCE = DotName.createSimple(io.quarkus.arc.InjectableInstance.class.getName()); + static final DotName PROVIDER = DotName.createSimple(jakarta.inject.Provider.class.getName()); static final DotName INCOMING = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Incoming.class.getName()); static final DotName INCOMINGS = DotName.createSimple(io.smallrye.reactive.messaging.annotations.Incomings.class.getName()); static final DotName OUTGOING = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Outgoing.class.getName()); diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java index 96234fc7cc580..03c4133fe4edb 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java @@ -20,7 +20,6 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.MethodParameterInfo; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -354,15 +353,19 @@ private void processIncomingType(DefaultSerdeDiscoveryState discovery, } private Type getInjectionPointType(AnnotationInstance annotation) { - switch (annotation.target().kind()) { - case FIELD: - return annotation.target().asField().type(); - case METHOD_PARAMETER: - MethodParameterInfo parameter = annotation.target().asMethodParameter(); - return parameter.method().parameterType(parameter.position()); - default: - return null; - } + return switch (annotation.target().kind()) { + case FIELD -> handleInstanceChannelInjection(annotation.target().asField().type()); + case METHOD_PARAMETER -> handleInstanceChannelInjection(annotation.target().asMethodParameter().type()); + default -> null; + }; + } + + private Type handleInstanceChannelInjection(Type type) { + return (DotNames.INSTANCE.equals(type.name()) + || DotNames.PROVIDER.equals(type.name()) + || DotNames.INJECTABLE_INSTANCE.equals(type.name())) + ? type.asParameterizedType().arguments().get(0) + : type; } private void handleAdditionalProperties(String channelName, boolean incoming, DefaultSerdeDiscoveryState discovery, diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java index 05289111c374d..cc971ba643bcb 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java @@ -13,6 +13,7 @@ import java.util.concurrent.CompletionStage; import java.util.function.Function; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import org.apache.avro.generic.GenericRecord; @@ -41,6 +42,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import io.quarkus.arc.InjectableInstance; import io.quarkus.commons.classloading.ClassLoaderHelper; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; @@ -111,6 +113,7 @@ boolean isKafkaConnector(List list, boolean in assertThat(configs) .extracting(RunTimeConfigurationDefaultBuildItem::getKey, RunTimeConfigurationDefaultBuildItem::getValue) + .hasSize(expectations.length) .allSatisfy(tuple -> { Object[] e = tuple.toArray(); String key = (String) e[0]; @@ -3048,5 +3051,26 @@ private static class ChannelChildSerializer { Multi channel2; } + @Test + void instanceInjectionPoint() { + Tuple[] expectations = { + tuple("mp.messaging.outgoing.channel1.value.serializer", "org.apache.kafka.common.serialization.StringSerializer"), + tuple("mp.messaging.incoming.channel2.value.deserializer", "org.apache.kafka.common.serialization.IntegerDeserializer"), + tuple("mp.messaging.outgoing.channel3.value.serializer", "org.apache.kafka.common.serialization.DoubleSerializer"), + }; + doTest(expectations, InstanceInjectionPoint.class); + } + + private static class InstanceInjectionPoint { + @Channel("channel1") + Instance> emitter1; + + @Channel("channel2") + Instance> channel2; + + @Channel("channel3") + InjectableInstance> channel3; + } + } diff --git a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DotNames.java b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DotNames.java index dc321cb954ec5..efff33c3c0b6c 100644 --- a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DotNames.java +++ b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DotNames.java @@ -4,6 +4,9 @@ final class DotNames { // @formatter:off + static final DotName INSTANCE = DotName.createSimple(jakarta.enterprise.inject.Instance.class.getName()); + static final DotName INJECTABLE_INSTANCE = DotName.createSimple(io.quarkus.arc.InjectableInstance.class.getName()); + static final DotName PROVIDER = DotName.createSimple(jakarta.inject.Provider.class.getName()); static final DotName INCOMING = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Incoming.class.getName()); static final DotName INCOMINGS = DotName.createSimple(io.smallrye.reactive.messaging.annotations.Incomings.class.getName()); static final DotName OUTGOING = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Outgoing.class.getName()); diff --git a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarSchemaDiscoveryProcessor.java b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarSchemaDiscoveryProcessor.java index 3f88ab082a283..f5539127a0149 100644 --- a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarSchemaDiscoveryProcessor.java +++ b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarSchemaDiscoveryProcessor.java @@ -10,7 +10,6 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.MethodParameterInfo; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -144,15 +143,19 @@ private static String incomingSchemaKey(String channelName) { } private Type getInjectionPointType(AnnotationInstance annotation) { - switch (annotation.target().kind()) { - case FIELD: - return annotation.target().asField().type(); - case METHOD_PARAMETER: - MethodParameterInfo parameter = annotation.target().asMethodParameter(); - return parameter.method().parameterType(parameter.position()); - default: - return null; - } + return switch (annotation.target().kind()) { + case FIELD -> handleInstanceChannelInjection(annotation.target().asField().type()); + case METHOD_PARAMETER -> handleInstanceChannelInjection(annotation.target().asMethodParameter().type()); + default -> null; + }; + } + + private Type handleInstanceChannelInjection(Type type) { + return (DotNames.INSTANCE.equals(type.name()) + || DotNames.PROVIDER.equals(type.name()) + || DotNames.INJECTABLE_INSTANCE.equals(type.name())) + ? type.asParameterizedType().arguments().get(0) + : type; } private void produceRuntimeConfigurationDefaultBuildItem(DefaultSchemaDiscoveryState discovery, diff --git a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DefaultSchemaConfigTest.java b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DefaultSchemaConfigTest.java index 898ade58c059d..b7e9e7e686948 100644 --- a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DefaultSchemaConfigTest.java +++ b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DefaultSchemaConfigTest.java @@ -15,8 +15,10 @@ import java.util.concurrent.CompletionStage; import java.util.function.Supplier; +import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.apache.avro.specific.AvroGenerated; import org.apache.pulsar.client.api.Messages; @@ -40,6 +42,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.commons.classloading.ClassLoaderHelper; import io.quarkus.deployment.annotations.BuildProducer; @@ -2108,5 +2111,26 @@ Multi> method4() { } } + @Test + void instanceInjectionPoint() { + Tuple[] expectations = { + tuple("mp.messaging.outgoing.channel1.schema", "STRING"), + tuple("mp.messaging.incoming.channel2.schema", "INT32"), + tuple("mp.messaging.outgoing.channel3.schema", "DOUBLE"), + }; + doTest(expectations, InstanceInjectionPoint.class); + } + + private static class InstanceInjectionPoint { + @Channel("channel1") + Instance> emitter1; + + @Channel("channel2") + Provider> channel2; + + @Channel("channel3") + InjectableInstance> channel3; + } + } From b43bdaf96d036df97e6c930de71ba89eb7ea531f Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 12 Nov 2024 16:02:46 +0000 Subject: [PATCH 22/39] Reduce allocation in matching of unknown properties (cherry picked from commit a04c136dbd865b2d55cfe56640cdcd933e1f0099) --- .../BuildTimeConfigurationReader.java | 33 ++--- .../RunTimeConfigurationGenerator.java | 110 +++++++---------- .../runtime/configuration/PropertiesUtil.java | 116 ++++-------------- 3 files changed, 87 insertions(+), 172 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index 0903a5b88b568..d5e5bf8e364c9 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -80,6 +80,7 @@ import io.smallrye.config.EnvConfigSource; import io.smallrye.config.ProfileConfigSourceInterceptor; import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.PropertyName; import io.smallrye.config.SecretKeys; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; @@ -626,22 +627,19 @@ ReadResult run() { objectsByClass.put(mapping.getKlass(), config.getConfigMapping(mapping.getKlass(), mapping.getPrefix())); } - // Build Time Values Recording - for (ConfigClass mapping : buildTimeMappings) { - Set mappedProperties = ConfigMappings.mappedProperties(mapping, allProperties); - for (String property : mappedProperties) { + Set buildTimeNames = getMappingsNames(buildTimeMappings); + Set buildTimeRunTimeNames = getMappingsNames(buildTimeRunTimeMappings); + Set runTimeNames = getMappingsNames(runTimeMappings); + for (String property : allProperties) { + PropertyName name = new PropertyName(property); + if (buildTimeNames.contains(name)) { unknownBuildProperties.remove(property); ConfigValue value = config.getConfigValue(property); if (value.getRawValue() != null) { allBuildTimeValues.put(value.getNameProfiled(), value.getRawValue()); } } - } - - // Build Time and Run Time Values Recording - for (ConfigClass mapping : buildTimeRunTimeMappings) { - Set mappedProperties = ConfigMappings.mappedProperties(mapping, allProperties); - for (String property : mappedProperties) { + if (buildTimeRunTimeNames.contains(name)) { unknownBuildProperties.remove(property); ConfigValue value = config.getConfigValue(property); if (value.getRawValue() != null) { @@ -649,12 +647,7 @@ ReadResult run() { buildTimeRunTimeValues.put(value.getNameProfiled(), value.getRawValue()); } } - } - - // Run Time Values Recording - for (ConfigClass mapping : runTimeMappings) { - Set mappedProperties = ConfigMappings.mappedProperties(mapping, allProperties); - for (String property : mappedProperties) { + if (runTimeNames.contains(name)) { unknownBuildProperties.remove(property); ConfigValue value = runtimeConfig.getConfigValue(property); if (value.getRawValue() != null) { @@ -1216,6 +1209,14 @@ private static void getDefaults( patternMap.getChild(childName)); } } + + private static Set getMappingsNames(final List configMappings) { + Set names = new HashSet<>(); + for (ConfigClass configMapping : configMappings) { + names.addAll(ConfigMappings.getProperties(configMapping).keySet()); + } + return PropertiesUtil.toPropertyNames(names); + } } public static final class ReadResult { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java index e82e137f8dd01..0d8e72863855b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java @@ -66,7 +66,7 @@ import io.smallrye.config.ConfigMappings; import io.smallrye.config.ConfigMappings.ConfigClass; import io.smallrye.config.Converters; -import io.smallrye.config.KeyMap; +import io.smallrye.config.PropertyName; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; @@ -86,10 +86,13 @@ public final class RunTimeConfigurationGenerator { public static final MethodDescriptor REINIT = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "reinit", void.class); public static final MethodDescriptor C_READ_CONFIG = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "readConfig", void.class); + + static final FieldDescriptor C_MAPPED_PROPERTIES = FieldDescriptor.of(CONFIG_CLASS_NAME, "mappedProperties", Set.class); + static final MethodDescriptor C_GENERATE_MAPPED_PROPERTIES = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, + "generateMappedProperties", Set.class); + static final MethodDescriptor PN_NEW = MethodDescriptor.ofConstructor(PropertyName.class, String.class); static final FieldDescriptor C_UNKNOWN = FieldDescriptor.of(CONFIG_CLASS_NAME, "unknown", Set.class); static final FieldDescriptor C_UNKNOWN_RUNTIME = FieldDescriptor.of(CONFIG_CLASS_NAME, "unknownRuntime", Set.class); - static final MethodDescriptor C_MAPPED_PROPERTIES = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "mappedProperties", - KeyMap.class); static final MethodDescriptor CD_INVALID_VALUE = MethodDescriptor.ofMethod(ConfigDiagnostic.class, "invalidValue", void.class, String.class, IllegalArgumentException.class); @@ -177,7 +180,6 @@ public final class RunTimeConfigurationGenerator { Object.class, String.class, Converter.class); static final MethodDescriptor SRCB_NEW = MethodDescriptor.ofConstructor(SmallRyeConfigBuilder.class); - static final MethodDescriptor SRCB_WITH_CONVERTER = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, "withConverter", ConfigBuilder.class, Class.class, int.class, Converter.class); static final MethodDescriptor SRCB_WITH_CUSTOMIZER = MethodDescriptor.ofMethod(AbstractConfigBuilder.class, @@ -185,15 +187,14 @@ public final class RunTimeConfigurationGenerator { static final MethodDescriptor SRCB_BUILD = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, "build", SmallRyeConfig.class); - static final MethodDescriptor PU_FILTER_PROPERTIES_IN_ROOTS = MethodDescriptor.ofMethod(PropertiesUtil.class, - "filterPropertiesInRoots", Iterable.class, Iterable.class, Set.class); - static final MethodDescriptor PU_IS_PROPERTY_QUARKUS_COMPOUND_NAME = MethodDescriptor.ofMethod(PropertiesUtil.class, "isPropertyQuarkusCompoundName", boolean.class, NameIterator.class); - static final MethodDescriptor PU_FILTER_UNKNOWN = MethodDescriptor.ofMethod(PropertiesUtil.class, "filterUnknown", - void.class, Set.class, KeyMap.class); + static final MethodDescriptor PU_IS_PROPERTY_IN_ROOTS = MethodDescriptor.ofMethod(PropertiesUtil.class, "isPropertyInRoots", + boolean.class, String.class, Set.class); static final MethodDescriptor HS_NEW = MethodDescriptor.ofConstructor(HashSet.class); static final MethodDescriptor HS_ADD = MethodDescriptor.ofMethod(HashSet.class, "add", boolean.class, Object.class); + static final MethodDescriptor HS_CONTAINS = MethodDescriptor.ofMethod(HashSet.class, "contains", boolean.class, + Object.class); // todo: more space-efficient sorted map impl static final MethodDescriptor TM_NEW = MethodDescriptor.ofConstructor(TreeMap.class); @@ -261,8 +262,8 @@ public static final class GenerateOperation implements AutoCloseable { roots = Assert.checkNotNullParam("builder.roots", builder.getBuildTimeReadResult().getAllRoots()); additionalTypes = Assert.checkNotNullParam("additionalTypes", builder.getAdditionalTypes()); cc = ClassCreator.builder().classOutput(classOutput).className(CONFIG_CLASS_NAME).setFinal(true).build(); + generateMappedProperties(); generateEmptyParsers(); - generateUnknownFilter(); // not instantiable try (MethodCreator mc = cc.getMethodCreator(MethodDescriptor.ofConstructor(CONFIG_CLASS_NAME))) { mc.setModifiers(Opcodes.ACC_PRIVATE); @@ -280,10 +281,13 @@ public static final class GenerateOperation implements AutoCloseable { clinit = cc.getMethodCreator(MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "", void.class)); clinit.setModifiers(Opcodes.ACC_STATIC); - cc.getFieldCreator(C_UNKNOWN).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + cc.getFieldCreator(C_MAPPED_PROPERTIES).setModifiers(Opcodes.ACC_STATIC); + clinit.writeStaticField(C_MAPPED_PROPERTIES, clinit.invokeStaticMethod(C_GENERATE_MAPPED_PROPERTIES)); + + cc.getFieldCreator(C_UNKNOWN).setModifiers(Opcodes.ACC_STATIC); clinit.writeStaticField(C_UNKNOWN, clinit.newInstance(HS_NEW)); - cc.getFieldCreator(C_UNKNOWN_RUNTIME).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + cc.getFieldCreator(C_UNKNOWN_RUNTIME).setModifiers(Opcodes.ACC_STATIC); clinit.writeStaticField(C_UNKNOWN_RUNTIME, clinit.newInstance(HS_NEW)); clinitNameBuilder = clinit.newInstance(SB_NEW); @@ -457,10 +461,6 @@ public void run() { // generate sweep for clinit configSweepLoop(siParserBody, clinit, clinitConfig, getRegisteredRoots(BUILD_AND_RUN_TIME_FIXED), Type.BUILD_TIME); - - clinit.invokeStaticMethod(PU_FILTER_UNKNOWN, - clinit.readStaticField(C_UNKNOWN), - clinit.invokeStaticMethod(C_MAPPED_PROPERTIES)); clinit.invokeStaticMethod(CD_UNKNOWN_PROPERTIES, clinit.readStaticField(C_UNKNOWN)); if (liveReloadPossible) { @@ -468,10 +468,6 @@ public void run() { } // generate sweep for run time configSweepLoop(rtParserBody, readConfig, runTimeConfig, getRegisteredRoots(RUN_TIME), Type.RUNTIME); - - readConfig.invokeStaticMethod(PU_FILTER_UNKNOWN, - readConfig.readStaticField(C_UNKNOWN_RUNTIME), - readConfig.invokeStaticMethod(C_MAPPED_PROPERTIES)); readConfig.invokeStaticMethod(CD_UNKNOWN_PROPERTIES_RT, readConfig.readStaticField(C_UNKNOWN_RUNTIME)); // generate ensure-initialized method @@ -525,33 +521,42 @@ public void run() { private void configSweepLoop(MethodDescriptor parserBody, MethodCreator method, ResultHandle config, Set registeredRoots, Type type) { - ResultHandle nameSet; - ResultHandle iterator; + ResultHandle propertyNames = method.invokeVirtualMethod(SRC_GET_PROPERTY_NAMES, config); + ResultHandle iterator = method.invokeInterfaceMethod(ITRA_ITERATOR, propertyNames); - nameSet = filterProperties(method, config, registeredRoots); - iterator = method.invokeInterfaceMethod(ITRA_ITERATOR, nameSet); + ResultHandle rootSet = method.newInstance(HS_NEW); + for (String registeredRoot : registeredRoots) { + method.invokeVirtualMethod(HS_ADD, rootSet, method.load(registeredRoot)); + } try (BytecodeCreator sweepLoop = method.createScope()) { try (BytecodeCreator hasNext = sweepLoop.ifNonZero(sweepLoop.invokeInterfaceMethod(ITR_HAS_NEXT, iterator)) .trueBranch()) { - ResultHandle key = hasNext.checkCast(hasNext.invokeInterfaceMethod(ITR_NEXT, iterator), String.class); + + // !mappedProperties.contains(new PropertyName(key)) continue sweepLoop; + hasNext.ifNonZero( + hasNext.invokeVirtualMethod(HS_CONTAINS, hasNext.readStaticField(C_MAPPED_PROPERTIES), + hasNext.newInstance(PN_NEW, key))) + .trueBranch().continueScope(sweepLoop); + // NameIterator keyIter = new NameIterator(key); ResultHandle keyIter = hasNext.newInstance(NI_NEW_STRING, key); - BranchResult unknownProperty = hasNext + + // if (PropertiesUtil.isPropertyQuarkusCompoundName(keyIter)) + BranchResult quarkusCompoundName = hasNext .ifNonZero(hasNext.invokeStaticMethod(PU_IS_PROPERTY_QUARKUS_COMPOUND_NAME, keyIter)); - try (BytecodeCreator trueBranch = unknownProperty.trueBranch()) { - ResultHandle unknown; - if (type == Type.BUILD_TIME) { - unknown = trueBranch.readStaticField(C_UNKNOWN); - } else { - unknown = trueBranch.readStaticField(C_UNKNOWN_RUNTIME); - } + try (BytecodeCreator trueBranch = quarkusCompoundName.trueBranch()) { + ResultHandle unknown = type == Type.BUILD_TIME ? trueBranch.readStaticField(C_UNKNOWN) + : trueBranch.readStaticField(C_UNKNOWN_RUNTIME); trueBranch.invokeVirtualMethod(HS_ADD, unknown, key); } + + hasNext.ifNonZero(hasNext.invokeStaticMethod(PU_IS_PROPERTY_IN_ROOTS, key, rootSet)).falseBranch() + .continueScope(sweepLoop); + // if (! keyIter.hasNext()) continue sweepLoop; hasNext.ifNonZero(hasNext.invokeVirtualMethod(NI_HAS_NEXT, keyIter)).falseBranch().continueScope(sweepLoop); - // if (! keyIter.nextSegmentEquals("quarkus")) continue sweepLoop; // parse(config, keyIter); hasNext.invokeStaticMethod(parserBody, config, keyIter); // continue sweepLoop; @@ -560,21 +565,6 @@ private void configSweepLoop(MethodDescriptor parserBody, MethodCreator method, } } - private ResultHandle filterProperties(MethodCreator method, ResultHandle config, Set registeredRoots) { - // Roots - ResultHandle rootSet; - rootSet = method.newInstance(HS_NEW); - for (String registeredRoot : registeredRoots) { - method.invokeVirtualMethod(HS_ADD, rootSet, method.load(registeredRoot)); - } - - // PropertyNames - ResultHandle properties = method.invokeVirtualMethod(SRC_GET_PROPERTY_NAMES, config); - - // Filtered Properties - return method.invokeStaticMethod(PU_FILTER_PROPERTIES_IN_ROOTS, properties, rootSet); - } - private Set getRegisteredRoots(ConfigPhase configPhase) { Set registeredRoots = new HashSet<>(); for (RootDefinition root : roots) { @@ -1190,13 +1180,7 @@ private FieldDescriptor getOrCreateConverterInstance(Field field, ConverterType return fd; } - static final MethodDescriptor KM_NEW = MethodDescriptor.ofConstructor(KeyMap.class); - static final MethodDescriptor KM_FIND_OR_ADD = MethodDescriptor.ofMethod(KeyMap.class, "findOrAdd", KeyMap.class, - String.class); - static final MethodDescriptor KM_PUT_ROOT_VALUE = MethodDescriptor.ofMethod(KeyMap.class, "putRootValue", Object.class, - Object.class); - - private void generateUnknownFilter() { + private void generateMappedProperties() { Set names = new HashSet<>(); for (ConfigClass buildTimeMapping : buildTimeConfigResult.getBuildTimeMappings()) { names.addAll(ConfigMappings.getProperties(buildTimeMapping).keySet()); @@ -1207,17 +1191,15 @@ private void generateUnknownFilter() { for (ConfigClass runtimeConfigMapping : buildTimeConfigResult.getRunTimeMappings()) { names.addAll(ConfigMappings.getProperties(runtimeConfigMapping).keySet()); } + Set propertyNames = PropertiesUtil.toPropertyNames(names); - // Add a method that generates a KeyMap that can check if a property is mapped by a @ConfigMapping - MethodCreator mc = cc.getMethodCreator(C_MAPPED_PROPERTIES); + MethodCreator mc = cc.getMethodCreator(C_GENERATE_MAPPED_PROPERTIES); mc.setModifiers(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC); - ResultHandle keyMap = mc.newInstance(KM_NEW); - for (String name : names) { - mc.invokeVirtualMethod(KM_PUT_ROOT_VALUE, mc.invokeVirtualMethod(KM_FIND_OR_ADD, keyMap, mc.load(name)), - mc.load(true)); + ResultHandle set = mc.newInstance(HS_NEW); + for (PropertyName propertyName : propertyNames) { + mc.invokeVirtualMethod(HS_ADD, set, mc.newInstance(PN_NEW, mc.load(propertyName.getName()))); } - - mc.returnValue(keyMap); + mc.returnValue(set); mc.close(); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java index 0a7b27e0f52cf..18978d1714028 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java @@ -1,86 +1,14 @@ package io.quarkus.runtime.configuration; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; +import java.util.HashMap; +import java.util.Map; import java.util.Set; -import io.smallrye.config.KeyMap; +import io.smallrye.config.PropertyName; public class PropertiesUtil { private PropertiesUtil() { - } - - /** - * @deprecated Use {@link PropertiesUtil#filterPropertiesInRoots(Iterable, Set)} instead. - */ - @Deprecated(forRemoval = true) - public static boolean isPropertyInRoot(Set roots, NameIterator propertyName) { - for (String root : roots) { - // match everything - if (root.length() == 0) { - return true; - } - - // A sub property from a namespace is always bigger - if (propertyName.getName().length() <= root.length()) { - continue; - } - - final NameIterator rootNi = new NameIterator(root); - // compare segments - while (rootNi.hasNext()) { - String segment = rootNi.getNextSegment(); - if (!propertyName.hasNext()) { - propertyName.goToStart(); - break; - } - - final String nextSegment = propertyName.getNextSegment(); - if (!segment.equals(nextSegment)) { - propertyName.goToStart(); - break; - } - - rootNi.next(); - propertyName.next(); - - // root has no more segments, and we reached this far so everything matched. - // on top, property still has more segments to do the mapping. - if (!rootNi.hasNext() && propertyName.hasNext()) { - propertyName.goToStart(); - return true; - } - } - } - - return false; - } - - public static Iterable filterPropertiesInRoots(final Iterable properties, final Set roots) { - if (roots.isEmpty()) { - return properties; - } - - // Will match everything, so no point in filtering - if (roots.contains("")) { - return properties; - } - - List matchedProperties = new ArrayList<>(); - for (String property : properties) { - // This is a Quarkus compound name, usually by setting something like `quarkus.foo.bar` in the YAML source - // TODO - We let it through to match it later again to place it in the right unknown reporting (static or runtime). We can improve this too. - if (property.startsWith("\"quarkus.")) { - matchedProperties.add(property); - continue; - } - - if (isPropertyInRoots(property, roots)) { - matchedProperties.add(property); - } - } - return matchedProperties; + throw new IllegalStateException("Utility class"); } public static boolean isPropertyInRoots(final String property, final Set roots) { @@ -121,23 +49,27 @@ public static boolean isPropertyQuarkusCompoundName(NameIterator propertyName) { return propertyName.getName().startsWith("\"quarkus."); } - /** - * Removes false positives of configuration properties marked as unknown. To populate the old @ConfigRoot, all - * properties are iterated and matched against known roots. With @ConfigMapping the process is different, so - * properties that are known to @ConfigMapping are not known to the @ConfigRoot, so they will be marked as being - * unknown. It is a bit easier to just double-check on the unknown properties and remove these false positives by - * matching them against the known properties of @ConfigMapping. - * - * @param unknownProperties the collected unknown properties from the old @ConfigRoot mapping - * @param filterPatterns the mapped patterns from the discovered @ConfigMapping - */ - public static void filterUnknown(Set unknownProperties, KeyMap filterPatterns) { - Set toRemove = new HashSet<>(); - for (String unknownProperty : unknownProperties) { - if (filterPatterns.hasRootValue(unknownProperty)) { - toRemove.add(unknownProperty); + public static Set toPropertyNames(final Set names) { + Map propertyNames = new HashMap<>(); + for (String name : names) { + PropertyName propertyName = new PropertyName(name); + if (propertyNames.containsKey(propertyName)) { + String existing = propertyNames.remove(propertyName); + if (existing.length() < name.length()) { + propertyNames.put(new PropertyName(existing), existing); + } else if (existing.length() > name.length()) { + propertyNames.put(propertyName, name); + } else { + if (existing.indexOf('*') <= name.indexOf('*')) { + propertyNames.put(new PropertyName(existing), existing); + } else { + propertyNames.put(propertyName, name); + } + } + } else { + propertyNames.put(propertyName, name); } } - unknownProperties.removeAll(toRemove); + return propertyNames.keySet(); } } From 1fb39459015ddd52035e77b18731c2bda073e321 Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Fri, 15 Nov 2024 15:54:41 -0500 Subject: [PATCH 23/39] Fix typo, use common ID syntax (cherry picked from commit f841486305e06e693ebdefaca0d922a4623eac52) --- docs/src/main/asciidoc/security-jwt.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index b2687eca370ed..fcc6ab1b97ea2 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -619,7 +619,7 @@ We suggest that you check out the quickstart solutions and explore the `security == Reference Guide -[supported-injection-scopes] +[[supported-injection-scopes]] === Supported Injection Scopes `@ApplicationScoped`, `@Singleton` and `@RequestScoped` outer bean injection scopes are all supported when an `org.eclipse.microprofile.jwt.JsonWebToken` is injected, with the `@RequestScoped` scoping for `JsonWebToken` enforced to ensure the current token is represented. From b4da38a118af78a58eed16ff2255d7551142c02a Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Thu, 14 Nov 2024 14:21:16 +0100 Subject: [PATCH 24/39] Updates to Infinispan 15.0.11.Final (cherry picked from commit 4e386593558c8fc341a14975c608b56079302202) --- bom/application/pom.xml | 2 +- .../deployment/devservices/InfinispanDevServiceProcessor.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 173d6630f1147..d5e051c4ed366 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -129,7 +129,7 @@ 1.2.6 2.2 5.10.5 - 15.0.10.Final + 15.0.11.Final 5.0.12.Final 3.1.8 4.1.111.Final diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java index 92199763171cf..31d74e0528c51 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java @@ -259,7 +259,7 @@ private static class QuarkusInfinispanContainer extends InfinispanContainer { public QuarkusInfinispanContainer(String clientName, InfinispanDevServicesConfig config, LaunchMode launchMode, boolean useSharedNetwork) { - super(config.imageName.orElse(IMAGE_BASENAME + ":" + Version.getVersion())); + super(config.imageName.orElse(IMAGE_BASENAME + ":" + Version.getUnbrandedVersion())); this.fixedExposedPort = config.port; this.useSharedNetwork = useSharedNetwork; if (launchMode == DEVELOPMENT) { From 0d75720d5a1bb8b661d205acecc8d97ac7325c3f Mon Sep 17 00:00:00 2001 From: luneo7 Date: Fri, 15 Nov 2024 15:57:18 -0600 Subject: [PATCH 25/39] Update smallrye-jwt (cherry picked from commit 8abb30dbac2aab658051f2047dd5dc2c34b5edbc) --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index d5e051c4ed366..27c60835238ce 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -54,7 +54,7 @@ 4.0.3 2.11.0 6.6.2 - 4.6.0 + 4.6.1 2.1.2 1.0.13 3.0.1 From b62877673f57ceead47fd5f462842a77df415b81 Mon Sep 17 00:00:00 2001 From: Julien Ponge Date: Thu, 14 Nov 2024 13:49:15 +0100 Subject: [PATCH 26/39] Coordinated Vert.x 4.5.11 upgrades - Bump to Netty 4.1.115.Final and fix SSL-related substitutions due to internal Netty breaking changes - Bump to Vert.x 4.5.11 - Bump Mutiny Vert.x bindings 3.16.0 - Re-aligned the Vert.x versions across Quarkus modules Fixes CVE-2024-47535 with Netty 4.1.115.Final (cherry picked from commit 9fd8dcb55c1064f964cc001bf3e7785b1b3d8b50) --- bom/application/pom.xml | 6 +- .../runtime/graal/NettySubstitutions.java | 60 ++++++++++++------- .../resteasy-reactive/pom.xml | 4 +- independent-projects/vertx-utils/pom.xml | 2 +- 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 27c60835238ce..f23973fc842db 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -58,7 +58,7 @@ 2.1.2 1.0.13 3.0.1 - 3.15.0 + 3.16.0 4.25.0 2.7.0 2.1.3 @@ -110,7 +110,7 @@ 2.6.0.Final 2.2.1.Final 3.8.0.Final - 4.5.10 + 4.5.11 4.5.14 4.4.16 4.1.5 @@ -132,7 +132,7 @@ 15.0.11.Final 5.0.12.Final 3.1.8 - 4.1.111.Final + 4.1.115.Final 1.16.0 1.0.4 3.6.1.Final diff --git a/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java b/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java index ce7cd265223b3..030d99653992e 100644 --- a/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java +++ b/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java @@ -173,7 +173,8 @@ final class Target_io_netty_handler_ssl_JdkSslServerContext { KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, - SecureRandom secureRandom, String keyStore) throws SSLException { + SecureRandom secureRandom, String keyStore, Target_io_netty_handler_ssl_ResumptionController resumptionController) + throws SSLException { } } @@ -181,12 +182,13 @@ final class Target_io_netty_handler_ssl_JdkSslServerContext { final class Target_io_netty_handler_ssl_JdkSslClientContext { @Alias - Target_io_netty_handler_ssl_JdkSslClientContext(Provider sslContextProvider, X509Certificate[] trustCertCollection, - TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, - String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, - CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, - long sessionCacheSize, long sessionTimeout, SecureRandom secureRandom, - String keyStoreType) throws SSLException { + Target_io_netty_handler_ssl_JdkSslClientContext(Provider sslContextProvider, + X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, + KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, + ApplicationProtocolConfig apn, String[] protocols, long sessionCacheSize, long sessionTimeout, + SecureRandom secureRandom, String keyStoreType, String endpointIdentificationAlgorithm, + Target_io_netty_handler_ssl_ResumptionController resumptionController) throws SSLException { } } @@ -222,43 +224,55 @@ final class Target_io_netty_handler_ssl_JdkAlpnSslEngine { } } +@TargetClass(className = "io.netty.handler.ssl.ResumptionController") +final class Target_io_netty_handler_ssl_ResumptionController { + + @Alias + Target_io_netty_handler_ssl_ResumptionController() { + + } +} + @TargetClass(className = "io.netty.handler.ssl.SslContext") final class Target_io_netty_handler_ssl_SslContext { @Substitute - static SslContext newServerContextInternal(SslProvider provider, Provider sslContextProvider, + static SslContext newServerContextInternal(SslProvider provider, + Provider sslContextProvider, X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, - X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, - KeyManagerFactory keyManagerFactory, Iterable ciphers, - CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, - long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, - String[] protocols, boolean startTls, boolean enableOcsp, - SecureRandom secureRandom, String keyStoreType, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp, SecureRandom secureRandom, String keyStoreType, Map.Entry, Object>... ctxOptions) throws SSLException { if (enableOcsp) { throw new IllegalArgumentException("OCSP is not supported with this SslProvider: " + provider); } + Target_io_netty_handler_ssl_ResumptionController resumptionController = new Target_io_netty_handler_ssl_ResumptionController(); return (SslContext) (Object) new Target_io_netty_handler_ssl_JdkSslServerContext(sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, - clientAuth, protocols, startTls, secureRandom, keyStoreType); + clientAuth, protocols, startTls, secureRandom, keyStoreType, resumptionController); } @Substitute - static SslContext newClientContextInternal(SslProvider provider, Provider sslContextProvider, - X509Certificate[] trustCert, - TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, - KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, - ApplicationProtocolConfig apn, String[] protocols, long sessionCacheSize, long sessionTimeout, - boolean enableOcsp, SecureRandom secureRandom, - String keyStoreType, Map.Entry, Object>... options) throws SSLException { + static SslContext newClientContextInternal(SslProvider provider, + Provider sslContextProvider, + X509Certificate[] trustCert, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, + long sessionCacheSize, long sessionTimeout, boolean enableOcsp, + SecureRandom secureRandom, String keyStoreType, String endpointIdentificationAlgorithm, + Map.Entry, Object>... options) throws SSLException { if (enableOcsp) { throw new IllegalArgumentException("OCSP is not supported with this SslProvider: " + provider); } + Target_io_netty_handler_ssl_ResumptionController resumptionController = new Target_io_netty_handler_ssl_ResumptionController(); return (SslContext) (Object) new Target_io_netty_handler_ssl_JdkSslClientContext(sslContextProvider, trustCert, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, - sessionTimeout, secureRandom, keyStoreType); + sessionTimeout, secureRandom, keyStoreType, endpointIdentificationAlgorithm, + resumptionController); } } diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 8a13dcabcfb69..e485b49589cae 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -58,7 +58,7 @@ 2.6.2 2.8.0 - 4.5.9 + 4.5.11 5.5.0 1.0.0.Final 2.18.1 @@ -67,7 +67,7 @@ 3.0.4 3.0.1 4.2.2 - 3.13.2 + 3.16.0 1.0.4 5.14.2 1.1.0 diff --git a/independent-projects/vertx-utils/pom.xml b/independent-projects/vertx-utils/pom.xml index 9e1e39cca4375..f256694260ca4 100644 --- a/independent-projects/vertx-utils/pom.xml +++ b/independent-projects/vertx-utils/pom.xml @@ -17,7 +17,7 @@ 3.6.1.Final - 4.5.7 + 4.5.11 From cd27052ffaba6d98ffcc49597f01cf6f8d802d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A5n=20Nguy=E1=BB=85n?= <59108692+tran4774@users.noreply.github.com> Date: Sun, 17 Nov 2024 01:13:31 +0700 Subject: [PATCH 27/39] Wrong index of ParameterizedType argument of Map when register type to be generated in JacksonCodeGenerator (cherry picked from commit ee68fdb3fb8747b068011f0fe84b87bcfbbc8902) --- .../jackson/deployment/processor/JacksonCodeGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java index b822619d7bc9f..b06abe3deaf15 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java @@ -193,7 +193,7 @@ protected FieldKind registerTypeToBeGenerated(Type fieldType, String typeName) { } } if (pType.arguments().size() == 2 && typeName.equals("java.util.Map")) { - registerTypeToBeGenerated(pType.arguments().get(1)); + registerTypeToBeGenerated(pType.arguments().get(0)); registerTypeToBeGenerated(pType.arguments().get(1)); return FieldKind.MAP; } From cb9e1337c26510313dc3fb1ec13eb4b571862963 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Mon, 18 Nov 2024 10:28:26 +0000 Subject: [PATCH 28/39] Reference maturity matrix from FAQ This reverts commit 693ddaf0df96e8c21e1bb467a94c115242942795 and updates 'model' to matrix Co-Authored-By: Guillaume Smet (cherry picked from commit 14946be6173779cb866034b2fb614b9551974294) --- docs/src/main/asciidoc/extension-faq.adoc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/src/main/asciidoc/extension-faq.adoc b/docs/src/main/asciidoc/extension-faq.adoc index 957c350541a2e..3c66bc25adc0a 100644 --- a/docs/src/main/asciidoc/extension-faq.adoc +++ b/docs/src/main/asciidoc/extension-faq.adoc @@ -18,9 +18,9 @@ The document header ends at the first blank line. Do not remove the blank line b ### Why would I want to write an extension? -See the xref:writing-extensions#extension-philosophy[extension philosophy]. - -One useful thing extensions can do is bundle other extensions. +See the xref:writing-extensions.adoc#extension-philosophy[extension philosophy]. +The xref:extension-maturity-matrix.adoc[extension maturity matrix] shows the kinds of capabilities extensions can offer. +Another useful thing extensions can do is bundle other extensions. Have a look at the link:https://quarkus.io/extensions/io.quarkiverse.microprofile/quarkus-microprofile/[Quarkus MicroProfile extension] for an example of aggregator extensions. ### Are there cases an extension isn't necessary? @@ -30,6 +30,11 @@ Not every problem needs an extension! For example, plain libraries can create new configuration elements and register classes with Jandex (this link:https://www.loicmathieu.fr/wordpress/en/informatique/quarkus-tip-comment-ne-pas-creer-une-extension-quarkus/[blog shows how]). +## How do I know what kind of capabilities I might want to include in an extension? + +Have a look at the xref:extension-maturity-matrix.adoc[extension maturity matrix]. + + ## Bytecode transformation ### How can I change the code of things on the classpath? From 75fd3d15b95a6c609c5f0574b0258362d15f2bc2 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Mon, 18 Nov 2024 12:48:22 +0000 Subject: [PATCH 29/39] Use asciidoc-style headers (cherry picked from commit ea35c4dc6b8be96191ee060ee0c9040035cb5152) --- docs/src/main/asciidoc/extension-faq.adoc | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/src/main/asciidoc/extension-faq.adoc b/docs/src/main/asciidoc/extension-faq.adoc index 3c66bc25adc0a..af7e78193ae4b 100644 --- a/docs/src/main/asciidoc/extension-faq.adoc +++ b/docs/src/main/asciidoc/extension-faq.adoc @@ -14,41 +14,41 @@ TODO: uncomment the above for experimental or tech-preview content. The document header ends at the first blank line. Do not remove the blank line between the header and the abstract summary. //// -## Should you write an extension? +== Should you write an extension? -### Why would I want to write an extension? +=== Why would I want to write an extension? See the xref:writing-extensions.adoc#extension-philosophy[extension philosophy]. The xref:extension-maturity-matrix.adoc[extension maturity matrix] shows the kinds of capabilities extensions can offer. Another useful thing extensions can do is bundle other extensions. Have a look at the link:https://quarkus.io/extensions/io.quarkiverse.microprofile/quarkus-microprofile/[Quarkus MicroProfile extension] for an example of aggregator extensions. -### Are there cases an extension isn't necessary? +=== Are there cases an extension isn't necessary? Not every problem needs an extension! If you're just bundling up external libraries (that aren't already extensions) and making minor adjustments, you might not need an extension. For example, plain libraries can create new configuration elements and register classes with Jandex (this link:https://www.loicmathieu.fr/wordpress/en/informatique/quarkus-tip-comment-ne-pas-creer-une-extension-quarkus/[blog shows how]). -## How do I know what kind of capabilities I might want to include in an extension? +== How do I know what kind of capabilities I might want to include in an extension? Have a look at the xref:extension-maturity-matrix.adoc[extension maturity matrix]. -## Bytecode transformation +== Bytecode transformation -### How can I change the code of things on the classpath? +=== How can I change the code of things on the classpath? A `BytecodeTransformerBuildItem` can be used to manipulate bytecode. For example, see this link:https://quarkus.io/blog/solving-problems-with-extensions/[blog about removed problematic bridge methods from a dependency]. -## CDI +== CDI -### I'm working with CDI, and I don't know how to ... +=== I'm working with CDI, and I don't know how to ... The xref:cdi-integration.adoc[CDI integration guide] presents solutions to a number of CDI-related use cases for extension authors. -### I have transformed a user class to add an injected field, but CDI isn't working +=== I have transformed a user class to add an injected field, but CDI isn't working What happens if an extension transforms a user class using `BytecodeTransformerBuildItem`, and replaces `@jakarta.annotation.Resource` with `@jakarta.inject.Inject`? The field will not be injected by Arc. Debugging will show the transformed class being loaded in the app, but it looks like Arc doesn't see the new code. @@ -59,29 +59,29 @@ The reason is that _all_ Quarkus's bytecode transformations are done after Jande Most extensions use Jandex as a source of truth to find out what to do. Those extensions won't see new/modified endpoints in the bytecode itself. The solution to this limitation is annotation transformers. You should also be aware that while Arc and Quarkus REST honour annotation transformers, not all extensions do. -### Something in my classpath has @Inject annotations, which are confusing CDI. How can I fix that? +=== Something in my classpath has @Inject annotations, which are confusing CDI. How can I fix that? You will need to implement an `AnnotationsTransformer` and strip out out the problematic injection sites. (Remember, if the use case involves CDI, it needs to be an `AnnotationsTransformer`, not a BytecodeTransformer`.) See link:https://quarkus.io/blog/solving-problems-with-extensions-2/[this blog] about on using an `AnnotationsTransformer` extension to clean non `@Inject` annotations from the Airline library so that it can be used in CDI-enabled runtimes. -## Cross-cutting concerns +== Cross-cutting concerns -### How can I redirect application logging to an external service? +=== How can I redirect application logging to an external service? A `LogHandlerBuildItem` is a convenient way to redirect application logs. See this link:https://quarkus.io/blog/quarkus-aws-cloudwatch_extension/[worked example of an extension which directs output to AWS CloudWatch]. -## Build and hosting infrastructure for extensions +== Build and hosting infrastructure for extensions -### Can I use Gradle to build my extension? +=== Can I use Gradle to build my extension? Yes, but it's not the most typical pattern. See the xref:building-my-first-extension.adoc#gradle-setup[Building Your First Extension Guide] for instructions on setting up a Gradle extension. Have a look at the link:https://quarkus.io/extensions/org.jobrunr/quarkus-jobrunr/[JobRunr extension] for an example implementation. -### If I want my extension to be in code.quarkus.io, does it have to be in the Quarkiverse GitHub org? +=== If I want my extension to be in code.quarkus.io, does it have to be in the Quarkiverse GitHub org? Registering an extension in the catalog is independent from where the source code is. The link:https://hub.quarkiverse.io[quarkiverse repository] has some shortcuts to make releasing and testing extensions easier, but any extension can link:https://hub.quarkiverse.io/checklistfornewprojects/#make-your-extension-available-in-the-tooling[register into the catalog]. -### My extension isn't showing up on extensions.quarkus.io +=== My extension isn't showing up on extensions.quarkus.io Every extension in the link:https://github.com/quarkusio/quarkus-extension-catalog/tree/main/extensions[extension catalog] should appear in http://code.quarkus.io, http://extensions.quarkus.io, and the command line tools. The web pages at http://extensions.quarkus.io are refreshed a few times a delay, so there may be a delay in new extensions showing up there. @@ -92,10 +92,10 @@ To debug a missing extension, first: - Check if the extension is listed in the http://https://registry.quarkus.io/q/swagger-ui/#/Client/get_client_extensions_all[Quarkus registry] list of all known extensions - Check if there has been a green link:https://github.com/quarkusio/extensions/actions/workflows/build_and_publish.yml[build of the extensions site] since updating the catalog -## Other topics +== Other topics -### What's the difference between a quickstart and a codestart? +=== What's the difference between a quickstart and a codestart? Both codestarts and quickstarts are designed to help users get coding quickly. A codestarts is a generated application and a quickstart is browsable source code. From 9b94c6ad1d1ce0e7ed986969a35d69df9ffe52db Mon Sep 17 00:00:00 2001 From: Inaki Villar Date: Thu, 14 Nov 2024 18:58:30 -0800 Subject: [PATCH 30/39] removing unused configuration of the application model in quarkusbuild tasks (cherry picked from commit e39915fda7d23eb861c310f30d9e94ead52eecf1) --- .../java/io/quarkus/gradle/QuarkusPlugin.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index 8e6b0a97d9d01..0a849539e132b 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -246,9 +246,6 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - task.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); - }); Property cacheLargeArtifacts = quarkusExt.getCacheLargeArtifacts(); @@ -298,9 +295,6 @@ public boolean isSatisfiedBy(Task t) { task.dependsOn(quarkusRequiredExtension); configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getBuilderName().set(quarkusRequiredExtension.flatMap(ImageCheckRequirementsTask::getOutputFile)); - task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - task.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); task.finalizedBy(quarkusBuild); }); @@ -308,17 +302,11 @@ public boolean isSatisfiedBy(Task t) { task.dependsOn(quarkusRequiredExtension); configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getBuilderName().set(quarkusRequiredExtension.flatMap(ImageCheckRequirementsTask::getOutputFile)); - task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - task.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); task.finalizedBy(quarkusBuild); }); tasks.register(DEPLOY_TASK_NAME, Deploy.class, task -> { configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); - task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - task.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); task.finalizedBy(quarkusBuild); }); @@ -327,9 +315,6 @@ public boolean isSatisfiedBy(Task t) { TaskProvider quarkusRun = tasks.register(QUARKUS_RUN_TASK_NAME, QuarkusRun.class, build -> { configureQuarkusBuildTask(project, build, quarkusBuildAppModelTask, serviceProvider); - build.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - build.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); build.dependsOn(quarkusBuild); }); From 80ee73260e315a29ac4f04b95b8b46f3c6d66f04 Mon Sep 17 00:00:00 2001 From: brunobat Date: Mon, 4 Nov 2024 12:15:16 +0000 Subject: [PATCH 31/39] Activate Native JFR monitoring if OTel metrics are used (cherry picked from commit 3a88f3ddf8b4dab1807c244539edb13ae1f3e29b) --- .../builditem/NativeMonitoringBuildItem.java | 20 ++++++++++++++++ .../pkg/steps/NativeImageBuildStep.java | 23 +++++++++++++++++-- .../deployment/metric/MetricProcessor.java | 7 ++++++ .../src/main/resources/application.properties | 1 - 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeMonitoringBuildItem.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeMonitoringBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeMonitoringBuildItem.java new file mode 100644 index 0000000000000..a137322ab5ac5 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeMonitoringBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.deployment.builditem; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.pkg.NativeConfig; + +/** + * A build item that indicates whether native monitoring is enabled and which option from {@link NativeConfig.MonitoringOption}. + * To be used in the native image generation. + */ +public final class NativeMonitoringBuildItem extends MultiBuildItem { + private final NativeConfig.MonitoringOption option; + + public NativeMonitoringBuildItem(NativeConfig.MonitoringOption option) { + this.option = option; + } + + public NativeConfig.MonitoringOption getOption() { + return this.option; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index fb8daf5b727cf..45cd6424d478a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -11,10 +11,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -24,6 +26,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; +import io.quarkus.deployment.builditem.NativeMonitoringBuildItem; import io.quarkus.deployment.builditem.SuppressNonRuntimeConfigChangedWarningBuildItem; import io.quarkus.deployment.builditem.nativeimage.ExcludeConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.JPMSExportBuildItem; @@ -115,6 +118,7 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, List nativeImageSecurityProviders, List nativeImageFeatures, NativeImageRunnerBuildItem nativeImageRunner, + List nativeMonitoringBuildItems, CurateOutcomeBuildItem curateOutcomeBuildItem) { Path outputDir; @@ -147,6 +151,7 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, .setGraalVMVersion(GraalVM.Version.CURRENT) .setNativeImageFeatures(nativeImageFeatures) .setContainerBuild(nativeImageRunner.isContainerBuild()) + .setNativeMonitoringOptions(nativeMonitoringBuildItems) .build(); List command = nativeImageArgs.getArgs(); @@ -196,6 +201,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon Optional processInheritIODisabledBuildItem, List nativeImageFeatures, Optional nativeImageAgentConfigDirectoryBuildItem, + List nativeMonitoringItems, NativeImageRunnerBuildItem nativeImageRunner) { if (nativeConfig.debug().enabled()) { copyJarSourcesToLib(outputTargetBuildItem, curateOutcomeBuildItem); @@ -254,6 +260,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon .setBrokenClasspath(incompleteClassPathAllowed.isAllow()) .setNativeImageSecurityProviders(nativeImageSecurityProviders) .setJPMSExportBuildItems(jpmsExportBuildItems) + .setNativeMonitoringOptions(nativeMonitoringItems) .setEnableModules(enableModules) .setNativeMinimalJavaVersions(nativeMinimalJavaVersions) .setUnsupportedOSes(unsupportedOses) @@ -605,6 +612,7 @@ static class Builder { private List nativeMinimalJavaVersions; private List unsupportedOSes; private List nativeImageFeatures; + private List nativeMonitoringItems; private Path outputDir; private String runnerJarName; private String pie = ""; @@ -713,6 +721,11 @@ public Builder setNativeImageName(String nativeImageName) { return this; } + public Builder setNativeMonitoringOptions(List options) { + this.nativeMonitoringItems = options; + return this; + } + @SuppressWarnings("deprecation") public NativeImageInvokerInfo build() { List nativeImageArgs = new ArrayList<>(); @@ -932,17 +945,23 @@ public NativeImageInvokerInfo build() { nativeImageArgs.add("-march=" + nativeConfig.march().get()); } - List monitoringOptions = new ArrayList<>(); + Set monitoringOptions = new LinkedHashSet<>(); if (!OS.WINDOWS.isCurrent() || containerBuild) { // --enable-monitoring=heapdump is not supported on Windows monitoringOptions.add(NativeConfig.MonitoringOption.HEAPDUMP); } + + if (nativeMonitoringItems != null && !nativeMonitoringItems.isEmpty()) { + monitoringOptions.addAll(nativeMonitoringItems.stream() + .map(NativeMonitoringBuildItem::getOption) + .collect(Collectors.toSet())); + } + if (nativeConfig.monitoring().isPresent()) { monitoringOptions.addAll(nativeConfig.monitoring().get()); } if (!monitoringOptions.isEmpty()) { nativeImageArgs.add("--enable-monitoring=" + monitoringOptions.stream() - .distinct() .map(o -> o.name().toLowerCase(Locale.ROOT)).collect(Collectors.joining(","))); } diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java index 1224d28e0c2c8..fcc57f9187165 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java @@ -22,7 +22,9 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.NativeMonitoringBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; +import io.quarkus.deployment.pkg.NativeConfig; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.metrics.cdi.MetricsProducer; import io.quarkus.opentelemetry.runtime.metrics.instrumentation.JvmMetricsService; @@ -33,6 +35,11 @@ public class MetricProcessor { private static final DotName METRIC_READER = DotName.createSimple(MetricReader.class.getName()); private static final DotName METRIC_PROCESSOR = DotName.createSimple(MetricProcessor.class.getName()); + @BuildStep + void addNativeMonitoring(BuildProducer nativeMonitoring) { + nativeMonitoring.produce(new NativeMonitoringBuildItem(NativeConfig.MonitoringOption.JFR)); + } + @BuildStep UnremovableBeanBuildItem ensureProducersAreRetained( CombinedIndexBuildItem indexBuildItem, diff --git a/integration-tests/opentelemetry/src/main/resources/application.properties b/integration-tests/opentelemetry/src/main/resources/application.properties index 68c3b9664276b..1992d76c95677 100644 --- a/integration-tests/opentelemetry/src/main/resources/application.properties +++ b/integration-tests/opentelemetry/src/main/resources/application.properties @@ -21,4 +21,3 @@ quarkus.security.users.embedded.plain-text=true quarkus.security.users.embedded.enabled=true quarkus.http.auth.basic=true -quarkus.native.monitoring=jfr From 50569d8453a7ba5416a3d764f0e4f1f38283209c Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Mon, 18 Nov 2024 11:40:59 +0100 Subject: [PATCH 32/39] Correct javadoc of ReflectiveHierarchyBuildItem (cherry picked from commit b165ea5300df063836b64c93f6e627898e8d7b31) --- .../builditem/nativeimage/ReflectiveHierarchyBuildItem.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java index 54b7c35d01ffe..7e3f44d2eaa82 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java @@ -24,6 +24,7 @@ * register the following: *

* - Superclasses + * - Subclasses * - Component types of collections * - Types used in bean properties (if method reflection is enabled) * - Field types (if field reflection is enabled) From 69b2416f66035e24fdb544411d135fba731c62a3 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Mon, 18 Nov 2024 21:24:52 -0300 Subject: [PATCH 33/39] Render ConfigMapping elements properly in the Configuration editor (cherry picked from commit 811819ee657de359a714dd6f1a76f19f1df7aa08) --- .../steps/ConfigDescriptionBuildStep.java | 156 ++++++------------ 1 file changed, 46 insertions(+), 110 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java index 4068bf65b1f16..df0e1ee7947d3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java @@ -13,9 +13,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; import java.util.Properties; import java.util.Set; import java.util.function.Consumer; @@ -87,7 +84,7 @@ public void accept(Container node) { String defaultDefault; final Class valueClass = field.getType(); - EffectiveConfigTypeAndValues effectiveConfigTypeAndValues = getTypeName(field); + EffectiveConfigTypeAndValues effectiveConfigTypeAndValues = getTypeName(valueClass, field.getGenericType()); if (valueClass == boolean.class) { defaultDefault = "false"; @@ -112,8 +109,8 @@ public void accept(Container node) { ret.add(new ConfigDescriptionBuildItem(name, defVal, javadoc.getProperty(javadocKey), - effectiveConfigTypeAndValues.getTypeName(), - effectiveConfigTypeAndValues.getAllowedValues(), + effectiveConfigTypeAndValues.typeName(), + effectiveConfigTypeAndValues.allowedValues(), configPhase)); } }); @@ -147,136 +144,75 @@ private void processMappings(List mappings, List valueClass = field.getType(); - return getTypeName(field, valueClass); - } - - private EffectiveConfigTypeAndValues getTypeName(Field field, Class valueClass) { - EffectiveConfigTypeAndValues typeAndValues = new EffectiveConfigTypeAndValues(); - String name = valueClass.getName(); + private EffectiveConfigTypeAndValues getTypeName(Class valueClass, Type genericType) { + final String name; + final List allowedValues = new ArrayList<>(); // Extract Optionals, Lists and Sets if ((valueClass.equals(Optional.class) || valueClass.equals(List.class) || valueClass.equals(Set.class))) { - - if (field != null) { - Type genericType = field.getGenericType(); - name = genericType.getTypeName(); + String thisName = valueClass.getName(); + if (genericType != null) { + thisName = genericType.getTypeName(); } - if (name.contains("<") && name.contains(">")) { - name = name.substring(name.lastIndexOf("<") + 1, name.indexOf(">")); + if (thisName.contains("<") && thisName.contains(">")) { + thisName = thisName.substring(thisName.lastIndexOf("<") + 1, thisName.indexOf(">")); } try { - Class c = Class.forName(name); - return getTypeName(null, c); + Class c = Class.forName(thisName); + return getTypeName(c, null); } catch (ClassNotFoundException ex) { // Then we use the name as is. } - } - - // Check other optionals - if (valueClass.equals(OptionalInt.class)) { - name = Integer.class.getName(); - } else if (valueClass.equals(OptionalDouble.class)) { - name = Double.class.getName(); - } else if (valueClass.equals(OptionalLong.class)) { - name = Long.class.getName(); - } - - // Check if this is an enum - if (Enum.class.isAssignableFrom(valueClass)) { + name = thisName; + } else if (Enum.class.isAssignableFrom(valueClass)) { + // Check if this is an enum name = Enum.class.getName(); Object[] values = valueClass.getEnumConstants(); for (Object v : values) { - Enum casted = (Enum) valueClass.cast(v); - typeAndValues.addAllowedValue(casted.name()); + Enum casted = (Enum) valueClass.cast(v); + allowedValues.add(casted.name()); } + } else { + // Map all primitives + name = switch (valueClass.getName()) { + case "java.util.OptionalInt", "int" -> Integer.class.getName(); + case "boolean" -> Boolean.class.getName(); + case "float" -> Float.class.getName(); + case "java.util.OptionalDouble", "double" -> Double.class.getName(); + case "java.util.OptionalLong", "long" -> Long.class.getName(); + case "byte" -> Byte.class.getName(); + case "short" -> Short.class.getName(); + case "char" -> Character.class.getName(); + default -> valueClass.getName(); + }; } // Special case for Log level if (valueClass.isAssignableFrom(Level.class)) { - typeAndValues.addAllowedValue(Level.ALL.getName()); - typeAndValues.addAllowedValue(Level.CONFIG.getName()); - typeAndValues.addAllowedValue(Level.FINE.getName()); - typeAndValues.addAllowedValue(Level.FINER.getName()); - typeAndValues.addAllowedValue(Level.FINEST.getName()); - typeAndValues.addAllowedValue(Level.INFO.getName()); - typeAndValues.addAllowedValue(Level.OFF.getName()); - typeAndValues.addAllowedValue(Level.SEVERE.getName()); - typeAndValues.addAllowedValue(Level.WARNING.getName()); - } - - // Map all primitives - if (name.equals("int")) { - name = Integer.class.getName(); - } else if (name.equals("boolean")) { - name = Boolean.class.getName(); - } else if (name.equals("float")) { - name = Float.class.getName(); - } else if (name.equals("double")) { - name = Double.class.getName(); - } else if (name.equals("long")) { - name = Long.class.getName(); - } else if (name.equals("byte")) { - name = Byte.class.getName(); - } else if (name.equals("short")) { - name = Short.class.getName(); - } else if (name.equals("char")) { - name = Character.class.getName(); - } - - typeAndValues.setTypeName(name); - return typeAndValues; + allowedValues.add(Level.ALL.getName()); + allowedValues.add(Level.CONFIG.getName()); + allowedValues.add(Level.FINE.getName()); + allowedValues.add(Level.FINER.getName()); + allowedValues.add(Level.FINEST.getName()); + allowedValues.add(Level.INFO.getName()); + allowedValues.add(Level.OFF.getName()); + allowedValues.add(Level.SEVERE.getName()); + allowedValues.add(Level.WARNING.getName()); + } + + return new EffectiveConfigTypeAndValues(name, allowedValues); } - static class EffectiveConfigTypeAndValues { - private String typeName; - private List allowedValues; - - public EffectiveConfigTypeAndValues() { - - } - - public EffectiveConfigTypeAndValues(String typeName) { - this.typeName = typeName; - } - - public EffectiveConfigTypeAndValues(String typeName, List allowedValues) { - this.typeName = typeName; - this.allowedValues = allowedValues; - } - - public String getTypeName() { - return typeName; - } - - public void setTypeName(String typeName) { - this.typeName = typeName; - } - - public List getAllowedValues() { - return allowedValues; - } - - public void setAllowedValues(List allowedValues) { - this.allowedValues = allowedValues; - } - - public void addAllowedValue(String v) { - if (allowedValues == null) { - allowedValues = new ArrayList<>(); - } - allowedValues.add(v); - } + private record EffectiveConfigTypeAndValues(String typeName, List allowedValues) { } } From 2442848d73c19e6fed4b3bd27e01e30e46aab933 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Tue, 19 Nov 2024 03:39:51 -0800 Subject: [PATCH 34/39] Update `CacheJsonRPCService.java` reference Signed-off-by: Emmanuel Ferdman (cherry picked from commit 9dc9fadcf213a3eb6fd325d502d6160a67c5ae69) --- docs/src/main/asciidoc/dev-ui.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/dev-ui.adoc b/docs/src/main/asciidoc/dev-ui.adoc index dedfcef592f0d..8a1ca192b8840 100644 --- a/docs/src/main/asciidoc/dev-ui.adoc +++ b/docs/src/main/asciidoc/dev-ui.adoc @@ -922,7 +922,7 @@ public JsonArray getAll() { // <2> <1> This example runs nonblocking. We could also return `Uni` <2> The method name `getAll` will be available in the Javascript -https://github.com/quarkusio/quarkus/blob/main/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devconsole/CacheJsonRPCService.java[Example code] +https://github.com/quarkusio/quarkus/blob/main/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devui/CacheJsonRPCService.java[Example code] *Webcomponent (Javascript) part* From 18bc840937858aff677fb4832e23de46f5739767 Mon Sep 17 00:00:00 2001 From: Bruno Marvin <51372744+msscsh@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:09:05 -0300 Subject: [PATCH 35/39] grammar corrections for US English (cherry picked from commit 52882dabf63c010454e04a6ce8915ca90f2acb17) --- docs/src/main/asciidoc/qute.adoc | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/src/main/asciidoc/qute.adoc b/docs/src/main/asciidoc/qute.adoc index 658832463d1d6..f8a91fe6af198 100644 --- a/docs/src/main/asciidoc/qute.adoc +++ b/docs/src/main/asciidoc/qute.adoc @@ -10,11 +10,11 @@ include::_attributes.adoc[] :topics: templating,qute :extensions: io.quarkus:quarkus-qute,io.quarkus:quarkus-resteasy-qute,io.quarkus:quarkus-rest-qute -Qute is a templating engine designed specifically to meet the Quarkus needs. -The usage of reflection is minimized to reduce the size of native images. +Qute is a templating engine developed specifically for Quarkus. +Reflection usage is minimized to reduce the size of native images. The API combines both the imperative and the non-blocking reactive style of coding. -In the development mode, all files located in `src/main/resources/templates` are watched for changes and modifications are immediately visible. -Furthermore, we try to detect most of the template problems at build time. +In development mode, all files located in `src/main/resources/templates` are monitored for changes, and modifications become visible immediately. +Furthermore, we aim to detect most template issues at build time. In this guide, you will learn how to easily render templates in your application. == Solution @@ -65,7 +65,7 @@ Let's start with a Hello World template: NOTE: Templates located in the `pub` directory are served via HTTP. This behavior is built-in, no controllers are needed. For example, the template `src/main/resources/templates/pub/foo.html` will be served from the paths `/foo` and `/foo.html` by default. -If your application is running, you can open your browser and hit: http://localhost:8080/hello?name=Martin +Once your application is running, you can open your browser and navigate to: http://localhost:8080/hello?name=Martin For more information about Qute Web options, see the https://docs.quarkiverse.io/quarkus-qute-web/dev/index.html[Qute Web guide]. @@ -144,7 +144,7 @@ Hello Martin! There's an alternate way to declare your templates in your Java code, which relies on the following convention: -- Organise your template files in the `/src/main/resources/templates` directory, by grouping them into one directory per resource class. So, if +- Organize your template files in the `/src/main/resources/templates` directory, by grouping them into one directory per resource class. So, if your `ItemResource` class references two templates `hello` and `goodbye`, place them at `/src/main/resources/templates/ItemResource/hello.txt` and `/src/main/resources/templates/ItemResource/goodbye.txt`. Grouping templates per resource class makes it easier to navigate to them. - In each of your resource class, declare a `@CheckedTemplate static class Template {}` class within your resource class. @@ -389,7 +389,7 @@ public class ItemResource { *Template extension methods* are used to extend the set of accessible properties of data objects. Sometimes, you're not in control of the classes that you want to use in your template, and you cannot add methods -to them. Template extension methods allows you to declare new method for those classes that will be available +to them. Template extension methods allow you to declare new methods for those classes that will be available from your templates just as if they belonged to the target class. Let's keep extending on our simple HTML page that contains the item name, price and add a discounted price. @@ -442,7 +442,7 @@ grouped by target type, or in a single `TemplateExtensions` class by convention. == Rendering Periodic Reports -Templating engine could be also very useful when rendering periodic reports. +The templating engine can also be very useful for rendering periodic reports. You'll need to add the `quarkus-scheduler` and `quarkus-qute` extensions first. In your `pom.xml` file, add: From 17a8b2e220d89e2f6fe3dbad0fd3730664aaf720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Tue, 19 Nov 2024 13:18:17 +0100 Subject: [PATCH 36/39] Make sure identity produced by `@TestSecurity` is augmented (cherry picked from commit e6a895b76b6f7c0faf965beebb42e3ea5faa97e3) --- docs/src/main/asciidoc/security-testing.adoc | 44 +++++++++++++++++ .../deployment/SecurityProcessor.java | 4 +- ...usPermissionSecurityIdentityAugmentor.java | 2 +- .../runtime/SecurityCheckRecorder.java | 8 ++-- .../it/resteasy/elytron/RootResource.java | 17 +++++++ .../elytron/TestSecurityTestCase.java | 22 +++++++++ .../quarkus/it/keycloak/CustomPermission.java | 10 ++++ .../it/keycloak/ProtectedJwtResource.java | 9 ++++ .../TestSecurityIdentityAugmentor.java | 37 ++++++++++++++ .../src/main/resources/application.properties | 2 + .../it/keycloak/TestSecurityLazyAuthTest.java | 48 +++++++++++++++++++ ...stractTestHttpAuthenticationMechanism.java | 34 ++++++++++++- .../QuarkusSecurityTestExtension.java | 48 ++++++++++++++++--- .../security/TestIdentityAssociation.java | 4 +- .../quarkus/test/security/TestSecurity.java | 12 ++++- 15 files changed, 282 insertions(+), 19 deletions(-) create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java create mode 100644 integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java diff --git a/docs/src/main/asciidoc/security-testing.adoc b/docs/src/main/asciidoc/security-testing.adoc index bed66779c75e3..f89af3f3c4af6 100644 --- a/docs/src/main/asciidoc/security-testing.adoc +++ b/docs/src/main/asciidoc/security-testing.adoc @@ -156,6 +156,50 @@ public String getDetail() { } ---- +It is also possible to set custom permissions like in the example below: + +[source,java] +---- +@PermissionsAllowed("see", permission = CustomPermission.class) +public String getDetail() { + return "detail"; +} +---- + +The `CustomPermission` needs to be granted to the `SecurityIdentity` created +by the `@TestSecurity` annotation with a `SecurityIdentityAugmentor` CDI bean: + +[source,java] +---- +@ApplicationScoped +public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + final SecurityIdentity augmentedIdentity; + if (shouldGrantCustomPermission(securityIdentity) { + augmentedIdentity = QuarkusSecurityIdentity.builder(securityIdentity) + .addPermission(new CustomPermission("see")).build(); + } else { + augmentedIdentity = securityIdentity; + } + return Uni.createFrom().item(augmentedIdentity); + } +} +---- + +Quarkus will only augment the `SecurityIdentity` created with the `@TestSecurity` annotation if you set +the `@TestSecurity#augmentors` annotation attribute to the `CustomSecurityIdentityAugmentor.class` like this: + +[source,java] +---- +@Test +@TestSecurity(user = "testUser", permissions = "see:detail", augmentors = CustomSecurityIdentityAugmentor.class) +void someTestMethod() { + ... +} +---- + === Mixing security tests If it becomes necessary to test security features using both `@TestSecurity` and Basic Auth (which is the fallback auth diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index 82fc4151c7ede..bd5b9bcf607e4 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -107,6 +107,7 @@ import io.quarkus.security.deployment.PermissionSecurityChecks.PermissionSecurityChecksBuilder; import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.IdentityProviderManagerCreator; +import io.quarkus.security.runtime.QuarkusPermissionSecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusSecurityRolesAllowedConfigBuilder; import io.quarkus.security.runtime.SecurityBuildTimeConfig; import io.quarkus.security.runtime.SecurityCheckRecorder; @@ -691,7 +692,8 @@ void configurePermissionCheckers(PermissionSecurityChecksBuilderBuildItem checke // - this processor relies on the bean archive index (cycle: idx -> additional bean -> idx) // - we have injection points (=> better validation from Arc) as checker beans are only requested from this augmentor var syntheticBeanConfigurator = SyntheticBeanBuildItem - .configure(SecurityIdentityAugmentor.class) + .configure(QuarkusPermissionSecurityIdentityAugmentor.class) + .addType(SecurityIdentityAugmentor.class) // ATM we do get augmentors from CDI once, no need to keep the instance in the CDI container .scope(Dependent.class) .unremovable() diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java index 300cd5e830204..7ca713dc4c4bd 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java @@ -15,7 +15,7 @@ * Adds a permission checker that grants access to the {@link QuarkusPermission} * when {@link QuarkusPermission#isGranted(SecurityIdentity)} is true. */ -final class QuarkusPermissionSecurityIdentityAugmentor implements SecurityIdentityAugmentor { +public final class QuarkusPermissionSecurityIdentityAugmentor implements SecurityIdentityAugmentor { /** * Permission checker only authorizes authenticated users and checkers shouldn't throw a security exception. diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java index 6667ba40b89ca..ad1e9ba9ca8a9 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java @@ -29,7 +29,6 @@ import io.quarkus.runtime.annotations.Recorder; import io.quarkus.security.ForbiddenException; import io.quarkus.security.StringPermission; -import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder; import io.quarkus.security.runtime.interceptor.SecurityConstrainer; import io.quarkus.security.runtime.interceptor.check.AuthenticatedCheck; @@ -436,10 +435,11 @@ private static Object convertMethodParamToPermParam(int i, Object methodArg, } } - public Function, SecurityIdentityAugmentor> createPermissionAugmentor() { - return new Function, SecurityIdentityAugmentor>() { + public Function, QuarkusPermissionSecurityIdentityAugmentor> createPermissionAugmentor() { + return new Function, QuarkusPermissionSecurityIdentityAugmentor>() { @Override - public SecurityIdentityAugmentor apply(SyntheticCreationalContext ctx) { + public QuarkusPermissionSecurityIdentityAugmentor apply( + SyntheticCreationalContext ctx) { return new QuarkusPermissionSecurityIdentityAugmentor(ctx.getInjectedReference(BlockingSecurityExecutor.class)); } }; diff --git a/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java b/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java index 11e6a1d3675b7..a8ff4305d70c9 100644 --- a/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java +++ b/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java @@ -16,6 +16,8 @@ import jakarta.ws.rs.core.SecurityContext; import io.quarkus.security.Authenticated; +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; import io.quarkus.security.identity.SecurityIdentity; @Path("/") @@ -73,4 +75,19 @@ public String getAttributes() { .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining(",")); } + + @GET + @Path("/test-security-permission-checker") + @PermissionsAllowed("see-principal") + public String getPrincipal(@Context SecurityContext sec) { + return sec.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" + principal.getName(); + } + + @PermissionChecker("see-principal") + boolean canSeePrincipal(SecurityContext sec) { + if (sec.getUserPrincipal() == null || sec.getUserPrincipal().getName() == null) { + return false; + } + return "meat loaf".equals(sec.getUserPrincipal().getName()); + } } diff --git a/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java b/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java index 25017f7014fc4..43158fc4d374c 100644 --- a/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java +++ b/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java @@ -11,6 +11,7 @@ import jakarta.inject.Inject; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -23,6 +24,7 @@ import io.quarkus.test.security.AttributeType; import io.quarkus.test.security.SecurityAttribute; import io.quarkus.test.security.TestSecurity; +import io.restassured.RestAssured; @QuarkusTest class TestSecurityTestCase { @@ -187,4 +189,24 @@ static Stream arrayParams() { arguments(new int[] { 1, 2 }, new String[] { "hello", "world" })); } + @Test + public void testPermissionChecker_anonymousUser() { + // user is not authenticated and access should not be granted by the permission checker + RestAssured.get("/test-security-permission-checker").then().statusCode(401); + } + + @Test + @TestSecurity(user = "authenticated-user") + public void testPermissionChecker_authenticatedUser() { + // user is authenticated, but access should not be granted by the permission checker + RestAssured.get("/test-security-permission-checker").then().statusCode(403); + } + + @Test + @TestSecurity(user = "meat loaf") + public void testPermissionChecker_authorizedUser() { + // user is authenticated and access should be granted by the permission checker + RestAssured.get("/test-security-permission-checker").then().statusCode(200) + .body(Matchers.is("meat loaf:meat loaf:meat loaf")); + } } diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java new file mode 100644 index 0000000000000..5325e49879a92 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java @@ -0,0 +1,10 @@ +package io.quarkus.it.keycloak; + +import java.security.BasicPermission; + +public class CustomPermission extends BasicPermission { + + public CustomPermission(String name) { + super(name); + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java index d9187dc7e9d1f..fdfbe0128baee 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java @@ -14,6 +14,7 @@ import org.eclipse.microprofile.jwt.JsonWebToken; import io.quarkus.security.Authenticated; +import io.quarkus.security.PermissionsAllowed; import io.quarkus.security.identity.SecurityIdentity; @Path("/web-app") @@ -40,6 +41,14 @@ public String testSecurity() { + principal.getName(); } + @GET + @Path("test-security-with-augmentors") + @PermissionsAllowed(permission = CustomPermission.class, value = "augmented") + public String testSecurityWithAugmentors() { + return securityContext.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" + + principal.getName(); + } + @POST @Path("test-security") @Consumes("application/json") diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..6e8662823ad16 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java @@ -0,0 +1,37 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +public class TestSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + private static volatile boolean invoked = false; + + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + invoked = true; + final SecurityIdentity identity; + if (securityIdentity.isAnonymous() || !"authorized-user".equals(securityIdentity.getPrincipal().getName())) { + identity = securityIdentity; + } else { + identity = QuarkusSecurityIdentity.builder(securityIdentity) + .addPermission(new CustomPermission("augmented")).build(); + } + return Uni.createFrom().item(identity); + } + + public static boolean isInvoked() { + return invoked; + } + + public static void resetInvoked() { + invoked = false; + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties index fa4f560b45c04..f843b520dc74e 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties @@ -1,3 +1,5 @@ +quarkus.keycloak.devservices.enabled=false + mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus smallrye.jwt.path.groups=realm_access/roles diff --git a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java index 45812da96c15a..a573cb8e6af55 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java +++ b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -7,6 +7,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; @@ -21,6 +22,53 @@ @TestHTTPEndpoint(ProtectedJwtResource.class) public class TestSecurityLazyAuthTest { + @Test + public void testTestSecurityAnnotationWithAugmentors_anonymousUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + // user is not authenticated and doesn't have required role granted by the augmentor + RestAssured.get("test-security-with-augmentors").then().statusCode(401); + // identity manager applies augmentors on anonymous identity + // because @TestSecurity is not in action and that's what we do for the anonymous requests + Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authenticated-user") + @Test + public void testTestSecurityAnnotationNoAugmentors_authenticatedUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + // user is authenticated, but doesn't have required role granted by the augmentor + // and no augmentors are applied + RestAssured.get("test-security-with-augmentors").then().statusCode(403); + Assertions.assertFalse(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authenticated-user", augmentors = TestSecurityIdentityAugmentor.class) + @Test + public void testTestSecurityAnnotationWithAugmentors_authenticatedUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + // user is authenticated, but doesn't have required role granted by the augmentor + RestAssured.get("test-security-with-augmentors").then().statusCode(403); + Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authorized-user") + @Test + public void testTestSecurityAnnotationNoAugmentors_authorizedUser() { + // should fail because no augmentors are applied + TestSecurityIdentityAugmentor.resetInvoked(); + RestAssured.get("test-security-with-augmentors").then().statusCode(403); + Assertions.assertFalse(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authorized-user", augmentors = TestSecurityIdentityAugmentor.class) + @Test + public void testTestSecurityAnnotationWithAugmentors_authorizedUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + RestAssured.get("test-security-with-augmentors").then().statusCode(200) + .body(is("authorized-user:authorized-user:authorized-user")); + Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked()); + } + @Test @TestAsUser1Viewer public void testWithDummyUser() { diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java b/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java index 1359b83e041f7..732b7623d79dd 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java @@ -1,15 +1,24 @@ package io.quarkus.test.security; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.ROUTING_CONTEXT_ATTRIBUTE; + import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import jakarta.annotation.PostConstruct; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import io.quarkus.runtime.LaunchMode; +import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; @@ -21,7 +30,11 @@ abstract class AbstractTestHttpAuthenticationMechanism implements HttpAuthentica @Inject TestIdentityAssociation testIdentityAssociation; + @Inject + BlockingSecurityExecutor blockingSecurityExecutor; + protected volatile String authMechanism = null; + protected volatile List> augmentors = null; @PostConstruct public void check() { @@ -32,8 +45,21 @@ public void check() { } @Override - public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - return Uni.createFrom().item(testIdentityAssociation.getTestIdentity()); + public Uni authenticate(RoutingContext event, IdentityProviderManager identityProviderManager) { + var identity = Uni.createFrom().item(testIdentityAssociation.getTestIdentity()); + if (augmentors != null && testIdentityAssociation.getTestIdentity() != null) { + var requestContext = new AuthenticationRequestContext() { + @Override + public Uni runBlocking(Supplier supplier) { + return blockingSecurityExecutor.executeBlocking(supplier); + } + }; + var requestAttributes = Map. of(ROUTING_CONTEXT_ATTRIBUTE, event); + for (var augmentor : augmentors) { + identity = identity.flatMap(i -> augmentor.get().augment(i, requestContext, requestAttributes)); + } + } + return identity; } @Override @@ -55,4 +81,8 @@ public Uni getCredentialTransport(RoutingContext contex void setAuthMechanism(String authMechanism) { this.authMechanism = authMechanism; } + + void setSecurityIdentityAugmentors(List> augmentors) { + this.augmentors = augmentors; + } } diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java index ef556bad11fc8..fc6ac4d24f67d 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java @@ -5,9 +5,11 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.security.Permission; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -17,8 +19,12 @@ import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.CDI; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusPermissionSecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; @@ -34,11 +40,13 @@ public class QuarkusSecurityTestExtension implements QuarkusTestBeforeEachCallba public void afterEach(QuarkusTestMethodContext context) { try { if (getAnnotationContainer(context).isPresent()) { - CDI.current().select(TestAuthController.class).get().setEnabled(true); - for (var testMechanism : CDI.current().select(AbstractTestHttpAuthenticationMechanism.class)) { + final ArcContainer container = Arc.container(); + container.select(TestAuthController.class).get().setEnabled(true); + for (var testMechanism : container.select(AbstractTestHttpAuthenticationMechanism.class)) { testMechanism.setAuthMechanism(null); + testMechanism.setSecurityIdentityAugmentors(null); } - var testIdentity = CDI.current().select(TestIdentityAssociation.class).get(); + var testIdentity = container.select(TestIdentityAssociation.class).get(); testIdentity.setTestIdentity(null); testIdentity.setPathBasedIdentity(false); } @@ -59,7 +67,8 @@ public void beforeEach(QuarkusTestMethodContext context) { var annotationContainer = annotationContainerOptional.get(); Annotation[] allAnnotations = annotationContainer.getElement().getAnnotations(); TestSecurity testSecurity = annotationContainer.getAnnotation(); - CDI.current().select(TestAuthController.class).get().setEnabled(testSecurity.authorizationEnabled()); + final ArcContainer container = Arc.container(); + container.select(TestAuthController.class).get().setEnabled(testSecurity.authorizationEnabled()); if (testSecurity.user().isEmpty()) { if (testSecurity.roles().length != 0) { throw new RuntimeException("Cannot specify roles without a username in @TestSecurity"); @@ -82,12 +91,37 @@ public void beforeEach(QuarkusTestMethodContext context) { } SecurityIdentity userIdentity = augment(user.build(), allAnnotations); - CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(userIdentity); + container.select(TestIdentityAssociation.class).get().setTestIdentity(userIdentity); if (!testSecurity.authMechanism().isEmpty()) { - for (var testMechanism : CDI.current().select(AbstractTestHttpAuthenticationMechanism.class)) { + for (var testMechanism : container.select(AbstractTestHttpAuthenticationMechanism.class)) { testMechanism.setAuthMechanism(testSecurity.authMechanism()); } - CDI.current().select(TestIdentityAssociation.class).get().setPathBasedIdentity(true); + container.select(TestIdentityAssociation.class).get().setPathBasedIdentity(true); + } + + // run SecurityIdentityAugmentors when: + List> augmentors = new ArrayList<>(); + // 1. user opted-in with @TestSecurity#augmentors, run augmentors listed by user + for (Class augmentorClass : testSecurity.augmentors()) { + var augmentorInstance = container.select(augmentorClass); + if (!augmentorInstance.isResolvable()) { + var testMethodName = context.getTestMethod() == null ? "" : context.getTestMethod().getName(); + throw new RuntimeException(""" + SecurityIdentityAugmentor class '%s' specified with '@TestSecurity#augmentors' annotation + attribute on method '%s' is not available as a CDI bean. + """.formatted(augmentorClass, testMethodName)); + } + augmentors.add(augmentorInstance); + } + // 2. @PermissionChecker is used, run the augmentor that enables this functionality + var quarkusPermissionAugmentor = container.select(QuarkusPermissionSecurityIdentityAugmentor.class); + if (quarkusPermissionAugmentor.isResolvable()) { + augmentors.add(quarkusPermissionAugmentor); + } + if (!augmentors.isEmpty()) { + for (var testMechanism : container.select(AbstractTestHttpAuthenticationMechanism.class)) { + testMechanism.setSecurityIdentityAugmentors(augmentors); + } } } } catch (Exception e) { diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java index 569a9f266e880..807209452d474 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java @@ -26,12 +26,12 @@ public void check() { } } - volatile SecurityIdentity testIdentity; + private volatile SecurityIdentity testIdentity; /** * Whether authentication is successful only if right mechanism was used to authenticate. */ - volatile boolean isPathBasedIdentity = false; + private volatile boolean isPathBasedIdentity = false; /** * A request scoped delegate that allows the system to function as normal when diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java index 36543039989e4..a0fc93a1725fd 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java @@ -8,6 +8,7 @@ import io.quarkus.security.PermissionsAllowed; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) @@ -35,11 +36,18 @@ * That is, permission is separated from actions with {@link PermissionsAllowed#PERMISSION_TO_ACTION_SEPARATOR}. * For example, value {@code see:detail} gives permission to {@code see} action {@code detail}. * All permissions are added as {@link io.quarkus.security.StringPermission}. - * If you need to test custom permissions, you can add them with - * {@link io.quarkus.security.identity.SecurityIdentityAugmentor}. + * {@link io.quarkus.security.PermissionChecker} methods always authorize matched {@link PermissionsAllowed#value()} + * permissions. This annotation attribute cannot grant access to permissions granted by the checker methods. */ String[] permissions() default {}; + /** + * Specify {@link SecurityIdentityAugmentor} CDI beans that should augment {@link SecurityIdentity} created with + * this annotation. By default, no identity augmentors are applied. Use this option if you need to test + * custom {@link PermissionsAllowed#permission()} added with the identity augmentors. + */ + Class[] augmentors() default {}; + /** * Adds attributes to a {@link SecurityIdentity} configured by this annotation. * The attributes can be retrieved by the {@link SecurityIdentity#getAttributes()} method. From f115ab5aefccced2ab7847897e78634858136227 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 15 Nov 2024 18:34:41 +0200 Subject: [PATCH 37/39] Consider resources from @TestProfile when checking for restart Closes: #44497 (cherry picked from commit 7776de928327962e7d17aa0273916de13d3a2d3a) --- .../test/common/TestResourceManager.java | 10 +++--- .../QuarkusIntegrationTestExtension.java | 3 +- .../test/junit/QuarkusMainTestExtension.java | 3 +- .../test/junit/QuarkusTestExtension.java | 3 +- .../quarkus/test/junit/TestResourceUtil.java | 31 ++++++++++++++++--- 5 files changed, 38 insertions(+), 12 deletions(-) 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 8521d7407200a..d5f606290bebe 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 @@ -319,13 +319,15 @@ private Set uniqueTestResourceClassEntries(Path testClas * Allows Quarkus to extra basic information about which test resources a test class will require */ public static Set testResourceComparisonInfo(Class testClass, - Path testClassLocation) { + Path testClassLocation, List entriesFromProfile) { Set uniqueEntries = getUniqueTestResourceClassEntries(testClass, testClassLocation, null); - if (uniqueEntries.isEmpty()) { + if (uniqueEntries.isEmpty() && entriesFromProfile.isEmpty()) { return Collections.emptySet(); } - Set result = new HashSet<>(uniqueEntries.size()); - for (TestResourceClassEntry entry : uniqueEntries) { + Set allEntries = new HashSet<>(uniqueEntries); + allEntries.addAll(entriesFromProfile); + Set result = new HashSet<>(allEntries.size()); + for (TestResourceClassEntry entry : allEntries) { Map args = new HashMap<>(entry.args); if (entry.configAnnotation != null) { args.put("configAnnotation", entry.configAnnotation.annotationType().getName()); 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 1dd9e846a844f..84f460e27704e 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 @@ -153,7 +153,8 @@ private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContex // we reload the test resources if we changed test class and if we had or will have per-test test resources boolean reloadTestResources = false; if ((state == null && !failedBoot) || wrongProfile || (reloadTestResources = isNewTestClass - && TestResourceUtil.testResourcesRequireReload(state, extensionContext.getRequiredTestClass()))) { + && TestResourceUtil.testResourcesRequireReload(state, extensionContext.getRequiredTestClass(), + selectedProfile))) { if (wrongProfile || reloadTestResources) { if (state != null) { try { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java index 71776250160ec..107c2f9b60b81 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java @@ -67,7 +67,8 @@ private void ensurePrepared(ExtensionContext extensionContext, Class nextTestClass) { + static boolean testResourcesRequireReload(QuarkusTestExtensionState state, Class nextTestClass, + Class nextTestClassProfile) { + QuarkusTestProfile profileInstance = null; + if (nextTestClassProfile != null) { + try { + profileInstance = nextTestClassProfile.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } Set existingTestResources = existingTestResources(state); - Set nextTestResources = nextTestResources(nextTestClass); + Set nextTestResources = nextTestResources(nextTestClass, + profileInstance); return TestResourceManager.testResourcesRequireReload(existingTestResources, nextTestResources); } @@ -54,9 +64,20 @@ static Set existingTestResources return Collections.emptySet(); } - static Set nextTestResources(Class requiredTestClass) { + static Set nextTestResources(Class requiredTestClass, + QuarkusTestProfile profileInstance) { + + List entriesFromProfile = Collections.emptyList(); + if (profileInstance != null) { + entriesFromProfile = new ArrayList<>(profileInstance.testResources().size()); + for (QuarkusTestProfile.TestResourceEntry entry : profileInstance.testResources()) { + entriesFromProfile.add(new TestResourceManager.TestResourceClassEntry(entry.getClazz(), entry.getArgs(), null, + entry.isParallel(), TestResourceScope.MATCHING_RESOURCES)); + } + } + return TestResourceManager - .testResourceComparisonInfo(requiredTestClass, getTestClassesLocation(requiredTestClass)); + .testResourceComparisonInfo(requiredTestClass, getTestClassesLocation(requiredTestClass), entriesFromProfile); } /** @@ -92,7 +113,7 @@ static List copyEntriesFromProfile( T instance = (T) testResourceClassEntryConstructor.newInstance( Class.forName(testResource.getClazz().getName(), true, classLoader), testResource.getArgs(), null, testResource.isParallel(), - Enum.valueOf(testResourceScopeClass, TestResourceScope.RESTRICTED_TO_CLASS.name())); + Enum.valueOf(testResourceScopeClass, TestResourceScope.MATCHING_RESOURCES.name())); result.add(instance); } From 84186cd2b696e41c3e35fd1e688cba310020a260 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Thu, 14 Nov 2024 10:39:33 +0100 Subject: [PATCH 38/39] Enforce the platform constraints when resolving the Quarkus bootstrap Gradle resolver dependencies (cherry picked from commit 94140f2523126e118b44ff52595f3a16fbd13489) --- .../io/quarkus/gradle/tasks/QuarkusDev.java | 83 +++++++++++-------- ...ApplicationDeploymentClasspathBuilder.java | 2 + 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java index 5cc8b1319ac2f..50018c18c965e 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java @@ -551,18 +551,41 @@ protected void modifyDevModeContext(GradleDevModeLauncher.Builder builder) { private void addQuarkusDevModeDeps(GradleDevModeLauncher.Builder builder, ApplicationModel appModel) { - ResolvedDependency coreDeployment = null; - for (ResolvedDependency d : appModel.getDependencies()) { - if (d.isDeploymentCp() && d.getArtifactId().equals("quarkus-core-deployment") - && d.getGroupId().equals("io.quarkus")) { - coreDeployment = d; - break; - } + var devModeDependencyConfiguration = getProject().getConfigurations() + .findByName(ApplicationDeploymentClasspathBuilder.QUARKUS_BOOTSTRAP_RESOLVER_CONFIGURATION); + if (devModeDependencyConfiguration == null) { + final Configuration platformConfig = getProject().getConfigurations().findByName( + ToolingUtils.toPlatformConfigurationName( + ApplicationDeploymentClasspathBuilder.getFinalRuntimeConfigName(LaunchMode.DEVELOPMENT))); + getProject().getConfigurations().register( + ApplicationDeploymentClasspathBuilder.QUARKUS_BOOTSTRAP_RESOLVER_CONFIGURATION, + configuration -> { + configuration.setCanBeConsumed(false); + configuration.extendsFrom(platformConfig); + configuration.getDependencies().add(getQuarkusGradleBootstrapResolver()); + configuration.getDependencies().add(getQuarkusCoreDeployment(appModel)); + }); + devModeDependencyConfiguration = getProject().getConfigurations() + .getByName(ApplicationDeploymentClasspathBuilder.QUARKUS_BOOTSTRAP_RESOLVER_CONFIGURATION); } - if (coreDeployment == null) { - throw new GradleException("Failed to locate io.quarkus:quarkus-core-deployment on the application build classpath"); + + for (ResolvedArtifact appDep : devModeDependencyConfiguration.getResolvedConfiguration().getResolvedArtifacts()) { + ModuleVersionIdentifier artifactId = appDep.getModuleVersion().getId(); + //we only use the launcher for launching from the IDE, we need to exclude it + if (!(artifactId.getGroup().equals("io.quarkus") + && artifactId.getName().equals("quarkus-ide-launcher"))) { + if (artifactId.getGroup().equals("io.quarkus") + && artifactId.getName().equals("quarkus-class-change-agent")) { + builder.jvmArgs("-javaagent:" + appDep.getFile().getAbsolutePath()); + } else { + builder.classpathEntry(ArtifactKey.of(appDep.getModuleVersion().getId().getGroup(), appDep.getName(), + appDep.getClassifier(), appDep.getExtension()), appDep.getFile()); + } + } } + } + private Dependency getQuarkusGradleBootstrapResolver() { final String pomPropsPath = "META-INF/maven/io.quarkus/quarkus-bootstrap-gradle-resolver/pom.properties"; final InputStream devModePomPropsIs = DevModeMain.class.getClassLoader().getResourceAsStream(pomPropsPath); if (devModePomPropsIs == null) { @@ -586,38 +609,26 @@ private void addQuarkusDevModeDeps(GradleDevModeLauncher.Builder builder, Applic if (devModeVersion == null) { throw new GradleException("Classpath resource " + pomPropsPath + " is missing version"); } - Dependency gradleResolverDep = getProject().getDependencies() .create(String.format("%s:%s:%s", devModeGroupId, devModeArtifactId, devModeVersion)); - Dependency coreDeploymentDep = getProject().getDependencies() - .create(String.format("%s:%s:%s", coreDeployment.getGroupId(), coreDeployment.getArtifactId(), - coreDeployment.getVersion())); - - final Configuration devModeDependencyConfiguration = getProject().getConfigurations() - .detachedConfiguration(gradleResolverDep, coreDeploymentDep); - - final String platformConfigName = ToolingUtils.toPlatformConfigurationName( - ApplicationDeploymentClasspathBuilder.getFinalRuntimeConfigName(LaunchMode.DEVELOPMENT)); - final Configuration platformConfig = getProject().getConfigurations().findByName(platformConfigName); - if (platformConfig != null) { - // apply the platforms - devModeDependencyConfiguration.extendsFrom(platformConfig); - } + return gradleResolverDep; + } - for (ResolvedArtifact appDep : devModeDependencyConfiguration.getResolvedConfiguration().getResolvedArtifacts()) { - ModuleVersionIdentifier artifactId = appDep.getModuleVersion().getId(); - //we only use the launcher for launching from the IDE, we need to exclude it - if (!(artifactId.getGroup().equals("io.quarkus") - && artifactId.getName().equals("quarkus-ide-launcher"))) { - if (artifactId.getGroup().equals("io.quarkus") - && artifactId.getName().equals("quarkus-class-change-agent")) { - builder.jvmArgs("-javaagent:" + appDep.getFile().getAbsolutePath()); - } else { - builder.classpathEntry(ArtifactKey.of(appDep.getModuleVersion().getId().getGroup(), appDep.getName(), - appDep.getClassifier(), appDep.getExtension()), appDep.getFile()); - } + private Dependency getQuarkusCoreDeployment(ApplicationModel appModel) { + ResolvedDependency coreDeployment = null; + for (ResolvedDependency d : appModel.getDependencies()) { + if (d.isDeploymentCp() && d.getArtifactId().equals("quarkus-core-deployment") + && d.getGroupId().equals("io.quarkus")) { + coreDeployment = d; + break; } } + if (coreDeployment == null) { + throw new GradleException("Failed to locate io.quarkus:quarkus-core-deployment on the application build classpath"); + } + return getProject().getDependencies() + .create(String.format("%s:%s:%s", coreDeployment.getGroupId(), coreDeployment.getArtifactId(), + coreDeployment.getVersion())); } private void addLocalProject(ResolvedDependency project, GradleDevModeLauncher.Builder builder, Set addeDeps, diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java index 70c195738f6bd..7fc07fdf4f152 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java @@ -37,6 +37,8 @@ public class ApplicationDeploymentClasspathBuilder { + public static final String QUARKUS_BOOTSTRAP_RESOLVER_CONFIGURATION = "quarkusBootstrapResolverConfiguration"; + private static String getLaunchModeAlias(LaunchMode mode) { if (mode == LaunchMode.DEVELOPMENT) { return "Dev"; From 9ea9a9f24e0cb5a33967387d8c9c369a234a71ca Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Tue, 19 Nov 2024 12:30:31 +0000 Subject: [PATCH 39/39] Remove customizer mappings from runtime recording Config (cherry picked from commit ada7e07ff98abf80371b5f58ebf4bd8e482816a6) --- .../configuration/BuildTimeConfigurationReader.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index d5e5bf8e364c9..b160e70246a94 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -84,6 +84,7 @@ import io.smallrye.config.SecretKeys; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; import io.smallrye.config.SysPropConfigSource; import io.smallrye.config.common.AbstractConfigSource; @@ -1112,6 +1113,17 @@ private SmallRyeConfig getConfigForRuntimeRecording() { builder.getProfiles().add(""); builder.getSources().clear(); builder.getSourceProviders().clear(); + builder.withCustomizers(new SmallRyeConfigBuilderCustomizer() { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + builder.getMappingsBuilder().getMappings().clear(); + } + + @Override + public int priority() { + return Integer.MAX_VALUE; + } + }); builder.setAddDefaultSources(false) // Customizers may duplicate sources, but not much we can do about it, we need to run them .addDiscoveredCustomizers()