From 338844374a266344ecb2523f9f9917e7de7a665d Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 6 Jun 2024 11:12:04 +0300 Subject: [PATCH 01/15] Introduce support for the TLS Registry in the REST Client Closes: #41005 (cherry picked from commit 586472c248188db0a7a210f6b2ac1fd94895386f) --- .../main/asciidoc/tls-registry-reference.adoc | 2 +- .../restclient/config/RestClientConfig.java | 17 +++ ...ClientFallbackConfigSourceInterceptor.java | 1 + .../restclient/config/RestClientsConfig.java | 14 ++ .../rest-client/deployment/pom.xml | 9 ++ .../tls/MtlsConfigFromRegistryCdiTest.java | 74 +++++++++++ .../tls/TlsConfigFromPropertiesCdiTest.java | 65 +++++++++ .../tls/TlsConfigFromRegistryCdiTest.java | 66 ++++++++++ .../tls/TlsConfigFromRegistryManualTest.java | 73 +++++++++++ .../reactive/{ssl => tls}/TrustAllTest.java | 2 +- .../rest-client/runtime/pom.xml | 8 ++ .../reactive/QuarkusRestClientBuilder.java | 6 + .../runtime/QuarkusRestClientBuilderImpl.java | 7 + .../runtime/RestClientBuilderImpl.java | 56 ++++++++ .../runtime/RestClientCDIDelegateBuilder.java | 28 +++- .../resteasy/reactive/client/TlsConfig.java | 80 +++++++++++ .../client/impl/ClientBuilderImpl.java | 124 +++++++++++++----- 17 files changed, 593 insertions(+), 39 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/MtlsConfigFromRegistryCdiTest.java create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromPropertiesCdiTest.java create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryCdiTest.java create mode 100644 extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryManualTest.java rename extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/{ssl => tls}/TrustAllTest.java (97%) create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/TlsConfig.java diff --git a/docs/src/main/asciidoc/tls-registry-reference.adoc b/docs/src/main/asciidoc/tls-registry-reference.adoc index 4b2198da13ebc..3c918ee0b26a8 100644 --- a/docs/src/main/asciidoc/tls-registry-reference.adoc +++ b/docs/src/main/asciidoc/tls-registry-reference.adoc @@ -518,4 +518,4 @@ When the application starts, the TLS registry performs some checks to ensure the - the cipher suites and protocols are valid - the CRLs are valid -If any of these checks fail, the application will fail to start. \ No newline at end of file +If any of these checks fail, the application will fail to start. diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java index 71c1ba1be8d2d..9267e5bc9fe31 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java @@ -42,6 +42,7 @@ public class RestClientConfig { EMPTY.keyStorePassword = Optional.empty(); EMPTY.keyStoreType = Optional.empty(); EMPTY.hostnameVerifier = Optional.empty(); + EMPTY.tlsConfigurationName = Optional.empty(); EMPTY.connectionTTL = Optional.empty(); EMPTY.connectionPoolSize = Optional.empty(); EMPTY.keepAliveEnabled = Optional.empty(); @@ -201,6 +202,20 @@ public class RestClientConfig { @ConfigItem public Optional hostnameVerifier; + /** + * The name of the TLS configuration to use. + *

+ * If not set and the default TLS configuration is configured ({@code quarkus.tls.*}) then that will be used. + * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + *

+ * If no TLS configuration is set, then the keys-tore, trust-store, etc. properties will be used. + *

+ * This property is not applicable to the RESTEasy Client. + */ + @ConfigItem + public Optional tlsConfigurationName; + /** * The time in ms for which a connection remains unused in the connection pool before being evicted and closed. * A timeout of {@code 0} means there is no timeout. @@ -317,6 +332,7 @@ public static RestClientConfig load(String configKey) { instance.keyStorePassword = getConfigValue(configKey, "key-store-password", String.class); instance.keyStoreType = getConfigValue(configKey, "key-store-type", String.class); instance.hostnameVerifier = getConfigValue(configKey, "hostname-verifier", String.class); + instance.tlsConfigurationName = getConfigValue(configKey, "tls-configuration-name", String.class); instance.connectionTTL = getConfigValue(configKey, "connection-ttl", Integer.class); instance.connectionPoolSize = getConfigValue(configKey, "connection-pool-size", Integer.class); instance.keepAliveEnabled = getConfigValue(configKey, "keep-alive-enabled", Boolean.class); @@ -358,6 +374,7 @@ public static RestClientConfig load(Class interfaceClass) { instance.keyStorePassword = getConfigValue(interfaceClass, "key-store-password", String.class); instance.keyStoreType = getConfigValue(interfaceClass, "key-store-type", String.class); instance.hostnameVerifier = getConfigValue(interfaceClass, "hostname-verifier", String.class); + instance.tlsConfigurationName = getConfigValue(interfaceClass, "tls-configuration-name", String.class); instance.connectionTTL = getConfigValue(interfaceClass, "connection-ttl", Integer.class); instance.connectionPoolSize = getConfigValue(interfaceClass, "connection-pool-size", Integer.class); instance.keepAliveEnabled = getConfigValue(interfaceClass, "keep-alive-enabled", Boolean.class); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java index a30fe54566ebf..84507edb3cb36 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientFallbackConfigSourceInterceptor.java @@ -38,6 +38,7 @@ public class RestClientFallbackConfigSourceInterceptor extends FallbackConfigSou CLIENT_PROPERTIES.put("key-store", "keyStore"); CLIENT_PROPERTIES.put("key-store-password", "keyStorePassword"); CLIENT_PROPERTIES.put("key-store-type", "keyStoreType"); + CLIENT_PROPERTIES.put("tls-configuration-name", "tlsConfigurationName"); CLIENT_PROPERTIES.put("follow-redirects", "followRedirects"); CLIENT_PROPERTIES.put("proxy-address", "proxyAddress"); CLIENT_PROPERTIES.put("query-param-style", "queryParamStyle"); diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java index b02e27baa1aa1..ab20ac8605a6b 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java @@ -278,6 +278,20 @@ public class RestClientsConfig { @ConfigItem public Optional keyStoreType; + /** + * The name of the TLS configuration to use. + *

+ * If not set and the default TLS configuration is configured ({@code quarkus.tls.*}) then that will be used. + * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + *

+ * If no TLS configuration is set, then the keys-tore, trust-store, etc. properties will be used. + *

+ * This property is not applicable to the RESTEasy Client. + */ + @ConfigItem + public Optional tlsConfigurationName; + /** * If this is true then HTTP/2 will be enabled. */ diff --git a/extensions/resteasy-reactive/rest-client/deployment/pom.xml b/extensions/resteasy-reactive/rest-client/deployment/pom.xml index a6c55e6b09299..3751bd26d6a59 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/pom.xml +++ b/extensions/resteasy-reactive/rest-client/deployment/pom.xml @@ -32,6 +32,10 @@ io.quarkus quarkus-rest-client-config-deployment + + io.quarkus + quarkus-tls-registry-deployment + io.quarkus @@ -98,6 +102,11 @@ stork-service-discovery-static-list test + + me.escoffier.certs + certificate-generator-junit5 + test + diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/MtlsConfigFromRegistryCdiTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/MtlsConfigFromRegistryCdiTest.java new file mode 100644 index 0000000000000..1d48304c8e113 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/MtlsConfigFromRegistryCdiTest.java @@ -0,0 +1,74 @@ +package io.quarkus.rest.client.reactive.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.ext.web.RoutingContext; +import me.escoffier.certs.Format; +import me.escoffier.certs.junit5.Certificate; +import me.escoffier.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "mtls-test", password = "secret", formats = { + Format.JKS, Format.PKCS12, Format.PEM }, client = true)) +public class MtlsConfigFromRegistryCdiTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, Resource.class) + .addAsResource(new File("target/certs/mtls-test-keystore.p12"), "server-keystore.p12") + .addAsResource(new File("target/certs/mtls-test-server-truststore.p12"), "server-truststore.p12") + .addAsResource(new File("target/certs/mtls-test-client-keystore.p12"), "client-keystore.p12") + .addAsResource(new File("target/certs/mtls-test-client-truststore.p12"), "client-truststore.p12")) + + .overrideConfigKey("quarkus.tls.server.key-store.p12.path", "server-keystore.p12") + .overrideConfigKey("quarkus.tls.server.key-store.p12.password", "secret") + .overrideConfigKey("quarkus.tls.server.trust-store.p12.path", "server-truststore.p12") + .overrideConfigKey("quarkus.tls.server.trust-store.p12.password", "secret") + .overrideConfigKey("quarkus.http.tls-configuration-name", "server") + + .overrideConfigKey("quarkus.tls.rest-client.key-store.p12.path", "client-keystore.p12") + .overrideConfigKey("quarkus.tls.rest-client.key-store.p12.password", "secret") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.p12.path", "client-truststore.p12") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.p12.password", "secret") + .overrideConfigKey("quarkus.rest-client.rc.url", "https://localhost:${quarkus.http.test-ssl-port:8444}") + .overrideConfigKey("quarkus.rest-client.rc.tls-configuration-name", "rest-client"); + + @RestClient + Client client; + + @Test + void shouldHello() { + assertThat(client.echo("w0rld")).isEqualTo("hello, w0rld"); + } + + @Path("/hello") + @RegisterRestClient(configKey = "rc") + public interface Client { + @POST + String echo(String name); + } + + @Path("/hello") + public static class Resource { + @POST + public String echo(String name, @Context RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + return "hello, " + name; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromPropertiesCdiTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromPropertiesCdiTest.java new file mode 100644 index 0000000000000..8526ad416012b --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromPropertiesCdiTest.java @@ -0,0 +1,65 @@ +package io.quarkus.rest.client.reactive.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.ext.web.RoutingContext; +import me.escoffier.certs.Format; +import me.escoffier.certs.junit5.Certificate; +import me.escoffier.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "tls-test", password = "secret", formats = { + Format.JKS, Format.PKCS12, Format.PEM })) +public class TlsConfigFromPropertiesCdiTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, Resource.class) + .addAsResource(new File("target/certs/tls-test-keystore.jks"), "keystore.jks") + .addAsResource(new File("target/certs/tls-test-truststore.jks"), "truststore.jks")) + .overrideConfigKey("quarkus.tls.key-store.jks.path", "keystore.jks") + .overrideConfigKey("quarkus.tls.key-store.jks.password", "secret") + + .overrideConfigKey("quarkus.rest-client.rc.url", "https://localhost:${quarkus.http.test-ssl-port:8444}") + .overrideConfigKey("quarkus.rest-client.rc.trust-store", "classpath:truststore.jks") + .overrideConfigKey("quarkus.rest-client.rc.trust-store-password", "secret"); + + @RestClient + Client client; + + @Test + void shouldHello() { + assertThat(client.echo("w0rld")).isEqualTo("hello, w0rld"); + } + + @Path("/hello") + @RegisterRestClient(configKey = "rc") + public interface Client { + @POST + String echo(String name); + } + + @Path("/hello") + public static class Resource { + @POST + public String echo(String name, @Context RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + return "hello, " + name; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryCdiTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryCdiTest.java new file mode 100644 index 0000000000000..09239c2e628f7 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryCdiTest.java @@ -0,0 +1,66 @@ +package io.quarkus.rest.client.reactive.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.vertx.ext.web.RoutingContext; +import me.escoffier.certs.Format; +import me.escoffier.certs.junit5.Certificate; +import me.escoffier.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "tls-test", password = "secret", formats = { + Format.JKS, Format.PKCS12, Format.PEM })) +public class TlsConfigFromRegistryCdiTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, Resource.class) + .addAsResource(new File("target/certs/tls-test-keystore.jks"), "keystore.jks") + .addAsResource(new File("target/certs/tls-test-truststore.jks"), "truststore.jks")) + .overrideConfigKey("quarkus.tls.key-store.jks.path", "keystore.jks") + .overrideConfigKey("quarkus.tls.key-store.jks.password", "secret") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.jks.path", "truststore.jks") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.jks.password", "secret") + + .overrideConfigKey("quarkus.rest-client.rc.url", "https://localhost:${quarkus.http.test-ssl-port:8444}") + .overrideConfigKey("quarkus.rest-client.rc.tls-configuration-name", "rest-client"); + + @RestClient + Client client; + + @Test + void shouldHello() { + assertThat(client.echo("w0rld")).isEqualTo("hello, w0rld"); + } + + @Path("/hello") + @RegisterRestClient(configKey = "rc") + public interface Client { + @POST + String echo(String name); + } + + @Path("/hello") + public static class Resource { + @POST + public String echo(String name, @Context RoutingContext rc) { + Assertions.assertThat(rc.request().connection().isSsl()).isTrue(); + Assertions.assertThat(rc.request().isSSL()).isTrue(); + Assertions.assertThat(rc.request().connection().sslSession()).isNotNull(); + return "hello, " + name; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryManualTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryManualTest.java new file mode 100644 index 0000000000000..8f246f30ef55f --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TlsConfigFromRegistryManualTest.java @@ -0,0 +1,73 @@ +package io.quarkus.rest.client.reactive.tls; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.net.URL; +import java.util.Optional; + +import jakarta.inject.Inject; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.tls.TlsConfiguration; +import io.quarkus.tls.TlsConfigurationRegistry; +import io.vertx.ext.web.RoutingContext; +import me.escoffier.certs.Format; +import me.escoffier.certs.junit5.Certificate; +import me.escoffier.certs.junit5.Certificates; + +@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "tls-test", password = "secret", formats = { + Format.JKS, Format.PKCS12, Format.PEM })) +public class TlsConfigFromRegistryManualTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Client.class, Resource.class) + .addAsResource(new File("target/certs/tls-test-keystore.jks"), "keystore.jks") + .addAsResource(new File("target/certs/tls-test-truststore.jks"), "truststore.jks")) + .overrideConfigKey("quarkus.tls.key-store.jks.path", "keystore.jks") + .overrideConfigKey("quarkus.tls.key-store.jks.password", "secret") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.jks.path", "truststore.jks") + .overrideConfigKey("quarkus.tls.rest-client.trust-store.jks.password", "secret"); + + @TestHTTPResource(tls = true) + URL url; + + @Inject + TlsConfigurationRegistry registry; + + @Test + void shouldHello() { + Optional maybeTlsConfiguration = TlsConfiguration.from(registry, Optional.of("rest-client")); + assertThat(maybeTlsConfiguration).isPresent(); + Client client = QuarkusRestClientBuilder.newBuilder().baseUrl(url).tlsConfiguration(maybeTlsConfiguration.get()) + .build(Client.class); + assertThat(client.echo("w0rld")).isEqualTo("hello, w0rld"); + } + + @Path("/hello") + public interface Client { + @POST + String echo(String name); + } + + @Path("/hello") + public static class Resource { + @POST + public String echo(String name, @Context RoutingContext rc) { + assertThat(rc.request().connection().isSsl()).isTrue(); + assertThat(rc.request().isSSL()).isTrue(); + assertThat(rc.request().connection().sslSession()).isNotNull(); + return "hello, " + name; + } + } +} diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ssl/TrustAllTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TrustAllTest.java similarity index 97% rename from extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ssl/TrustAllTest.java rename to extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TrustAllTest.java index 8014a4b7a1f23..fa2bab6630adb 100644 --- a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/ssl/TrustAllTest.java +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/tls/TrustAllTest.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.client.reactive.ssl; +package io.quarkus.rest.client.reactive.tls; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; diff --git a/extensions/resteasy-reactive/rest-client/runtime/pom.xml b/extensions/resteasy-reactive/rest-client/runtime/pom.xml index affc60590db5a..596db0e5e613d 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-client/runtime/pom.xml @@ -26,6 +26,10 @@ io.quarkus quarkus-rest-client-config + + io.quarkus + quarkus-tls-registry + io.smallrye.stork stork-api @@ -44,6 +48,10 @@ + + io.quarkus + quarkus-tls-registry + diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java index c67adff8fa9e9..1b475b298deaa 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/QuarkusRestClientBuilder.java @@ -21,6 +21,7 @@ import io.quarkus.rest.client.reactive.runtime.QuarkusRestClientBuilderImpl; import io.quarkus.rest.client.reactive.runtime.RestClientBuilderImpl; +import io.quarkus.tls.TlsConfiguration; import io.vertx.core.http.HttpClientOptions; /** @@ -114,6 +115,11 @@ static QuarkusRestClientBuilder newBuilder() { */ QuarkusRestClientBuilder readTimeout(long timeout, TimeUnit unit); + /** + * Set the transport layer security configuration configured by Quarkus + */ + QuarkusRestClientBuilder tlsConfiguration(TlsConfiguration tlsConfiguration); + /** * Specifies the SSL context to use when creating secured transport connections to server endpoints from web targets * created by the client instance that is using this SSL context. diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java index 5a663f92641c4..884f878844e56 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/QuarkusRestClientBuilderImpl.java @@ -20,6 +20,7 @@ import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; import io.quarkus.rest.client.reactive.runtime.context.ClientHeadersFactoryContextResolver; import io.quarkus.rest.client.reactive.runtime.context.HttpClientOptionsContextResolver; +import io.quarkus.tls.TlsConfiguration; import io.vertx.core.http.HttpClientOptions; public class QuarkusRestClientBuilderImpl implements QuarkusRestClientBuilder { @@ -54,6 +55,12 @@ public QuarkusRestClientBuilder readTimeout(long timeout, TimeUnit unit) { return this; } + @Override + public QuarkusRestClientBuilder tlsConfiguration(TlsConfiguration tlsConfiguration) { + proxy.tlsConfiguration(tlsConfiguration); + return this; + } + @Override public QuarkusRestClientBuilder sslContext(SSLContext sslContext) { proxy.sslContext(sslContext); diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index f5c68a2a5d4c8..8d1dd587eb6f4 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -27,6 +28,7 @@ import org.eclipse.microprofile.rest.client.RestClientDefinitionException; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.jboss.resteasy.reactive.client.TlsConfig; import org.jboss.resteasy.reactive.client.api.ClientLogger; import org.jboss.resteasy.reactive.client.api.InvalidRestClientDefinitionException; import org.jboss.resteasy.reactive.client.api.LoggingScope; @@ -45,6 +47,10 @@ import io.quarkus.rest.client.reactive.runtime.ProxyAddressUtil.HostAndPort; import io.quarkus.restclient.config.RestClientLoggingConfig; import io.quarkus.restclient.config.RestClientsConfig; +import io.quarkus.tls.TlsConfiguration; +import io.vertx.core.net.KeyCertOptions; +import io.vertx.core.net.SSLOptions; +import io.vertx.core.net.TrustOptions; /** * Builder implementation for MicroProfile Rest Client @@ -101,6 +107,56 @@ public RestClientBuilderImpl readTimeout(long timeout, TimeUnit timeUnit) { return this; } + public RestClientBuilderImpl tlsConfiguration(TlsConfiguration tlsConfiguration) { + clientBuilder.tlsConfig(new TlsConfig() { + @Override + public KeyStore getKeyStore() { + return tlsConfiguration.getKeyStore(); + } + + @Override + public KeyCertOptions getKeyStoreOptions() { + return tlsConfiguration.getKeyStoreOptions(); + } + + @Override + public KeyStore getTrustStore() { + return tlsConfiguration.getTrustStore(); + } + + @Override + public TrustOptions getTrustStoreOptions() { + return tlsConfiguration.getTrustStoreOptions(); + } + + @Override + public SSLOptions getSSLOptions() { + return tlsConfiguration.getSSLOptions(); + } + + @Override + public SSLContext createSSLContext() throws Exception { + return tlsConfiguration.createSSLContext(); + } + + @Override + public Optional getHostnameVerificationAlgorithm() { + return tlsConfiguration.getHostnameVerificationAlgorithm(); + } + + @Override + public boolean usesSni() { + return tlsConfiguration.usesSni(); + } + + @Override + public boolean isTrustAll() { + return tlsConfiguration.isTrustAll(); + } + }); + return this; + } + @Override public RestClientBuilderImpl sslContext(SSLContext sslContext) { clientBuilder.sslContext(sslContext); diff --git a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java index fce45202ece0a..186ca14629a67 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java +++ b/extensions/resteasy-reactive/rest-client/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java @@ -26,10 +26,13 @@ import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; import org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder; +import io.quarkus.arc.Arc; import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder; import io.quarkus.restclient.config.RestClientConfig; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.runtime.configuration.MemorySize; +import io.quarkus.tls.TlsConfiguration; +import io.quarkus.tls.TlsConfigurationRegistry; public class RestClientCDIDelegateBuilder { @@ -68,7 +71,7 @@ void configureBuilder(QuarkusRestClientBuilder builder) { configureBaseUrl(builder); configureTimeouts(builder); configureProviders(builder); - configureSsl(builder); + configureTLS(builder); configureRedirects(builder); configureQueryParamStyle(builder); configureProxy(builder); @@ -216,8 +219,29 @@ private void configureShared(QuarkusRestClientBuilder builder) { } } - private void configureSsl(QuarkusRestClientBuilder builder) { + private void configureTLS(QuarkusRestClientBuilder builder) { + Optional maybeConfiguration = resolveTlsConfigurationForRegistry(); + if (maybeConfiguration.isPresent()) { + builder.tlsConfiguration(maybeConfiguration.get()); + } else { + configureTLSFromProperties(builder); + } + } + + private Optional resolveTlsConfigurationForRegistry() { + if (Arc.container() != null) { + var registry = Arc.container().select(TlsConfigurationRegistry.class).orNull(); + if (registry != null) { + Optional maybeTlsConfigurationName = oneOf(clientConfigByClassName().tlsConfigurationName, + clientConfigByConfigKey().tlsConfigurationName, + configRoot.tlsConfigurationName); + return TlsConfiguration.from(registry, maybeTlsConfigurationName); + } + } + return Optional.empty(); + } + private void configureTLSFromProperties(QuarkusRestClientBuilder builder) { Optional maybeTrustStore = oneOf(clientConfigByClassName().trustStore, clientConfigByConfigKey().trustStore, configRoot.trustStore); if (maybeTrustStore.isPresent() && !maybeTrustStore.get().isBlank() && !NONE.equals(maybeTrustStore.get())) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/TlsConfig.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/TlsConfig.java new file mode 100644 index 0000000000000..1d85e98c0996f --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/TlsConfig.java @@ -0,0 +1,80 @@ +package org.jboss.resteasy.reactive.client; + +import java.security.KeyStore; +import java.util.Optional; + +import javax.net.ssl.SSLContext; + +import io.vertx.core.net.KeyCertOptions; +import io.vertx.core.net.SSLOptions; +import io.vertx.core.net.TrustOptions; + +public interface TlsConfig { + + /** + * Returns the key store. + * + * @return the key store if configured. + */ + KeyStore getKeyStore(); + + /** + * Returns the key store options. + * + * @return the key store options if configured. + */ + KeyCertOptions getKeyStoreOptions(); + + /** + * Returns the trust store. + * + * @return the trust store if configured. + */ + KeyStore getTrustStore(); + + /** + * Returns the trust store options. + * + * @return the trust store options if configured. + */ + TrustOptions getTrustStoreOptions(); + + /** + * Returns the (Vert.x) SSL options. + * + * @return the {@link SSLOptions}, {@code null} if not configured. + */ + SSLOptions getSSLOptions(); + + /** + * Creates and returns the SSL Context. + * + * @return the {@link SSLContext}, {@code null} if not configured. + * @throws Exception if the SSL Context cannot be created. + */ + SSLContext createSSLContext() throws Exception; + + /** + * Returns the hostname verification algorithm for this configuration. + * {@code "NONE"} means no hostname verification. + * + * @return the hostname verification algorithm. + */ + Optional getHostnameVerificationAlgorithm(); + + /** + * Returns whether the key store is configured to use SNI. + * When SNI is used, the client indicate the server name during the TLS handshake, allowing the server to select the + * right certificate. + * + * @return {@code true} if the key store is configured to use SNI, {@code false} otherwise. + */ + boolean usesSni(); + + /** + * Returns whether the trust store is configured to trust all certificates. + * + * @return {@code true} if the trust store is configured to trust all certificates, {@code false} otherwise. + */ + boolean isTrustAll(); +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java index 12e4381f7088d..8bfcc874f444c 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java @@ -27,6 +27,7 @@ import jakarta.ws.rs.ext.MessageBodyWriter; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.TlsConfig; import org.jboss.resteasy.reactive.client.api.ClientLogger; import org.jboss.resteasy.reactive.client.api.LoggingScope; import org.jboss.resteasy.reactive.client.interceptors.ClientGZIPDecodingInterceptor; @@ -40,25 +41,17 @@ import io.vertx.core.http.HttpVersion; import io.vertx.core.net.JksOptions; import io.vertx.core.net.ProxyOptions; +import io.vertx.core.net.SSLOptions; public class ClientBuilderImpl extends ClientBuilder { private static final Logger log = Logger.getLogger(ClientBuilderImpl.class); private static final ClientContextResolver CLIENT_CONTEXT_RESOLVER = ClientContextResolver.getInstance(); - private static final char[] EMPTY_CHAR_ARARAY = new char[0]; + private static final char[] EMPTY_CHAR_ARRAY = new char[0]; public static final String PIPE = Pattern.quote("|"); private ConfigurationImpl configuration; - private HostnameVerifier hostnameVerifier; - private KeyStore keyStore; - private char[] keystorePassword; - private SSLContext sslContext; - private KeyStore trustStore; - private char[] trustStorePassword; - private boolean http2; - private boolean alpn; - private String proxyHost; private int proxyPort; private String proxyPassword; @@ -66,11 +59,23 @@ public class ClientBuilderImpl extends ClientBuilder { private String nonProxyHosts; private boolean followRedirects; + + private boolean http2; + private boolean alpn; + + // security settings + private KeyStore keyStore; + private char[] keystorePassword; + private KeyStore trustStore; + private char[] trustStorePassword; private boolean trustAll; private boolean verifyHost = true; + // overridden security settings + private TlsConfig tlsConfig; private LoggingScope loggingScope; private Integer loggingBodySize = 100; + private int maxChunkSize = 8096; private MultiQueryParamMode multiQueryParamMode; @@ -89,6 +94,11 @@ public ClientBuilder withConfig(Configuration config) { return this; } + public ClientBuilder tlsConfig(TlsConfig tlsConfig) { + this.tlsConfig = tlsConfig; + return this; + } + @Override public ClientBuilder sslContext(SSLContext sslContext) { // TODO @@ -215,29 +225,10 @@ public ClientImpl build() { options.setAlpnVersions(List.of(HttpVersion.HTTP_2, HttpVersion.HTTP_1_1)); } - options.setVerifyHost(verifyHost); - if (trustAll) { - options.setTrustAll(true); - options.setVerifyHost(false); - } - - char[] effectiveTrustStorePassword = trustStorePassword == null ? EMPTY_CHAR_ARARAY : trustStorePassword; - Buffer keyStore = asBuffer(this.keyStore, keystorePassword); - Buffer trustStore = asBuffer(this.trustStore, effectiveTrustStorePassword); - if (keyStore != null || trustStore != null) { - options = options.setSsl(true); - if (keyStore != null) { - JksOptions jks = new JksOptions(); - jks.setValue(keyStore); - jks.setPassword(new String(keystorePassword)); - options = options.setKeyStoreOptions(jks); - } - if (trustStore != null) { - JksOptions jks = new JksOptions(); - jks.setValue(trustStore); - jks.setPassword(new String(effectiveTrustStorePassword)); - options.setTrustStoreOptions(jks); - } + if (tlsConfig != null) { + populateSecurityOptionsFromTlsConfig(options); + } else { + populateSecurityOptionsFromExplicitTlsProperties(options); } if (proxyHost != null) { @@ -297,8 +288,8 @@ public ClientImpl build() { return new ClientImpl(options, configuration, CLIENT_CONTEXT_RESOLVER.resolve(Thread.currentThread().getContextClassLoader()), - hostnameVerifier, - sslContext, + null, + null, followRedirects, multiQueryParamMode, loggingScope, @@ -306,6 +297,69 @@ public ClientImpl build() { } + private void populateSecurityOptionsFromTlsConfig(HttpClientOptions options) { + options.setSsl(true); + + if (tlsConfig.getTrustStoreOptions() != null) { + options.setTrustOptions(tlsConfig.getTrustStoreOptions()); + } + + // For mTLS: + if (tlsConfig.getKeyStoreOptions() != null) { + options.setKeyCertOptions(tlsConfig.getKeyStoreOptions()); + } + + if (tlsConfig.isTrustAll()) { + options.setTrustAll(true); + } + if (tlsConfig.getHostnameVerificationAlgorithm().isPresent() + && tlsConfig.getHostnameVerificationAlgorithm().get().equals("NONE")) { + // Only disable hostname verification if the algorithm is explicitly set to NONE + options.setVerifyHost(false); + } + + SSLOptions sslOptions = tlsConfig.getSSLOptions(); + if (sslOptions != null) { + options.setSslHandshakeTimeout(sslOptions.getSslHandshakeTimeout()); + options.setSslHandshakeTimeoutUnit(sslOptions.getSslHandshakeTimeoutUnit()); + for (String suite : sslOptions.getEnabledCipherSuites()) { + options.addEnabledCipherSuite(suite); + } + for (Buffer buffer : sslOptions.getCrlValues()) { + options.addCrlValue(buffer); + } + options.setEnabledSecureTransportProtocols(sslOptions.getEnabledSecureTransportProtocols()); + options.setUseAlpn(sslOptions.isUseAlpn()); + } + } + + private void populateSecurityOptionsFromExplicitTlsProperties(HttpClientOptions options) { + options.setVerifyHost(verifyHost); + if (trustAll) { + options.setTrustAll(true); + options.setVerifyHost(false); + } + + char[] effectiveTrustStorePassword = trustStorePassword == null ? EMPTY_CHAR_ARRAY : trustStorePassword; + Buffer keyStore = asBuffer(this.keyStore, keystorePassword); + Buffer trustStore = asBuffer(this.trustStore, effectiveTrustStorePassword); + if (keyStore != null || trustStore != null) { + options.setSsl(true); + if (keyStore != null) { + JksOptions jks = new JksOptions(); + jks.setValue(keyStore); + jks.setPassword(new String(keystorePassword)); + options.setKeyStoreOptions(jks); + } + if (trustStore != null) { + JksOptions jks = new JksOptions(); + jks.setValue(trustStore); + jks.setPassword(new String(effectiveTrustStorePassword)); + options.setTrustStoreOptions(jks); + } + } + } + private void configureNonProxyHosts(HttpClientOptions options, String nonProxyHosts) { if (nonProxyHosts != null) { for (String host : nonProxyHosts.split(PIPE)) { From 21d6d7c19409f08ec1030469ec6c2d56b0be2d3b Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 14 Jun 2024 17:01:47 +0200 Subject: [PATCH 02/15] Avoid NumberFormatException for Java 1.8 I had to update a very old Quarkus project that was having 1.8 as a maven.compiler.target and this led to an error that is less than ideal. This will make you go a bit further and get a proper message saying Java 1.8 is not supported. (cherry picked from commit c41dd13c9921deacbe54f77d156b23c841fd4e0a) --- .../main/java/io/quarkus/devtools/project/JavaVersion.java | 6 +++++- .../java/io/quarkus/devtools/project/JavaVersionTest.java | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java index cd3ba46ea4453..d443bc65b04ec 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/JavaVersion.java @@ -19,7 +19,11 @@ private JavaVersion() { } public JavaVersion(String version) { - this.version = version; + if (version != null && version.startsWith("1.")) { + this.version = version.substring(2); + } else { + this.version = version; + } } public boolean isEmpty() { diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java index 0b982913ff880..329801653dac8 100644 --- a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/JavaVersionTest.java @@ -14,6 +14,12 @@ class JavaVersionTest { + @Test + public void givenJavaVersion8ShouldReturn8() { + assertEquals(8, new JavaVersion("8").getAsInt()); + assertEquals(8, new JavaVersion("1.8").getAsInt()); + } + @Test public void givenJavaVersion17ShouldReturn17() { assertEquals("17", computeJavaVersion(JAVA, "17")); From 19cd763fd75cf6b3960b04642040cdcfb2c676ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 21:33:40 +0000 Subject: [PATCH 03/15] Bump io.smallrye.beanbag:smallrye-beanbag-bom from 1.5.0 to 1.5.1 Bumps [io.smallrye.beanbag:smallrye-beanbag-bom](https://github.com/smallrye/smallrye-beanbag) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/smallrye/smallrye-beanbag/releases) - [Commits](https://github.com/smallrye/smallrye-beanbag/compare/1.5.0...1.5.1) --- updated-dependencies: - dependency-name: io.smallrye.beanbag:smallrye-beanbag-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] (cherry picked from commit 7eb9223f581cec8c759bce8d814ac7f849f532da) --- independent-projects/extension-maven-plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/independent-projects/extension-maven-plugin/pom.xml b/independent-projects/extension-maven-plugin/pom.xml index d45da6dfe91fe..978c9e4eb27bc 100644 --- a/independent-projects/extension-maven-plugin/pom.xml +++ b/independent-projects/extension-maven-plugin/pom.xml @@ -39,7 +39,7 @@ 11 3.9.7 2.17.1 - 1.5.0 + 1.5.1 5.10.2 From e28e5262a5635b30e91903042d5938c58020962a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 21:39:07 +0000 Subject: [PATCH 04/15] Bump io.smallrye.beanbag:smallrye-beanbag-maven from 1.5.0 to 1.5.1 Bumps [io.smallrye.beanbag:smallrye-beanbag-maven](https://github.com/smallrye/smallrye-beanbag) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/smallrye/smallrye-beanbag/releases) - [Commits](https://github.com/smallrye/smallrye-beanbag/compare/1.5.0...1.5.1) --- updated-dependencies: - dependency-name: io.smallrye.beanbag:smallrye-beanbag-maven dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] (cherry picked from commit 93541f1e64a4a41436791e52503e956bb01d4777) --- independent-projects/bootstrap/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index ae20107f0ddb6..969fe757279a5 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -72,7 +72,7 @@ 2.0 3.5.1 2.3.0 - 1.5.0 + 1.5.1 8.8 0.0.10 0.1.3 From 73bf0e6c1c5cb86bd2f8f97669c84825bd84becb Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Sun, 16 Jun 2024 13:37:21 +0200 Subject: [PATCH 05/15] Do not add quarkus-tls-registry twice in quarkus-rest-client (cherry picked from commit 12249cdd6b512ed0ff2924b5fd6412bcfbc1e968) --- extensions/resteasy-reactive/rest-client/runtime/pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/extensions/resteasy-reactive/rest-client/runtime/pom.xml b/extensions/resteasy-reactive/rest-client/runtime/pom.xml index 596db0e5e613d..73cb9951b3adf 100644 --- a/extensions/resteasy-reactive/rest-client/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-client/runtime/pom.xml @@ -48,10 +48,6 @@ - - io.quarkus - quarkus-tls-registry - From 03b5df2da74260911f315d4dc0575c9b79bd3ba1 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 17 Jun 2024 10:04:04 +0200 Subject: [PATCH 06/15] Scheduler: add custom thread factory to use a specific thread name - also fix the CronTrigger "fired" log TRACE message (cherry picked from commit e3fa7f189562303e725a6f14b15fd2d1ed2b3a70) --- .../scheduler/runtime/SimpleScheduler.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index 75cfca5b6010c..ce6d7482341da 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -16,7 +16,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import jakarta.annotation.PreDestroy; @@ -135,8 +137,26 @@ public SimpleScheduler(SchedulerContext context, SchedulerRuntimeConfig schedule return; } + ThreadFactory tf = new ThreadFactory() { + + private final AtomicInteger threadNumber = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable runnable) { + Thread t = new Thread(Thread.currentThread().getThreadGroup(), runnable, + "quarkus-scheduler-trigger-check-" + threadNumber.getAndIncrement(), + 0); + if (t.isDaemon()) { + t.setDaemon(false); + } + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + }; // This executor is used to check all registered triggers every second - this.scheduledExecutor = new JBossScheduledThreadPoolExecutor(1, new Runnable() { + this.scheduledExecutor = new JBossScheduledThreadPoolExecutor(1, tf, new Runnable() { @Override public void run() { // noop @@ -589,7 +609,7 @@ ZonedDateTime evaluate(ZonedDateTime now) { if (lastExecution.isPresent()) { ZonedDateTime lastTruncated = lastExecution.get().truncatedTo(ChronoUnit.SECONDS); if (zonedNow.isAfter(lastTruncated) && lastFireTime.isBefore(lastTruncated)) { - LOG.tracef("%s fired, last=", this, lastTruncated); + LOG.tracef("%s fired, last=%s", this, lastTruncated); lastFireTime = zonedNow; return lastTruncated; } From 921d160546b64d19bbf0ffc57fe4836d4e888e44 Mon Sep 17 00:00:00 2001 From: Andy Damevin Date: Fri, 14 Jun 2024 12:20:07 +0200 Subject: [PATCH 07/15] Introduce version check in base codestarts and use them (cherry picked from commit 4ff600ccef96fea3f6c3f1a86535d9ea118adb96) --- .../buildtool/maven/base/pom.tpl.qute.xml | 2 +- independent-projects/tools/codestarts/pom.xml | 4 ++++ .../core/reader/QuteCodestartFileReader.java | 18 +++++++++++++++ .../reader/QuteCodestartFileReaderTest.java | 23 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 independent-projects/tools/codestarts/src/test/java/io/quarkus/devtools/codestarts/core/reader/QuteCodestartFileReaderTest.java diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/base/pom.tpl.qute.xml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/base/pom.tpl.qute.xml index 511b172afc41b..3d32afccb8efd 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/base/pom.tpl.qute.xml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/buildtool/maven/base/pom.tpl.qute.xml @@ -140,7 +140,7 @@ build generate-code generate-code-tests - {#if generate-native} + {#if generate-native and quarkus.platform.version.compareVersionTo("3.12") >= 0} native-image-agent {/if} diff --git a/independent-projects/tools/codestarts/pom.xml b/independent-projects/tools/codestarts/pom.xml index e93c8abfb7583..c6a726533f1b7 100644 --- a/independent-projects/tools/codestarts/pom.xml +++ b/independent-projects/tools/codestarts/pom.xml @@ -18,6 +18,10 @@ io.quarkus.qute qute-core + + org.apache.maven + maven-artifact + io.quarkus quarkus-bootstrap-app-model diff --git a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/reader/QuteCodestartFileReader.java b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/reader/QuteCodestartFileReader.java index d26b1ec9774c9..b2d6f606cb610 100644 --- a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/reader/QuteCodestartFileReader.java +++ b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/reader/QuteCodestartFileReader.java @@ -6,8 +6,10 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletionStage; +import java.util.regex.Pattern; import org.apache.commons.io.FilenameUtils; +import org.apache.maven.artifact.versioning.ComparableVersion; import io.quarkus.devtools.codestarts.CodestartException; import io.quarkus.devtools.codestarts.CodestartResource; @@ -30,6 +32,7 @@ final class QuteCodestartFileReader implements CodestartFileReader { private static final String ENTRY_QUTE_FLAG = ".entry.qute"; public static final String INCLUDE_QUTE_FLAG = ".include.qute"; public static final String SKIP_TAG = ""; + private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)(\\.(\\d+))?(\\.(\\d+))?"); @Override public boolean matches(String fileName) { @@ -187,9 +190,24 @@ public CompletionStage resolve(EvalContext context) { return CompletedStage.of(value.endsWith((String) e)); }); } + + case "compareVersionTo": + if (context.getParams().size() == 1) { + return context.evaluate(context.getParams().get(0)).thenCompose(e -> { + return CompletedStage.of(compareVersionTo(value, (String) e)); + }); + } default: return Results.notFound(context); } } } + + static int compareVersionTo(String currentVersionString, String comparedVersionString) { + if (!VERSION_PATTERN.matcher(comparedVersionString).matches()) { + throw new IllegalArgumentException("Let's not put template condition on qualifier: " + comparedVersionString); + } + return new ComparableVersion(currentVersionString).compareTo(new ComparableVersion(comparedVersionString)); + } + } diff --git a/independent-projects/tools/codestarts/src/test/java/io/quarkus/devtools/codestarts/core/reader/QuteCodestartFileReaderTest.java b/independent-projects/tools/codestarts/src/test/java/io/quarkus/devtools/codestarts/core/reader/QuteCodestartFileReaderTest.java new file mode 100644 index 0000000000000..732a943af22b3 --- /dev/null +++ b/independent-projects/tools/codestarts/src/test/java/io/quarkus/devtools/codestarts/core/reader/QuteCodestartFileReaderTest.java @@ -0,0 +1,23 @@ +package io.quarkus.devtools.codestarts.core.reader; + +import static io.quarkus.devtools.codestarts.core.reader.QuteCodestartFileReader.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class QuteCodestartFileReaderTest { + + @Test + void testCompareVersion() { + assertThat(compareVersionTo("3.12.0.Final", "1.0")).isGreaterThan(0); + assertThat(compareVersionTo("3.12.0.Final", "3.12")).isEqualTo(0); + assertThat(compareVersionTo("3.13.0", "3.12")).isGreaterThan(0); + assertThat(compareVersionTo("3.2.1", "3.12")).isLessThan(0); + assertThat(compareVersionTo("999-SNAPSHOT", "3.12")).isGreaterThan(0); + assertThat(compareVersionTo("1.0.0.Final-redhat-00001", "1.0")).isGreaterThan(0); + assertThat(compareVersionTo("1.0.1.Final-redhat-00001", "1.0")).isGreaterThan(0); + assertThatThrownBy(() -> compareVersionTo("999-SNAP", "1.0.1.Final-redhat-00001")) + .isInstanceOf(IllegalArgumentException.class); + } +} From 7c09a87f1b1559186cf6314947751111f1be9056 Mon Sep 17 00:00:00 2001 From: Auri Munoz Date: Fri, 14 Jun 2024 11:50:18 +0200 Subject: [PATCH 08/15] Include several fixes related to #40344 (cherry picked from commit c62ce05bc8dabf454b67e1003a748837074898bd) --- bom/application/pom.xml | 2 +- .../RepositoryMethodsImplementor.java | 52 +-- .../RepositoryPropertiesProvider.java | 3 - .../rest/deployment/ResourceImplementor.java | 5 +- .../ResourceMethodsImplementor.java | 6 +- .../deployment/SpringDataRestProcessor.java | 1 - .../data/rest/JpaRecordsRepository.java | 8 + .../spring/data/rest/JpaResourceTest.java | 438 ++++++++++++++++++ .../paged/DefaultPagedResourceBisTest.java | 73 --- .../quarkus/it/spring/data/rest/Article.java | 73 +++ .../data/rest/ArticleJpaRepository.java | 6 + .../quarkus/it/spring/data/rest/Library.java | 60 +++ .../src/main/resources/import.sql | 7 + .../spring/data/rest/SpringDataRestTest.java | 48 ++ 14 files changed, 661 insertions(+), 121 deletions(-) create mode 100644 extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaRecordsRepository.java create mode 100644 extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaResourceTest.java delete mode 100644 extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/paged/DefaultPagedResourceBisTest.java create mode 100644 integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Article.java create mode 100644 integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/ArticleJpaRepository.java create mode 100644 integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Library.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 52a87a02356fc..d6809b2101257 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -172,7 +172,7 @@ 0.3.0 4.14.0 6.1.SP2 - 3.2.SP1 + 3.2.SP2 6.2 3.2 5.12.0 diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryMethodsImplementor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryMethodsImplementor.java index 099b21998494b..22b936f891448 100644 --- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryMethodsImplementor.java +++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryMethodsImplementor.java @@ -53,10 +53,6 @@ public class RepositoryMethodsImplementor implements ResourceMethodsImplementor public static final MethodDescriptor LIST_PAGED = ofMethod(PagingAndSortingRepository.class, "findAll", org.springframework.data.domain.Page.class, Pageable.class); - //ListPagingAndSortingRepository - public static final MethodDescriptor LIST_SORTED = ofMethod(ListPagingAndSortingRepository.class, "findAll", - List.class, org.springframework.data.domain.Sort.class); - private static final Class PANACHE_PAGE = io.quarkus.panache.common.Page.class; private static final Class PANACHE_SORT = io.quarkus.panache.common.Sort.class; @@ -83,7 +79,7 @@ public RepositoryMethodsImplementor(IndexView index, EntityClassHelper entityCla } // CrudRepository Iterable findAll(); - public void implementListIterable(ClassCreator classCreator, String repositoryInterfaceName) { + public void implementIterable(ClassCreator classCreator, String repositoryInterfaceName) { if (entityClassHelper.isCrudRepository(repositoryInterfaceName) && !entityClassHelper.isPagingAndSortingRepository(repositoryInterfaceName)) { MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, Sort.class, @@ -91,26 +87,29 @@ public void implementListIterable(ClassCreator classCreator, String repositoryIn ResultHandle repository = getRepositoryInstance(methodCreator, repositoryInterfaceName); ResultHandle result = methodCreator.invokeInterfaceMethod(LIST_ITERABLE, repository); methodCreator.returnValue(result); - LOGGER.infof("Method code: %s ", methodCreator.getMethodDescriptor().toString()); + LOGGER.debugf("Method code: %s ", methodCreator.getMethodDescriptor().toString()); methodCreator.close(); } } //ListCrudRepository List findAll(); public void implementList(ClassCreator classCreator, String repositoryInterfaceName) { - if (entityClassHelper.isListCrudRepository(repositoryInterfaceName)) { + if (entityClassHelper.isListCrudRepository(repositoryInterfaceName) + && !entityClassHelper.isListPagingAndSortingRepository(repositoryInterfaceName)) { MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, Sort.class, String.class, Map.class); ResultHandle repository = getRepositoryInstance(methodCreator, repositoryInterfaceName); ResultHandle result = methodCreator.invokeInterfaceMethod(LIST, repository); methodCreator.returnValue(result); - LOGGER.infof("Method code: %s ", methodCreator.toString()); + LOGGER.debugf("Method code: %s ", methodCreator.toString()); methodCreator.close(); } } // PagingAndSortingRepository Page findAll(Pageable pageable); - public void implementListPaged(ClassCreator classCreator, String repositoryInterfaceName) { + // PagingAndSortingRepository Iterable findAll(Pageable pageable); + // ListPagingAndSortingRepository List findAll(Sort sort); + public void implementPagedList(ClassCreator classCreator, String repositoryInterfaceName) { if (entityClassHelper.isPagingAndSortingRepository(repositoryInterfaceName)) { MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, io.quarkus.panache.common.Sort.class, String.class, Map.class); @@ -124,26 +123,7 @@ public void implementListPaged(ClassCreator classCreator, String repositoryInter ofMethod(org.springframework.data.domain.Page.class, "getContent", List.class), resultPage); methodCreator.returnValue(result); - LOGGER.infof("Method code: %s ", methodCreator.toString()); - methodCreator.close(); - } - } - - //ListPagingAndSortingRepository List findAll(Sort sort); - public void implementListSort(ClassCreator classCreator, String repositoryInterfaceName) { - if (entityClassHelper.isListPagingAndSortingRepository(repositoryInterfaceName)) { - MethodCreator methodCreator = classCreator.getMethodCreator("list", List.class, Page.class, - io.quarkus.panache.common.Sort.class, String.class, Map.class); - ResultHandle page = methodCreator.getMethodParam(0); - ResultHandle sort = methodCreator.getMethodParam(1); - ResultHandle pageable = toPageable(methodCreator, page, sort); - ResultHandle repository = getRepositoryInstance(methodCreator, repositoryInterfaceName); - ResultHandle resultPage = methodCreator.invokeInterfaceMethod(LIST_SORTED, repository, pageable); - ResultHandle result = methodCreator.invokeInterfaceMethod( - ofMethod(org.springframework.data.domain.Page.class, "getContent", List.class), resultPage); - - methodCreator.returnValue(result); - LOGGER.infof("Method code: %s ", methodCreator.toString()); + LOGGER.debugf("Method code: %s ", methodCreator.toString()); methodCreator.close(); } } @@ -163,7 +143,7 @@ public void implementListPageCount(ClassCreator classCreator, String repositoryI } else { methodCreator.throwException(RuntimeException.class, "Method not implemented"); } - LOGGER.infof("Method code: %s ", methodCreator.toString()); + LOGGER.debugf("Method code: %s ", methodCreator.toString()); methodCreator.close(); } @@ -175,7 +155,7 @@ public void implementListById(ClassCreator classCreator, String repositoryInterf ResultHandle repository = getRepositoryInstance(methodCreator, repositoryInterfaceName); ResultHandle result = methodCreator.invokeInterfaceMethod(LIST_BY_ID, repository, ids); methodCreator.returnValue(result); - LOGGER.infof("Method code: %s ", methodCreator.toString()); + LOGGER.debugf("Method code: %s ", methodCreator.toString()); methodCreator.close(); } } @@ -191,7 +171,7 @@ public void implementGet(ClassCreator classCreator, String repositoryInterfaceNa } else { methodCreator.throwException(RuntimeException.class, "Method not implemented"); } - LOGGER.infof("Method code: %s ", methodCreator.toString()); + LOGGER.debugf("Method code: %s ", methodCreator.toString()); methodCreator.close(); } @@ -208,7 +188,7 @@ public void implementAdd(ClassCreator classCreator, String repositoryInterfaceNa methodCreator.throwException(RuntimeException.class, "Method not implemented"); } - LOGGER.infof("Method code: %s ", methodCreator.toString()); + LOGGER.debugf("Method code: %s ", methodCreator.toString()); methodCreator.close(); } @@ -223,7 +203,7 @@ public void implementAddList(ClassCreator classCreator, String repositoryInterfa } else { methodCreator.throwException(RuntimeException.class, "Method not implemented"); } - LOGGER.infof("Method code: %s ", methodCreator.toString()); + LOGGER.debugf("Method code: %s ", methodCreator.toString()); methodCreator.close(); } @@ -240,7 +220,7 @@ public void implementUpdate(ClassCreator classCreator, String repositoryInterfac } else { methodCreator.throwException(RuntimeException.class, "Method not implemented"); } - LOGGER.infof("Method code: %s ", methodCreator.toString()); + LOGGER.debugf("Method code: %s ", methodCreator.toString()); methodCreator.close(); } @@ -261,7 +241,7 @@ public void implementDelete(ClassCreator classCreator, String repositoryInterfac } else { methodCreator.throwException(RuntimeException.class, "Method not implemented"); } - LOGGER.infof("Method code: %s ", methodCreator.toString()); + LOGGER.debugf("Method code: %s ", methodCreator.toString()); methodCreator.close(); } diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryPropertiesProvider.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryPropertiesProvider.java index 68e630c90ac4c..4bae869b393b6 100644 --- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryPropertiesProvider.java +++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/RepositoryPropertiesProvider.java @@ -6,7 +6,6 @@ import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.LIST; import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.LIST_ITERABLE; import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.LIST_PAGED; -import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.LIST_SORTED; import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.SAVE_LIST; import static io.quarkus.spring.data.rest.deployment.RepositoryMethodsImplementor.UPDATE; @@ -34,8 +33,6 @@ protected Map> getMethodPredicates() { methodPredicates.put("listPaged", methodInfo -> methodInfo.name().equals(LIST_PAGED.getName()) && methodInfo.parametersCount() == 1 && methodInfo.parameterType(0).name().equals(PAGEABLE)); - methodPredicates.put("listSorted", - methodInfo -> methodInfo.name().equals(LIST_SORTED.getName()) && methodInfo.parameterTypes().isEmpty()); methodPredicates.put("addAll", methodInfo -> methodInfo.name().equals(SAVE_LIST.getName())); methodPredicates.put("get", methodInfo -> methodInfo.name().equals(GET.getName())); methodPredicates.put("add", methodInfo -> methodInfo.name().equals(ADD.getName())); diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceImplementor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceImplementor.java index 4f0f503378822..1679853f15c81 100644 --- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceImplementor.java +++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceImplementor.java @@ -33,10 +33,9 @@ public String implement(ClassOutput classOutput, String resourceType, String ent .build(); classCreator.addAnnotation(ApplicationScoped.class); - methodsImplementor.implementListIterable(classCreator, resourceType); + methodsImplementor.implementIterable(classCreator, resourceType); methodsImplementor.implementList(classCreator, resourceType); - methodsImplementor.implementListSort(classCreator, resourceType); - methodsImplementor.implementListPaged(classCreator, resourceType); + methodsImplementor.implementPagedList(classCreator, resourceType); methodsImplementor.implementAddList(classCreator, resourceType); methodsImplementor.implementListById(classCreator, resourceType); methodsImplementor.implementListPageCount(classCreator, resourceType); diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceMethodsImplementor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceMethodsImplementor.java index 8024240813660..8edd59aa96541 100644 --- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceMethodsImplementor.java +++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/ResourceMethodsImplementor.java @@ -15,16 +15,14 @@ public interface ResourceMethodsImplementor { void implementList(ClassCreator classCreator, String repositoryInterface); - void implementListIterable(ClassCreator classCreator, String repositoryInterface); + void implementIterable(ClassCreator classCreator, String repositoryInterface); - void implementListPaged(ClassCreator classCreator, String repositoryInterface); + void implementPagedList(ClassCreator classCreator, String repositoryInterface); void implementListPageCount(ClassCreator classCreator, String repositoryInterface); void implementListById(ClassCreator classCreator, String repositoryInterface); - public void implementListSort(ClassCreator classCreator, String repositoryInterface); - void implementGet(ClassCreator classCreator, String repositoryInterface); void implementAdd(ClassCreator classCreator, String repositoryInterface); diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java index 8c6122950b36b..32b7b50b0371e 100644 --- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java +++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java @@ -111,7 +111,6 @@ private void implementResources(Capabilities capabilities, BuildProducer unremovableBeansProducer, ResourceMethodsImplementor methodsImplementor, IndexView index, - // ResourcePropertiesProvider propertiesProvider, List repositoriesToImplement) { ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(implementationsProducer); ResourceImplementor resourceImplementor = new ResourceImplementor(methodsImplementor); diff --git a/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaRecordsRepository.java b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaRecordsRepository.java new file mode 100644 index 0000000000000..571df125d78d7 --- /dev/null +++ b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaRecordsRepository.java @@ -0,0 +1,8 @@ +package io.quarkus.spring.data.rest; + +import org.springframework.data.jpa.repository.JpaRepository; + +import io.quarkus.spring.data.rest.paged.Record; + +public interface JpaRecordsRepository extends JpaRepository { +} diff --git a/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaResourceTest.java b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaResourceTest.java new file mode 100644 index 0000000000000..c5caf26481a44 --- /dev/null +++ b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/JpaResourceTest.java @@ -0,0 +1,438 @@ +package io.quarkus.spring.data.rest; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.is; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import jakarta.ws.rs.core.Link; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.spring.data.rest.paged.Record; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.http.Header; +import io.restassured.http.Headers; +import io.restassured.path.json.JsonPath; +import io.restassured.response.Response; + +class JpaResourceTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(AbstractEntity.class, Record.class, JpaRecordsRepository.class) + .addAsResource("application.properties") + .addAsResource("import.sql")); + + @Test + void shouldGet() { + given().accept("application/json") + .when().get("/jpa-records/1") + .then().statusCode(200) + .and().body("id", is(equalTo(1))) + .and().body("name", is(equalTo("first"))); + } + + @Test + void shouldNotGetNonExistent() { + given().accept("application/json") + .when().get("/jpa-records/1000") + .then().statusCode(404); + } + + @Test + void shouldGetHal() { + given().accept("application/hal+json") + .when().get("/jpa-records/1") + .then().statusCode(200) + .and().body("id", is(equalTo(1))) + .and().body("name", is(equalTo("first"))) + .and().body("_links.add.href", endsWith("/jpa-records")) + .and().body("_links.list.href", endsWith("/jpa-records")) + .and().body("_links.self.href", endsWith("/jpa-records/1")) + .and().body("_links.update.href", endsWith("/jpa-records/1")) + .and().body("_links.remove.href", endsWith("/jpa-records/1")); + } + + @Test + void shouldNotGetNonExistentHal() { + given().accept("application/hal+json") + .when().get("/jpa-records/1000") + .then().statusCode(404); + } + + @Test + void shouldList() { + Response response = given().accept("application/json") + .when().get("/jpa-records") + .thenReturn(); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body().jsonPath().getList("id")).contains(1, 2); + assertThat(response.body().jsonPath().getList("name")).contains("first", "second"); + + Map expectedLinks = new HashMap<>(2); + expectedLinks.put("first", "/jpa-records?page=0&size=20"); + expectedLinks.put("last", "/jpa-records?page=0&size=20"); + assertLinks(response.headers(), expectedLinks); + } + + @Test + void shouldListHal() { + given().accept("application/hal+json") + .when().get("/jpa-records") + .then().statusCode(200).log().all() + .and().body("_embedded.jpa-records.id", hasItems(1, 2)) + .and().body("_embedded.jpa-records.name", hasItems("first", "second")) + .and() + .body("_embedded.jpa-records._links.add.href", + hasItems(endsWith("/jpa-records"), endsWith("/jpa-records"))) + .and() + .body("_embedded.jpa-records._links.list.href", + hasItems(endsWith("/jpa-records"), endsWith("/jpa-records"))) + .and() + .body("_embedded.jpa-records._links.self.href", + hasItems(endsWith("/jpa-records/1"), endsWith("/jpa-records/2"))) + .and() + .body("_embedded.jpa-records._links.update.href", + hasItems(endsWith("/jpa-records/1"), endsWith("/jpa-records/2"))) + .and() + .body("_embedded.jpa-records._links.remove.href", + hasItems(endsWith("/jpa-records/1"), endsWith("/jpa-records/2"))) + .and().body("_links.add.href", endsWith("/jpa-records")) + .and().body("_links.list.href", endsWith("/jpa-records")) + .and().body("_links.first.href", endsWith("/jpa-records?page=0&size=20")) + .and().body("_links.last.href", endsWith("/jpa-records?page=0&size=20")); + } + + @Test + void shouldListFirstPage() { + Response initResponse = given().accept("application/json") + .when().get("/jpa-records") + .thenReturn(); + List ids = initResponse.body().jsonPath().getList("id"); + List names = initResponse.body().jsonPath().getList("name"); + int lastPage = ids.size() - 1; + + Response response = given().accept("application/json") + .and().queryParam("page", 0) + .and().queryParam("size", 1) + .when().get("/jpa-records") + .thenReturn(); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body().jsonPath().getList("id")).containsOnly(ids.get(0)); + assertThat(response.body().jsonPath().getList("name")).containsOnly(names.get(0)); + + Map expectedLinks = new HashMap<>(3); + expectedLinks.put("first", "/jpa-records?page=0&size=1"); + expectedLinks.put("last", "/jpa-records?page=" + lastPage + "&size=1"); + expectedLinks.put("next", "/jpa-records?page=1&size=1"); + assertLinks(response.headers(), expectedLinks); + } + + @Test + void shouldListFirstPageHal() { + Response initResponse = given().accept("application/json") + .when().get("/jpa-records") + .thenReturn(); + List ids = initResponse.body().jsonPath().getList("id"); + List names = initResponse.body().jsonPath().getList("name"); + int lastPage = ids.size() - 1; + + given().accept("application/hal+json") + .and().queryParam("page", 0) + .and().queryParam("size", 1) + .when().get("/jpa-records") + .then().statusCode(200) + .and().body("_embedded.jpa-records.id", contains(ids.get(0))) + .and().body("_embedded.jpa-records.name", contains(names.get(0))) + .and() + .body("_embedded.jpa-records._links.add.href", + hasItems(endsWith("/jpa-records"), endsWith("/jpa-records"))) + .and() + .body("_embedded.jpa-records._links.list.href", + hasItems(endsWith("/jpa-records"), endsWith("/jpa-records"))) + .and() + .body("_embedded.jpa-records._links.self.href", + contains(endsWith("/jpa-records/" + ids.get(0)))) + .and() + .body("_embedded.jpa-records._links.update.href", + contains(endsWith("/jpa-records/" + ids.get(0)))) + .and() + .body("_embedded.jpa-records._links.remove.href", + contains(endsWith("/jpa-records/" + ids.get(0)))) + .and().body("_links.add.href", endsWith("/jpa-records")) + .and().body("_links.list.href", endsWith("/jpa-records")) + .and().body("_links.first.href", endsWith("/jpa-records?page=0&size=1")) + .and().body("_links.last.href", endsWith("/jpa-records?page=" + lastPage + "&size=1")) + .and().body("_links.next.href", endsWith("/jpa-records?page=1&size=1")); + } + + @Test + void shouldListLastPage() { + Response initResponse = given().accept("application/json") + .when().get("/jpa-records") + .thenReturn(); + List ids = initResponse.body().jsonPath().getList("id"); + List names = initResponse.body().jsonPath().getList("name"); + int lastPage = ids.size() - 1; + + Response response = given().accept("application/json") + .and().queryParam("page", lastPage) + .and().queryParam("size", 1) + .when().get("/jpa-records") + .thenReturn(); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body().jsonPath().getList("id")).containsOnly(ids.get(lastPage)); + assertThat(response.body().jsonPath().getList("name")).containsOnly(names.get(lastPage)); + + Map expectedLinks = new HashMap<>(3); + expectedLinks.put("first", "/jpa-records?page=0&size=1"); + expectedLinks.put("last", "/jpa-records?page=" + lastPage + "&size=1"); + expectedLinks.put("previous", "/jpa-records?page=" + (lastPage - 1) + "&size=1"); + assertLinks(response.headers(), expectedLinks); + } + + @Test + void shouldListLastPageHal() { + Response initResponse = given().accept("application/json") + .when().get("/jpa-records") + .thenReturn(); + List ids = initResponse.body().jsonPath().getList("id"); + List names = initResponse.body().jsonPath().getList("name"); + int lastPage = ids.size() - 1; + + given().accept("application/hal+json") + .and().queryParam("page", lastPage) + .and().queryParam("size", 1) + .when().get("/jpa-records") + .then().statusCode(200) + .and().body("_embedded.jpa-records.id", contains(ids.get(lastPage))) + .and().body("_embedded.jpa-records.name", contains(names.get(lastPage))) + .and() + .body("_embedded.jpa-records._links.add.href", + hasItems(endsWith("/jpa-records"), endsWith("/jpa-records"))) + .and() + .body("_embedded.jpa-records._links.list.href", + hasItems(endsWith("/jpa-records"), endsWith("/jpa-records"))) + .and() + .body("_embedded.jpa-records._links.self.href", + contains(endsWith("/jpa-records/" + ids.get(lastPage)))) + .and() + .body("_embedded.jpa-records._links.update.href", + contains(endsWith("/jpa-records/" + ids.get(lastPage)))) + .and() + .body("_embedded.jpa-records._links.remove.href", + contains(endsWith("/jpa-records/" + ids.get(lastPage)))) + .and().body("_links.add.href", endsWith("/jpa-records")) + .and().body("_links.list.href", endsWith("/jpa-records")) + .and().body("_links.first.href", endsWith("/jpa-records?page=0&size=1")) + .and().body("_links.last.href", endsWith("/jpa-records?page=" + lastPage + "&size=1")) + .and().body("_links.previous.href", endsWith("/jpa-records?page=" + (lastPage - 1) + "&size=1")); + } + + @Test + void shouldNotGetNonExistentPage() { + given().accept("application/json") + .and().queryParam("page", 100) + .when().get("/jpa-records") + .then().statusCode(200) + .and().body("id", is(empty())); + } + + @Test + void shouldNotGetNegativePageOrSize() { + given().accept("application/json") + .and().queryParam("page", -1) + .and().queryParam("size", -1) + .when().get("/jpa-records") + .then().statusCode(200) + // Invalid page and size parameters are replaced with defaults + .and().body("id", hasItems(1, 2)); + } + + @Test + void shouldListAscending() { + Response response = given().accept("application/json") + .when().get("/jpa-records?sort=name,id") + .thenReturn(); + + List actualNames = response.body().jsonPath().getList("name"); + List expectedNames = new LinkedList<>(actualNames); + expectedNames.sort(Comparator.naturalOrder()); + assertThat(actualNames).isEqualTo(expectedNames); + } + + @Test + void shouldListDescending() { + Response response = given().accept("application/json") + .when().get("/jpa-records?sort=-name,id") + .thenReturn(); + + List actualNames = response.body().jsonPath().getList("name"); + List expectedNames = new LinkedList<>(actualNames); + expectedNames.sort(Comparator.reverseOrder()); + assertThat(actualNames).isEqualTo(expectedNames); + } + + @Test + void shouldCreate() { + Response response = given().accept("application/json") + .and().contentType("application/json") + .and().body("{\"name\": \"test-create\"}") + .when().post("/jpa-records") + .thenReturn(); + assertThat(response.statusCode()).isEqualTo(201); + + String location = response.header("Location"); + int id = Integer.parseInt(location.substring(response.header("Location").lastIndexOf("/") + 1)); + JsonPath body = response.body().jsonPath(); + assertThat(body.getInt("id")).isEqualTo(id); + assertThat(body.getString("name")).isEqualTo("test-create"); + + given().accept("application/json") + .when().get(location) + .then().statusCode(200) + .and().body("id", is(equalTo(id))) + .and().body("name", is(equalTo("test-create"))); + } + + @Test + void shouldCreateHal() { + Response response = given().accept("application/hal+json") + .and().contentType("application/json") + .and().body("{\"name\": \"test-create-hal\"}") + .when().post("/jpa-records") + .thenReturn(); + assertThat(response.statusCode()).isEqualTo(201); + + String location = response.header("Location"); + int id = Integer.parseInt(location.substring(response.header("Location").lastIndexOf("/") + 1)); + JsonPath body = response.body().jsonPath(); + assertThat(body.getInt("id")).isEqualTo(id); + assertThat(body.getString("name")).isEqualTo("test-create-hal"); + assertThat(body.getString("_links.add.href")).endsWith("/jpa-records"); + assertThat(body.getString("_links.list.href")).endsWith("/jpa-records"); + assertThat(body.getString("_links.self.href")).endsWith("/jpa-records/" + id); + assertThat(body.getString("_links.update.href")).endsWith("/jpa-records/" + id); + assertThat(body.getString("_links.remove.href")).endsWith("/jpa-records/" + id); + + given().accept("application/json") + .when().get(location) + .then().statusCode(200) + .and().body("id", is(equalTo(id))) + .and().body("name", is(equalTo("test-create-hal"))); + } + + @Test + void shouldCreateAndUpdate() { + Response createResponse = given().accept("application/json") + .and().contentType("application/json") + .and().body("{\"id\": \"101\", \"name\": \"test-update-create\"}") + .when().put("/jpa-records/101") + .thenReturn(); + assertThat(createResponse.statusCode()).isEqualTo(201); + + String location = createResponse.header("Location"); + int id = Integer.parseInt(location.substring(createResponse.header("Location").lastIndexOf("/") + 1)); + JsonPath body = createResponse.body().jsonPath(); + assertThat(body.getInt("id")).isEqualTo(id); + assertThat(body.getString("name")).isEqualTo("test-update-create"); + + given().accept("application/json") + .and().contentType("application/json") + .and().body("{\"id\": \"" + id + "\", \"name\": \"test-update\"}") + .when().put(location) + .then() + .statusCode(204); + given().accept("application/json") + .when().get(location) + .then().statusCode(200) + .and().body("id", is(equalTo(id))) + .and().body("name", is(equalTo("test-update"))); + } + + @Test + void shouldCreateAndUpdateHal() { + Response createResponse = given().accept("application/hal+json") + .and().contentType("application/json") + .and().body("{\"id\": \"102\", \"name\": \"test-update-create-hal\"}") + .when().put("/jpa-records/102") + .thenReturn(); + assertThat(createResponse.statusCode()).isEqualTo(201); + + String location = createResponse.header("Location"); + int id = Integer.parseInt(location.substring(createResponse.header("Location").lastIndexOf("/") + 1)); + JsonPath body = createResponse.body().jsonPath(); + assertThat(body.getInt("id")).isEqualTo(id); + assertThat(body.getString("name")).isEqualTo("test-update-create-hal"); + assertThat(body.getString("_links.add.href")).endsWith("/jpa-records"); + assertThat(body.getString("_links.list.href")).endsWith("/jpa-records"); + assertThat(body.getString("_links.self.href")).endsWith("/jpa-records/" + id); + assertThat(body.getString("_links.update.href")).endsWith("/jpa-records/" + id); + assertThat(body.getString("_links.remove.href")).endsWith("/jpa-records/" + id); + + given().accept("application/json") + .and().contentType("application/json") + .and().body("{\"id\": \"" + id + "\", \"name\": \"test-update-hal\"}") + .when().put(location) + .then() + .statusCode(204); + given().accept("application/json") + .when().get(location) + .then().statusCode(200) + .and().body("id", is(equalTo(id))) + .and().body("name", is(equalTo("test-update-hal"))); + } + + @Test + void shouldCreateAndDelete() { + Response createResponse = given().accept("application/json") + .and().contentType("application/json") + .and().body("{\"name\": \"test-delete\"}") + .when().post("/jpa-records") + .thenReturn(); + assertThat(createResponse.statusCode()).isEqualTo(201); + + String location = createResponse.header("Location"); + when().delete(location) + .then().statusCode(204); + when().get(location) + .then().statusCode(404); + } + + @Test + void shouldNotDeleteNonExistent() { + when().delete("/jpa-records/1000") + .then().statusCode(404); + } + + private void assertLinks(Headers headers, Map expectedLinks) { + List links = new LinkedList<>(); + for (Header header : headers.getList("Link")) { + links.add(Link.valueOf(header.getValue())); + } + assertThat(links).hasSize(expectedLinks.size()); + for (Map.Entry expectedLink : expectedLinks.entrySet()) { + assertThat(links).anySatisfy(link -> { + assertThat(link.getUri().toString()).endsWith(expectedLink.getValue()); + assertThat(link.getRel()).isEqualTo(expectedLink.getKey()); + }); + } + } + +} diff --git a/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/paged/DefaultPagedResourceBisTest.java b/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/paged/DefaultPagedResourceBisTest.java deleted file mode 100644 index a110521962bd2..0000000000000 --- a/extensions/spring-data-rest/deployment/src/test/java/io/quarkus/spring/data/rest/paged/DefaultPagedResourceBisTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package io.quarkus.spring.data.rest.paged; - -import static io.restassured.RestAssured.given; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.hasItems; - -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import jakarta.ws.rs.core.Link; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import io.quarkus.spring.data.rest.AbstractEntity; -import io.quarkus.spring.data.rest.CrudAndPagedRecordsRepository; -import io.quarkus.test.QuarkusUnitTest; -import io.restassured.http.Header; -import io.restassured.http.Headers; - -class DefaultPagedResourceBisTest { - @RegisterExtension - static final QuarkusUnitTest TEST = new QuarkusUnitTest() - .withApplicationRoot((jar) -> jar - .addClasses(AbstractEntity.class, Record.class, CrudAndPagedRecordsRepository.class) - .addAsResource("application.properties") - .addAsResource("import.sql")); - - @Test - // @Disabled - void shouldListHal() { - given().accept("application/hal+json") - .when().get("/crud-and-paged-records") - .then().statusCode(200).log().all() - .and().body("_embedded.crud-and-paged-records.id", hasItems(1, 2)) - .and().body("_embedded.crud-and-paged-records.name", hasItems("first", "second")) - .and() - .body("_embedded.crud-and-paged-records._links.add.href", - hasItems(endsWith("/crud-and-paged-records"), endsWith("/crud-and-paged-records"))) - .and() - .body("_embedded.crud-and-paged-records._links.list.href", - hasItems(endsWith("/crud-and-paged-records"), endsWith("/crud-and-paged-records"))) - .and() - .body("_embedded.crud-and-paged-records._links.self.href", - hasItems(endsWith("/crud-and-paged-records/1"), endsWith("/crud-and-paged-records/2"))) - .and() - .body("_embedded.crud-and-paged-records._links.update.href", - hasItems(endsWith("/crud-and-paged-records/1"), endsWith("/crud-and-paged-records/2"))) - .and() - .body("_embedded.crud-and-paged-records._links.remove.href", - hasItems(endsWith("/crud-and-paged-records/1"), endsWith("/crud-and-paged-records/2"))) - .and().body("_links.add.href", endsWith("/crud-and-paged-records")) - .and().body("_links.list.href", endsWith("/crud-and-paged-records")) - .and().body("_links.first.href", endsWith("/crud-and-paged-records?page=0&size=20")) - .and().body("_links.last.href", endsWith("/crud-and-paged-records?page=0&size=20")); - } - - private void assertLinks(Headers headers, Map expectedLinks) { - List links = new LinkedList<>(); - for (Header header : headers.getList("Link")) { - links.add(Link.valueOf(header.getValue())); - } - assertThat(links).hasSize(expectedLinks.size()); - for (Map.Entry expectedLink : expectedLinks.entrySet()) { - assertThat(links).anySatisfy(link -> { - assertThat(link.getUri().toString()).endsWith(expectedLink.getValue()); - assertThat(link.getRel()).isEqualTo(expectedLink.getKey()); - }); - } - } -} diff --git a/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Article.java b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Article.java new file mode 100644 index 0000000000000..b4ef189219470 --- /dev/null +++ b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Article.java @@ -0,0 +1,73 @@ +package io.quarkus.it.spring.data.rest; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotBlank; + +import com.fasterxml.jackson.annotation.JsonBackReference; + +@Entity +public class Article { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // @Length(min = 2, max = 50, message = "length must be between {min} and {max}") + @NotBlank(message = "Name may not be blank") + private String name; + + // @Length(min = 2, max = 50, message = "length must be between {min} and {max}") + @NotBlank(message = "Author may not be blank") + private String author; + + @JsonBackReference + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "library_id") + private Library library; + + public Article() { + } + + public Article(String name, String author) { + this.name = name; + this.author = author; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public Library getLibrary() { + return library; + } + + public void setLibrary(Library library) { + this.library = library; + } +} diff --git a/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/ArticleJpaRepository.java b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/ArticleJpaRepository.java new file mode 100644 index 0000000000000..2dc5bd7c91988 --- /dev/null +++ b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/ArticleJpaRepository.java @@ -0,0 +1,6 @@ +package io.quarkus.it.spring.data.rest; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleJpaRepository extends JpaRepository { +} diff --git a/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Library.java b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Library.java new file mode 100644 index 0000000000000..d1d07a1371725 --- /dev/null +++ b/integration-tests/spring-data-rest/src/main/java/io/quarkus/it/spring/data/rest/Library.java @@ -0,0 +1,60 @@ +package io.quarkus.it.spring.data.rest; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotBlank; + +import com.fasterxml.jackson.annotation.JsonManagedReference; + +@Entity +public class Library { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Name may not be blank") + private String name; + + @JsonManagedReference + @OneToMany(mappedBy = "library", cascade = CascadeType.ALL) + private List
articles = new ArrayList<>(); + + public Library() { + } + + public Library(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List
getArticles() { + return articles; + } + + public void setArticles(List
articles) { + this.articles = articles; + } +} diff --git a/integration-tests/spring-data-rest/src/main/resources/import.sql b/integration-tests/spring-data-rest/src/main/resources/import.sql index 0fefc273330cc..4769c879ec1b4 100644 --- a/integration-tests/spring-data-rest/src/main/resources/import.sql +++ b/integration-tests/spring-data-rest/src/main/resources/import.sql @@ -4,3 +4,10 @@ alter sequence Author_SEQ restart with 2; insert into book(id, title, author_id) values (1, 'Crime and Punishment', 1); insert into book(id, title, author_id) values (2, 'Idiot', 1); alter sequence Book_SEQ restart with 3; + +INSERT INTO library(name) VALUES('Library1'); + +INSERT INTO article(name, author, library_id) VALUES ('Aeneid','Virgil', 1); +INSERT INTO article(name, author, library_id) VALUES ('Beach House','James Patterson',1); +INSERT INTO article(name, author) VALUES ('Cadillac Desert','Marc Reisner'); +INSERT INTO article(name, author) VALUES ('Dagon and Other Macabre Tales','H.P. Lovecraft '); diff --git a/integration-tests/spring-data-rest/src/test/java/io/quarkus/it/spring/data/rest/SpringDataRestTest.java b/integration-tests/spring-data-rest/src/test/java/io/quarkus/it/spring/data/rest/SpringDataRestTest.java index c1d1e08bf2f66..caa599b4af964 100644 --- a/integration-tests/spring-data-rest/src/test/java/io/quarkus/it/spring/data/rest/SpringDataRestTest.java +++ b/integration-tests/spring-data-rest/src/test/java/io/quarkus/it/spring/data/rest/SpringDataRestTest.java @@ -7,13 +7,25 @@ import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; import jakarta.json.Json; import jakarta.json.JsonObject; +import jakarta.ws.rs.core.Link; +import org.apache.http.HttpStatus; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.Header; +import io.restassured.http.Headers; import io.restassured.response.Response; @QuarkusTest @@ -33,6 +45,9 @@ class SpringDataRestTest { private static final String IDIOT_TITLE = "Idiot"; + protected static final List ORIGINAL_ARTICLES = Arrays.asList("Aeneid", "Beach House", "Cadillac Desert", + "Dagon and Other Macabre Tales"); + @Test void shouldGetAuthor() { given().accept("application/json") @@ -245,4 +260,37 @@ void shouldNotUpdateBookWithBlankTitle() { .and().body("parameterViolations[0].path", equalTo("update.entity.title")) .and().body("parameterViolations[0].message", equalTo("must not be blank")); } + + @Test + void sorting() { + //Test repository sorting + List articleNamesSortedDesc = new ArrayList<>(getItemsAfterUpdates()); + articleNamesSortedDesc.sort(Comparator.reverseOrder()); + Response response = given() + .accept("application/json") + .queryParam("sort", "-name") + .when().get("/article-jpa") + .then() + .statusCode(HttpStatus.SC_OK).extract().response(); + List articleNamesRepositorySortedDesc = response.jsonPath().getList("name"); + assertEquals(articleNamesSortedDesc, articleNamesRepositorySortedDesc); + } + + protected List getItemsAfterUpdates() { + return ORIGINAL_ARTICLES; + } + + private void assertLinks(Headers headers, Map expectedLinks) { + List links = new LinkedList<>(); + for (Header header : headers.getList("Link")) { + links.add(Link.valueOf(header.getValue())); + } + assertThat(links).hasSize(expectedLinks.size()); + for (Map.Entry expectedLink : expectedLinks.entrySet()) { + assertThat(links).anySatisfy(link -> { + assertThat(link.getUri().toString()).endsWith(expectedLink.getValue()); + assertThat(link.getRel()).isEqualTo(expectedLink.getKey()); + }); + } + } } From f8a71128c1140b12a72df317fc6d382527b83e9e Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 17 Jun 2024 13:47:54 +0200 Subject: [PATCH 09/15] WebSockets Next: document ping/pong messages (cherry picked from commit 8c3d8e9f353573a2f9cde5cee774fa3498eea3a4) --- .../asciidoc/websockets-next-reference.adoc | 26 ++++++++++++++++--- .../websockets/next/OnPongMessage.java | 5 ++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index 11e14ba18a2c4..4b93f261aa4eb 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -610,12 +610,28 @@ Item find(Item item) { 1. Specify the codec to use for both the deserialization of the incoming message 2. Specify the codec to use for the serialization of the outgoing message -== Handle Pong message +== Ping/pong messages -The `@OnPongMessage` annotation is used to consume pong messages. -A websocket endpoint must declare at most one method annotated with `@OnPongMessage`. +A https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2[ping message] may serve as a keepalive or to verify the remote endpoint. +A https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3[pong message] is sent in response to a ping message and it must have an identical payload. -The method must accept a single parameter of type `Buffer`: +The server automatically responds to a ping message sent from the client. +In other words, there is no need for `@OnPingMessage` callback declared on an endpoint. + +The server can send ping messages to a connected client. +The `WebSocketConnection` declares methods to send ping messages; there is a non-blocking variant: `WebSocketConnection#sendPing(Buffer)` and a blocking variant: `WebSocketConnection#sendPingAndAwait(Buffer)`. +By default, the ping messages are not sent automatically. +However, the configuration property `quarkus.websockets-next.server.auto-ping-interval` can be used to set the interval after which, the server sends a ping message to a connected client automatically. + +[source,properties] +---- +quarkus.websockets-next.server.auto-ping-interval=2 <1> +---- +<1> Sends a ping message to a connected client every 2 seconds. + +The `@OnPongMessage` annotation is used to define a callback that consumes pong messages sent from the client. +An endpoint must declare at most one method annotated with `@OnPongMessage`. +The callback method must return either `void` or `Uni`, and it must accept a single parameter of type `Buffer`. [source,java] ---- @@ -625,6 +641,8 @@ void pong(Buffer data) { } ---- +NOTE: The server can also send unsolicited pong messages that may serve as a unidirectional heartbeat. There is a non-blocking variant: `WebSocketConnection#sendPong(Buffer)` and also a blocking variant: `WebSocketConnection#sendPongAndAwait(Buffer)`. + [[websocket-next-security]] == Security diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OnPongMessage.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OnPongMessage.java index 4491ba0150a75..b8efe45d646e2 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OnPongMessage.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OnPongMessage.java @@ -27,9 +27,8 @@ *

*

    *
  • Methods returning {@code void} are considered blocking and should be executed on a worker thread.
  • - *
  • Methods returning {@link io.smallrye.mutiny.Uni} or {@link io.smallrye.mutiny.Multi} are considered non-blocking and - * should be executed on an event loop thread.
  • - *
  • Methods returning any other type are considered blocking and should be executed on a worker thread.
  • + *
  • Methods returning {@code io.smallrye.mutiny.Uni} are considered non-blocking and should be executed on an event + * loop thread.
  • *
* *

Method parameters

From 60b738080185834847ffc934b61b7c0a330a33b2 Mon Sep 17 00:00:00 2001 From: Andy Damevin Date: Mon, 17 Jun 2024 14:18:58 +0200 Subject: [PATCH 10/15] Fix index.html for compat before 3.9 and dynamic index (cherry picked from commit 0c265bc31254d9a12e371f607cf289881327eb78) --- .../META-INF/resources/index.tpl.qute.html | 288 ++++++++++++++++++ ...tentMergeCodestartFileStrategyHandler.java | 13 +- 2 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/src/main/resources/META-INF/resources/index.tpl.qute.html diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/src/main/resources/META-INF/resources/index.tpl.qute.html b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/src/main/resources/META-INF/resources/index.tpl.qute.html new file mode 100644 index 0000000000000..4d248f14aaa89 --- /dev/null +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus/project/quarkus/base/src/main/resources/META-INF/resources/index.tpl.qute.html @@ -0,0 +1,288 @@ +{#if quarkus.platform.version.compareVersionTo("3.9") < 0} + + + + + {project.artifact-id} - {project.version} + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/{config.file-name}

+

Static assets: src/main/resources/META-INF/resources/

+

Code: {language.dir.code}

+

Generated starter code:

+
    + {merged-content} +
+
+
+ {#if input.selected-extensions} +

Selected extensions

+
    + {#for ext in input.selected-extensions} + {#if ext.guide} +
  • {ext.name} (guide)
  • + {#else} +
  • {ext.name}
  • + {/if} + {/for} +
+ {/if} +
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + +{/if} diff --git a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/strategy/ContentMergeCodestartFileStrategyHandler.java b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/strategy/ContentMergeCodestartFileStrategyHandler.java index ab80c50ededb0..04dc4ce84d6b0 100644 --- a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/strategy/ContentMergeCodestartFileStrategyHandler.java +++ b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/strategy/ContentMergeCodestartFileStrategyHandler.java @@ -9,6 +9,13 @@ import io.quarkus.devtools.codestarts.CodestartStructureException; import io.quarkus.devtools.codestarts.core.reader.TargetFile; +/** + * + * @deprecated this was a quick-n-dirty way to allow extensions to provide content to the index.html, + * we don't need it anymore with the dynamic index.html. If we need something similar in the future let's find a + * more elegant way. + */ +@Deprecated final class ContentMergeCodestartFileStrategyHandler implements CodestartFileStrategyHandler { static final String NAME = "content-merge"; @@ -40,6 +47,10 @@ public void process(Path targetDirectory, String relativePath, List return; } createDirectories(targetPath); - writeFile(targetPath, template.get().getContent().replace("{merged-content}", mergedContent.toString())); + final String content = template.get().getContent(); + if (content.isBlank()) { + return; + } + writeFile(targetPath, content.replace("{merged-content}", mergedContent.toString())); } } From ec8bab3befbc1cbc5725c6fcdd8e5feac3373e27 Mon Sep 17 00:00:00 2001 From: Andre F de Miranda Date: Mon, 17 Jun 2024 21:34:53 +1000 Subject: [PATCH 11/15] Document DateTimeAttribute's `%{time, ` form Fixes #40614 (cherry picked from commit 9f7e3f74722c82e68ebcfdd97451fdabb3cbec70) --- docs/src/main/asciidoc/http-reference.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 396f2e571acda..b7f9786b4db77 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -421,6 +421,7 @@ include::{generated-dir}/config/quarkus-vertx-http-config-group-access-log-confi |First line of the request | `%r` | `%{REQUEST_LINE}` |HTTP status code of the response | `%s` | `%{RESPONSE_CODE}` |Date and time, in Common Log Format format | `%t` | `%{DATE_TIME}` +|Date and time as defined by a DateTimeFormatter compliant string | | `%{time,date_fime_formatter_string}` |Remote user that was authenticated | `%u` | `%{REMOTE_USER}` |Requested URL path | `%U` | `%{REQUEST_URL}` |Request relative path | `%R` | `%{REQUEST_PATH}` From bed47fcc4171775e3fb6e79c310d8f8f38b968c2 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Mon, 17 Jun 2024 18:14:41 +0200 Subject: [PATCH 12/15] Add note about setting token-state-manager.encryption-secret if no oidc secret is set (cherry picked from commit 60534ab5f7afbdee6862203a814564490103c9bd) --- .../security-oidc-code-flow-authentication-tutorial.adoc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc index c0bc3c54661dc..63d99eab3b2fc 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc @@ -183,6 +183,15 @@ Finally, the `quarkus.http.auth.permission.authenticated` permission is set to t In this case, all paths are protected by a policy that ensures only `authenticated` users can access them. For more information, see xref:security-authorize-web-endpoints-reference.adoc[Security Authorization Guide]. +[NOTE] +==== +When you do not configure a client secret with `quarkus.oidc.credentials.secret`, it is recommended to configure `quarkus.oidc.token-state-manager.encryption-secret`. + +The `quarkus.oidc.token-state-manager.encryption-secret` enables the default token state manager to encrypt the user tokens in a browser cookie. If this key is not defined, and the `quarkus.oidc.credentials.secret` fallback is not configured, Quarkus uses a random key. A random key causes existing logins to be invalidated either on application restart or in environment with multiple instances of your application. Alternatively, encryption can also be disabled by setting `quarkus.oidc.token-state-manager.encryption-required` to `false`. However, you should disable secret encryption in development environments only. + +The encryption secret is recommended to be 32 chars long. For example, `quarkus.oidc.token-state-manager.encryption-secret=AyM1SysPpbyDfgZld3umj1qzKObwVMk` +==== + == Start and configure the Keycloak server To start a Keycloak server, use Docker and run the following command: From 1d40fdaa5bf570e6c9b1265878245956f96cfd83 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 14 Jun 2024 17:19:38 +0200 Subject: [PATCH 13/15] Improve unsupported properties in SpringDataJPAProcessor Also improve the Javadoc of sqlLoadScript as multiple files are supported now. (cherry picked from commit bf8e4f265524dc8dc53c266d157836df3f24ec42) --- .../orm/deployment/HibernateOrmConfigPersistenceUnit.java | 8 ++++---- .../spring/data/deployment/SpringDataJPAProcessor.java | 6 +++--- .../quarkus/it/spring/data/jpa/PropertyWarningsPMT.java | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java index 1f29cbd83ca83..94fe4d3b64c94 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmConfigPersistenceUnit.java @@ -47,10 +47,10 @@ public interface HibernateOrmConfigPersistenceUnit { // @formatter:off /** - * Path to a file containing the SQL statements to execute when Hibernate ORM starts. + * Paths to files containing the SQL statements to execute when Hibernate ORM starts. * - * The file is retrieved from the classpath resources, - * so it must be located in the resources directory (e.g. `src/main/resources`). + * The files are retrieved from the classpath resources, + * so they must be located in the resources directory (e.g. `src/main/resources`). * * The default value for this setting differs depending on the Quarkus launch mode: * @@ -82,7 +82,7 @@ public interface HibernateOrmConfigPersistenceUnit { * @asciidoclet */ // @formatter:on - @ConfigDocDefault("import.sql in DEV, TEST ; no-file otherwise") + @ConfigDocDefault("import.sql in dev and test modes ; no-file otherwise") Optional> sqlLoadScript(); /** diff --git a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SpringDataJPAProcessor.java b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SpringDataJPAProcessor.java index c21689a83f796..104d9167067b7 100644 --- a/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SpringDataJPAProcessor.java +++ b/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/SpringDataJPAProcessor.java @@ -206,12 +206,12 @@ private void detectAndLogSpecificSpringPropertiesIfExist() { case SPRING_DATASOURCE_DATA: notSupportedProperties = notSupportedProperties + "\t- " + QUARKUS_HIBERNATE_ORM_SQL_LOAD_SCRIPT + " could be used to load data instead of " + SPRING_DATASOURCE_DATA - + " but it does not support either comma separated list of resources or resources with ant-style patterns as " + + " but it does not support ant-style patterns as " + SPRING_DATASOURCE_DATA - + " does, it accepts the name of the file containing the SQL statements to execute when when Hibernate ORM starts.\n"; + + " does, it accepts the name of files containing the SQL statements to execute when Hibernate ORM starts.\n"; break; default: - notSupportedProperties = notSupportedProperties + "\t- " + sp + "\n"; + notSupportedProperties = notSupportedProperties + "\t- " + sp + " does not have a Quarkus equivalent\n"; break; } } diff --git a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/PropertyWarningsPMT.java b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/PropertyWarningsPMT.java index 074f3152d02ed..20e25c460badc 100644 --- a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/PropertyWarningsPMT.java +++ b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/PropertyWarningsPMT.java @@ -42,7 +42,7 @@ public void ensureProperQuarkusPropertiesLogged() { .contains( "spring.jpa.hibernate.naming.implicit-strategy should be replaced by quarkus.hibernate-orm.implicit-naming-strategy") .contains( - "quarkus.hibernate-orm.sql-load-script could be used to load data instead of spring.datasource.data but it does not support either comma separated list of resources or resources with ant-style patterns as spring.datasource.data does, it accepts the name of the file containing the SQL statements to execute when when Hibernate ORM starts"); + "quarkus.hibernate-orm.sql-load-script could be used to load data instead of spring.datasource.data but it does not support ant-style patterns as spring.datasource.data does, it accepts the name of files containing the SQL statements to execute when Hibernate ORM starts"); }); }); }); From 99780a670930a07e23e48c3b7e76944e0dfb8221 Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Mon, 17 Jun 2024 23:20:00 +0200 Subject: [PATCH 14/15] Add kafka-version.properties as native image resource Fixes #40851 (cherry picked from commit b658288734b171ae87322c321e9eb71226e65088) --- .../java/io/quarkus/kafka/client/deployment/KafkaProcessor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java index 41f3415a761c6..295c223570ee9 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java @@ -222,6 +222,7 @@ public void build( BuildProducer sslNativeSupport) { final Set toRegister = new HashSet<>(); + nativeLibs.produce(new NativeImageResourceBuildItem("kafka/kafka-version.properties")); collectImplementors(toRegister, indexBuildItem, Serializer.class); collectImplementors(toRegister, indexBuildItem, Deserializer.class); collectImplementors(toRegister, indexBuildItem, Partitioner.class); From ec0e72bd8df74e2856a4bae2b762945a942282e3 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 18 Jun 2024 15:13:41 +0200 Subject: [PATCH 15/15] Upgrade SmallRye BeanBag to 1.5.2 This will fix compatibility with mvnd 1.0.0. (cherry picked from commit e5fd5b1b43f529ea5ecc1c80337ab1a5f0d70747) --- independent-projects/bootstrap/pom.xml | 2 +- independent-projects/extension-maven-plugin/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/independent-projects/bootstrap/pom.xml b/independent-projects/bootstrap/pom.xml index 969fe757279a5..4ea736e1b20c5 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -72,7 +72,7 @@ 2.0 3.5.1 2.3.0 - 1.5.1 + 1.5.2 8.8 0.0.10 0.1.3 diff --git a/independent-projects/extension-maven-plugin/pom.xml b/independent-projects/extension-maven-plugin/pom.xml index 978c9e4eb27bc..c1070b66a8c3c 100644 --- a/independent-projects/extension-maven-plugin/pom.xml +++ b/independent-projects/extension-maven-plugin/pom.xml @@ -39,7 +39,7 @@ 11 3.9.7 2.17.1 - 1.5.1 + 1.5.2 5.10.2