From 9699fb8c0c4758d567ee743281e97559d97d0848 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 9 Jan 2024 10:26:47 +0200 Subject: [PATCH 0001/2353] Introduce @CheckReturnValue into common Arc build items This makes the IDE warn extension authors when they have created a build item but have not used it. I encountered this in #38083 when I wrote something like: ```java UnremovableBeanBuildItem.beanTypes(Foo.class, Bar.class) ``` in a hurry and didn't stop to consider that the build item was never being produced --- .../quarkus/arc/deployment/AdditionalBeanBuildItem.java | 3 +++ .../quarkus/arc/deployment/UnremovableBeanBuildItem.java | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AdditionalBeanBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AdditionalBeanBuildItem.java index a2dcde480edd2..e71fff1413121 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AdditionalBeanBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AdditionalBeanBuildItem.java @@ -10,6 +10,7 @@ import org.jboss.jandex.DotName; import io.quarkus.builder.item.MultiBuildItem; +import io.smallrye.common.annotation.CheckReturnValue; /** * This build item is used to specify one or more additional bean classes to be analyzed during bean discovery. @@ -39,6 +40,7 @@ public static Builder builder() { * @param beanClass * @return a new build item */ + @CheckReturnValue public static AdditionalBeanBuildItem unremovableOf(Class beanClass) { return new AdditionalBeanBuildItem(Collections.singletonList(beanClass.getName()), false, null); } @@ -49,6 +51,7 @@ public static AdditionalBeanBuildItem unremovableOf(Class beanClass) { * @param beanClass * @return a new build item */ + @CheckReturnValue public static AdditionalBeanBuildItem unremovableOf(String beanClass) { return new AdditionalBeanBuildItem(Collections.singletonList(beanClass), false, null); } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/UnremovableBeanBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/UnremovableBeanBuildItem.java index 5984271398bf1..28fd64fbd3445 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/UnremovableBeanBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/UnremovableBeanBuildItem.java @@ -19,6 +19,7 @@ import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.BeanInfo; import io.quarkus.builder.item.MultiBuildItem; +import io.smallrye.common.annotation.CheckReturnValue; /** * This build item is used to exclude beans that would be normally removed if the config property @@ -88,6 +89,7 @@ public Set getClassNames() { * @param classNames * @return a new build item */ + @CheckReturnValue public static UnremovableBeanBuildItem beanClassNames(String... classNames) { Set names = new HashSet<>(); Collections.addAll(names, classNames); @@ -100,6 +102,7 @@ public static UnremovableBeanBuildItem beanClassNames(String... classNames) { * @param classNames * @return a new build item */ + @CheckReturnValue public static UnremovableBeanBuildItem beanClassNames(Set classNames) { return new UnremovableBeanBuildItem(new BeanClassNamesExclusion(classNames)); } @@ -110,6 +113,7 @@ public static UnremovableBeanBuildItem beanClassNames(Set classNames) { * @param typeNames * @return a new build item */ + @CheckReturnValue public static UnremovableBeanBuildItem beanTypes(DotName... typeNames) { Set names = new HashSet<>(); Collections.addAll(names, typeNames); @@ -122,6 +126,7 @@ public static UnremovableBeanBuildItem beanTypes(DotName... typeNames) { * @param typeNames * @return a new build item */ + @CheckReturnValue public static UnremovableBeanBuildItem beanTypes(Class... types) { return new UnremovableBeanBuildItem(new BeanTypesExclusion( Arrays.stream(types).map(Class::getName).map(DotName::createSimple).collect(Collectors.toSet()))); @@ -133,6 +138,7 @@ public static UnremovableBeanBuildItem beanTypes(Class... types) { * @param typeNames * @return a new build item */ + @CheckReturnValue public static UnremovableBeanBuildItem beanTypes(Set typeNames) { return new UnremovableBeanBuildItem(new BeanTypesExclusion(typeNames)); } @@ -145,6 +151,7 @@ public static UnremovableBeanBuildItem beanTypes(Set typeNames) { * @param annotationName * @return a new build item */ + @CheckReturnValue public static UnremovableBeanBuildItem beanClassAnnotation(DotName annotationName) { return new UnremovableBeanBuildItem(new BeanClassAnnotationExclusion(annotationName)); } @@ -157,6 +164,7 @@ public static UnremovableBeanBuildItem beanClassAnnotation(DotName annotationNam * @param annotationName * @return a new build item */ + @CheckReturnValue public static UnremovableBeanBuildItem beanClassAnnotation(String nameStartsWith) { return new UnremovableBeanBuildItem(new BeanClassAnnotationExclusion(nameStartsWith)); } @@ -167,6 +175,7 @@ public static UnremovableBeanBuildItem beanClassAnnotation(String nameStartsWith * @param annotationName * @return a new build item */ + @CheckReturnValue public static UnremovableBeanBuildItem targetWithAnnotation(DotName annotationName) { return new UnremovableBeanBuildItem(new Predicate() { @Override From 20c37fcac38be1a6fd1e3aa2ec8957113ec53907 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 22:41:03 +0000 Subject: [PATCH 0002/2353] Bump com.google.api.grpc:proto-google-common-protos Bumps [com.google.api.grpc:proto-google-common-protos](https://github.com/googleapis/sdk-platform-java) from 2.36.0 to 2.37.1. - [Release notes](https://github.com/googleapis/sdk-platform-java/releases) - [Changelog](https://github.com/googleapis/sdk-platform-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/sdk-platform-java/commits) --- updated-dependencies: - dependency-name: com.google.api.grpc:proto-google-common-protos dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 30fa6fe4e3a88..fca3db60dd511 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ 1.2.1 3.25.0 ${protoc.version} - 2.36.0 + 2.37.1 7.4.0 From 4e6c884936a8c27dc5af0f6b629f66d913f0e75d Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Wed, 27 Mar 2024 13:49:51 +0100 Subject: [PATCH 0003/2353] Bump Strimzi Oauth Client version in bom from 0.14.0 to 0.15.0 --- bom/application/pom.xml | 2 +- integration-tests/kafka-oauth-keycloak/pom.xml | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index dca533ccc6524..3967095a9cc55 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -217,7 +217,7 @@ 2.4.0 6.9.0.202403050737-r - 0.14.0 + 0.15.0 9.37.3 0.9.6 0.0.6 diff --git a/integration-tests/kafka-oauth-keycloak/pom.xml b/integration-tests/kafka-oauth-keycloak/pom.xml index 91314691c89ca..43f19d3917acf 100644 --- a/integration-tests/kafka-oauth-keycloak/pom.xml +++ b/integration-tests/kafka-oauth-keycloak/pom.xml @@ -44,11 +44,6 @@ io.strimzi kafka-oauth-client - - - io.strimzi - kafka-oauth-common - From 744ccfb1fbf7014cfd230599f503cffc4c9775b7 Mon Sep 17 00:00:00 2001 From: Sauli Ketola Date: Sat, 16 Mar 2024 10:12:18 +0200 Subject: [PATCH 0004/2353] Handle UnmarshalException as Bad Request If UnmarshalException is thrown (which happens for example when the xml is invalid) then a WebApplicationException is thrown with status code 400 Bad Request. This is also how resteasy-reactive-jackson handles invalid JSON. --- .../reactive/jaxb/deployment/test/SimpleXmlTest.java | 11 +++++++++++ .../serialisers/ServerJaxbMessageBodyReader.java | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/extensions/resteasy-reactive/rest-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/SimpleXmlTest.java b/extensions/resteasy-reactive/rest-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/SimpleXmlTest.java index 4cfe409be76ff..b9ce38f92fc64 100644 --- a/extensions/resteasy-reactive/rest-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/SimpleXmlTest.java +++ b/extensions/resteasy-reactive/rest-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/SimpleXmlTest.java @@ -75,6 +75,17 @@ public void testXml() { assertEquals(person.getLast(), secondPerson.getLast()); } + @Test + public void testInvalidXml() { + RestAssured + .with() + .body("Bob") + .contentType("application/xml") + .post("/simple/person") + .then() + .statusCode(400); + } + @Test public void testLargeXmlPost() { StringBuilder sb = new StringBuilder(); diff --git a/extensions/resteasy-reactive/rest-jaxb/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/runtime/serialisers/ServerJaxbMessageBodyReader.java b/extensions/resteasy-reactive/rest-jaxb/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/runtime/serialisers/ServerJaxbMessageBodyReader.java index 6a8226fba6d03..ab833016c9bc7 100644 --- a/extensions/resteasy-reactive/rest-jaxb/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/runtime/serialisers/ServerJaxbMessageBodyReader.java +++ b/extensions/resteasy-reactive/rest-jaxb/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/runtime/serialisers/ServerJaxbMessageBodyReader.java @@ -12,10 +12,12 @@ import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Providers; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBElement; import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.UnmarshalException; import jakarta.xml.bind.Unmarshaller; import org.jboss.resteasy.reactive.common.util.StreamUtil; @@ -73,6 +75,8 @@ protected Object unmarshal(InputStream entityStream, Class type) { JAXBElement item = getUnmarshall(type) .unmarshal(new StreamSource(entityStream), type); return item.getValue(); + } catch (UnmarshalException e) { + throw new WebApplicationException(e, Response.Status.BAD_REQUEST); } catch (JAXBException e) { throw new RuntimeException(e); } From 4cfa3a3260655a050ae3682ebdd0b68d142258a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 22:09:32 +0000 Subject: [PATCH 0005/2353] Bump grpc.version from 1.62.2 to 1.63.0 Bumps `grpc.version` from 1.62.2 to 1.63.0. Updates `io.grpc:grpc-bom` from 1.62.2 to 1.63.0 - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.62.2...v1.63.0) Updates `io.grpc:protoc-gen-grpc-java` from 1.62.2 to 1.63.0 - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.62.2...v1.63.0) --- updated-dependencies: - dependency-name: io.grpc:grpc-bom dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.grpc:protoc-gen-grpc-java dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9398fb32cce29..8f40c27a0e333 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ 6.11.0 - 1.62.2 + 1.63.0 1.2.1 3.25.0 ${protoc.version} From 62615a6b33fedbc0f94be96652b744d8ffd3ee39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Fri, 12 Apr 2024 19:18:32 +0200 Subject: [PATCH 0006/2353] Validate Tenant annotation applied before authentication happened --- .../io/quarkus/oidc/runtime/OidcRecorder.java | 25 ++++++++ ...JakartaRestResourceHttpPermissionTest.java | 40 ++++++++++-- ...JakartaRestResourceHttpPermissionTest.java | 40 ++++++++++-- .../it/keycloak/TenantEchoResource.java | 13 ++++ .../keycloak/AnnotationBasedTenantTest.java | 62 ++++++++++++++----- 5 files changed, 157 insertions(+), 23 deletions(-) diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index f67d7f851f41d..0568683eda84a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -44,6 +44,7 @@ import io.quarkus.runtime.TlsConfig; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.TokenAuthenticationRequest; @@ -600,6 +601,30 @@ public Consumer apply(String tenantId) { return new Consumer() { @Override public void accept(RoutingContext routingContext) { + OidcTenantConfig tenantConfig = routingContext.get(OidcTenantConfig.class.getName()); + if (tenantConfig != null) { + // authentication has happened before @Tenant annotation was matched with the HTTP request + String tenantUsedForAuth = tenantConfig.tenantId.orElse(null); + if (tenantId.equals(tenantUsedForAuth)) { + // @Tenant selects the same tenant as already selected + return; + } else { + // @Tenant selects the different tenant than already selected + throw new AuthenticationFailedException( + """ + The '%1$s' selected with the @Tenant annotation must be used to authenticate + the request but it was already authenticated with the '%2$s' tenant. It + can happen if the '%1$s' is selected with an annotation but '%2$s' is + resolved during authentication required by the HTTP Security Policy which + is enforced before the JAX-RS chain is run. In such cases, please set the + 'quarkus.http.auth.permission."permissions".applies-to=JAXRS' to all HTTP + Security Policies which secure the same REST endpoints as the ones + where the '%1$s' tenant is resolved by the '@Tenant' annotation. + """ + .formatted(tenantId, tenantUsedForAuth)); + } + } + LOG.debugf("@Tenant annotation set a '%s' tenant id on the %s request path", tenantId, routingContext.request().path()); routingContext.put(OidcUtils.TENANT_ID_SET_BY_ANNOTATION, tenantId); diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java index a8324f72cfc2c..0eb468a1bcdc0 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java @@ -12,10 +12,12 @@ import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.test.utils.TestIdentityController; import io.quarkus.security.test.utils.TestIdentityProvider; import io.quarkus.test.QuarkusUnitTest; @@ -40,18 +42,23 @@ public class JakartaRestResourceHttpPermissionTest { "quarkus.http.auth.permission.root.paths=/\n" + "quarkus.http.auth.permission.root.policy=authenticated\n" + "quarkus.http.auth.permission.dot.paths=dot,dot/\n" + - "quarkus.http.auth.permission.dot.policy=authenticated\n"; + "quarkus.http.auth.permission.dot.policy=authenticated\n" + + "quarkus.http.auth.permission.jax-rs.paths=jax-rs\n" + + "quarkus.http.auth.permission.jax-rs.policy=admin-role\n" + + "quarkus.http.auth.policy.admin-role.roles-allowed=admin"; @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar .addClasses(TestIdentityProvider.class, TestIdentityController.class, ApiResource.class, - RootResource.class, PublicResource.class) + RootResource.class, PublicResource.class, JaxRsResource.class) .addAsResource(new StringAsset(APP_PROPS), "application.properties")); @BeforeAll public static void setup() { - TestIdentityController.resetRoles().add("test", "test", "test"); + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("test", "test", "test"); } @TestHTTPResource @@ -97,6 +104,15 @@ public void testSecuredNotFound(String path) { assurePathAuthenticated(path, 404); } + @Test + public void testJaxRsRolesHttpSecurityPolicy() { + // insufficient role, expected admin + assurePath("/jax-rs", 401); + assurePath("///jax-rs///", 401); + + assurePath("/jax-rs", 200, "admin", true, "admin:admin"); + } + private static String getLastNonEmptySegmentContent(String path) { while (path.endsWith("/") || path.endsWith(".")) { path = path.substring(0, path.length() - 1); @@ -104,6 +120,18 @@ private static String getLastNonEmptySegmentContent(String path) { return path.substring(path.lastIndexOf('/') + 1); } + @Path("jax-rs") + public static class JaxRsResource { + + @Inject + SecurityIdentity identity; + + @GET + public String getPrincipalName() { + return identity.getPrincipal().getName(); + } + } + @Path("/api") public static class ApiResource { @@ -201,13 +229,17 @@ private void assurePathAuthenticated(String path, String body) { } private void assurePath(String path, int expectedStatusCode, String body, boolean auth) { + assurePath(path, expectedStatusCode, body, auth, "test:test"); + } + + private void assurePath(String path, int expectedStatusCode, String body, boolean auth, String credentials) { var httpClient = vertx.createHttpClient(); try { httpClient .request(HttpMethod.GET, url.getPort(), url.getHost(), path) .map(r -> { if (auth) { - r.putHeader("Authorization", "Basic " + encodeBase64URLSafeString("test:test".getBytes())); + r.putHeader("Authorization", "Basic " + encodeBase64URLSafeString(credentials.getBytes())); } return r; }) diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java index 220102abd6348..e6ff7c5e18e7b 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java @@ -14,10 +14,12 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.test.utils.TestIdentityController; import io.quarkus.security.test.utils.TestIdentityProvider; import io.quarkus.test.QuarkusUnitTest; @@ -40,19 +42,24 @@ public class JakartaRestResourceHttpPermissionTest { "quarkus.http.auth.permission.root.paths=/\n" + "quarkus.http.auth.permission.root.policy=authenticated\n" + "quarkus.http.auth.permission.fragment.paths=/#stuff,/#stuff/\n" + - "quarkus.http.auth.permission.fragment.policy=authenticated\n"; + "quarkus.http.auth.permission.fragment.policy=authenticated\n" + + "quarkus.http.auth.permission.jax-rs.paths=jax-rs\n" + + "quarkus.http.auth.permission.jax-rs.policy=admin-role\n" + + "quarkus.http.auth.policy.admin-role.roles-allowed=admin"; private static WebClient client; @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar .addClasses(TestIdentityProvider.class, TestIdentityController.class, ApiResource.class, - RootResource.class, PublicResource.class) + RootResource.class, PublicResource.class, JaxRsResource.class) .addAsResource(new StringAsset(APP_PROPS), "application.properties")); @BeforeAll public static void setup() { - TestIdentityController.resetRoles().add("test", "test", "test"); + TestIdentityController.resetRoles() + .add("admin", "admin", "admin") + .add("test", "test", "test"); } @AfterAll @@ -106,6 +113,15 @@ public void testNotSecuredPaths(String path) { assurePathAuthenticated(path); } + @Test + public void testJaxRsRolesHttpSecurityPolicy() { + // insufficient role, expected admin + assurePath("/jax-rs", 401); + assurePath("///jax-rs///", 401); + + assurePath("/jax-rs", 200, "admin", true, "admin"); + } + private static String getLastNonEmptySegmentContent(String path) { while (path.endsWith("/") || path.endsWith(".")) { path = path.substring(0, path.length() - 1); @@ -113,6 +129,18 @@ private static String getLastNonEmptySegmentContent(String path) { return path.substring(path.lastIndexOf('/') + 1); } + @Path("jax-rs") + public static class JaxRsResource { + + @Inject + SecurityIdentity identity; + + @GET + public String getPrincipalName() { + return identity.getPrincipal().getName(); + } + } + @Path("/api") public static class ApiResource { @@ -212,9 +240,13 @@ private void assurePathAuthenticated(String path, String body) { } private void assurePath(String path, int expectedStatusCode, String body, boolean auth) { + assurePath(path, expectedStatusCode, body, auth, "test"); + } + + private void assurePath(String path, int expectedStatusCode, String body, boolean auth, String username) { var req = getClient().get(url.getPort(), url.getHost(), path); if (auth) { - req.basicAuthentication("test", "test"); + req.basicAuthentication(username, username); } var result = req.send(); await().atMost(REQUEST_TIMEOUT).until(result::isComplete); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantEchoResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantEchoResource.java index 5636556933e8c..4440ddf1a8e4e 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantEchoResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TenantEchoResource.java @@ -54,6 +54,19 @@ public String getHrTenantIdentityAugmentation() { return getTenantInternal(); } + @Path("/http-security-policy-applies-all-diff") + @GET + public String httpSecurityPolicyAppliesAllDiff() { + throw new IllegalStateException("An exception should have been thrown because authentication happened" + + " before Tenant was selected with the @Tenant annotation"); + } + + @Path("/http-security-policy-applies-all-same") + @GET + public String httpSecurityPolicyAppliesAllSame() { + return getTenantInternal(); + } + private String getTenantInternal() { return OidcUtils.TENANT_ID_ATTRIBUTE + "=" + routingContext.get(OidcUtils.TENANT_ID_ATTRIBUTE) + ", static.tenant.id=" + routingContext.get("static.tenant.id") diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java index a072936710d3e..eac6beda6d99e 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/AnnotationBasedTenantTest.java @@ -28,6 +28,7 @@ public Map getConfigOverrides() { Map.entry("quarkus.oidc.hr.auth-server-url", "http://localhost:8180/auth/realms/quarkus2/"), Map.entry("quarkus.oidc.hr.client-id", "quarkus-app"), Map.entry("quarkus.oidc.hr.credentials.secret", "secret"), + Map.entry("quarkus.oidc.hr.tenant-paths", "/api/tenant-echo/http-security-policy-applies-all-same"), Map.entry("quarkus.oidc.hr.token.audience", "http://hr.service"), Map.entry("quarkus.http.auth.policy.roles1.roles-allowed", "role1"), Map.entry("quarkus.http.auth.policy.roles2.roles-allowed", "role2"), @@ -59,7 +60,11 @@ public Map getConfigOverrides() { Map.entry("quarkus.http.auth.permission.identity-augmentation.paths", "/api/tenant-echo/hr-identity-augmentation"), Map.entry("quarkus.http.auth.permission.identity-augmentation.policy", "roles3"), - Map.entry("quarkus.http.auth.permission.identity-augmentation.applies-to", "JAXRS")); + Map.entry("quarkus.http.auth.permission.identity-augmentation.applies-to", "JAXRS"), + Map.entry("quarkus.http.auth.permission.tenant-annotation-applies-all.paths", + "/api/tenant-echo/http-security-policy-applies-all-diff,/api/tenant-echo/http-security-policy-applies-all-same"), + Map.entry("quarkus.http.auth.permission.tenant-annotation-applies-all.policy", "admin-role"), + Map.entry("quarkus.http.auth.policy.admin-role.roles-allowed", "admin")); } } @@ -204,9 +209,7 @@ public void testClassicHttpSecurityPolicyWithRbac() { token = getTokenWithRole("role1"); RestAssured.given().auth().oauth2(token) .when().get("/api/tenant-echo2/hr-classic-perm-check") - .then().statusCode(200) - .body(Matchers.equalTo(("tenant-id=hr, static.tenant.id=null, name=alice, " - + OidcUtils.TENANT_ID_SET_BY_ANNOTATION + "=hr"))); + .then().statusCode(401); token = getTokenWithRole("wrong-role"); RestAssured.given().auth().oauth2(token) @@ -239,21 +242,18 @@ public void testJaxRsAndClassicHttpSecurityPolicyNoRbac() { token = getTokenWithRole("role2"); RestAssured.given().auth().oauth2(token) .when().get("/api/tenant-echo/hr-classic-and-jaxrs-perm-check") - .then().statusCode(403); + .then().statusCode(401); // roles allowed security check (created for @RolesAllowed) fails over missing role "role3" token = getTokenWithRole("role2", "role1"); RestAssured.given().auth().oauth2(token) .when().get("/api/tenant-echo/hr-classic-and-jaxrs-perm-check") - .then().statusCode(403); + .then().statusCode(401); token = getTokenWithRole("role3", "role2", "role1"); RestAssured.given().auth().oauth2(token) .when().get("/api/tenant-echo/hr-classic-and-jaxrs-perm-check") - .then().statusCode(200) - // static tenant is null as the permission check "combined-part1" happened before @Tenant - .body(Matchers.equalTo(("tenant-id=hr, static.tenant.id=null, name=alice, " - + OidcUtils.TENANT_ID_SET_BY_ANNOTATION + "=hr"))); + .then().statusCode(401); } finally { server.stop(); } @@ -282,15 +282,12 @@ public void testJaxRsAndClassicHttpSecurityPolicyWithRbac() { token = getTokenWithRole("role2"); RestAssured.given().auth().oauth2(token) .when().get("/api/tenant-echo2/hr-classic-and-jaxrs-perm-check") - .then().statusCode(403); + .then().statusCode(401); token = getTokenWithRole("role2", "role1"); RestAssured.given().auth().oauth2(token) .when().get("/api/tenant-echo2/hr-classic-and-jaxrs-perm-check") - .then().statusCode(200) - // static tenant is null as the permission check "combined-part1" happened before @Tenant - .body(Matchers.equalTo(("tenant-id=hr, static.tenant.id=null, name=alice, " - + OidcUtils.TENANT_ID_SET_BY_ANNOTATION + "=hr"))); + .then().statusCode(401); } finally { server.stop(); } @@ -325,6 +322,31 @@ public void testJaxRsIdentityAugmentation() { } } + @Test + public void testPolicyAppliedBeforeTenantAnnotationMatched() { + WiremockTestResource server = new WiremockTestResource(); + server.start(); + try { + // policy applied before @Tenant annotation has been matched and different tenant has been used for auth + // than the one that @Tenant annotation selects + var token = getNonHrTenantAccessToken(Set.of("admin")); + RestAssured.given().auth().oauth2(token) + .when().get("/api/tenant-echo/http-security-policy-applies-all-diff") + .then().statusCode(401); + + // policy applied before @Tenant annotation has been matched and different tenant has been used for auth + // than the one that @Tenant annotation selects + token = getTokenWithRole("admin"); + RestAssured.given().auth().oauth2(token) + .when().get("/api/tenant-echo/http-security-policy-applies-all-same") + .then().statusCode(200) + .body(Matchers + .equalTo("tenant-id=null, static.tenant.id=hr, name=alice, tenant-id-set-by-annotation=null")); + } finally { + server.stop(); + } + } + private static String getTokenWithRole(String... roles) { return Jwt.preferredUserName("alice") .groups(Set.of(roles)) @@ -333,4 +355,14 @@ private static String getTokenWithRole(String... roles) { .keyId("1") .sign("privateKey.jwk"); } + + private String getNonHrTenantAccessToken(Set groups) { + return Jwt.preferredUserName("alice") + .groups(groups) + .issuer("https://server.example.com") + .audience("https://service.example.com") + .jws() + .keyId("1") + .sign(); + } } From 8df6fcb08bd27d2966138af2f01297d7a39f403e Mon Sep 17 00:00:00 2001 From: Sanne Grinovero Date: Wed, 17 Apr 2024 12:09:51 +0100 Subject: [PATCH 0007/2353] Remove support for the SecurityManager --- .../security/ldap/DelegatingLdapContext.java | 13 ++++------ .../ldap/QuarkusDirContextFactory.java | 11 +++----- .../ArcConstraintValidatorFactoryImpl.java | 13 +--------- .../runtime/QuarkusRestClientBuilder.java | 13 ++-------- .../spi/QuarkusClassloadingService.java | 12 +++------ .../quarkus/bootstrap/util/PropertyUtils.java | 26 ++----------------- .../registry/config/PropertiesUtil.java | 26 ++----------------- 7 files changed, 19 insertions(+), 95 deletions(-) diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java index c90a9da278e2c..6f43ceb731a5a 100644 --- a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/DelegatingLdapContext.java @@ -1,7 +1,5 @@ package io.quarkus.elytron.security.ldap; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Hashtable; import javax.naming.Binding; @@ -26,7 +24,6 @@ import org.wildfly.common.Assert; import org.wildfly.security.auth.realm.ldap.ThreadLocalSSLSocketFactory; -import org.wildfly.security.manager.action.SetContextClassLoaderAction; class DelegatingLdapContext implements LdapContext { @@ -46,7 +43,7 @@ interface CloseHandler { } // for needs of newInstance() - private DelegatingLdapContext(DirContext delegating, SocketFactory socketFactory) throws NamingException { + private DelegatingLdapContext(DirContext delegating, SocketFactory socketFactory) { this.delegating = delegating; this.closeHandler = null; // close handler should not be applied to copy this.socketFactory = socketFactory; @@ -488,10 +485,10 @@ private ClassLoader getSocketFactoryClassLoader() { } private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) { - return doPrivileged(new SetContextClassLoaderAction(targetClassLoader)); + final Thread currentThread = Thread.currentThread(); + final ClassLoader original = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(targetClassLoader); + return original; } - private static T doPrivileged(final PrivilegedAction action) { - return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); - } } diff --git a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java index 36118d8864f6a..1fe3324d0aa50 100644 --- a/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java +++ b/extensions/elytron-security-ldap/runtime/src/main/java/io/quarkus/elytron/security/ldap/QuarkusDirContextFactory.java @@ -1,7 +1,5 @@ package io.quarkus.elytron.security.ldap; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.time.Duration; import java.util.Hashtable; @@ -15,7 +13,6 @@ import javax.security.auth.callback.PasswordCallback; import org.wildfly.security.auth.realm.ldap.DirContextFactory; -import org.wildfly.security.manager.action.SetContextClassLoaderAction; public class QuarkusDirContextFactory implements DirContextFactory { // private static final ElytronMessages log = Logger.getMessageLogger(ElytronMessages.class, "org.wildfly.security"); @@ -142,10 +139,10 @@ public void returnContext(DirContext context) { } private ClassLoader setClassLoaderTo(final ClassLoader targetClassLoader) { - return doPrivileged(new SetContextClassLoaderAction(targetClassLoader)); + final Thread currentThread = Thread.currentThread(); + final ClassLoader original = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(targetClassLoader); + return original; } - private static T doPrivileged(final PrivilegedAction action) { - return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); - } } diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java index 6662d05dfb3f7..174ce705d62af 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/ArcConstraintValidatorFactoryImpl.java @@ -1,7 +1,5 @@ package io.quarkus.hibernate.validator.runtime; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.IdentityHashMap; import java.util.Map; @@ -34,7 +32,7 @@ public class ArcConstraintValidatorFactoryImpl implements ConstraintValidatorFac } return instance; } - return run(NewInstance.action(key, "ConstraintValidator")); + return NewInstance.action(key, "ConstraintValidator").run(); } @Override @@ -45,13 +43,4 @@ public void releaseInstance(ConstraintValidator instance) { } } - /** - * Runs the given privileged action, using a privileged block if required. - *

- * NOTE: This must never be changed into a publicly available method to avoid execution of arbitrary - * privileged actions within HV's protection domain. - */ - private T run(PrivilegedAction action) { - return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run(); - } } diff --git a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java index ead9b44944922..7ecc462c52d12 100644 --- a/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java +++ b/extensions/resteasy-classic/resteasy-client/runtime/src/main/java/io/quarkus/restclient/runtime/QuarkusRestClientBuilder.java @@ -17,11 +17,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.security.AccessController; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; -import java.security.PrivilegedAction; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; @@ -397,7 +395,7 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi * @return list of proxy hosts */ private List getProxyHostsAsRegex() { - String noProxyHostsSysProps = getSystemProperty("http.nonProxyHosts", null); + String noProxyHostsSysProps = System.getProperty("http.nonProxyHosts", null); if (noProxyHostsSysProps == null) { noProxyHostsSysProps = "localhost|127.*|[::1]"; } else { @@ -414,7 +412,7 @@ private List getProxyHostsAsRegex() { */ private boolean useURLConnection() { if (useURLConnection == null) { - String defaultToURLConnection = getSystemProperty( + String defaultToURLConnection = System.getProperty( "org.jboss.resteasy.microprofile.defaultToURLConnectionHttpClient", "false"); useURLConnection = defaultToURLConnection.equalsIgnoreCase("true"); } @@ -820,13 +818,6 @@ private static BeanManager getBeanManager() { } } - private String getSystemProperty(String key, String def) { - if (System.getSecurityManager() == null) { - return System.getProperty(key, def); - } - return AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty(key, def)); - } - private final MpClientBuilderImpl builderDelegate; private final ConfigurationWrapper configurationWrapper; diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java index 8fc89fdeb99c4..359f9c4417aaa 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/spi/QuarkusClassloadingService.java @@ -1,9 +1,5 @@ package io.quarkus.smallrye.graphql.runtime.spi; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; - import graphql.schema.PropertyDataFetcherHelper; import io.smallrye.graphql.execution.Classes; import io.smallrye.graphql.spi.ClassloadingService; @@ -38,12 +34,10 @@ public Class loadClass(String className) { if (Classes.isPrimitive(className)) { return Classes.getPrimativeClassType(className); } else { - return AccessController.doPrivileged((PrivilegedExceptionAction>) () -> { - ClassLoader cl = classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader; - return loadClass(className, cl); - }); + ClassLoader cl = classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader; + return loadClass(className, cl); } - } catch (PrivilegedActionException | ClassNotFoundException pae) { + } catch (ClassNotFoundException pae) { throw new RuntimeException("Can not load class [" + className + "]", pae); } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java index 5a15d4b205b38..8751c22210ce4 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/PropertyUtils.java @@ -5,8 +5,6 @@ import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -40,32 +38,12 @@ public static String getUserHome() { public static String getProperty(final String name, String defValue) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name, defValue); - } - }); - } else { - return System.getProperty(name, defValue); - } + return System.getProperty(name, defValue); } public static String getProperty(final String name) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name); - } - }); - } else { - return System.getProperty(name); - } + return System.getProperty(name); } public static final Boolean getBooleanOrNull(String name) { diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java index 57125c8938306..2e4c52b4228f4 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/config/PropertiesUtil.java @@ -1,7 +1,5 @@ package io.quarkus.registry.config; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.Locale; public class PropertiesUtil { @@ -26,32 +24,12 @@ public static String getUserHome() { public static String getProperty(final String name, String defValue) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name, defValue); - } - }); - } else { - return System.getProperty(name, defValue); - } + return System.getProperty(name, defValue); } public static String getProperty(final String name) { assert name != null : "name is null"; - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public String run() { - return System.getProperty(name); - } - }); - } else { - return System.getProperty(name); - } + return System.getProperty(name); } public static final Boolean getBooleanOrNull(String name) { From 09d77d09fe7441e8c88b1af4d5125d48ab16c187 Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Wed, 17 Apr 2024 08:01:35 -0500 Subject: [PATCH 0008/2353] Prevent double-warning when using deprecated properties Fixes #40047 --- .../BuildTimeConfigurationReader.java | 2 ++ .../configuration/ConfigCompatibility.java | 24 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index 8b0cb7c4b1076..62ef5f74e3043 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -1086,6 +1086,7 @@ public String getValue(final String propertyName) { builder.getSources().clear(); builder.getSourceProviders().clear(); builder.setAddDefaultSources(false) + .withInterceptors(ConfigCompatibility.FrontEnd.nonLoggingInstance(), ConfigCompatibility.BackEnd.instance()) .addDiscoveredCustomizers() .withProfiles(config.getProfiles()) .withSources(sourceProperties); @@ -1099,6 +1100,7 @@ public String getValue(final String propertyName) { builder.getSources().clear(); builder.getSourceProviders().clear(); builder.setAddDefaultSources(false) + .withInterceptors(ConfigCompatibility.FrontEnd.nonLoggingInstance(), ConfigCompatibility.BackEnd.instance()) .addDiscoveredCustomizers() .withSources(sourceProperties) .withSources(new MapBackedConfigSource( diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java index 54007f07a354f..20b1853ae0c17 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigCompatibility.java @@ -114,9 +114,13 @@ public static final class FrontEnd implements ConfigSourceInterceptor { @Serial private static final long serialVersionUID = -3438497970389074611L; - private static final FrontEnd instance = new FrontEnd(); + private static final FrontEnd instance = new FrontEnd(true); + private static final FrontEnd nonLoggingInstance = new FrontEnd(false); - private FrontEnd() { + private final boolean logging; + + private FrontEnd(final boolean logging) { + this.logging = logging; } public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { @@ -155,11 +159,13 @@ public boolean hasNext() { // get the replacement names List list = fn.apply(context, new NameIterator(next)); subIter = list.iterator(); - // todo: print these warnings when mapping the configuration so they cannot appear more than once - if (list.isEmpty()) { - log.warnf("Configuration property '%s' has been deprecated and will be ignored", next); - } else { - log.warnf("Configuration property '%s' has been deprecated and replaced by: %s", next, list); + if (logging) { + // todo: print these warnings when mapping the configuration so they cannot appear more than once + if (list.isEmpty()) { + log.warnf("Configuration property '%s' has been deprecated and will be ignored", next); + } else { + log.warnf("Configuration property '%s' has been deprecated and replaced by: %s", next, list); + } } } return true; @@ -179,6 +185,10 @@ public String next() { public static FrontEnd instance() { return instance; } + + public static FrontEnd nonLoggingInstance() { + return nonLoggingInstance; + } } /** From 71ee011a0e70813eacd5cffcc9dbd988fef9d0f5 Mon Sep 17 00:00:00 2001 From: Selim Date: Sat, 16 Mar 2024 23:44:15 +0300 Subject: [PATCH 0009/2353] update default values for salt & iteration index --- docs/src/main/asciidoc/security-jdbc.adoc | 6 +----- .../security/jdbc/BcryptPasswordKeyMapperConfig.java | 10 ++++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/src/main/asciidoc/security-jdbc.adoc b/docs/src/main/asciidoc/security-jdbc.adoc index 7a3e8a906f15d..fba33d27df325 100644 --- a/docs/src/main/asciidoc/security-jdbc.adoc +++ b/docs/src/main/asciidoc/security-jdbc.adoc @@ -209,8 +209,6 @@ quarkus.security.jdbc.enabled=true quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM test_user u WHERE u.username=? <1> quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true <2> quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1 quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2 <3> quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups ---- @@ -218,7 +216,7 @@ quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups The `elytron-security-jdbc` extension requires at least one principal query to authenticate the user and its identity. <1> We define a parameterized SQL statement (with exactly 1 parameter) which should return the user's password plus any additional information you want to load. -<2> We configure the password mapper with the position of the password field in the `SELECT` fields and other information like salt, hash encoding, etc. Setting the salt and iteration count indexes to `-1` is required for MCF. +<2> The password mapper is configured with the position of the password field in the `SELECT` fields. The hash is stored in the Modular Crypt Format (MCF) because the salt and iteration count indexes are set to `-1` by default. You can override them in order to decompose each element into three separate columns. <3> We use `attribute-mappings` to bind the `SELECT` projection fields (i.e. `u.role` here) to the target Principal representation attributes. [NOTE] @@ -311,8 +309,6 @@ quarkus.security.jdbc.enabled=true quarkus.security.jdbc.principal-query.sql=SELECT u.password FROM test_user u WHERE u.username=? quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=-1 -quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-index=-1 quarkus.security.jdbc.principal-query.roles.sql=SELECT r.role_name FROM test_role r, test_user_role ur WHERE ur.username=? AND ur.role_id = r.id quarkus.security.jdbc.principal-query.roles.datasource=permissions diff --git a/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java b/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java index 5848f998f5836..d7c43a70a89f6 100644 --- a/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java +++ b/extensions/elytron-security-jdbc/runtime/src/main/java/io/quarkus/elytron/security/jdbc/BcryptPasswordKeyMapperConfig.java @@ -34,9 +34,10 @@ public interface BcryptPasswordKeyMapperConfig { Encoding hashEncoding(); /** - * The index (1 based numbering) of the column containing the Bcrypt salt + * The index (1 based numbering) of the column containing the Bcrypt salt. The default value of `-1` implies that the salt + * is stored in the password column using the Modular Crypt Format (MCF) standard. */ - @WithDefault("0") + @WithDefault("-1") int saltIndex(); /** @@ -46,9 +47,10 @@ public interface BcryptPasswordKeyMapperConfig { Encoding saltEncoding(); /** - * The index (1 based numbering) of the column containing the Bcrypt iteration count + * The index (1 based numbering) of the column containing the Bcrypt iteration count. The default value of `-1` implies that + * the iteration count is stored in the password column using the Modular Crypt Format (MCF) standard. */ - @WithDefault("0") + @WithDefault("-1") int iterationCountIndex(); default PasswordKeyMapper toPasswordKeyMapper() { From 48e3ea937c4207072d06e9739c126846ecaa6fe0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 22:53:46 +0000 Subject: [PATCH 0010/2353] Bump resteasy.version from 6.2.7.Final to 6.2.8.Final Bumps `resteasy.version` from 6.2.7.Final to 6.2.8.Final. Updates `org.jboss.resteasy:resteasy-bom` from 6.2.7.Final to 6.2.8.Final - [Release notes](https://github.com/resteasy/resteasy/releases) - [Commits](https://github.com/resteasy/resteasy/compare/6.2.7.Final...6.2.8.Final) Updates `org.jboss.resteasy:resteasy-core` from 6.2.7.Final to 6.2.8.Final - [Release notes](https://github.com/resteasy/resteasy/releases) - [Commits](https://github.com/resteasy/resteasy/compare/6.2.7.Final...6.2.8.Final) Updates `org.jboss.resteasy:resteasy-core-spi` from 6.2.7.Final to 6.2.8.Final - [Release notes](https://github.com/resteasy/resteasy/releases) - [Commits](https://github.com/resteasy/resteasy/compare/6.2.7.Final...6.2.8.Final) Updates `org.jboss.resteasy:resteasy-json-binding-provider` from 6.2.7.Final to 6.2.8.Final Updates `org.jboss.resteasy:resteasy-json-p-provider` from 6.2.7.Final to 6.2.8.Final Updates `org.jboss.resteasy:resteasy-jaxb-provider` from 6.2.7.Final to 6.2.8.Final Updates `org.jboss.resteasy:resteasy-jackson2-provider` from 6.2.7.Final to 6.2.8.Final Updates `org.jboss.resteasy:resteasy-rxjava2` from 6.2.7.Final to 6.2.8.Final - [Release notes](https://github.com/resteasy/resteasy/releases) - [Commits](https://github.com/resteasy/resteasy/compare/6.2.7.Final...6.2.8.Final) Updates `org.jboss.resteasy:resteasy-links` from 6.2.7.Final to 6.2.8.Final - [Release notes](https://github.com/resteasy/resteasy/releases) - [Commits](https://github.com/resteasy/resteasy/compare/6.2.7.Final...6.2.8.Final) --- updated-dependencies: - dependency-name: org.jboss.resteasy:resteasy-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jboss.resteasy:resteasy-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jboss.resteasy:resteasy-core-spi dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jboss.resteasy:resteasy-json-binding-provider dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jboss.resteasy:resteasy-json-p-provider dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jboss.resteasy:resteasy-jaxb-provider dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jboss.resteasy:resteasy-jackson2-provider dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jboss.resteasy:resteasy-rxjava2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jboss.resteasy:resteasy-links dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- .../java/io/quarkus/it/spring/data/jpa/CountryResourceTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 841bbd2f06c1f..646489e64e3ba 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -26,7 +26,7 @@ 1.1.5 2.1.5.Final 3.1.2.Final - 6.2.7.Final + 6.2.8.Final 0.33.0 0.2.4 0.1.15 diff --git a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/CountryResourceTest.java b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/CountryResourceTest.java index c3e5c3dafb9bf..3f77ae0ad5871 100644 --- a/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/CountryResourceTest.java +++ b/integration-tests/spring-data-jpa/src/test/java/io/quarkus/it/spring/data/jpa/CountryResourceTest.java @@ -80,7 +80,7 @@ void testGetOne() { .body(containsString("Greece")); when().get("/country/getOne/100").then() - .statusCode(500); + .statusCode(400); } @Test From 259a9e6464e943fb98bab512e982f4af3beebb26 Mon Sep 17 00:00:00 2001 From: Rolfe Dlugy-Hegwer Date: Wed, 17 Apr 2024 13:07:53 -0400 Subject: [PATCH 0011/2353] Tweak auth content based on QE feedback in rhbq docs --- .../security-authorize-web-endpoints-reference.adoc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index 5f3f37c8a39ae..a9f947132287f 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -446,7 +446,7 @@ quarkus.http.auth.policy.role-policy3.roles-allowed=admin quarkus.http.auth.permission.roles3.paths=/secured/admin/* quarkus.http.auth.permission.roles3.policy=role-policy3 ---- -<1> Role `root` will be able to access `/secured/user/*` and `/secured/admin/*` paths. +<1> Role `root` will be able to access `/secured/user/\*` and `/secured/admin/*` paths. <2> The `/secured/*` path can only be accessed by authenticated users. This way, you have secured the `/secured/all` path and so on. <3> Shared permissions are always applied before unshared ones, therefore a `SecurityIdentity` with the `root` role will have the `user` role as well. @@ -460,11 +460,11 @@ based on the common security annotations `@RolesAllowed`, `@DenyAll`, `@PermitAl [options="header"] |=== s| Annotation type s| Description -s| @DenyAll | Specifies that no security roles are allowed to invoke the specified methods. -s| @PermitAll | Specifies that all security roles are allowed to invoke the specified methods. +s| `@DenyAll` | Specifies that no security roles are allowed to invoke the specified methods. +s| `@PermitAll` | Specifies that all security roles are allowed to invoke the specified methods. `@PermitAll` lets everybody in, even without authentication. -s| @RolesAllowed | Specifies the list of security roles allowed to access methods in an application. +s| `@RolesAllowed` | Specifies the list of security roles allowed to access methods in an application. As an equivalent to `@RolesAllowed("**")`, {project-name} also provides the `io.quarkus.security.Authenticated` annotation that permits any authenticated user to access the resource. |=== @@ -540,7 +540,6 @@ However, if that property is not specified, a role named `User` is required as a |=== .Example of a property expressions usage in the `@RolesAllowed` annotation - [source,properties] ---- admin=Administrator @@ -551,6 +550,8 @@ tester.role=Tester all-roles=Administrator,Software,Tester,User ---- +[[subject-access-control-example]] +.Subject access control example [source,java] ---- import java.security.Principal; From c881b5f6a958595d75be0f8f10e785a7c8da33d6 Mon Sep 17 00:00:00 2001 From: Fouad Almalki Date: Wed, 17 Apr 2024 23:13:01 +0300 Subject: [PATCH 0012/2353] Adapt new behavior of System.console() since JDK22 --- .../io/quarkus/dev/console/BasicConsole.java | 2 +- .../quarkus/dev/console/QuarkusConsole.java | 2 +- .../io/quarkus/dev/console/TerminalUtils.java | 27 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 core/devmode-spi/src/main/java/io/quarkus/dev/console/TerminalUtils.java diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/console/BasicConsole.java b/core/devmode-spi/src/main/java/io/quarkus/dev/console/BasicConsole.java index f125065d0bccd..211e9aabc6ea0 100644 --- a/core/devmode-spi/src/main/java/io/quarkus/dev/console/BasicConsole.java +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/console/BasicConsole.java @@ -29,7 +29,7 @@ protected Boolean initialValue() { public BasicConsole(boolean color, boolean inputSupport, PrintStream printStream, Console console) { this(color, inputSupport, (s) -> { - if (console != null) { + if (TerminalUtils.isTerminal(console)) { console.writer().print(s); console.writer().flush(); } else { diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/console/QuarkusConsole.java b/core/devmode-spi/src/main/java/io/quarkus/dev/console/QuarkusConsole.java index c78f741202b86..a659d4324287b 100644 --- a/core/devmode-spi/src/main/java/io/quarkus/dev/console/QuarkusConsole.java +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/console/QuarkusConsole.java @@ -120,7 +120,7 @@ public static boolean hasColorSupport() { } else { // on sane operating systems having a console is a good indicator // you are attached to a TTY with colors. - return System.console() != null; + return TerminalUtils.isTerminal(System.console()); } } diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/console/TerminalUtils.java b/core/devmode-spi/src/main/java/io/quarkus/dev/console/TerminalUtils.java new file mode 100644 index 0000000000000..132b6ff13ac61 --- /dev/null +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/console/TerminalUtils.java @@ -0,0 +1,27 @@ +package io.quarkus.dev.console; + +import java.io.Console; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class TerminalUtils { + + private static final Logger LOGGER = Logger.getLogger(TerminalUtils.class.getName()); + + public static boolean isTerminal(Console console) { + if (console == null) { + return false; + } + + if (Runtime.version().feature() < 22) { // isTerminal was introduced in Java 22 + return true; + } + + try { + return (boolean) Console.class.getMethod("isTerminal").invoke(console); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to invoke System.console().isTerminal() via Reflection API", e); + return false; + } + } +} From 7f695ffa975a68f8b6737e7f262167031f6b83b1 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Tue, 16 Apr 2024 11:40:37 +1000 Subject: [PATCH 0013/2353] Rename webjars-locator to web-dependency-locator Signed-off-by: Phillip Kruger --- bom/application/pom.xml | 14 +++- devtools/bom-descriptor-json/pom.xml | 2 +- docs/pom.xml | 2 +- docs/src/main/asciidoc/http-reference.adoc | 82 ++++++++++++------- docs/src/main/asciidoc/web.adoc | 2 +- extensions/pom.xml | 2 +- .../deployment/pom.xml | 8 +- .../deployment/ImportMapBuildItem.java | 2 +- .../WebDependencyLocatorConfig.java} | 6 +- .../WebDependencyLocatorProcessor.java} | 14 ++-- .../deployment/devui/WebDependencyAsset.java} | 10 +-- .../WebDependencyLibrariesBuildItem.java | 23 ++++++ .../devui/WebDependencyLibrary.java | 32 ++++++++ ...DependencyLocatorDevModeApiProcessor.java} | 79 +++++++++--------- .../WebDependencyLocatorDevUIProcessor.java} | 28 +++---- .../qwc-web-dependency-locator-importmap.js} | 4 +- .../qwc-web-dependency-locator-libraries.js} | 20 ++--- .../locator/test/ImportMapTest.java | 4 +- .../locator/test/PostResource.java | 2 +- .../WebDependencyLocatorDevModeTest.java} | 4 +- .../WebDependencyLocatorRootPathTest.java} | 4 +- .../test/WebDependencyLocatorTest.java} | 4 +- .../WebDependencyLocatorTestSupport.java} | 4 +- .../pom.xml | 4 +- .../runtime/pom.xml | 8 +- .../WebDependencyLocatorRecorder.java} | 25 +++--- .../resources/META-INF/quarkus-extension.yaml | 8 +- .../devui/WebJarLibrariesBuildItem.java | 23 ------ .../deployment/devui/WebJarLibrary.java | 32 -------- relocations/generaterelocations.java | 5 ++ relocations/pom.xml | 2 + .../pom.xml | 22 +++++ relocations/quarkus-webjars-locator/pom.xml | 22 +++++ 33 files changed, 295 insertions(+), 208 deletions(-) rename extensions/{webjars-locator => web-dependency-locator}/deployment/pom.xml (93%) rename extensions/{webjars-locator/deployment/src/main/java/io/quarkus/webjar => web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency}/locator/deployment/ImportMapBuildItem.java (85%) rename extensions/{webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorConfig.java => web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java} (72%) rename extensions/{webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorStandaloneBuildStep.java => web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java} (94%) rename extensions/{webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarAsset.java => web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyAsset.java} (70%) create mode 100644 extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLibrariesBuildItem.java create mode 100644 extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLibrary.java rename extensions/{webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java => web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLocatorDevModeApiProcessor.java} (59%) rename extensions/{webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java => web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLocatorDevUIProcessor.java} (54%) rename extensions/{webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-importmap.js => web-dependency-locator/deployment/src/main/resources/dev-ui/qwc-web-dependency-locator-importmap.js} (87%) rename extensions/{webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-webjar-libraries.js => web-dependency-locator/deployment/src/main/resources/dev-ui/qwc-web-dependency-locator-libraries.js} (80%) rename extensions/{webjars-locator/deployment/src/test/java/io/quarkus/webjar => web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency}/locator/test/ImportMapTest.java (91%) rename extensions/{webjars-locator/deployment/src/test/java/io/quarkus/webjar => web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency}/locator/test/PostResource.java (88%) rename extensions/{webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorDevModeTest.java => web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorDevModeTest.java} (97%) rename extensions/{webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorRootPathTest.java => web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorRootPathTest.java} (95%) rename extensions/{webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTest.java => web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorTest.java} (95%) rename extensions/{webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTestSupport.java => web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorTestSupport.java} (76%) rename extensions/{webjars-locator => web-dependency-locator}/pom.xml (84%) rename extensions/{webjars-locator => web-dependency-locator}/runtime/pom.xml (84%) rename extensions/{webjars-locator/runtime/src/main/java/io/quarkus/webjar/locator/runtime/WebJarLocatorRecorder.java => web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java} (69%) rename extensions/{webjars-locator => web-dependency-locator}/runtime/src/main/resources/META-INF/quarkus-extension.yaml (51%) delete mode 100644 extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java delete mode 100644 extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrary.java create mode 100644 relocations/quarkus-webjars-locator-deployment/pom.xml create mode 100644 relocations/quarkus-webjars-locator/pom.xml diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 841bbd2f06c1f..0328d5e8631d9 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -3125,12 +3125,12 @@ io.quarkus - quarkus-webjars-locator + quarkus-web-dependency-locator ${project.version} io.quarkus - quarkus-webjars-locator-deployment + quarkus-web-dependency-locator-deployment ${project.version} @@ -6670,6 +6670,16 @@ quarkus-smallrye-reactive-messaging-rabbitmq-deployment ${project.version} + + io.quarkus + quarkus-webjars-locator + ${project.version} + + + io.quarkus + quarkus-webjars-locator-deployment + ${project.version} + diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 426891e36c7da..69722b1317e61 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -2867,7 +2867,7 @@ io.quarkus - quarkus-webjars-locator + quarkus-web-dependency-locator ${project.version} pom test diff --git a/docs/pom.xml b/docs/pom.xml index e70a317eb71e5..89070a2c285f9 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -2883,7 +2883,7 @@ io.quarkus - quarkus-webjars-locator-deployment + quarkus-web-dependency-locator-deployment ${project.version} pom test diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 5201ebbc4df5e..736b0649dc77d 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -10,8 +10,9 @@ include::_attributes.adoc[] :numbered: :sectnums: :sectnumlevels: 4 -:topics: http,web,webjars,vertx,servlet,undertow +:topics: http,web,webjars,mvnpm,vertx,servlet,undertow :extensions: io.quarkus:quarkus-vertx-http +:web-locator-ga: quarkus-web-dependency-locator This document clarifies different HTTP functionalities available in Quarkus. @@ -32,74 +33,95 @@ was chosen as it is the standard location for resources in `jar` files as define Quarkus can be used without Servlet, following this convention allows existing code that places its resources in this location to function correctly. -[[from-mvnpm]] -=== From mvnpm +=== From web dependencies like webjars or mvnpm -If you are using https://mvnpm.org/[mvnpm], as for the following JQuery dependency: +==== WebJars +If you are using https://www.webjars.org[WebJars], like the following JQuery one: [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- - org.mvnpm - bootstrap - 5.3.3 - runtime + org.webjars + jquery + 3.1.1 ---- [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -runtimeOnly("org.mvnpm:bootstrap:5.3.3") ----- - -You can import it in your HTML like this: -[source,html] ----- - +implementation("org.webjars:jquery:3.1.1") ---- - -[[from-webjars]] -=== From WebJars - -If you are using webjars, like the following JQuery one: +and rather write `/webjars/jquery/jquery.min.js` instead of `/webjars/jquery/3.1.1/jquery.min.js` +in your HTML files, you can add the `{web-locator-ga}` extension to your project. +To use it, add the following to your project's dependencies: [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- - org.webjars - jquery - 3.1.1 + io.quarkus + {web-locator-ga} ---- [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -implementation("org.webjars:jquery:3.1.1") +implementation("io.quarkus:{web-locator-ga}") ---- -and rather write `/webjars/jquery/jquery.min.js` instead of `/webjars/jquery/3.1.1/jquery.min.js` -in your HTML files, you can add the `quarkus-webjars-locator` extension to your project. -To use it, add the following to your project's dependencies: +==== Mvnpm + +If you are using https://mvnpm.org[mvnpm], like the following Lit one: [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] .pom.xml ---- - io.quarkus - quarkus-webjars-locator + org.mvnpm + lit + 3.1.2 ---- [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- -implementation("io.quarkus:quarkus-webjars-locator") +implementation("org.mvnpm:lit:3.1.2") +---- + +you can use the `{web-locator-ga}` as described above to reference the resource without the version, however with mvnpm you can +also use https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap[importmaps]. + +The importmap is generated by the `{web-locator-ga}` extension, and available at `/_importmap/generated_importmap.js`. +This mean adding the following to your `index.html` will allow you to import web libraries by name: + +[source,html] ---- + + + + + + My app + + <1> + + + + + +---- +<1> Use the generated importmap +<2> Import web libraries +<3> Import your own files, this can be done by adding `quarkus.web-dependency-locator.import-mappings.app/ = /app/` to the config. Any key-value pair can be added. + === From a local directory diff --git a/docs/src/main/asciidoc/web.adoc b/docs/src/main/asciidoc/web.adoc index 1f58a62ef29fb..932f265e1d194 100644 --- a/docs/src/main/asciidoc/web.adoc +++ b/docs/src/main/asciidoc/web.adoc @@ -28,7 +28,7 @@ You can find more information in the xref:http-reference#serving-static-resource However, if you want to insert scripts, styles, and libraries in your web pages, you have 3 options: a. Consume libraries from public CDNs such as cdnjs, unpkg, jsDelivr and more, or copy them to your `META-INF/resources` directory. -b. Use runtime web dependencies such as mvnpm.org or webjars, when added to your pom.xml or build.gradle they can be directly xref:http-reference#from-mvnpm[accessed from your web pages]. +b. Use runtime web dependencies such as mvnpm.org or webjars, when added to your pom.xml or build.gradle they can be directly xref:http-reference#mvnpm[accessed from your web pages]. c. Package your scripts (js, ts), styles (css, scss), and web dependencies together using a bundler (see xref:#bundling[below]). NOTE: *We recommend using a bundler for production* as it offers better control, consistency, security, and performance. The good news is that Quarkus makes it really easy and fast with the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Quarkus Web Bundler extension]. diff --git a/extensions/pom.xml b/extensions/pom.xml index ccd152cf03acc..64169dfa035a7 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -38,7 +38,7 @@ undertow websockets websockets-next - webjars-locator + web-dependency-locator resteasy-reactive reactive-routes apache-httpclient diff --git a/extensions/webjars-locator/deployment/pom.xml b/extensions/web-dependency-locator/deployment/pom.xml similarity index 93% rename from extensions/webjars-locator/deployment/pom.xml rename to extensions/web-dependency-locator/deployment/pom.xml index c7e61954ff64b..3c540aa3dcdd5 100644 --- a/extensions/webjars-locator/deployment/pom.xml +++ b/extensions/web-dependency-locator/deployment/pom.xml @@ -3,14 +3,14 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - quarkus-webjars-locator-parent + quarkus-web-dependency-locator-parent io.quarkus 999-SNAPSHOT 4.0.0 - quarkus-webjars-locator-deployment - Quarkus - WebJar Locator - Deployment + quarkus-web-dependency-locator-deployment + Quarkus - Web Dependency Locator - Deployment @@ -30,7 +30,7 @@ io.quarkus - quarkus-webjars-locator + quarkus-web-dependency-locator io.mvnpm diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/ImportMapBuildItem.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/ImportMapBuildItem.java similarity index 85% rename from extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/ImportMapBuildItem.java rename to extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/ImportMapBuildItem.java index 9b951366ab031..bef3aa6a6b9ba 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/ImportMapBuildItem.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/ImportMapBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.deployment; +package io.quarkus.webdependency.locator.deployment; import io.quarkus.builder.item.SimpleBuildItem; diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorConfig.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java similarity index 72% rename from extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorConfig.java rename to extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java index 2676e483b2c79..6592ea84704d1 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorConfig.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.deployment; +package io.quarkus.webdependency.locator.deployment; import java.util.Map; @@ -6,10 +6,10 @@ import io.quarkus.runtime.annotations.ConfigRoot; /** - * Build time configuration for WebJar Locator. + * Build time configuration for Web Dependency Locator. */ @ConfigRoot -public class WebJarLocatorConfig { +public class WebDependencyLocatorConfig { /** * If the version reroute is enabled. diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorStandaloneBuildStep.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java similarity index 94% rename from extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorStandaloneBuildStep.java rename to extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java index bf3af18e035a1..2841ccae3bfc1 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/WebJarLocatorStandaloneBuildStep.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.deployment; +package io.quarkus.webdependency.locator.deployment; import java.io.IOException; import java.io.UncheckedIOException; @@ -29,11 +29,11 @@ import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.webjar.locator.runtime.WebJarLocatorRecorder; +import io.quarkus.webdependency.locator.runtime.WebDependencyLocatorRecorder; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; -public class WebJarLocatorStandaloneBuildStep { +public class WebDependencyLocatorProcessor { private static final String WEBJARS_PREFIX = "META-INF/resources/webjars"; private static final String WEBJARS_NAME = "webjars"; @@ -41,18 +41,18 @@ public class WebJarLocatorStandaloneBuildStep { private static final String MVNPM_PREFIX = "META-INF/resources/_static"; private static final String MVNPM_NAME = "mvnpm"; - private static final Logger log = Logger.getLogger(WebJarLocatorStandaloneBuildStep.class.getName()); + private static final Logger log = Logger.getLogger(WebDependencyLocatorProcessor.class.getName()); @BuildStep @Record(ExecutionTime.RUNTIME_INIT) - public void findWebjarsAndCreateHandler( - WebJarLocatorConfig config, + public void findWebDependenciesAndCreateHandler( + WebDependencyLocatorConfig config, HttpBuildTimeConfig httpConfig, BuildProducer feature, BuildProducer routes, BuildProducer im, CurateOutcomeBuildItem curateOutcome, - WebJarLocatorRecorder recorder) throws Exception { + WebDependencyLocatorRecorder recorder) throws Exception { LibInfo webjarsLibInfo = getLibInfo(curateOutcome, WEBJARS_PREFIX, WEBJARS_NAME); LibInfo mvnpmNameLibInfo = getLibInfo(curateOutcome, MVNPM_PREFIX, MVNPM_NAME); diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarAsset.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyAsset.java similarity index 70% rename from extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarAsset.java rename to extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyAsset.java index 75b488dfd92bb..aa455eb2e5d90 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarAsset.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyAsset.java @@ -1,11 +1,11 @@ -package io.quarkus.webjar.locator.deployment.devui; +package io.quarkus.webdependency.locator.deployment.devui; import java.util.List; -public class WebJarAsset { +public class WebDependencyAsset { private String name; - private List children; + private List children; private boolean fileAsset; private String urlPart; @@ -17,11 +17,11 @@ public void setName(String name) { this.name = name; } - public List getChildren() { + public List getChildren() { return children; } - public void setChildren(List children) { + public void setChildren(List children) { this.children = children; } diff --git a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLibrariesBuildItem.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLibrariesBuildItem.java new file mode 100644 index 0000000000000..aaa5dfdcba7ed --- /dev/null +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLibrariesBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.webdependency.locator.deployment.devui; + +import java.util.List; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class WebDependencyLibrariesBuildItem extends MultiBuildItem { + private final String provider; + private final List webDependencyLibraries; + + public WebDependencyLibrariesBuildItem(String provider, List webDependencyLibraries) { + this.provider = provider; + this.webDependencyLibraries = webDependencyLibraries; + } + + public List getWebDependencyLibraries() { + return this.webDependencyLibraries; + } + + public String getProvider() { + return this.provider; + } +} diff --git a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLibrary.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLibrary.java new file mode 100644 index 0000000000000..899fb51901fcd --- /dev/null +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLibrary.java @@ -0,0 +1,32 @@ +package io.quarkus.webdependency.locator.deployment.devui; + +public class WebDependencyLibrary { + + private final String webDependencyName; + private String version; + private WebDependencyAsset rootAsset; // must be a list to work with vaadin-grid + + public WebDependencyLibrary(String webDependencyName) { + this.webDependencyName = webDependencyName; + } + + public String getWebDependencyName() { + return webDependencyName; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public WebDependencyAsset getRootAsset() { + return rootAsset; + } + + public void setRootAsset(WebDependencyAsset rootAsset) { + this.rootAsset = rootAsset; + } +} diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLocatorDevModeApiProcessor.java similarity index 59% rename from extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java rename to extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLocatorDevModeApiProcessor.java index cc2c688e56ea3..5a5ef624e6c2d 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevModeApiProcessor.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLocatorDevModeApiProcessor.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.deployment.devui; +package io.quarkus.webdependency.locator.deployment.devui; import java.io.IOException; import java.io.UncheckedIOException; @@ -28,79 +28,80 @@ import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -public class WebJarLocatorDevModeApiProcessor { +public class WebDependencyLocatorDevModeApiProcessor { private static final String PREFIX = "META-INF/resources/"; private static final String WEBJARS_PATH = "webjars"; private static final String MVNPM_PATH = "_static"; - private static final Logger log = Logger.getLogger(WebJarLocatorDevModeApiProcessor.class.getName()); + private static final Logger log = Logger.getLogger(WebDependencyLocatorDevModeApiProcessor.class.getName()); @BuildStep(onlyIf = IsDevelopment.class) - public void findWebjarsAssets( + public void findWebDependenciesAssets( HttpBuildTimeConfig httpConfig, CurateOutcomeBuildItem curateOutcome, - BuildProducer webJarLibrariesProducer) { + BuildProducer webDependencyLibrariesProducer) { - final List webJarLibraries = getLibraries(httpConfig, curateOutcome, WEBJARS_PATH); - webJarLibrariesProducer.produce(new WebJarLibrariesBuildItem("webjars", webJarLibraries)); + final List webJarLibraries = getLibraries(httpConfig, curateOutcome, WEBJARS_PATH); + webDependencyLibrariesProducer.produce(new WebDependencyLibrariesBuildItem("webjars", webJarLibraries)); - final List mvnpmLibraries = getLibraries(httpConfig, curateOutcome, MVNPM_PATH); - webJarLibrariesProducer.produce(new WebJarLibrariesBuildItem("mvnpm", mvnpmLibraries)); + final List mvnpmLibraries = getLibraries(httpConfig, curateOutcome, MVNPM_PATH); + webDependencyLibrariesProducer.produce(new WebDependencyLibrariesBuildItem("mvnpm", mvnpmLibraries)); } - private List getLibraries(HttpBuildTimeConfig httpConfig, + private List getLibraries(HttpBuildTimeConfig httpConfig, CurateOutcomeBuildItem curateOutcome, String path) { - final List webJarLibraries = new ArrayList<>(); + final List webDependencyLibraries = new ArrayList<>(); final List providers = QuarkusClassLoader.getElements(PREFIX + path, false); if (!providers.isEmpty()) { - // Map of webjar artifact keys to class path elements - final Map webJarKeys = providers.stream() + // Map of webDependency artifact keys to class path elements + final Map webDependencyKeys = providers.stream() .filter(provider -> provider.getDependencyKey() != null && provider.isRuntime()) .collect(Collectors.toMap(ClassPathElement::getDependencyKey, provider -> provider, (a, b) -> b, () -> new HashMap<>(providers.size()))); - if (!webJarKeys.isEmpty()) { + if (!webDependencyKeys.isEmpty()) { // The root path of the application final String rootPath = httpConfig.rootPath; - // The root path of the webjars - final String webjarRootPath = (rootPath.endsWith("/")) ? rootPath + path + "/" : rootPath + "/" + path + "/"; + // The root path of the webDependencies + final String webDependencyRootPath = (rootPath.endsWith("/")) ? rootPath + path + "/" + : rootPath + "/" + path + "/"; - // For each packaged webjar dependency, create a WebJarLibrary object + // For each packaged web dependency, create a WebDependencyLibrary object curateOutcome.getApplicationModel().getDependencies().stream() - .map(dep -> createWebJarLibrary(dep, webjarRootPath, webJarKeys, path)) - .filter(Objects::nonNull).forEach(webJarLibraries::add); + .map(dep -> createWebDependencyLibrary(dep, webDependencyRootPath, webDependencyKeys, path)) + .filter(Objects::nonNull).forEach(webDependencyLibraries::add); } } - return webJarLibraries; + return webDependencyLibraries; } - private WebJarLibrary createWebJarLibrary(ResolvedDependency dep, - String webjarRootPath, - Map webJarKeys, + private WebDependencyLibrary createWebDependencyLibrary(ResolvedDependency dep, + String webDependencyRootPath, + Map webDependencyKeys, String path) { // If the dependency is not a runtime class path dependency, return null if (!dep.isRuntimeCp()) { return null; } - final ClassPathElement provider = webJarKeys.get(dep.getKey()); + final ClassPathElement provider = webDependencyKeys.get(dep.getKey()); if (provider == null) { return null; } - final WebJarLibrary webJarLibrary = new WebJarLibrary(provider.getDependencyKey().getArtifactId()); + final WebDependencyLibrary webDependencyLibrary = new WebDependencyLibrary(provider.getDependencyKey().getArtifactId()); provider.apply(tree -> { - final Path webjarsDir = tree.getPath(PREFIX + path); + final Path webDependenciesDir = tree.getPath(PREFIX + path); final Path nameDir; - try (Stream webjarsDirPaths = Files.list(webjarsDir)) { - nameDir = webjarsDirPaths.filter(Files::isDirectory).findFirst().orElseThrow(() -> new IOException( - "Could not find name directory for " + dep.getKey().getArtifactId() + " in " + webjarsDir)); + try (Stream webDependenciesDirPaths = Files.list(webDependenciesDir)) { + nameDir = webDependenciesDirPaths.filter(Files::isDirectory).findFirst().orElseThrow(() -> new IOException( + "Could not find name directory for " + dep.getKey().getArtifactId() + " in " + webDependenciesDir)); } catch (IOException e) { throw new UncheckedIOException(e); } final Path versionDir; Path root = nameDir; - // The base URL for the webjar - final StringBuilder urlBase = new StringBuilder(webjarRootPath); + // The base URL for the Web Dependency + final StringBuilder urlBase = new StringBuilder(webDependencyRootPath); boolean appendRootPart = true; try { // If the version directory exists, use it as a root, otherwise use the name directory @@ -113,11 +114,11 @@ private WebJarLibrary createWebJarLibrary(ResolvedDependency dep, log.warn("Could not find version directory for " + dep.getKey().getArtifactId() + " " + dep.getVersion() + " in " + nameDir + ", falling back to name directory"); } - webJarLibrary.setVersion(dep.getVersion()); + webDependencyLibrary.setVersion(dep.getVersion()); try { - // Create the asset tree for the webjar and set it as the root asset + // Create the asset tree for the web dependency and set it as the root asset var asset = createAssetForLibrary(root, urlBase.toString(), appendRootPart); - webJarLibrary.setRootAsset(asset); + webDependencyLibrary.setRootAsset(asset); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -125,13 +126,13 @@ private WebJarLibrary createWebJarLibrary(ResolvedDependency dep, return null; }); - return webJarLibrary; + return webDependencyLibrary; } - private WebJarAsset createAssetForLibrary(Path rootPath, String urlBase, boolean appendRootPart) + private WebDependencyAsset createAssetForLibrary(Path rootPath, String urlBase, boolean appendRootPart) throws IOException { //If it is a directory, go deeper, otherwise add the file - var root = new WebJarAsset(); + var root = new WebDependencyAsset(); root.setName(rootPath.getFileName().toString()); root.setChildren(new LinkedList<>()); root.setFileAsset(false); @@ -143,7 +144,7 @@ private WebJarAsset createAssetForLibrary(Path rootPath, String urlBase, boolean var childDir = createAssetForLibrary(childPath, urlBase, true); root.getChildren().add(childDir); } else { - var childFile = new WebJarAsset(); + var childFile = new WebDependencyAsset(); childFile.setName(childPath.getFileName().toString()); childFile.setFileAsset(true); childFile.setUrlPart(urlBase + childFile.getName()); @@ -152,7 +153,7 @@ private WebJarAsset createAssetForLibrary(Path rootPath, String urlBase, boolean } } // Sort the children by name - root.getChildren().sort(Comparator.comparing(WebJarAsset::getName)); + root.getChildren().sort(Comparator.comparing(WebDependencyAsset::getName)); return root; } diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLocatorDevUIProcessor.java similarity index 54% rename from extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java rename to extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLocatorDevUIProcessor.java index c13cf38105351..d4079a352e21d 100644 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLocatorDevUIProcessor.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/devui/WebDependencyLocatorDevUIProcessor.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.deployment.devui; +package io.quarkus.webdependency.locator.deployment.devui; import java.util.ArrayList; import java.util.List; @@ -9,38 +9,38 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.Page; -import io.quarkus.webjar.locator.deployment.ImportMapBuildItem; +import io.quarkus.webdependency.locator.deployment.ImportMapBuildItem; -public class WebJarLocatorDevUIProcessor { +public class WebDependencyLocatorDevUIProcessor { @BuildStep(onlyIf = IsDevelopment.class) public void createPages(BuildProducer cardPageProducer, - List webJarLibrariesBuildItems, + List webDependencyLibrariesBuildItems, Optional importMapBuildItem) { - List webJarLibraries = new ArrayList<>(); - for (WebJarLibrariesBuildItem webJarLibrariesBuildItem : webJarLibrariesBuildItems) { - webJarLibraries.addAll(webJarLibrariesBuildItem.getWebJarLibraries()); + List webDependencyLibraries = new ArrayList<>(); + for (WebDependencyLibrariesBuildItem webDependencyLibrariesBuildItem : webDependencyLibrariesBuildItems) { + webDependencyLibraries.addAll(webDependencyLibrariesBuildItem.getWebDependencyLibraries()); } CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); - if (!webJarLibraries.isEmpty()) { - // WebJar Libraries - cardPageBuildItem.addBuildTimeData("webJarLibraries", webJarLibraries); + if (!webDependencyLibraries.isEmpty()) { + // Web Dependency Libraries + cardPageBuildItem.addBuildTimeData("webDependencyLibraries", webDependencyLibraries); - // WebJar Asset List + // Web Dependency Asset List cardPageBuildItem.addPage(Page.webComponentPageBuilder() - .componentLink("qwc-webjar-locator-webjar-libraries.js") + .componentLink("qwc-web-dependency-locator-libraries.js") .title("Web libraries") .icon("font-awesome-solid:folder-tree") - .staticLabel(String.valueOf(webJarLibraries.size()))); + .staticLabel(String.valueOf(webDependencyLibraries.size()))); if (importMapBuildItem.isPresent()) { cardPageBuildItem.addBuildTimeData("importMap", importMapBuildItem.get().getImportMap()); // ImportMap cardPageBuildItem.addPage(Page.webComponentPageBuilder() - .componentLink("qwc-webjar-locator-importmap.js") + .componentLink("qwc-web-dependency-locator-importmap.js") .title("Import Map") .icon("font-awesome-solid:diagram-project")); diff --git a/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-importmap.js b/extensions/web-dependency-locator/deployment/src/main/resources/dev-ui/qwc-web-dependency-locator-importmap.js similarity index 87% rename from extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-importmap.js rename to extensions/web-dependency-locator/deployment/src/main/resources/dev-ui/qwc-web-dependency-locator-importmap.js index fa732bb3ff901..7e61772282b07 100644 --- a/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-importmap.js +++ b/extensions/web-dependency-locator/deployment/src/main/resources/dev-ui/qwc-web-dependency-locator-importmap.js @@ -3,7 +3,7 @@ import {importMap} from 'build-time-data'; import '@quarkus-webcomponents/codeblock'; -export class QwcWebjarLocatorImportmap extends LitElement { +export class QwcWebDependencyLocatorImportmap extends LitElement { static styles = css` :host{ @@ -45,4 +45,4 @@ export class QwcWebjarLocatorImportmap extends LitElement { } } -customElements.define('qwc-webjar-locator-importmap', QwcWebjarLocatorImportmap) \ No newline at end of file +customElements.define('qwc-web-dependency-locator-importmap', QwcWebDependencyLocatorImportmap) \ No newline at end of file diff --git a/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-webjar-libraries.js b/extensions/web-dependency-locator/deployment/src/main/resources/dev-ui/qwc-web-dependency-locator-libraries.js similarity index 80% rename from extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-webjar-libraries.js rename to extensions/web-dependency-locator/deployment/src/main/resources/dev-ui/qwc-web-dependency-locator-libraries.js index 576e6fe5d75ec..c74d9fc1261f9 100644 --- a/extensions/webjars-locator/deployment/src/main/resources/dev-ui/qwc-webjar-locator-webjar-libraries.js +++ b/extensions/web-dependency-locator/deployment/src/main/resources/dev-ui/qwc-web-dependency-locator-libraries.js @@ -1,5 +1,5 @@ import {LitElement, html, css} from 'lit'; -import {webJarLibraries} from 'build-time-data'; +import {webDependencyLibraries} from 'build-time-data'; import '@vaadin/tabsheet'; import '@vaadin/tabs'; import '@vaadin/grid'; @@ -10,7 +10,7 @@ import {notifier} from 'notifier'; import {columnBodyRenderer} from '@vaadin/grid/lit.js'; -export class QwcWebjarLocatorWebjarLibraries extends LitElement { +export class QwcWebDependencyLocatorLibraries extends LitElement { static styles = css` .full-height { @@ -19,25 +19,25 @@ export class QwcWebjarLocatorWebjarLibraries extends LitElement { `; static properties = { - _webJarLibraries: {}, + _webDependencyLibraries: {}, }; constructor() { super(); - this._webJarLibraries = webJarLibraries; + this._webDependencyLibraries = webDependencyLibraries; } render() { return html` - ${this._webJarLibraries.map(webjar => html` - - ${webjar.webJarName + " (" + webjar.version + ")"} + ${this._webDependencyLibraries.map(webDependency => html` + + ${webDependency.webDependencyName + " (" + webDependency.version + ")"} `)} - ${this._webJarLibraries.map(webjar => this._renderLibraryAssets(webjar))} + ${this._webDependencyLibraries.map(webDependency => this._renderLibraryAssets(webDependency))} @@ -54,7 +54,7 @@ export class QwcWebjarLocatorWebjarLibraries extends LitElement { }; return html` -

+
@@ -101,4 +101,4 @@ export class QwcWebjarLocatorWebjarLibraries extends LitElement { } -customElements.define('qwc-webjar-locator-webjar-libraries', QwcWebjarLocatorWebjarLibraries) \ No newline at end of file +customElements.define('qwc-web-dependency-locator-libraries', QwcWebDependencyLocatorLibraries) \ No newline at end of file diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/ImportMapTest.java b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/ImportMapTest.java similarity index 91% rename from extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/ImportMapTest.java rename to extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/ImportMapTest.java index 02f01cb64b614..d8e33b7463bf1 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/ImportMapTest.java +++ b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/ImportMapTest.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.test; +package io.quarkus.webdependency.locator.test; import static org.hamcrest.Matchers.containsString; @@ -12,7 +12,7 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; -public class ImportMapTest extends WebJarLocatorTestSupport { +public class ImportMapTest extends WebDependencyLocatorTestSupport { private static final String META_INF_RESOURCES = "META-INF/resources/"; @RegisterExtension diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/PostResource.java b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/PostResource.java similarity index 88% rename from extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/PostResource.java rename to extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/PostResource.java index f595f6478dcdf..60f13a0ab3913 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/PostResource.java +++ b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/PostResource.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.test; +package io.quarkus.webdependency.locator.test; import jakarta.annotation.PreDestroy; import jakarta.ws.rs.POST; diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorDevModeTest.java b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorDevModeTest.java similarity index 97% rename from extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorDevModeTest.java rename to extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorDevModeTest.java index fad3ee7262948..104155f574d27 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorDevModeTest.java +++ b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorDevModeTest.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.test; +package io.quarkus.webdependency.locator.test; import static org.hamcrest.core.Is.is; @@ -10,7 +10,7 @@ import io.quarkus.test.QuarkusDevModeTest; import io.restassured.RestAssured; -public class WebJarLocatorDevModeTest extends WebJarLocatorTestSupport { +public class WebDependencyLocatorDevModeTest extends WebDependencyLocatorTestSupport { private static final String META_INF_RESOURCES = "META-INF/resources/"; @RegisterExtension diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorRootPathTest.java b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorRootPathTest.java similarity index 95% rename from extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorRootPathTest.java rename to extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorRootPathTest.java index 6efa49606eb43..013dd4bba1e3f 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorRootPathTest.java +++ b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorRootPathTest.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.test; +package io.quarkus.webdependency.locator.test; import static org.hamcrest.core.Is.is; @@ -12,7 +12,7 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; -public class WebJarLocatorRootPathTest extends WebJarLocatorTestSupport { +public class WebDependencyLocatorRootPathTest extends WebDependencyLocatorTestSupport { private static final String META_INF_RESOURCES = "META-INF/resources/"; @RegisterExtension diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTest.java b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorTest.java similarity index 95% rename from extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTest.java rename to extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorTest.java index 715a2d042f187..9bd790756e09d 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTest.java +++ b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorTest.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.test; +package io.quarkus.webdependency.locator.test; import static org.hamcrest.core.Is.is; @@ -12,7 +12,7 @@ import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; -public class WebJarLocatorTest extends WebJarLocatorTestSupport { +public class WebDependencyLocatorTest extends WebDependencyLocatorTestSupport { private static final String META_INF_RESOURCES = "META-INF/resources/"; @RegisterExtension diff --git a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTestSupport.java b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorTestSupport.java similarity index 76% rename from extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTestSupport.java rename to extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorTestSupport.java index 0afe0cfae437c..64dce233c1ea0 100644 --- a/extensions/webjars-locator/deployment/src/test/java/io/quarkus/webjar/locator/test/WebJarLocatorTestSupport.java +++ b/extensions/web-dependency-locator/deployment/src/test/java/io/quarkus/webdependency/locator/test/WebDependencyLocatorTestSupport.java @@ -1,6 +1,6 @@ -package io.quarkus.webjar.locator.test; +package io.quarkus.webdependency.locator.test; -class WebJarLocatorTestSupport { +class WebDependencyLocatorTestSupport { static final String JQUERY_UI_VERSION = System.getProperty("webjar.jquery-ui.version"); static final String MOMENTJS_VERSION = System.getProperty("webjar.momentjs.version"); diff --git a/extensions/webjars-locator/pom.xml b/extensions/web-dependency-locator/pom.xml similarity index 84% rename from extensions/webjars-locator/pom.xml rename to extensions/web-dependency-locator/pom.xml index 62e69ce2dd90b..b856aa3b890f9 100644 --- a/extensions/webjars-locator/pom.xml +++ b/extensions/web-dependency-locator/pom.xml @@ -10,8 +10,8 @@ 4.0.0 - quarkus-webjars-locator-parent - Quarkus - WebJar Locator + quarkus-web-dependency-locator-parent + Quarkus - Web Dependency Locator pom deployment diff --git a/extensions/webjars-locator/runtime/pom.xml b/extensions/web-dependency-locator/runtime/pom.xml similarity index 84% rename from extensions/webjars-locator/runtime/pom.xml rename to extensions/web-dependency-locator/runtime/pom.xml index 5b7b55e4cdcb1..51703d373ba25 100644 --- a/extensions/webjars-locator/runtime/pom.xml +++ b/extensions/web-dependency-locator/runtime/pom.xml @@ -3,15 +3,15 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - quarkus-webjars-locator-parent + quarkus-web-dependency-locator-parent io.quarkus 999-SNAPSHOT 4.0.0 - quarkus-webjars-locator - Quarkus - WebJar Locator - Runtime - Simplify paths for WebJar dependencies + quarkus-web-dependency-locator + Quarkus - Web Dependency Locator - Runtime + Simplify paths and importmap support for Web dependencies diff --git a/extensions/webjars-locator/runtime/src/main/java/io/quarkus/webjar/locator/runtime/WebJarLocatorRecorder.java b/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java similarity index 69% rename from extensions/webjars-locator/runtime/src/main/java/io/quarkus/webjar/locator/runtime/WebJarLocatorRecorder.java rename to extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java index 55f22be5cf44a..2e27661e4480a 100644 --- a/extensions/webjars-locator/runtime/src/main/java/io/quarkus/webjar/locator/runtime/WebJarLocatorRecorder.java +++ b/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java @@ -1,4 +1,4 @@ -package io.quarkus.webjar.locator.runtime; +package io.quarkus.webdependency.locator.runtime; import java.util.Map; @@ -9,32 +9,33 @@ import io.vertx.ext.web.RoutingContext; @Recorder -public class WebJarLocatorRecorder { +public class WebDependencyLocatorRecorder { - public Handler getHandler(String webjarsRootUrl, Map webjarNameToVersionMap) { + public Handler getHandler(String webDependenciesRootUrl, + Map webDependencyNameToVersionMap) { return (event) -> { String path = event.normalizedPath(); - if (path.startsWith(webjarsRootUrl)) { - String rest = path.substring(webjarsRootUrl.length()); - String webjar = rest.substring(0, rest.indexOf('/')); - if (webjarNameToVersionMap.containsKey(webjar)) { + if (path.startsWith(webDependenciesRootUrl)) { + String rest = path.substring(webDependenciesRootUrl.length()); + String webdep = rest.substring(0, rest.indexOf('/')); + if (webDependencyNameToVersionMap.containsKey(webdep)) { // Check this is not the actual path (ex: /webjars/jquery/${jquery.version}/... int endOfVersion = rest.indexOf('/', rest.indexOf('/') + 1); if (endOfVersion == -1) { endOfVersion = rest.length(); } String nextPathEntry = rest.substring(rest.indexOf('/') + 1, endOfVersion); - if (webjarNameToVersionMap.get(webjar) == null - || nextPathEntry.equals(webjarNameToVersionMap.get(webjar))) { + if (webDependencyNameToVersionMap.get(webdep) == null + || nextPathEntry.equals(webDependencyNameToVersionMap.get(webdep))) { // go to the next handler (which should be the static resource handler, if one exists) event.next(); } else { // reroute to the real resource - event.reroute(webjarsRootUrl + webjar + "/" - + webjarNameToVersionMap.get(webjar) + rest.substring(rest.indexOf('/'))); + event.reroute(webDependenciesRootUrl + webdep + "/" + + webDependencyNameToVersionMap.get(webdep) + rest.substring(rest.indexOf('/'))); } } else { - // this is not a webjar that we know about + // this is not a web dependency that we know about event.fail(404); } } else { diff --git a/extensions/webjars-locator/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/web-dependency-locator/runtime/src/main/resources/META-INF/quarkus-extension.yaml similarity index 51% rename from extensions/webjars-locator/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename to extensions/web-dependency-locator/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 3ad6eb5bec22d..ff9725c2b3805 100644 --- a/extensions/webjars-locator/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/web-dependency-locator/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -1,12 +1,14 @@ --- artifact: ${project.groupId}:${project.artifactId}:${project.version} -name: "WebJar Locator" +name: "Web Dependency Locator" metadata: - short-name: "webjars-locator" + short-name: "web-dependency-locator" keywords: - "web" - "webjar" - guide: "https://quarkus.io/guides/http-reference#webjar-locator-support" + - "mvnpm" + - "importmap" + guide: "https://quarkus.io/guides/http-reference" categories: - "web" status: "stable" diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java deleted file mode 100644 index 709d0d21f88c0..0000000000000 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrariesBuildItem.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.quarkus.webjar.locator.deployment.devui; - -import java.util.List; - -import io.quarkus.builder.item.MultiBuildItem; - -public final class WebJarLibrariesBuildItem extends MultiBuildItem { - private final String provider; - private final List webJarLibraries; - - public WebJarLibrariesBuildItem(String provider, List webJarLibraries) { - this.provider = provider; - this.webJarLibraries = webJarLibraries; - } - - public List getWebJarLibraries() { - return this.webJarLibraries; - } - - public String getProvider() { - return this.provider; - } -} diff --git a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrary.java b/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrary.java deleted file mode 100644 index 27138ebc39448..0000000000000 --- a/extensions/webjars-locator/deployment/src/main/java/io/quarkus/webjar/locator/deployment/devui/WebJarLibrary.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.quarkus.webjar.locator.deployment.devui; - -public class WebJarLibrary { - - private final String webJarName; - private String version; - private WebJarAsset rootAsset; // must be a list to work with vaadin-grid - - public WebJarLibrary(String webJarName) { - this.webJarName = webJarName; - } - - public String getWebJarName() { - return webJarName; - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public WebJarAsset getRootAsset() { - return rootAsset; - } - - public void setRootAsset(WebJarAsset rootAsset) { - this.rootAsset = rootAsset; - } -} diff --git a/relocations/generaterelocations.java b/relocations/generaterelocations.java index 8c8042e5b01f1..cce239e9ff322 100755 --- a/relocations/generaterelocations.java +++ b/relocations/generaterelocations.java @@ -144,6 +144,11 @@ public class generaterelocations implements Runnable { RELOCATIONS.put("quarkus-smallrye-reactive-messaging-mqtt-deployment", smallryeReactiveMessagingRelocation); RELOCATIONS.put("quarkus-smallrye-reactive-messaging-rabbitmq", smallryeReactiveMessagingRelocation); RELOCATIONS.put("quarkus-smallrye-reactive-messaging-rabbitmq-deployment", smallryeReactiveMessagingRelocation); + + Function webjarsLocatorRelocation = a -> Relocation.ofArtifactId(a, a.replace("webjars-locator", "web-dependency-locator"), + "3.10"); + RELOCATIONS.put("quarkus-webjars-locator", webjarsLocatorRelocation); + RELOCATIONS.put("quarkus-webjars-locator-deployment", webjarsLocatorRelocation); } private static final String RELOCATION_POM_TEMPLATE = "\n" + // diff --git a/relocations/pom.xml b/relocations/pom.xml index 5f147f47c930a..74c679ecd5ea4 100644 --- a/relocations/pom.xml +++ b/relocations/pom.xml @@ -100,6 +100,8 @@ quarkus-smallrye-reactive-messaging-pulsar-deployment quarkus-smallrye-reactive-messaging-rabbitmq quarkus-smallrye-reactive-messaging-rabbitmq-deployment + quarkus-webjars-locator + quarkus-webjars-locator-deployment diff --git a/relocations/quarkus-webjars-locator-deployment/pom.xml b/relocations/quarkus-webjars-locator-deployment/pom.xml new file mode 100644 index 0000000000000..a555a5b09851b --- /dev/null +++ b/relocations/quarkus-webjars-locator-deployment/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-relocations-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-webjars-locator-deployment + + + + io.quarkus + quarkus-web-dependency-locator-deployment + ${project.version} + Update the artifactId in your project build file. Refer to https://github.com/quarkusio/quarkus/wiki/Migration-Guide-3.10 for more information. + + + \ No newline at end of file diff --git a/relocations/quarkus-webjars-locator/pom.xml b/relocations/quarkus-webjars-locator/pom.xml new file mode 100644 index 0000000000000..1985d8b5e21ea --- /dev/null +++ b/relocations/quarkus-webjars-locator/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-relocations-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-webjars-locator + + + + io.quarkus + quarkus-web-dependency-locator + ${project.version} + Update the artifactId in your project build file. Refer to https://github.com/quarkusio/quarkus/wiki/Migration-Guide-3.10 for more information. + + + \ No newline at end of file From fc126279f63418949a9b903d009884ccee8a7f3f Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 18 Apr 2024 08:34:07 +0200 Subject: [PATCH 0014/2353] Make test release workflow more consistent with release --- .github/workflows/release-build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 33adbbdecaa23..c7ebbe6f2f743 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -30,10 +30,14 @@ jobs: run: | ./mvnw --settings .github/mvn-settings.xml \ -B \ + -Dscan=false \ + -Dno-build-cache \ + -Dgradle.cache.local.enabled=false \ + -Dgradle.cache.remote.enabled=false \ -Prelease \ -DskipTests -DskipITs \ -Ddokka \ - -Dmaven.repo.local=$HOME/release/repository \ + -Dno-test-modules \ -Dgpg.skip \ clean install - name: Report From e44d7a70e4dd19e939a4ee2d312364479909d431 Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Thu, 18 Apr 2024 11:12:48 +0200 Subject: [PATCH 0015/2353] Update kafka doc: Emitter `Emitter` is not an annotation. --- docs/src/main/asciidoc/kafka.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index a9d105aa244ab..ff532b0c198ab 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -1017,7 +1017,7 @@ In this case the producer will use this method as generator to create an infinit @Outgoing("prices-out") CompletionStage> generate(); ---- -=== Sending messages with @Emitter +=== Sending messages with Emitter Sometimes, you need to have an imperative way of sending messages. From 9840da101ce9e351b433b5112bc2a9148e5069e3 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 18 Apr 2024 12:58:08 +0200 Subject: [PATCH 0016/2353] Qute type-safe messages: add convenient way to localize enum constants - resolves #40089 --- docs/src/main/asciidoc/qute-reference.adoc | 34 +++ .../MessageBundleMethodBuildItem.java | 31 +- .../deployment/MessageBundleProcessor.java | 281 ++++++++++++++++-- .../qute/deployment/QuteProcessor.java | 14 +- .../i18n/MessageBundleEnumTest.java | 74 +++++ .../i18n/MessageBundleLogicalLineTest.java | 10 +- .../deployment/i18n/MessageBundleTest.java | 2 +- .../quarkus/qute/deployment/i18n/MyEnum.java | 10 + .../test/resources/messages/enu.properties | 13 + .../test/resources/messages/enu_cs.properties | 13 + .../java/io/quarkus/qute/i18n/Message.java | 29 +- 11 files changed, 462 insertions(+), 49 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java create mode 100644 extensions/qute/deployment/src/test/resources/messages/enu.properties create mode 100644 extensions/qute/deployment/src/test/resources/messages/enu_cs.properties diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index dea89afaf0357..8b31a7c50f139 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -2858,6 +2858,40 @@ public class MyBean { ---- <1> The annotation value is a locale tag string (IETF). +===== Enums + +There is a convenient way to localize enums. +If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined: + +[source,java] +---- +@Message <1> +String methodName(MyEnum enum); +---- +<1> The value is intentionally not provided. There's also no key for the method in a localized file. + +Then it receives a generated template: + +[source,html] +---- +{#when enumParamName} + {#is CONSTANT1}{msg:methodName_CONSTANT1} + {#is CONSTANT2}{msg:methodName_CONSTANT2} +{/when} +---- + +Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and values for all constant message keys: + +[source,poperties] +---- +methodName_CONSTANT1=Value 1 +methodName_CONSTANT2=Value 2 +---- + +In a template, an enum constant can be localized with a message bundle method like `{msg:methodName(enumConstant)}`. + +TIP: There is also <> - a convenient annotation to access enum constants in a template. + ==== Message Templates Every method of a message bundle interface must define a message template. The value is normally defined by `io.quarkus.qute.i18n.Message#value()`, diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java index 3509d439e5821..56809719f7b0a 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleMethodBuildItem.java @@ -3,11 +3,12 @@ import org.jboss.jandex.MethodInfo; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; /** * Represents a message bundle method. *

- * Note that templates that contain no expressions don't need to be validated. + * Note that templates that contain no expressions/sections don't need to be validated. */ public final class MessageBundleMethodBuildItem extends MultiBuildItem { @@ -36,14 +37,27 @@ public String getKey() { return key; } + /** + * + * @return the template id or {@code null} if there is no need to use qute; i.e. no expression/section found + */ public String getTemplateId() { return templateId; } + /** + * For example, there is no corresponding method for generated enum constant message keys. + * + * @return the method or {@code null} if there is no corresponding method declared on the message bundle interface + */ public MethodInfo getMethod() { return method; } + public boolean hasMethod() { + return method != null; + } + public String getTemplate() { return template; } @@ -65,4 +79,19 @@ public boolean isDefaultBundle() { return isDefaultBundle; } + /** + * + * @return the path + * @see TemplateAnalysis#path + */ + public String getPathForAnalysis() { + if (method != null) { + return method.declaringClass().name() + "#" + method.name(); + } + if (templateId != null) { + return templateId; + } + return bundleName + "_" + key; + } + } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index 56e4529393b9b..3b45e64f29e94 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -38,6 +38,7 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassInfo.NestingType; import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; @@ -85,6 +86,8 @@ import io.quarkus.qute.Namespaces; import io.quarkus.qute.Resolver; import io.quarkus.qute.SectionHelperFactory; +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.deployment.QuteProcessor.JavaMemberLookupConfig; import io.quarkus.qute.deployment.QuteProcessor.MatchResult; import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis; @@ -266,7 +269,7 @@ List processBundles(BeanArchiveIndexBuildItem beanArchiv // Generate implementations // name -> impl class Map generatedImplementations = generateImplementations(bundles, generatedClasses, - messageTemplateMethods); + messageTemplateMethods, index); // Register synthetic beans for (MessageBundleBuildItem bundle : bundles) { @@ -393,8 +396,11 @@ void validateMessageBundleMethods(TemplatesAnalysisBuildItem templatesAnalysis, if (messageBundleMethod != null) { // All top-level expressions without a namespace should be mapped to a param Set usedParamNames = new HashSet<>(); - Set paramNames = IntStream.range(0, messageBundleMethod.getMethod().parametersCount()) - .mapToObj(idx -> getParameterName(messageBundleMethod.getMethod(), idx)).collect(Collectors.toSet()); + Set paramNames = messageBundleMethod.hasMethod() + ? IntStream.range(0, messageBundleMethod.getMethod().parametersCount()) + .mapToObj(idx -> getParameterName(messageBundleMethod.getMethod(), idx)) + .collect(Collectors.toSet()) + : Set.of(); for (Expression expression : analysis.expressions) { validateExpression(incorrectExpressions, messageBundleMethod, expression, paramNames, usedParamNames, globals); @@ -431,9 +437,8 @@ private void validateExpression(BuildProducer inco // Expression has no type info or type info that does not match a method parameter // expressions that have incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(), - name + " is not a parameter of the message bundle method " - + messageBundleMethod.getMethod().declaringClass().name() + "#" - + messageBundleMethod.getMethod().name() + "()", + name + " is not a parameter of the message bundle method: " + + messageBundleMethod.getPathForAnalysis(), expression.getOrigin())); } else { usedParamNames.add(name); @@ -568,6 +573,10 @@ public String apply(String id) { MethodInfo method = methods.get(methodPart.getName()); if (method == null) { + if (methods.containsKey(methodPart.getName())) { + // Skip validation - enum constant key + continue; + } if (!methodPart.isVirtualMethod() || methodPart.asVirtualMethod().getParameters().isEmpty()) { // The method template may contain no expressions method = defaultBundleInterface.method(methodPart.getName()); @@ -690,7 +699,8 @@ void generateExamplePropertiesFiles(List messageBu private Map generateImplementations(List bundles, BuildProducer generatedClasses, - BuildProducer messageTemplateMethods) throws IOException { + BuildProducer messageTemplateMethods, + IndexView index) throws IOException { Map generatedTypes = new HashMap<>(); @@ -701,29 +711,33 @@ private Map generateImplementations(List // take message templates not specified by Message#value from corresponding localized file Map defaultKeyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, - bundle.getDefaultLocale(), bundleInterface.methods(), null); + bundle.getDefaultLocale(), bundleInterface.methods(), null, index); MergeClassInfoWrapper bundleInterfaceWrapper = new MergeClassInfoWrapper(bundleInterface, null, null); + // Generate implementation for the default bundle interface String bundleImpl = generateImplementation(bundle, null, null, bundleInterfaceWrapper, - defaultClassOutput, messageTemplateMethods, defaultKeyToMap, null); + defaultClassOutput, messageTemplateMethods, defaultKeyToMap, null, index); generatedTypes.put(bundleInterface.name().toString(), bundleImpl); + + // Generate imeplementation for each localized interface for (Entry entry : bundle.getLocalizedInterfaces().entrySet()) { ClassInfo localizedInterface = entry.getValue(); // take message templates not specified by Message#value from corresponding localized file Map keyToMap = getLocalizedFileKeyToTemplate(bundle, bundleInterface, entry.getKey(), - localizedInterface.methods(), localizedInterface); + localizedInterface.methods(), localizedInterface, index); MergeClassInfoWrapper localizedInterfaceWrapper = new MergeClassInfoWrapper(localizedInterface, bundleInterface, keyToMap); generatedTypes.put(entry.getValue().name().toString(), generateImplementation(bundle, bundleInterface, bundleImpl, localizedInterfaceWrapper, - defaultClassOutput, messageTemplateMethods, keyToMap, null)); + defaultClassOutput, messageTemplateMethods, keyToMap, null, index)); } + // Generate implementation for each localized file for (Entry entry : bundle.getLocalizedFiles().entrySet()) { Path localizedFile = entry.getValue(); - var keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); + var keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile, index); String locale = entry.getKey(); ClassOutput localeAwareGizmoAdaptor = new GeneratedClassGizmoAdaptor(generatedClasses, @@ -739,19 +753,19 @@ public String apply(String className) { })); generatedTypes.put(localizedFile.toString(), generateImplementation(bundle, bundleInterface, bundleImpl, new SimpleClassInfoWrapper(bundleInterface), - localeAwareGizmoAdaptor, messageTemplateMethods, keyToTemplate, locale)); + localeAwareGizmoAdaptor, messageTemplateMethods, keyToTemplate, locale, index)); } } return generatedTypes; } private Map getLocalizedFileKeyToTemplate(MessageBundleBuildItem bundle, - ClassInfo bundleInterface, String locale, List methods, ClassInfo localizedInterface) + ClassInfo bundleInterface, String locale, List methods, ClassInfo localizedInterface, IndexView index) throws IOException { Path localizedFile = bundle.getMergeCandidates().get(locale); if (localizedFile != null) { - Map keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile); + Map keyToTemplate = parseKeyToTemplateFromLocalizedFile(bundleInterface, localizedFile, index); if (!keyToTemplate.isEmpty()) { // keep message templates if value wasn't provided by Message#value @@ -785,7 +799,7 @@ private Map getLocalizedFileKeyToTemplate(MessageBundleBuildItem } private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundleInterface, - Path localizedFile) throws IOException { + Path localizedFile, IndexView index) throws IOException { Map keyToTemplate = new HashMap<>(); for (ListIterator it = Files.readAllLines(localizedFile).listIterator(); it.hasNext();) { String line = it.next(); @@ -804,7 +818,7 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle "Missing key/value separator\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); } String key = line.substring(0, eqIdx).strip(); - if (!hasMessageBundleMethod(bundleInterface, key)) { + if (!hasMessageBundleMethod(bundleInterface, key) && !isEnumConstantMessageKey(key, index, bundleInterface)) { throw new MessageBundleException( "Message bundle method " + key + "() not found on: " + bundleInterface + "\n\t- file: " + localizedFile + "\n\t- line " + it.previousIndex()); @@ -822,6 +836,42 @@ private Map parseKeyToTemplateFromLocalizedFile(ClassInfo bundle return keyToTemplate; } + /** + * + * @param key + * @param bundleInterface + * @return {@code true} if the given key represents an enum constant message key, such as {@code myEnum_CONSTANT1} + * @see #toEnumConstantKey(String, String) + */ + boolean isEnumConstantMessageKey(String key, IndexView index, ClassInfo bundleInterface) { + if (key.isBlank()) { + return false; + } + int lastIdx = key.lastIndexOf("_"); + if (lastIdx != -1 && lastIdx != key.length()) { + String methodName = key.substring(0, lastIdx); + String constant = key.substring(lastIdx + 1, key.length()); + MethodInfo method = messageBundleMethod(bundleInterface, methodName); + if (method != null && method.parametersCount() == 1) { + Type paramType = method.parameterType(0); + if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) { + ClassInfo maybeEnum = index.getClassByName(paramType.name()); + if (maybeEnum != null && maybeEnum.isEnum()) { + if (maybeEnum.fields().stream() + .filter(FieldInfo::isEnumConstant) + .map(FieldInfo::name) + .anyMatch(constant::equals)) { + return true; + } + throw new MessageBundleException( + String.format("%s is not an enum constant of %: %s", constant, maybeEnum, key)); + } + } + } + } + return false; + } + private void constructLine(StringBuilder builder, Iterator it) { if (it.hasNext()) { String nextLine = adaptLine(it.next()); @@ -839,19 +889,22 @@ private String adaptLine(String line) { } private boolean hasMessageBundleMethod(ClassInfo bundleInterface, String name) { + return messageBundleMethod(bundleInterface, name) != null; + } + + private MethodInfo messageBundleMethod(ClassInfo bundleInterface, String name) { for (MethodInfo method : bundleInterface.methods()) { if (method.name().equals(name)) { - return true; + return method; } } - return false; + return null; } private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo defaultBundleInterface, - String defaultBundleImpl, - ClassInfoWrapper bundleInterfaceWrapper, ClassOutput classOutput, + String defaultBundleImpl, ClassInfoWrapper bundleInterfaceWrapper, ClassOutput classOutput, BuildProducer messageTemplateMethods, - Map messageTemplates, String locale) { + Map messageTemplates, String locale, IndexView index) { ClassInfo bundleInterface = bundleInterfaceWrapper.getClassInfo(); LOG.debugf("Generate bundle implementation for %s", bundleInterface); @@ -884,7 +937,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d ClassCreator bundleCreator = builder.build(); // key -> method - Map keyMap = new LinkedHashMap<>(); + Map keyMap = new LinkedHashMap<>(); List methods = new ArrayList<>(bundleInterfaceWrapper.methods()); // Sort methods methods.sort(Comparator.comparing(MethodInfo::name).thenComparing(Comparator.comparing(MethodInfo::toString))); @@ -927,7 +980,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d if (keyMap.containsKey(key)) { throw new MessageBundleException(String.format("Duplicate key [%s] found on %s", key, bundleInterface)); } - keyMap.put(key, method); + keyMap.put(key, new SimpleMessageMethod(method)); String messageTemplate = messageTemplates.get(method.name()); if (messageTemplate == null) { @@ -940,6 +993,50 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d method.parameterTypes().toArray(new Type[] {}))).annotation(Names.MESSAGE)); } + // We need some special handling for enum message bundle methods + // A message bundle method that accepts an enum and has no message template receives a generated template: + // {#when enumParamName} + // {#is CONSTANT1}{msg:org_acme_MyEnum_CONSTANT1} + // {#is CONSTANT2}{msg:org_acme_MyEnum_CONSTANT2} + // ... + // {/when} + // Furthermore, a special message method is generated for each enum constant + if (messageTemplate == null && method.parametersCount() == 1) { + Type paramType = method.parameterType(0); + if (paramType.kind() == org.jboss.jandex.Type.Kind.CLASS) { + ClassInfo maybeEnum = index.getClassByName(paramType.name()); + if (maybeEnum != null && maybeEnum.isEnum()) { + StringBuilder generatedMessageTemplate = new StringBuilder("{#when ") + .append(getParameterName(method, 0)) + .append("}"); + Set enumConstants = maybeEnum.fields().stream().filter(FieldInfo::isEnumConstant) + .map(FieldInfo::name).collect(Collectors.toSet()); + for (String enumConstant : enumConstants) { + // org_acme_MyEnum_CONSTANT1 + String enumConstantKey = toEnumConstantKey(method.name(), enumConstant); + String enumConstantTemplate = messageTemplates.get(enumConstantKey); + if (enumConstantTemplate == null) { + throw new TemplateException( + String.format("Enum constant message not found in bundle [%s] for key: %s", + bundleName + (locale != null ? "_" + locale : ""), enumConstantKey)); + } + generatedMessageTemplate.append("{#is ") + .append(enumConstant) + .append("}{") + .append(bundle.getName()) + .append(":") + .append(enumConstantKey) + .append("}"); + generateEnumConstantMessageMethod(bundleCreator, bundleName, locale, bundleInterface, + defaultBundleInterface, enumConstantKey, keyMap, enumConstantTemplate, + messageTemplateMethods); + } + generatedMessageTemplate.append("{/when}"); + messageTemplate = generatedMessageTemplate.toString(); + } + } + } + if (messageTemplate == null) { throw new MessageBundleException( String.format("Message template for key [%s] is missing for default locale [%s]", key, @@ -948,6 +1045,7 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d String templateId = null; if (messageTemplate.contains("}")) { + // Qute is needed - at least one expression/section found if (defaultBundleInterface != null) { if (locale == null) { AnnotationInstance localizedAnnotation = bundleInterface.declaredAnnotation(Names.LOCALIZED); @@ -975,6 +1073,12 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d // Create a template instance ResultHandle templateInstance = bundleMethod .invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE, template); + if (locale != null) { + bundleMethod.invokeInterfaceMethod( + MethodDescriptor.ofMethod(TemplateInstance.class, "setLocale", TemplateInstance.class, + String.class), + templateInstance, bundleMethod.load(locale)); + } List paramTypes = method.parameterTypes(); if (!paramTypes.isEmpty()) { // Set data @@ -1002,6 +1106,62 @@ private String generateImplementation(MessageBundleBuildItem bundle, ClassInfo d return generatedName.replace('/', '.'); } + private String toEnumConstantKey(String methodName, String enumConstant) { + return methodName + "_" + enumConstant; + } + + private void generateEnumConstantMessageMethod(ClassCreator bundleCreator, String bundleName, String locale, + ClassInfo bundleInterface, ClassInfo defaultBundleInterface, String enumConstantKey, + Map keyMap, String messageTemplate, + BuildProducer messageTemplateMethods) { + String templateId = null; + if (messageTemplate.contains("}")) { + if (defaultBundleInterface != null) { + if (locale == null) { + AnnotationInstance localizedAnnotation = bundleInterface + .declaredAnnotation(Names.LOCALIZED); + locale = localizedAnnotation.value().asString(); + } + templateId = bundleName + "_" + locale + "_" + enumConstantKey; + } else { + templateId = bundleName + "_" + enumConstantKey; + } + } + + MessageBundleMethodBuildItem messageBundleMethod = new MessageBundleMethodBuildItem(bundleName, enumConstantKey, + templateId, null, messageTemplate, + defaultBundleInterface == null); + messageTemplateMethods.produce(messageBundleMethod); + + MethodCreator enumConstantMethod = bundleCreator.getMethodCreator(enumConstantKey, + String.class); + + if (!messageBundleMethod.isValidatable()) { + // No expression/tag - no need to use qute + enumConstantMethod.returnValue(enumConstantMethod.load(messageTemplate)); + } else { + // Obtain the template, e.g. msg_org_acme_MyEnum_CONSTANT1 + ResultHandle template = enumConstantMethod.invokeStaticMethod( + io.quarkus.qute.deployment.Descriptors.BUNDLES_GET_TEMPLATE, + enumConstantMethod.load(templateId)); + // Create a template instance + ResultHandle templateInstance = enumConstantMethod + .invokeInterfaceMethod(io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE, template); + if (locale != null) { + enumConstantMethod.invokeInterfaceMethod( + MethodDescriptor.ofMethod(TemplateInstance.class, "setLocale", TemplateInstance.class, + String.class), + templateInstance, enumConstantMethod.load(locale)); + } + // Render the template + enumConstantMethod.returnValue(enumConstantMethod.invokeInterfaceMethod( + io.quarkus.qute.deployment.Descriptors.TEMPLATE_INSTANCE_RENDER, templateInstance)); + } + + keyMap.put(enumConstantKey, + new EnumConstantMessageMethod(enumConstantMethod.getMethodDescriptor())); + } + /** * @return {@link Message#value()} if value was provided */ @@ -1035,7 +1195,7 @@ static String getParameterName(MethodInfo method, int position) { return name; } - private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreator, Map keyMap) { + private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreator, Map keyMap) { MethodCreator resolve = bundleCreator.getMethodCreator("resolve", CompletionStage.class, EvalContext.class); String resolveMethodPrefix = bundleCreator.getClassName().contains("/") ? bundleCreator.getClassName().substring(bundleCreator.getClassName().lastIndexOf('/') + 1) @@ -1106,7 +1266,7 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat int resolveIndex = 0; MethodCreator resolveGroup = null; - for (Entry entry : keyMap.entrySet()) { + for (Entry entry : keyMap.entrySet()) { if (resolveGroup == null || groupIndex++ >= groupLimit) { groupIndex = 0; String resolveMethodName = resolveMethodPrefix + "_resolve_" + resolveIndex++; @@ -1147,16 +1307,18 @@ private void implementResolve(String defaultBundleImpl, ClassCreator bundleCreat } } - private void addMessageMethod(MethodCreator resolve, String key, MethodInfo method, ResultHandle name, + private void addMessageMethod(MethodCreator resolve, String key, MessageMethod method, ResultHandle name, ResultHandle evaluatedParams, ResultHandle ret, String bundleClass) { List methodParams = method.parameterTypes(); BytecodeCreator matched = resolve.ifTrue(Gizmo.equals(resolve, resolve.load(key), name)) .trueBranch(); - if (method.parameterTypes().isEmpty()) { + if (methodParams.isEmpty()) { matched.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, ret, - matched.invokeInterfaceMethod(method, matched.getThis())); + method.isMessageBundleInterfaceMethod() + ? matched.invokeInterfaceMethod(method.descriptor(), matched.getThis()) + : matched.invokeVirtualMethod(method.descriptor(), matched.getThis())); matched.returnValue(ret); } else { // The CompletionStage upon which we invoke whenComplete() @@ -1200,7 +1362,9 @@ private void addMessageMethod(MethodCreator resolve, String key, MethodInfo meth exception.getCaughtException()); tryCatch.assign(invokeRet, - tryCatch.invokeInterfaceMethod(MethodDescriptor.of(method), whenThis, paramsHandle)); + method.isMessageBundleInterfaceMethod() + ? tryCatch.invokeInterfaceMethod(method.descriptor(), whenThis, paramsHandle) + : tryCatch.invokeVirtualMethod(method.descriptor(), whenThis, paramsHandle)); tryCatch.invokeVirtualMethod(Descriptors.COMPLETABLE_FUTURE_COMPLETE, whenRet, invokeRet); // CompletableFuture.completeExceptionally(Throwable) @@ -1424,4 +1588,61 @@ public final MethodInfo method(String name, Type... parameters) { return classInfo.method(name, parameters); } } + + interface MessageMethod { + + List parameterTypes(); + + MethodDescriptor descriptor(); + + default boolean isMessageBundleInterfaceMethod() { + return true; + } + + } + + static class SimpleMessageMethod implements MessageMethod { + + final MethodInfo method; + + SimpleMessageMethod(MethodInfo method) { + this.method = method; + } + + @Override + public List parameterTypes() { + return method.parameterTypes(); + } + + @Override + public MethodDescriptor descriptor() { + return MethodDescriptor.of(method); + } + + } + + static class EnumConstantMessageMethod implements MessageMethod { + + final MethodDescriptor descriptor; + + EnumConstantMessageMethod(MethodDescriptor descriptor) { + this.descriptor = descriptor; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public MethodDescriptor descriptor() { + return descriptor; + } + + @Override + public boolean isMessageBundleInterfaceMethod() { + return false; + } + + } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 5a57f0d95f270..7353f30506eaa 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -714,10 +714,12 @@ public void beforeParsing(ParserHelper parserHelper) { MessageBundleMethodBuildItem messageBundleMethod = messageBundleMethodsMap.get(templateId); if (messageBundleMethod != null) { MethodInfo method = messageBundleMethod.getMethod(); - for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { - Type paramType = it.next(); - String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); - parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType)); + if (method != null) { + for (ListIterator it = method.parameterTypes().listIterator(); it.hasNext();) { + Type paramType = it.next(); + String name = MessageBundleProcessor.getParameterName(method, it.previousIndex()); + parserHelper.addParameter(name, getCheckedTemplateParameterTypeName(paramType)); + } } } } @@ -759,9 +761,7 @@ public void beforeParsing(ParserHelper parserHelper) { for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) { Template template = dummyEngine.parse(messageBundleMethod.getTemplate(), null, messageBundleMethod.getTemplateId()); analysis.add(new TemplateAnalysis(messageBundleMethod.getTemplateId(), template.getGeneratedId(), - template.getExpressions(), template.getParameterDeclarations(), - messageBundleMethod.getMethod().declaringClass().name() + "#" + messageBundleMethod.getMethod().name() - + "()", + template.getExpressions(), template.getParameterDeclarations(), messageBundleMethod.getPathForAnalysis(), template.getFragmentIds())); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java new file mode 100644 index 0000000000000..8ac3a9e739810 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleEnumTest.java @@ -0,0 +1,74 @@ +package io.quarkus.qute.deployment.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Template; +import io.quarkus.qute.i18n.Message; +import io.quarkus.qute.i18n.MessageBundle; +import io.quarkus.test.QuarkusUnitTest; + +public class MessageBundleEnumTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Messages.class, MyEnum.class) + .addAsResource("messages/enu.properties") + .addAsResource("messages/enu_cs.properties") + .addAsResource(new StringAsset( + "{enu:myEnum(MyEnum:ON)}::{enu:myEnum(MyEnum:OFF)}::{enu:myEnum(MyEnum:UNDEFINED)}::" + + "{enu:shortEnum(MyEnum:ON)}::{enu:shortEnum(MyEnum:OFF)}::{enu:shortEnum(MyEnum:UNDEFINED)}::" + + "{enu:foo(MyEnum:ON)}::{enu:foo(MyEnum:OFF)}::{enu:foo(MyEnum:UNDEFINED)}::" + + "{enu:locFileOverride(MyEnum:ON)}::{enu:locFileOverride(MyEnum:OFF)}::{enu:locFileOverride(MyEnum:UNDEFINED)}"), + "templates/foo.html")); + + @Inject + Template foo; + + @Test + public void testMessages() { + assertEquals("On::Off::Undefined::1::0::U::+::-::_::on::off::undefined", foo.render()); + assertEquals("Zapnuto::Vypnuto::Nedefinováno::1::0::N::+::-::_::zap::vyp::nedef", + foo.instance().setLocale("cs").render()); + } + + @MessageBundle(value = "enu", locale = "en") + public interface Messages { + + // Replaced with: + // @Message("{#when myEnum}" + // + "{#is ON}{enu:myEnum_ON}" + // + "{#is OFF}{enu:myEnum_OFF}" + // + "{#is UNDEFINED}{enu:myEnum_UNDEFINED}" + // + "{/when}") + @Message + String myEnum(MyEnum myEnum); + + // Replaced with: + // @Message("{#when myEnum}" + // + "{#is ON}{enu:shortEnum_ON}" + // + "{#is OFF}{enu:shortEnum_OFF}" + // + "{#is UNDEFINED}{enu:shortEnum_UNDEFINED}" + // + "{/when}") + @Message + String shortEnum(MyEnum myEnum); + + @Message("{#when myEnum}" + + "{#is ON}+" + + "{#is OFF}-" + + "{#else}_" + + "{/when}") + String foo(MyEnum myEnum); + + @Message + String locFileOverride(MyEnum myEnum); + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java index 89c944458e999..fcc4f14a9c414 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleLogicalLineTest.java @@ -10,7 +10,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.qute.Template; -import io.quarkus.qute.TemplateEnum; import io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; import io.quarkus.test.QuarkusUnitTest; @@ -20,7 +19,7 @@ public class MessageBundleLogicalLineTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(Messages.class) + .addClasses(Messages.class, MyEnum.class) .addAsResource("messages/msg_cs.properties") .addAsResource(new StringAsset( "{msg:hello('Edgar')}::{msg:helloNextLine('Edgar')}::{msg:fruits}::{msg:myEnum(MyEnum:OFF)}"), @@ -58,11 +57,4 @@ public interface Messages { } - @TemplateEnum - public enum MyEnum { - ON, - OFF, - UNDEFINED - } - } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java index 6acf6738cb8ed..c9349a722dd84 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleTest.java @@ -83,7 +83,7 @@ public void testResolvers() { foo.instance().render()); assertEquals("Hello world! Ahoj Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!", foo.instance().setAttribute(MessageBundles.ATTRIBUTE_LOCALE, Locale.forLanguageTag("cs")).render()); - assertEquals("Hallo Welt! Hallo Jachym! Hello you guys! Hello alpha! Hello! Hello foo from alpha!", + assertEquals("Hallo Welt! Hallo Jachym! Hallo you guys! Hello alpha! Hello! Hello foo from alpha!", foo.instance().setLocale(Locale.GERMAN).render()); assertEquals("Dot test!", engine.parse("{msg:['dot.test']}").render()); assertEquals("Hello world! Hello Malachi Constant!", diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java new file mode 100644 index 0000000000000..7e26e81d95345 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MyEnum.java @@ -0,0 +1,10 @@ +package io.quarkus.qute.deployment.i18n; + +import io.quarkus.qute.TemplateEnum; + +@TemplateEnum +public enum MyEnum { + ON, + OFF, + UNDEFINED +} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu.properties b/extensions/qute/deployment/src/test/resources/messages/enu.properties new file mode 100644 index 0000000000000..072f933eb0881 --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu.properties @@ -0,0 +1,13 @@ +myEnum_ON=On +myEnum_OFF=Off +myEnum_UNDEFINED=Undefined + +shortEnum_ON=1 +shortEnum_OFF=0 +shortEnum_UNDEFINED=U + +locFileOverride={#when myEnum}\ + {#is ON}on\ + {#is OFF}off\ + {#else}undefined\ + {/when} \ No newline at end of file diff --git a/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties new file mode 100644 index 0000000000000..e3f5c0a2ae6de --- /dev/null +++ b/extensions/qute/deployment/src/test/resources/messages/enu_cs.properties @@ -0,0 +1,13 @@ +myEnum_ON=Zapnuto +myEnum_OFF=Vypnuto +myEnum_UNDEFINED=Nedefinováno + +shortEnum_ON=1 +shortEnum_OFF=0 +shortEnum_UNDEFINED=N + +locFileOverride={#when myEnum}\ + {#is ON}zap\ + {#is OFF}vyp\ + {#else}nedef\ + {/when} \ No newline at end of file diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java index 8f4c68664af85..93c5fbe6b1327 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/Message.java @@ -14,7 +14,8 @@ * {@link MessageBundle#defaultKey()}. *

* The {@link #value()} defines the template of a message. The method parameters can be used in this template. All the message - * templates are validated at build time. + * templates are validated at build time. If there is no template defined the template from a localized file is taken. In case + * the value is not provided at all the build fails. *

* Note that any method declared on a message bundle interface is consireded a message bundle method. If not annotated with this * annotation then the defaulted values are used for the key and template. @@ -22,6 +23,30 @@ * All message bundle methods must return {@link String}. If a message bundle method does not return string then the build * fails. * + *

Enums

+ * There is a convenient way to localize enums. + *

+ * If there is a message bundle method that accepts a single parameter of an enum type and has no message template defined then + * it + * receives a generated template: + * + *

+ * {#when enumParamName}
+ *     {#is CONSTANT1}{msg:methodName_CONSTANT1}
+ *     {#is CONSTANT2}{msg:methodName_CONSTANT2}
+ * {/when}
+ * 
+ * + * Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and + * values for all constant message keys: + * + *
+ * methodName_CONSTANT1=Value 1
+ * methodName_CONSTANT2=Value 2
+ * 
+ * + * In a template, an enum constant can be localized with a message bundle method {@code msg:methodName(enumConstant)}. + * * @see MessageBundle */ @Retention(RUNTIME) @@ -69,6 +94,8 @@ * This value has higher priority over a message template specified in a localized file, and it's * considered a good practice to specify it. In case the value is not provided and there is no * match in the localized file too, the build fails. + *

+ * There is a convenient way to localize enums. See the javadoc of {@link Message}. * * @return the message template */ From e59693a5bebb1cb20af4eb7e5f7d17e453f9afd4 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 18 Apr 2024 16:33:33 +0200 Subject: [PATCH 0017/2353] Fix license URL in JReleaser config --- devtools/cli/distribution/jreleaser-maintenance.yml | 2 +- devtools/cli/distribution/jreleaser-preview.yml | 2 +- devtools/cli/distribution/jreleaser.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/devtools/cli/distribution/jreleaser-maintenance.yml b/devtools/cli/distribution/jreleaser-maintenance.yml index 15772e91a8ba7..5847f0b4229eb 100644 --- a/devtools/cli/distribution/jreleaser-maintenance.yml +++ b/devtools/cli/distribution/jreleaser-maintenance.yml @@ -18,7 +18,7 @@ project: - java links: homepage: https://quarkus.io - license: https://github.com/quarkusio/quarkus/blob/main/LICENSE.txt + license: https://github.com/quarkusio/quarkus/blob/main/LICENSE release: github: diff --git a/devtools/cli/distribution/jreleaser-preview.yml b/devtools/cli/distribution/jreleaser-preview.yml index cbfeab135468c..1de4d9a832d17 100644 --- a/devtools/cli/distribution/jreleaser-preview.yml +++ b/devtools/cli/distribution/jreleaser-preview.yml @@ -18,7 +18,7 @@ project: - java links: homepage: https://quarkus.io - license: https://github.com/quarkusio/quarkus/blob/main/LICENSE.txt + license: https://github.com/quarkusio/quarkus/blob/main/LICENSE release: github: diff --git a/devtools/cli/distribution/jreleaser.yml b/devtools/cli/distribution/jreleaser.yml index d8be28fdd50d2..e4e37b0216b86 100644 --- a/devtools/cli/distribution/jreleaser.yml +++ b/devtools/cli/distribution/jreleaser.yml @@ -18,7 +18,7 @@ project: - java links: homepage: https://quarkus.io - license: https://github.com/quarkusio/quarkus/blob/main/LICENSE.txt + license: https://github.com/quarkusio/quarkus/blob/main/LICENSE release: github: From 1c9f618d34ed01a340b7770bf44e18bcc802f606 Mon Sep 17 00:00:00 2001 From: Gwenneg Lepage Date: Thu, 18 Apr 2024 19:49:19 +0200 Subject: [PATCH 0018/2353] Do not increment metrics on CaffeineCache#getIfPresent call --- .../runtime/caffeine/CaffeineCacheImpl.java | 3 - .../quarkus/it/cache/ExpensiveResource.java | 4 +- .../it/cache/GetIfPresentResource.java | 36 +++++++++++ .../src/main/resources/application.properties | 1 + .../io/quarkus/it/cache/CacheTestCase.java | 60 +++++++++++++++++-- 5 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 integration-tests/cache/src/main/java/io/quarkus/it/cache/GetIfPresentResource.java diff --git a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheImpl.java b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheImpl.java index c8baff5992b7a..dff31eb8dab1e 100644 --- a/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheImpl.java +++ b/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/caffeine/CaffeineCacheImpl.java @@ -127,13 +127,10 @@ public CompletableFuture getIfPresent(Object key) { Objects.requireNonNull(key, NULL_KEYS_NOT_SUPPORTED_MSG); CompletableFuture existingCacheValue = cache.getIfPresent(key); - // record metrics, if not null apply casting if (existingCacheValue == null) { - statsCounter.recordMisses(1); return null; } else { LOGGER.tracef("Key [%s] found in cache [%s]", key, cacheInfo.name); - statsCounter.recordHits(1); // cast, but still throw the CacheException in case it fails return unwrapCacheValueOrThrowable(existingCacheValue) diff --git a/integration-tests/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java b/integration-tests/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java index 8efdcf9abfdc0..84cdc5a3a75bd 100644 --- a/integration-tests/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java +++ b/integration-tests/cache/src/main/java/io/quarkus/it/cache/ExpensiveResource.java @@ -13,11 +13,13 @@ @Path("/expensive-resource") public class ExpensiveResource { + public static final String EXPENSIVE_RESOURCE_CACHE_NAME = "expensiveResourceCache"; + private int invocations; @GET @Path("/{keyElement1}/{keyElement2}/{keyElement3}") - @CacheResult(cacheName = "expensiveResourceCache", lockTimeout = 5000) + @CacheResult(cacheName = EXPENSIVE_RESOURCE_CACHE_NAME, lockTimeout = 5000) public ExpensiveResponse getExpensiveResponse(@PathParam("keyElement1") @CacheKey String keyElement1, @PathParam("keyElement2") @CacheKey String keyElement2, @PathParam("keyElement3") @CacheKey String keyElement3, @QueryParam("foo") String foo) { diff --git a/integration-tests/cache/src/main/java/io/quarkus/it/cache/GetIfPresentResource.java b/integration-tests/cache/src/main/java/io/quarkus/it/cache/GetIfPresentResource.java new file mode 100644 index 0000000000000..14e9feb8082c4 --- /dev/null +++ b/integration-tests/cache/src/main/java/io/quarkus/it/cache/GetIfPresentResource.java @@ -0,0 +1,36 @@ +package io.quarkus.it.cache; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +import java.util.concurrent.CompletionStage; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestPath; + +import io.quarkus.cache.Cache; +import io.quarkus.cache.CacheName; +import io.quarkus.cache.CaffeineCache; + +@Path("/get-if-present") +public class GetIfPresentResource { + + public static final String GET_IF_PRESENT_CACHE_NAME = "getIfPresentCache"; + + @CacheName(GET_IF_PRESENT_CACHE_NAME) + Cache cache; + + @GET + @Path("/{key}") + public CompletionStage getIfPresent(@RestPath String key) { + return cache.as(CaffeineCache.class).getIfPresent(key); + } + + @PUT + @Path("/{key}") + public void put(@RestPath String key, String value) { + cache.as(CaffeineCache.class).put(key, completedFuture(value)); + } +} diff --git a/integration-tests/cache/src/main/resources/application.properties b/integration-tests/cache/src/main/resources/application.properties index b94edbb983678..9f87c19434340 100644 --- a/integration-tests/cache/src/main/resources/application.properties +++ b/integration-tests/cache/src/main/resources/application.properties @@ -8,5 +8,6 @@ quarkus.cache.caffeine."forest".expire-after-write=10M quarkus.cache.caffeine."expensiveResourceCache".expire-after-write=10M quarkus.cache.caffeine."expensiveResourceCache".metrics-enabled=true +quarkus.cache.caffeine."getIfPresentCache".metrics-enabled=true io.quarkus.it.cache.SunriseRestClient/mp-rest/url=${test.url} diff --git a/integration-tests/cache/src/test/java/io/quarkus/it/cache/CacheTestCase.java b/integration-tests/cache/src/test/java/io/quarkus/it/cache/CacheTestCase.java index 48fc07d224685..64197281e8775 100644 --- a/integration-tests/cache/src/test/java/io/quarkus/it/cache/CacheTestCase.java +++ b/integration-tests/cache/src/test/java/io/quarkus/it/cache/CacheTestCase.java @@ -1,5 +1,8 @@ package io.quarkus.it.cache; +import static io.quarkus.it.cache.ExpensiveResource.EXPENSIVE_RESOURCE_CACHE_NAME; +import static io.quarkus.it.cache.GetIfPresentResource.GET_IF_PRESENT_CACHE_NAME; +import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -14,20 +17,65 @@ public class CacheTestCase { @Test - public void testCache() { + void testCache() { + assertMetrics(EXPENSIVE_RESOURCE_CACHE_NAME, 0, 0, 0); + runExpensiveRequest(); + assertMetrics(EXPENSIVE_RESOURCE_CACHE_NAME, 1, 1, 0); + runExpensiveRequest(); + assertMetrics(EXPENSIVE_RESOURCE_CACHE_NAME, 1, 1, 1); + runExpensiveRequest(); - when().get("/expensive-resource/invocations").then().statusCode(200).body(is("1")); + assertMetrics(EXPENSIVE_RESOURCE_CACHE_NAME, 1, 1, 2); - String metricsResponse = when().get("/q/metrics").then().extract().asString(); - assertTrue(metricsResponse.contains("cache_puts_total{cache=\"expensiveResourceCache\"} 1.0")); - assertTrue(metricsResponse.contains("cache_gets_total{cache=\"expensiveResourceCache\",result=\"miss\"} 1.0")); - assertTrue(metricsResponse.contains("cache_gets_total{cache=\"expensiveResourceCache\",result=\"hit\"} 2.0")); + when().get("/expensive-resource/invocations").then().statusCode(200).body(is("1")); } private void runExpensiveRequest() { when().get("/expensive-resource/I/love/Quarkus?foo=bar").then().statusCode(200).body("result", is("I love Quarkus too!")); } + + @Test + void testGetIfPresentMetrics() { + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 0, 0, 0); + + String cacheKey = "foo"; + String cacheValue = "bar"; + + given().pathParam("key", cacheKey) + .when().get("/get-if-present/{key}") + .then().statusCode(204); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 0, 1, 0); + + given().pathParam("key", cacheKey) + .when().get("/get-if-present/{key}") + .then().statusCode(204); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 0, 2, 0); + + given().pathParam("key", cacheKey).body(cacheValue) + .when().put("/get-if-present/{key}") + .then().statusCode(204); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 1, 2, 0); + + given().pathParam("key", cacheKey) + .when().get("/get-if-present/{key}") + .then().statusCode(200).body(is(cacheValue)); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 1, 2, 1); + + given().pathParam("key", cacheKey) + .when().get("/get-if-present/{key}") + .then().statusCode(200).body(is(cacheValue)); + assertMetrics(GET_IF_PRESENT_CACHE_NAME, 1, 2, 2); + } + + private void assertMetrics(String cacheName, double expectedPuts, double expectedMisses, double expectedHits) { + String metricsResponse = when().get("/q/metrics").then().extract().asString(); + assertTrue(metricsResponse.contains(String.format("cache_puts_total{cache=\"%s\"} %.1f", cacheName, expectedPuts))); + assertTrue(metricsResponse + .contains(String.format("cache_gets_total{cache=\"%s\",result=\"miss\"} %.1f", cacheName, expectedMisses))); + assertTrue(metricsResponse + .contains(String.format("cache_gets_total{cache=\"%s\",result=\"hit\"} %.1f", cacheName, expectedHits))); + } } From 796348d1b679853cea9a0d1ae83c2ca92ef2840c Mon Sep 17 00:00:00 2001 From: Ales Justin Date: Fri, 14 Apr 2023 13:04:49 +0200 Subject: [PATCH 0019/2353] Initial Observability extension - devservices, devresources, LGTM Grafana OTel LGTM additional Documentation Fix enforcer configuration and make names consistent Update descriptors with latest dependency state Abstract Testcontainers usage, own Container SPI. --- bom/application/pom.xml | 50 ++++ .../java/io/quarkus/deployment/Feature.java | 1 + .../console/StartupLogCompressor.java | 19 +- .../quarkus/runtime/util/EnumerationUtil.java | 69 +++++ devtools/bom-descriptor-json/pom.xml | 13 + docs/pom.xml | 13 + docs/src/main/asciidoc/dev-services.adoc | 8 + .../observability-devservices-lgtm.adoc | 282 ++++++++++++++++++ .../asciidoc/observability-devservices.adoc | 42 +++ docs/src/main/asciidoc/opentelemetry.adoc | 18 +- .../devservices/common/ContainerLocator.java | 26 ++ .../observability-devservices/common/pom.xml | 27 ++ .../common/ContainerConstants.java | 15 + .../config/AbstractContainerConfig.java | 51 ++++ .../common/config/AbstractGrafanaConfig.java | 51 ++++ .../common/config/ContainerConfig.java | 69 +++++ .../common/config/ContainerConfigUtil.java | 41 +++ .../common/config/GrafanaConfig.java | 33 ++ .../common/config/LgtmConfig.java | 23 ++ .../common/config/ModulesConfiguration.java | 8 + .../deployment/pom.xml | 81 +++++ .../deployment/DevResourcesBuildItem.java | 6 + .../deployment/DevResourcesProcessor.java | 51 ++++ .../ObservabilityDevServiceProcessor.java | 216 ++++++++++++++ extensions/observability-devservices/pom.xml | 56 ++++ .../observability-devservices/runtime/pom.xml | 89 ++++++ .../runtime/DevResourceShutdownRecorder.java | 12 + .../runtime/DevResourcesConfigBuilder.java | 18 ++ .../config/ObservabilityConfiguration.java | 35 +++ .../sink/lgtm/pom.xml | 26 ++ .../testcontainers/pom.xml | 45 +++ .../testcontainers/GrafanaContainer.java | 35 +++ .../testcontainers/LgtmContainer.java | 45 +++ .../ObservabilityContainer.java | 72 +++++ .../testlibs/devresource-common/pom.xml | 39 +++ .../observability/devresource/Container.java | 21 ++ .../DevResourceLifecycleManager.java | 83 ++++++ .../devresource/DevResources.java | 88 ++++++ .../devresource/DevResourcesConfigSource.java | 28 ++ .../testlibs/devresource-lgtm/pom.xml | 26 ++ .../devresource/lgtm/LgtmResource.java | 54 ++++ ...ty.devresource.DevResourceLifecycleManager | 1 + .../devresource-testcontainers/pom.xml | 39 +++ .../testcontainers/ContainerResource.java | 45 +++ .../TestcontainerContainer.java | 47 +++ .../testlibs/pom.xml | 22 ++ extensions/pom.xml | 1 + integration-tests/observability-lgtm/pom.xml | 121 ++++++++ .../observability/example/SimpleEndpoint.java | 48 +++ .../src/main/resources/application.properties | 17 ++ .../observability/test/LgtmLifecycleTest.java | 17 ++ .../observability/test/LgtmResourcesIT.java | 11 + .../observability/test/LgtmResourcesTest.java | 14 + .../observability/test/LgtmServicesTest.java | 11 + .../observability/test/LgtmTestBase.java | 33 ++ .../test/support/DevResourcesTestProfile.java | 14 + .../test/support/GrafanaClient.java | 92 ++++++ .../QuarkusTestResourceTestProfile.java | 14 + .../test/support/QueryResult.java | 73 +++++ .../observability/test/support/User.java | 14 + .../src/test/resources/application.properties | 16 + integration-tests/pom.xml | 1 + 62 files changed, 2631 insertions(+), 5 deletions(-) create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/util/EnumerationUtil.java create mode 100644 docs/src/main/asciidoc/observability-devservices-lgtm.adoc create mode 100644 docs/src/main/asciidoc/observability-devservices.adoc create mode 100644 extensions/observability-devservices/common/pom.xml create mode 100644 extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/ContainerConstants.java create mode 100644 extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/AbstractContainerConfig.java create mode 100644 extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/AbstractGrafanaConfig.java create mode 100644 extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ContainerConfig.java create mode 100644 extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ContainerConfigUtil.java create mode 100644 extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/GrafanaConfig.java create mode 100644 extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/LgtmConfig.java create mode 100644 extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ModulesConfiguration.java create mode 100644 extensions/observability-devservices/deployment/pom.xml create mode 100644 extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesBuildItem.java create mode 100644 extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesProcessor.java create mode 100644 extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/ObservabilityDevServiceProcessor.java create mode 100644 extensions/observability-devservices/pom.xml create mode 100644 extensions/observability-devservices/runtime/pom.xml create mode 100644 extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/DevResourceShutdownRecorder.java create mode 100644 extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/DevResourcesConfigBuilder.java create mode 100644 extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/config/ObservabilityConfiguration.java create mode 100644 extensions/observability-devservices/sink/lgtm/pom.xml create mode 100644 extensions/observability-devservices/testcontainers/pom.xml create mode 100644 extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/GrafanaContainer.java create mode 100644 extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/LgtmContainer.java create mode 100644 extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/ObservabilityContainer.java create mode 100644 extensions/observability-devservices/testlibs/devresource-common/pom.xml create mode 100644 extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/Container.java create mode 100644 extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResourceLifecycleManager.java create mode 100644 extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResources.java create mode 100644 extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResourcesConfigSource.java create mode 100644 extensions/observability-devservices/testlibs/devresource-lgtm/pom.xml create mode 100644 extensions/observability-devservices/testlibs/devresource-lgtm/src/main/java/io/quarkus/observability/devresource/lgtm/LgtmResource.java create mode 100644 extensions/observability-devservices/testlibs/devresource-lgtm/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager create mode 100644 extensions/observability-devservices/testlibs/devresource-testcontainers/pom.xml create mode 100644 extensions/observability-devservices/testlibs/devresource-testcontainers/src/main/java/io/quarkus/observability/devresource/testcontainers/ContainerResource.java create mode 100644 extensions/observability-devservices/testlibs/devresource-testcontainers/src/main/java/io/quarkus/observability/devresource/testcontainers/TestcontainerContainer.java create mode 100644 extensions/observability-devservices/testlibs/pom.xml create mode 100644 integration-tests/observability-lgtm/pom.xml create mode 100644 integration-tests/observability-lgtm/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java create mode 100644 integration-tests/observability-lgtm/src/main/resources/application.properties create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmLifecycleTest.java create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmResourcesIT.java create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmResourcesTest.java create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmServicesTest.java create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmTestBase.java create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/DevResourcesTestProfile.java create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/GrafanaClient.java create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/QuarkusTestResourceTestProfile.java create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/QueryResult.java create mode 100644 integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/User.java create mode 100644 integration-tests/observability-lgtm/src/test/resources/application.properties diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 841bbd2f06c1f..f74037599a9a9 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -226,6 +226,8 @@ 1.1.0 3.0.0 2.12.3 + + 0.16.0 1.0.10 @@ -490,6 +492,18 @@ import + + + io.prometheus + simpleclient + ${prometheus.version} + + + io.prometheus + simpleclient_common + ${prometheus.version} + + @@ -2991,6 +3005,42 @@ quarkus-virtual-threads-deployment ${project.version} + + io.quarkus + quarkus-observability-devservices-common + ${project.version} + + + io.quarkus + quarkus-observability-devservices + ${project.version} + + + io.quarkus + quarkus-observability-devservices-lgtm + ${project.version} + provided + + + io.quarkus + quarkus-observability-testcontainers + ${project.version} + + + io.quarkus + quarkus-observability-devresource-common + ${project.version} + + + io.quarkus + quarkus-observability-devresource-testcontainers + ${project.version} + + + io.quarkus + quarkus-observability-devresource-lgtm + ${project.version} + diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index 14b9fd7c89bf8..25c5149188eb4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -69,6 +69,7 @@ public enum Feature { NARAYANA_LRA, NARAYANA_STM, NEO4J, + OBSERVABILITY, OIDC, OIDC_CLIENT, RESTEASY_CLIENT_OIDC_FILTER, diff --git a/core/deployment/src/main/java/io/quarkus/deployment/console/StartupLogCompressor.java b/core/deployment/src/main/java/io/quarkus/deployment/console/StartupLogCompressor.java index 7576025324987..afd9f86a17fc9 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/console/StartupLogCompressor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/console/StartupLogCompressor.java @@ -3,6 +3,7 @@ import java.io.Closeable; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiPredicate; @@ -26,18 +27,28 @@ public class StartupLogCompressor implements Closeable, BiPredicate toDump = new ArrayList<>(); final AtomicInteger COUNTER = new AtomicInteger(); final Predicate additionalThreadPredicate; + final Predicate linePredicate; // test if we always print the line / log public StartupLogCompressor(String name, @SuppressWarnings("unused") Optional consoleInstalledBuildItem, @SuppressWarnings("unused") LoggingSetupBuildItem loggingSetupBuildItem) { - this(name, consoleInstalledBuildItem, loggingSetupBuildItem, (s) -> false); + this(name, consoleInstalledBuildItem, loggingSetupBuildItem, s -> false); } public StartupLogCompressor(String name, @SuppressWarnings("unused") Optional consoleInstalledBuildItem, @SuppressWarnings("unused") LoggingSetupBuildItem loggingSetupBuildItem, Predicate additionalThreadPredicate) { - this.additionalThreadPredicate = additionalThreadPredicate; + this(name, consoleInstalledBuildItem, loggingSetupBuildItem, additionalThreadPredicate, s -> false); + } + + public StartupLogCompressor(String name, + @SuppressWarnings("unused") Optional consoleInstalledBuildItem, + @SuppressWarnings("unused") LoggingSetupBuildItem loggingSetupBuildItem, + Predicate additionalThreadPredicate, + Predicate linePredicate) { + this.additionalThreadPredicate = Objects.requireNonNull(additionalThreadPredicate); + this.linePredicate = Objects.requireNonNull(linePredicate); if (QuarkusConsole.INSTANCE.isAnsiSupported()) { QuarkusConsole.installRedirects(); this.name = name; @@ -74,8 +85,8 @@ public void closeAndDumpCaptured() { @Override public boolean test(String s, Boolean errorStream) { - if (thread == null) { - //not installed + if (thread == null || linePredicate.test(s)) { + //not installed or line predicate tested to true return true; } Thread current = Thread.currentThread(); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/util/EnumerationUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/util/EnumerationUtil.java new file mode 100644 index 0000000000000..6dbab086bb060 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/util/EnumerationUtil.java @@ -0,0 +1,69 @@ +package io.quarkus.runtime.util; + +import java.util.Enumeration; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * Transform to "old school" Enumeration from Iterator/Spliterator/Stream + */ +public class EnumerationUtil { + public static Enumeration from(Iterator iterator) { + Objects.requireNonNull(iterator); + + return new Enumeration() { + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + @Override + public T nextElement() { + return iterator.next(); + } + }; + } + + public static Enumeration from(Spliterator spliterator) { + Objects.requireNonNull(spliterator); + + class Adapter implements Enumeration, Consumer { + boolean valueReady; + T nextElement; + + public void accept(T t) { + this.valueReady = true; + this.nextElement = t; + } + + public boolean hasMoreElements() { + if (!this.valueReady) { + spliterator.tryAdvance(this); + } + + return this.valueReady; + } + + public T nextElement() { + if (!this.valueReady && !this.hasMoreElements()) { + throw new NoSuchElementException(); + } else { + this.valueReady = false; + T t = this.nextElement; + this.nextElement = null; + return t; + } + } + } + + return new Adapter(); + } + + public static Enumeration from(Stream stream) { + return from(stream.spliterator()); + } +} diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 426891e36c7da..b86dea58cf920 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -1591,6 +1591,19 @@ + + io.quarkus + quarkus-observability-devservices + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-oidc diff --git a/docs/pom.xml b/docs/pom.xml index e70a317eb71e5..d5d80164248f1 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1607,6 +1607,19 @@ + + io.quarkus + quarkus-observability-devservices-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-oidc-deployment diff --git a/docs/src/main/asciidoc/dev-services.adoc b/docs/src/main/asciidoc/dev-services.adoc index 01fd6fdf4e77b..77014c01a1b61 100644 --- a/docs/src/main/asciidoc/dev-services.adoc +++ b/docs/src/main/asciidoc/dev-services.adoc @@ -165,6 +165,14 @@ More information can be found in the xref:elasticsearch-dev-services.adoc[Elasti include::{generated-dir}/config/quarkus-elasticsearch-devservices-elasticsearch-dev-services-build-time-config.adoc[opts=optional, leveloffset=+1] +=== Observability + +The Observability Dev Services will be enabled when the `quarkus-observability-devservices` extension is present in your application, and +there is at least one dev resource on the classpath. More information can be found in the +xref:observability-devservices.adoc[Observability Dev Services Guide]. + +include::{generated-dir}/config/quarkus-observability-config-observability-configuration.adoc[opts=optional, leveloffset=+1] + == Dev Services beyond the Quarkus Platform Many Quarkiverse extensions which are not in the Quarkus Platform also offer Dev Services. diff --git a/docs/src/main/asciidoc/observability-devservices-lgtm.adoc b/docs/src/main/asciidoc/observability-devservices-lgtm.adoc new file mode 100644 index 0000000000000..df199401954e4 --- /dev/null +++ b/docs/src/main/asciidoc/observability-devservices-lgtm.adoc @@ -0,0 +1,282 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Observability Dev Services with Grafana OTel LGTM + +include::_attributes.adoc[] +:categories: observability,devservices,telemetry,metrics,tracing,logging, opentelemetry, micrometer, prometheus, tempo, loki, grafana +:summary: Instructions on how to use Grafana Otel LGTM +:topics: observability,grafana,lgtm,otlp,opentelemetry,devservices,micrometer +:extensions: io.quarkus:quarkus-observability-devservices + +https://github.com/grafana/docker-otel-lgtm[OTel-LGTM] is `all-in-one` Docker image containing OpenTelemetry's https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/README.md[OTLP] as the protocol to transport metrics, tracing and logging data to an https://opentelemetry.io/docs/collector[OpenTelemetry Collector] which then stores signals data into https://prometheus.io/[Prometheus] (metrics), https://github.com/grafana/tempo[Tempo] (traces) and https://github.com/grafana/loki[Loki] (logs), only to have it visualized by https://github.com/grafana/grafana[Grafana]. It's used by Quarkus Observability to provide the Grafana OTel LGTM Dev Resource. + +== Configuring your project + +Add the Quarkus Grafana OTel LGTM sink (where data goes) extension to your build file: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-observability-devservices-lgtm + provided + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("quarkus-observability-devservices-lgtm") +---- + +=== Metrics + +If you're using https://micrometer.io/[MicroMeter's] Quarkiverse OTLP registry to push metrics to Grafana OTel LGTM, this is how you would define the export endpoint url; where `quarkus.otel-collector.url` is provided by the Observability Dev Services extension. + +[source,properties] +---- +# Micrometer OTLP registry +%test.quarkus.micrometer.export.otlp.url=http://${quarkus.otel-collector.url}/v1/metrics +%dev.quarkus.micrometer.export.otlp.url=http://${quarkus.otel-collector.url}/v1/metrics +%prod.quarkus.micrometer.export.otlp.url=http://localhost:4318/v1/metrics +---- +Please note that the `${quarkus.otel-collector.url}` value is generated by quarkus when it starts the Grafana OTel LGTM Dev Resource. + +Along OTel collector enpoint url, LGTM Dev Resource also provides a Grafana endpoint url - under `quarkus.grafana.url` property. + +In this case LGTM Dev Resource would be automatically started and used by Observability Dev Services. + +If you don't want all the hassle with Dev Services (e.g. lookup and re-use of existing running containers, etc) you can simply disable Dev Services and enable just Dev Resource usage: + +[source,properties] +---- +quarkus.observability.enabled=false +quarkus.observability.dev-resources=true +---- + +=== Tracing + +Just add the quarkus-opentelemetry extension to your build file: +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-opentelemetry + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-opentelemetry") +---- + +On the `application.properties` file, you can define: +[source,properties] +---- +# OpenTelemetry +quarkus.otel.exporter.otlp.traces.protocol=http/protobuf +%test.quarkus.otel.exporter.otlp.traces.endpoint=http://${quarkus.otel-collector.url} +%dev.quarkus.otel.exporter.otlp.traces.endpoint=http://${quarkus.otel-collector.url} +%prod.quarkus.otel.exporter.otlp.traces.endpoint=http://localhost:4318 +---- +=== Access Grafana + +Once you start your app in dev mode: + +include::{includes}/devtools/dev.adoc[] + +You will see a message like this: + +[source, log] +---- +Lgtm Dev Services Starting: 2024-02-20 11:15:24,540 INFO [org.tes.con.wai.str.HttpWaitStrategy] (build-32) /loving_chatelet: Waiting for 60 seconds for URL: http://localhost:61907/ (where port 61907 maps to container port 3000) +---- +Remember that Grafana is accessible in an ephemeral port, so you need to check the logs to see which port is being used. In this example, it's port 61907. + +If you miss the message you can always check the port with this Docker command: +[source, bash] +---- +docker ps | grep grafana +---- +=== Tests + +And for the least 'auto-magical' usage in the tests, simply disable both (Dev Resources are already disabled by default): + +[source,properties] +---- +quarkus.observability.enabled=false +---- + +And then explicitly list LGTM Dev Resource in the test as a `@QuarkusTestResource` resource: +[source, java] +---- +@QuarkusTest +@QuarkusTestResource(value = LgtmResource.class, restrictToAnnotatedClass = true) +@TestProfile(QuarkusTestResourceTestProfile.class) +public class LgtmLifecycleTest extends LgtmTestBase { +} +---- + +== Testing full Grafana OTel LGTM stack - example + +Use existing Quarkus MicroMeter OTLP registry + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkiverse.micrometer.registry + quarkus-micrometer-registry-otlp + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkiverse.micrometer.registry:quarkus-micrometer-registry-otlp") +---- + +On the test `application.properties` file, you need to define: +[source,properties] +---- +# Micrometer OTLP registry +quarkus.micrometer.export.otlp.url=http://${quarkus.otel-collector.url}/v1/metrics +# OpenTelemetry +quarkus.otel.exporter.otlp.traces.protocol=http/protobuf +quarkus.otel.exporter.otlp.traces.endpoint=http://${quarkus.otel-collector.url} +---- + +Simply inject the Meter registry into your code -- it will periodically push metrics to Grafana LGTM's OTLP HTTP endpoint. + +[source, java] +---- +@Path("/api") +public class SimpleEndpoint { + private static final Logger log = Logger.getLogger(SimpleEndpoint.class); + + @Inject + MeterRegistry registry; + + @PostConstruct + public void start() { + Gauge.builder("xvalue", arr, a -> arr[0]) + .baseUnit("X") + .description("Some random x") + .tag("my_key", "x") + .register(registry); + } + + // ... +} +---- + +Where you can then check Grafana's datasource API for existing metrics data. + +[source, java] +---- +public class LgtmTestBase { + + @ConfigProperty(name = "quarkus.grafana.url") + String url; // NOTE -- injected Grafana endpoint url! + + @Test + public void testTracing() { + String response = RestAssured.get("/api/poke?f=100").body().asString(); + System.out.println(response); + GrafanaClient client = new GrafanaClient("http://" + url, "admin", "admin"); + Awaitility.await().atMost(61, TimeUnit.SECONDS).until( + client::user, + u -> "admin".equals(u.login)); + Awaitility.await().atMost(61, TimeUnit.SECONDS).until( + () -> client.query("xvalue_X"), + result -> !result.data.result.isEmpty()); + } + +} + +// simple Grafana HTTP client + +public class GrafanaClient { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final String url; + private final String username; + private final String password; + + public GrafanaClient(String url, String username, String password) { + this.url = url; + this.username = username; + this.password = password; + } + + private void handle( + String path, + Function method, + HttpResponse.BodyHandler bodyHandler, + BiConsumer, T> consumer) { + try { + String credentials = username + ":" + password; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + + HttpClient httpClient = HttpClient.newHttpClient(); + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(url + path)) + .header("Authorization", "Basic " + encodedCredentials); + HttpRequest request = method.apply(builder).build(); + + HttpResponse response = httpClient.send(request, bodyHandler); + int code = response.statusCode(); + if (code < 200 || code > 299) { + throw new IllegalStateException("Bad response: " + code + " >> " + response.body()); + } + consumer.accept(response, response.body()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + + public User user() { + AtomicReference ref = new AtomicReference<>(); + handle( + "/api/user", + HttpRequest.Builder::GET, + HttpResponse.BodyHandlers.ofString(), + (r, b) -> { + try { + User user = MAPPER.readValue(b, User.class); + ref.set(user); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + }); + return ref.get(); + } + + public QueryResult query(String query) { + AtomicReference ref = new AtomicReference<>(); + handle( + "/api/datasources/proxy/1/api/v1/query?query=" + query, + HttpRequest.Builder::GET, + HttpResponse.BodyHandlers.ofString(), + (r, b) -> { + try { + QueryResult result = MAPPER.readValue(b, QueryResult.class); + ref.set(result); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + }); + return ref.get(); + } +} + +---- diff --git a/docs/src/main/asciidoc/observability-devservices.adoc b/docs/src/main/asciidoc/observability-devservices.adoc new file mode 100644 index 0000000000000..e906b22741cd6 --- /dev/null +++ b/docs/src/main/asciidoc/observability-devservices.adoc @@ -0,0 +1,42 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Observability Dev Services +include::_attributes.adoc[] +:categories: observability,devservices,telemetry,metrics,tracing,logging +:summary: Entry point for Observability DevServices +:topics: observability,grafana,lgtm,prometheus,victoriametrics,jaeger,otel,otlp +:extensions: io.quarkus:quarkus-observability-devservices + +We are already familiar with xref:dev-services.adoc[Dev Service] concept, but in the case of Observability we need a way to orchestrate and connect more than a single dev service, usually a whole stack of them; e.g. a metrics agent periodically scraping application for metrics, pushing them into timeseries database, and Grafana feeding graphs of this timeseries data. + +With this in mind, we added a new concept of Dev Resource, an adapter between Dev Service concept and https://testcontainers.com/[Testcontainers]. And since we now have fine-grained services - with the Dev Resource per container, we can take this even further, allowing the user to choose the way to use this new Dev Resource concept: + +NOTE: Each Dev Resource implementation is an `@QuarkusTestResourceLifecycleManager` implementation as well + +* leave it to Dev Services to pick-up various Dev Resources from classpath, and apply xref:dev-services.adoc[Dev Service] concept to it + +* explicitly disable Dev Services and enable Dev Resources and use less-heavy concept of starting and stopping Dev Resources + +* explicitly disable both Dev Services and Dev Resources, and use Quarkus' `@QuarkusTestResource` testing concept (see Note) + +You can either add Observability extension dependency along with needed Dev Resources dependencies, or you use existing `sinks` - pom.xml files which add Observability extension dependency along with other required dependencies for certain technology stacks; e.g. `victoriametrics` sink would have `quarkus-observability-devresource-victoriametrics` and `quarkus-victoriametrics-client` dependencies already included in the `pom.xml`. + +[NOTE] +==== +Make sure you set the `scope` of these sink dependencies to `provided`, otherwise libraries such as Testcontainers will end-up in your app's production libraries: +[source, xml] +---- + + io.quarkus + quarkus-observability-devservices-... + provided + +---- +==== + +Let's see how all of this looks in practice, with the usual `all-in-one` Grafana usage, in the form of https://github.com/grafana/docker-otel-lgtm[OTel-LGTM] Docker image. + +* xref:observability-devservices-lgtm.adoc[Getting Started with Grafana-OTel-LGTM] diff --git a/docs/src/main/asciidoc/opentelemetry.adoc b/docs/src/main/asciidoc/opentelemetry.adoc index 8616bad99244c..5462114043b5b 100644 --- a/docs/src/main/asciidoc/opentelemetry.adoc +++ b/docs/src/main/asciidoc/opentelemetry.adoc @@ -167,7 +167,21 @@ If you need to enable or disable the exporter at runtime, you can use the < locateContainer(String serviceName, boolean sh } } + /** + * @return container id, if exists + */ + public Optional locateContainer(String serviceName, boolean shared, LaunchMode launchMode, + BiConsumer consumer) { + if (shared && launchMode == LaunchMode.DEVELOPMENT) { + return lookup(serviceName) + .map(container -> { + Arrays.stream(container.getPorts()) + .filter(cp -> Objects.nonNull(cp.getPublicPort()) && Objects.nonNull(cp.getPrivatePort())) + .forEach(cp -> { + ContainerAddress containerAddress = new ContainerAddress( + container.getId(), + DockerClientFactory.instance().dockerHostIpAddress(), + cp.getPublicPort()); + consumer.accept(cp.getPrivatePort(), containerAddress); + }); + return container.getId(); + }); + } else { + return Optional.empty(); + } + } + public Optional locatePublicPort(String serviceName, boolean shared, LaunchMode launchMode, int privatePort) { if (shared && launchMode == LaunchMode.DEVELOPMENT) { return lookup(serviceName) diff --git a/extensions/observability-devservices/common/pom.xml b/extensions/observability-devservices/common/pom.xml new file mode 100644 index 0000000000000..0ad62360c8ce4 --- /dev/null +++ b/extensions/observability-devservices/common/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + quarkus-observability-devservices-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devservices-common + Quarkus - Observability Dev Services - Common + + + + io.quarkus + quarkus-core + provided + + + io.smallrye.config + smallrye-config-core + provided + + + diff --git a/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/ContainerConstants.java b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/ContainerConstants.java new file mode 100644 index 0000000000000..630f29824e42a --- /dev/null +++ b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/ContainerConstants.java @@ -0,0 +1,15 @@ +package io.quarkus.observability.common; + +public final class ContainerConstants { + + // Images + + public static final String LGTM = "docker.io/grafana/otel-lgtm:0.4.0"; + + // Ports + + public static final int GRAFANA_PORT = 3000; + + public static final int OTEL_GRPC_EXPORTER_PORT = 4317; + public static final int OTEL_HTTP_EXPORTER_PORT = 4318; +} diff --git a/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/AbstractContainerConfig.java b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/AbstractContainerConfig.java new file mode 100644 index 0000000000000..3ddefb2292994 --- /dev/null +++ b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/AbstractContainerConfig.java @@ -0,0 +1,51 @@ +package io.quarkus.observability.common.config; + +import java.util.Locale; +import java.util.Optional; +import java.util.Set; + +public abstract class AbstractContainerConfig implements ContainerConfig { + + private final String imageName; + private final boolean shared; + + public AbstractContainerConfig(String imageName) { + this(imageName, true); + } + + public AbstractContainerConfig(String imageName, boolean shared) { + this.imageName = imageName; + this.shared = shared; + } + + @Override + public boolean enabled() { + return true; + } + + @Override + public String imageName() { + return imageName; + } + + @Override + public boolean shared() { + return shared; + } + + @Override + public Optional> networkAliases() { + return Optional.empty(); + } + + @Override + public String label() { + String sn = getClass().getSimpleName().toLowerCase(Locale.ROOT); + return "quarkus-dev-resource-" + sn; + } + + @Override + public String serviceName() { + return "quarkus"; + } +} diff --git a/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/AbstractGrafanaConfig.java b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/AbstractGrafanaConfig.java new file mode 100644 index 0000000000000..b73cea0232961 --- /dev/null +++ b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/AbstractGrafanaConfig.java @@ -0,0 +1,51 @@ +package io.quarkus.observability.common.config; + +import java.time.Duration; + +import io.quarkus.observability.common.ContainerConstants; + +public abstract class AbstractGrafanaConfig extends AbstractContainerConfig implements GrafanaConfig { + + private final String username; + private final String password; + private final int grafanaPort; + + public AbstractGrafanaConfig(String imageName) { + this(imageName, true, "admin", "admin", ContainerConstants.GRAFANA_PORT); + } + + public AbstractGrafanaConfig(String imageName, boolean shared) { + this(imageName, shared, "admin", "admin", ContainerConstants.GRAFANA_PORT); + } + + public AbstractGrafanaConfig(String imageName, String username, String password, int grafanaPort) { + this(imageName, true, username, password, grafanaPort); + } + + public AbstractGrafanaConfig(String imageName, boolean shared, String username, String password, int grafanaPort) { + super(imageName, shared); + this.username = username; + this.password = password; + this.grafanaPort = grafanaPort; + } + + @Override + public String username() { + return username; + } + + @Override + public String password() { + return password; + } + + @Override + public int grafanaPort() { + return grafanaPort; + } + + @Override + public Duration timeout() { + return Duration.ofMinutes(1); + } +} diff --git a/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ContainerConfig.java b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ContainerConfig.java new file mode 100644 index 0000000000000..d6bb59e981d58 --- /dev/null +++ b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ContainerConfig.java @@ -0,0 +1,69 @@ +package io.quarkus.observability.common.config; + +import java.util.Optional; +import java.util.Set; + +import io.smallrye.config.WithDefault; + +public interface ContainerConfig { + + /** + * If DevServices has been explicitly enabled or disabled. DevServices is generally enabled + * by default, unless there is an existing configuration present. + *

+ * When DevServices is enabled Quarkus will attempt to automatically configure and start + * a containers when running in Dev or Test mode and when Docker is running. + */ + @WithDefault("true") + boolean enabled(); + + /** + * The container image name to use, for container based DevServices providers. + */ + String imageName(); + + /** + * Indicates if the container managed by Quarkus Dev Services is shared. + * When shared, Quarkus looks for running containers using label-based service discovery. + * If a matching container is found, it is used, and so a second one is not started. + * Otherwise, Dev Services starts a new container. + *

+ * The discovery uses the {@code quarkus-dev-service-label} label. + * The value is configured using the {@code service-name} property. + *

+ * Container sharing is only used in dev mode. + */ + @WithDefault("true") + boolean shared(); + + /** + * Network aliases. + * + * @return metwork aliases + */ + Optional> networkAliases(); + + /** + * The full name of the label attached to the started container. + * This label is used when {@code shared} is set to {@code true}. + * In this case, before starting a container, Dev Services for looks for a container with th label + * set to the configured value. If found, it will use this container instead of starting a new one. Otherwise, it + * starts a new container with this label set to the specified value. + *

+ * This property is used when you need multiple shared containers. + */ + String label(); + + /** + * The value of the {@code quarkus-dev-service} label attached to the started container. + * This property is used when {@code shared} is set to {@code true}. + * In this case, before starting a container, Dev Services for looks for a container with the + * {@code quarkus-dev-service} label + * set to the configured value. If found, it will use this container instead of starting a new one. Otherwise, it + * starts a new container with the {@code quarkus-dev-service} label set to the specified value. + *

+ * This property is used when you need multiple shared containers. + */ + @WithDefault("quarkus") + String serviceName(); +} diff --git a/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ContainerConfigUtil.java b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ContainerConfigUtil.java new file mode 100644 index 0000000000000..8d3ce6d1a0b42 --- /dev/null +++ b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ContainerConfigUtil.java @@ -0,0 +1,41 @@ +package io.quarkus.observability.common.config; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; + +public class ContainerConfigUtil { + /** + * We need a per config method equals, + * so that we know when the config changes. + */ + public static boolean isEqual(ContainerConfig cc1, ContainerConfig cc2) { + Class c1 = cc1.getClass(); + Class c2 = cc1.getClass(); + if (!c1.equals(c2)) { + return false; + } + + Class i = Arrays.stream(c1.getInterfaces()) + .filter(ContainerConfig.class::isAssignableFrom) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Missing ContainerConfig based interface")); + Method[] methods = i.getMethods(); // should get all config methods + for (Method m : methods) { + Object v1 = invoke(m, cc1); + Object v2 = invoke(m, cc2); + if (!Objects.equals(v1, v2)) { + return false; + } + } + return true; + } + + private static Object invoke(Method m, Object target) { + try { + return m.invoke(target); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/GrafanaConfig.java b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/GrafanaConfig.java new file mode 100644 index 0000000000000..229522ab8e027 --- /dev/null +++ b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/GrafanaConfig.java @@ -0,0 +1,33 @@ +package io.quarkus.observability.common.config; + +import java.time.Duration; + +import io.smallrye.config.WithDefault; + +public interface GrafanaConfig extends ContainerConfig { + + // copied from ContainerConfig, config hierarchy workaround + + @WithDefault("true") + boolean enabled(); + + @WithDefault("true") + boolean shared(); + + @WithDefault("quarkus") + String serviceName(); + + // --- + + @WithDefault("admin") + String username(); + + @WithDefault("admin") + String password(); + + @WithDefault("3000") + int grafanaPort(); + + @WithDefault("PT1M") + Duration timeout(); +} diff --git a/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/LgtmConfig.java b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/LgtmConfig.java new file mode 100644 index 0000000000000..ae7a239cb647f --- /dev/null +++ b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/LgtmConfig.java @@ -0,0 +1,23 @@ +package io.quarkus.observability.common.config; + +import java.util.Optional; +import java.util.Set; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +public interface LgtmConfig extends GrafanaConfig { + @WithDefault(ContainerConstants.LGTM) + String imageName(); + + @WithDefault("lgtm,lgtm.testcontainer.docker") + Optional> networkAliases(); + + @WithDefault("quarkus-dev-service-lgtm") + String label(); + + @WithDefault("4318") + int otlpPort(); +} diff --git a/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ModulesConfiguration.java b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ModulesConfiguration.java new file mode 100644 index 0000000000000..7625f622234ce --- /dev/null +++ b/extensions/observability-devservices/common/src/main/java/io/quarkus/observability/common/config/ModulesConfiguration.java @@ -0,0 +1,8 @@ +package io.quarkus.observability.common.config; + +import io.quarkus.runtime.annotations.ConfigDocSection; + +public interface ModulesConfiguration { + @ConfigDocSection + LgtmConfig lgtm(); +} diff --git a/extensions/observability-devservices/deployment/pom.xml b/extensions/observability-devservices/deployment/pom.xml new file mode 100644 index 0000000000000..b2d7788a0da5c --- /dev/null +++ b/extensions/observability-devservices/deployment/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + quarkus-observability-devservices-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devservices-deployment + Quarkus - Observability Dev Services - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-devservices-deployment + + + io.quarkus + quarkus-kubernetes-spi + + + io.quarkus + quarkus-observability-devservices + + + + io.quarkus + quarkus-junit5-internal + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + org.awaitility + awaitility + test + + + io.rest-assured + rest-assured + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesBuildItem.java b/extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesBuildItem.java new file mode 100644 index 0000000000000..4bde7af481a59 --- /dev/null +++ b/extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesBuildItem.java @@ -0,0 +1,6 @@ +package io.quarkus.observability.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +final class DevResourcesBuildItem extends SimpleBuildItem { +} diff --git a/extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesProcessor.java b/extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesProcessor.java new file mode 100644 index 0000000000000..88d17c7c34d13 --- /dev/null +++ b/extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/DevResourcesProcessor.java @@ -0,0 +1,51 @@ +package io.quarkus.observability.deployment; + +import java.util.function.BooleanSupplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; +import io.quarkus.deployment.builditem.ShutdownContextBuildItem; +import io.quarkus.observability.runtime.DevResourceShutdownRecorder; +import io.quarkus.observability.runtime.DevResourcesConfigBuilder; +import io.quarkus.observability.runtime.config.ObservabilityConfiguration; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = DevResourcesProcessor.IsEnabled.class) +class DevResourcesProcessor { + private static final Logger log = LoggerFactory.getLogger(DevResourcesProcessor.class); + private static final String FEATURE = "devresources"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + public RunTimeConfigBuilderBuildItem registerDevResourcesConfigSource() { + log.info("Adding dev resources config builder"); + return new RunTimeConfigBuilderBuildItem(DevResourcesConfigBuilder.class); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public DevResourcesBuildItem shutdownDevResources(DevResourceShutdownRecorder recorder, ShutdownContextBuildItem shutdown) { + recorder.shutdown(shutdown); + return new DevResourcesBuildItem(); + } + + public static class IsEnabled implements BooleanSupplier { + ObservabilityConfiguration config; + + public boolean getAsBoolean() { + return config.devResources() && !config.enabled(); + } + } + +} diff --git a/extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/ObservabilityDevServiceProcessor.java b/extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/ObservabilityDevServiceProcessor.java new file mode 100644 index 0000000000000..48c5932448d18 --- /dev/null +++ b/extensions/observability-devservices/deployment/src/main/java/io/quarkus/observability/deployment/ObservabilityDevServiceProcessor.java @@ -0,0 +1,216 @@ +package io.quarkus.observability.deployment; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.logging.Logger; + +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; +import io.quarkus.deployment.builditem.DevServicesResultBuildItem; +import io.quarkus.deployment.builditem.DockerStatusBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; +import io.quarkus.devservices.common.ContainerLocator; +import io.quarkus.observability.common.config.ContainerConfig; +import io.quarkus.observability.common.config.ContainerConfigUtil; +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.devresource.Container; +import io.quarkus.observability.devresource.DevResourceLifecycleManager; +import io.quarkus.observability.devresource.DevResources; +import io.quarkus.observability.runtime.config.ObservabilityConfiguration; +import io.quarkus.runtime.LaunchMode; + +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class, + ObservabilityDevServiceProcessor.IsEnabled.class }) +class ObservabilityDevServiceProcessor { + private static final Logger log = Logger.getLogger(ObservabilityDevServiceProcessor.class); + + private static final Map devServices = new ConcurrentHashMap<>(); + private static final Map capturedDevServicesConfigurations = new ConcurrentHashMap<>(); + private static final Map firstStart = new ConcurrentHashMap<>(); + + public static class IsEnabled implements BooleanSupplier { + ObservabilityConfiguration config; + + public boolean getAsBoolean() { + return config.enabled() && !config.devResources(); + } + } + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(Feature.OBSERVABILITY); + } + + private String devId(DevResourceLifecycleManager dev) { + String sn = dev.getClass().getSimpleName(); + int p = sn.indexOf("Resource"); + return sn.substring(0, p != -1 ? p : sn.length()); + } + + @BuildStep + public void startContainers(LaunchModeBuildItem launchMode, + DockerStatusBuildItem dockerStatusBuildItem, + ObservabilityConfiguration configuration, + Optional consoleInstalledBuildItem, + CuratedApplicationShutdownBuildItem closeBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem, + GlobalDevServicesConfig devServicesConfig, + BuildProducer services) { + + if (!configuration.enabled()) { + log.infof("Observability dev services are disabled in config"); + return; + } + + if (!dockerStatusBuildItem.isDockerAvailable()) { + log.warn("Please get a working Docker instance"); + return; + } + + @SuppressWarnings("rawtypes") + List resources = DevResources.resources(); + // this should throw an exception on a duplicate + //noinspection ResultOfMethodCallIgnored + resources.stream().collect(Collectors.toMap(this::devId, Function.identity())); + + @SuppressWarnings("rawtypes") + Stream stream = resources.stream(); + if (configuration.parallel()) { + stream = stream.parallel(); + } + + stream.forEach(dev -> { + String devId = devId(dev); + + // only do get, not remove, so it can be re-used + DevServicesResultBuildItem.RunningDevService devService = devServices.get(devId); + ContainerConfig currentDevServicesConfiguration = dev.config(configuration); + + if (devService != null) { + ContainerConfig capturedDevServicesConfiguration = capturedDevServicesConfigurations.get(devId); + boolean equalConfig = ContainerConfigUtil.isEqual(capturedDevServicesConfiguration, + currentDevServicesConfiguration); + if (equalConfig) { + log.debugf("Equal config, re-using existing %s container", devId); + services.produce(devService.toBuildItem()); + return; + } + try { + devService.close(); + } catch (Throwable e) { + log.errorf("Failed to stop %s container", devId, e); + } + } + + devServices.remove(devId); // clean-up + capturedDevServicesConfigurations.put(devId, currentDevServicesConfiguration); + + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + devId + " Dev Services Starting:", + consoleInstalledBuildItem, + loggingSetupBuildItem, + s -> false, + s -> s.contains(getClass().getSimpleName())); // log if it comes from this class + try { + DevServicesResultBuildItem.RunningDevService newDevService = startContainer( + devId, + dev, + currentDevServicesConfiguration, + configuration, + devServicesConfig.timeout); + if (newDevService == null) { + compressor.closeAndDumpCaptured(); + return; + } else { + compressor.close(); + } + + devService = newDevService; + devServices.put(devId, newDevService); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); + } + + if (firstStart.computeIfAbsent(devId, x -> true)) { + Runnable closeTask = () -> { + DevServicesResultBuildItem.RunningDevService current = devServices.get(devId); + if (current != null) { + try { + current.close(); + } catch (Throwable t) { + log.errorf("Failed to stop %s container", devId, t); + } + } + firstStart.remove(devId); + //noinspection resource + devServices.remove(devId); + capturedDevServicesConfigurations.remove(devId); + }; + closeBuildItem.addCloseTask(closeTask, true); + } + + services.produce(devService.toBuildItem()); + }); + } + + private DevServicesResultBuildItem.RunningDevService startContainer( + String devId, + DevResourceLifecycleManager dev, + ContainerConfig capturedDevServicesConfiguration, + ModulesConfiguration root, + Optional timeout) { + + if (!capturedDevServicesConfiguration.enabled()) { + // explicitly disabled + log.debugf("Not starting Dev Services for %s as it has been disabled in the config", devId); + return null; + } + + if (!dev.enable()) { + return null; + } + + final Supplier defaultContainerSupplier = () -> { + Container container = dev.container(capturedDevServicesConfiguration, root); + timeout.ifPresent(container::withStartupTimeout); + Map config = dev.start(); + log.infof("Dev Service %s started, config: %s", devId, config); + return new DevServicesResultBuildItem.RunningDevService( + Feature.OBSERVABILITY.getName(), container.getContainerId(), + container.closeableCallback(capturedDevServicesConfiguration.serviceName()), config); + }; + + Map config = new LinkedHashMap<>(); // old config + ContainerLocator containerLocator = new ContainerLocator(capturedDevServicesConfiguration.label(), 0); // can be 0, as we don't use it + return containerLocator + .locateContainer( + capturedDevServicesConfiguration.serviceName(), capturedDevServicesConfiguration.shared(), + LaunchMode.current(), (p, ca) -> config.putAll(dev.config(p, ca.getHost(), ca.getPort()))) + .map(cid -> { + log.infof("Dev Service %s re-used, config: %s", devId, config); + return new DevServicesResultBuildItem.RunningDevService(Feature.OBSERVABILITY.getName(), cid, + null, config); + }) + .orElseGet(defaultContainerSupplier); + } +} diff --git a/extensions/observability-devservices/pom.xml b/extensions/observability-devservices/pom.xml new file mode 100644 index 0000000000000..972d9e41e83b7 --- /dev/null +++ b/extensions/observability-devservices/pom.xml @@ -0,0 +1,56 @@ + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-observability-devservices-parent + Quarkus - Observability Dev Services - Parent + pom + + common + testcontainers + testlibs + deployment + runtime + + sink/lgtm + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce + + + + + classpath:enforcer-rules/quarkus-banned-dependencies.xml + + + classpath:enforcer-rules/quarkus-banned-dependencies-okhttp.xml + + + + + + + + + \ No newline at end of file diff --git a/extensions/observability-devservices/runtime/pom.xml b/extensions/observability-devservices/runtime/pom.xml new file mode 100644 index 0000000000000..3d4b5ec69174e --- /dev/null +++ b/extensions/observability-devservices/runtime/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + quarkus-observability-devservices-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devservices + Quarkus - Observability Dev Services - Runtime + Serve and consume Observability Dev Services + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-core + + + + io.quarkus + quarkus-observability-devservices-common + + + io.quarkus + quarkus-observability-devresource-common + + + + + io.quarkus + quarkus-junit5-internal + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + generate-extension-descriptor + + extension-descriptor + + process-resources + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/DevResourceShutdownRecorder.java b/extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/DevResourceShutdownRecorder.java new file mode 100644 index 0000000000000..0d63c09b081dc --- /dev/null +++ b/extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/DevResourceShutdownRecorder.java @@ -0,0 +1,12 @@ +package io.quarkus.observability.runtime; + +import io.quarkus.observability.devresource.DevResources; +import io.quarkus.runtime.ShutdownContext; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class DevResourceShutdownRecorder { + public void shutdown(ShutdownContext context) { + context.addLastShutdownTask(DevResources::stop); + } +} diff --git a/extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/DevResourcesConfigBuilder.java b/extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/DevResourcesConfigBuilder.java new file mode 100644 index 0000000000000..8d327a1d4bb0c --- /dev/null +++ b/extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/DevResourcesConfigBuilder.java @@ -0,0 +1,18 @@ +package io.quarkus.observability.runtime; + +import io.quarkus.observability.devresource.DevResourcesConfigSource; +import io.quarkus.runtime.configuration.ConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilder; + +public class DevResourcesConfigBuilder implements ConfigBuilder { + @Override + public SmallRyeConfigBuilder configBuilder(SmallRyeConfigBuilder builder) { + return builder.withSources(new DevResourcesConfigSource()); + } + + @Override + public int priority() { + // greater than any default Microprofile ConfigSource + return 500; + } +} diff --git a/extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/config/ObservabilityConfiguration.java b/extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/config/ObservabilityConfiguration.java new file mode 100644 index 0000000000000..e9327be656c1e --- /dev/null +++ b/extensions/observability-devservices/runtime/src/main/java/io/quarkus/observability/runtime/config/ObservabilityConfiguration.java @@ -0,0 +1,35 @@ +package io.quarkus.observability.runtime.config; + +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.observability") +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public interface ObservabilityConfiguration extends ModulesConfiguration { + /** + * If DevServices has been explicitly enabled or disabled. DevServices is generally enabled + * by default, unless there is an existing configuration present. + *

+ * When DevServices is enabled Quarkus will attempt to automatically configure and start + * a containers when running in Dev or Test mode and when Docker is running. + */ + @WithDefault("true") + boolean enabled(); + + /** + * Enable simplified usage of dev resources, + * instead of full observability processing. + * Make sure @code{enabled} is set to false. + */ + @WithDefault("false") + boolean devResources(); + + /** + * Do we start the dev services in parallel. + */ + @WithDefault("false") + boolean parallel(); +} diff --git a/extensions/observability-devservices/sink/lgtm/pom.xml b/extensions/observability-devservices/sink/lgtm/pom.xml new file mode 100644 index 0000000000000..467432630e33d --- /dev/null +++ b/extensions/observability-devservices/sink/lgtm/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + quarkus-observability-devservices-parent + io.quarkus + 999-SNAPSHOT + ../../pom.xml + + + quarkus-observability-devservices-lgtm + Quarkus - Observability Dev Services - LGTM + + + + io.quarkus + quarkus-observability-devservices + + + io.quarkus + quarkus-observability-devresource-lgtm + + + diff --git a/extensions/observability-devservices/testcontainers/pom.xml b/extensions/observability-devservices/testcontainers/pom.xml new file mode 100644 index 0000000000000..5f4a15c826d64 --- /dev/null +++ b/extensions/observability-devservices/testcontainers/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + quarkus-observability-devservices-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-testcontainers + Quarkus - Observability Dev Services - Testcontainers + + + + io.quarkus + quarkus-devservices-common + + + io.quarkus + quarkus-observability-devservices-common + + + org.testcontainers + testcontainers + + + junit + junit + + + + + io.quarkus + quarkus-junit4-mock + + + org.junit.jupiter + junit-jupiter-api + test + + + + \ No newline at end of file diff --git a/extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/GrafanaContainer.java b/extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/GrafanaContainer.java new file mode 100644 index 0000000000000..0218c6911b3cf --- /dev/null +++ b/extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/GrafanaContainer.java @@ -0,0 +1,35 @@ +package io.quarkus.observability.testcontainers; + +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.containers.wait.strategy.WaitStrategy; + +import io.quarkus.observability.common.config.GrafanaConfig; + +@SuppressWarnings("resource") +public abstract class GrafanaContainer, C extends GrafanaConfig> + extends ObservabilityContainer { + protected static final String DATASOURCES_PATH = "/etc/grafana/provisioning/datasources/custom.yaml"; + + protected C config; + + public GrafanaContainer(C config) { + super(config); + this.config = config; + withEnv("GF_SECURITY_ADMIN_USER", config.username()); + withEnv("GF_SECURITY_ADMIN_PASSWORD", config.password()); + addExposedPort(config.grafanaPort()); + waitingFor(grafanaWaitStrategy()); + } + + public int getGrafanaPort() { + return getMappedPort(config.grafanaPort()); + } + + private WaitStrategy grafanaWaitStrategy() { + return new HttpWaitStrategy() + .forPath("/") + .forPort(config.grafanaPort()) + .forStatusCode(200) + .withStartupTimeout(config.timeout()); + } +} diff --git a/extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/LgtmContainer.java b/extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/LgtmContainer.java new file mode 100644 index 0000000000000..fd31bab3be9c0 --- /dev/null +++ b/extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/LgtmContainer.java @@ -0,0 +1,45 @@ +package io.quarkus.observability.testcontainers; + +import java.util.Optional; +import java.util.Set; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.observability.common.config.AbstractGrafanaConfig; +import io.quarkus.observability.common.config.LgtmConfig; + +public class LgtmContainer extends GrafanaContainer { + protected static final String LGTM_NETWORK_ALIAS = "ltgm.testcontainer.docker"; + + public LgtmContainer() { + this(new LgtmConfigImpl()); + } + + public LgtmContainer(LgtmConfig config) { + super(config); + addExposedPorts(config.otlpPort()); + } + + public int getOtlpPort() { + return getMappedPort(config.otlpPort()); + } + + protected static class LgtmConfigImpl extends AbstractGrafanaConfig implements LgtmConfig { + public LgtmConfigImpl() { + this(ContainerConstants.LGTM); + } + + public LgtmConfigImpl(String imageName) { + super(imageName); + } + + @Override + public Optional> networkAliases() { + return Optional.of(Set.of("lgtm", LGTM_NETWORK_ALIAS)); + } + + @Override + public int otlpPort() { + return ContainerConstants.OTEL_HTTP_EXPORTER_PORT; + } + } +} diff --git a/extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/ObservabilityContainer.java b/extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/ObservabilityContainer.java new file mode 100644 index 0000000000000..bcc761dddd58a --- /dev/null +++ b/extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/ObservabilityContainer.java @@ -0,0 +1,72 @@ +package io.quarkus.observability.testcontainers; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; + +import io.quarkus.observability.common.config.ContainerConfig; + +@SuppressWarnings("resource") +public abstract class ObservabilityContainer, C extends ContainerConfig> + extends GenericContainer { + private final Logger log = LoggerFactory.getLogger(getClass()); + private final Logger dockerLog = LoggerFactory.getLogger(getClass().getName() + ".docker"); + + public ObservabilityContainer(C config) { + super(DockerImageName.parse(config.imageName())); + withLogConsumer(frameConsumer()); + withLabel(config.label(), config.serviceName()); + Optional> aliases = config.networkAliases(); + aliases.map(s -> s.toArray(new String[0])).ifPresent(this::withNetworkAliases); + if (config.shared()) { + withNetwork(Network.SHARED); + } + } + + protected Consumer frameConsumer() { + return frame -> logger().debug(frame.getUtf8String().stripTrailing()); + } + + protected byte[] getResourceAsBytes(String resource) { + try (InputStream in = getClass().getClassLoader().getResourceAsStream(resource)) { + return in.readAllBytes(); + } catch (IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + + @SuppressWarnings("OctalInteger") + protected void addFileToContainer(byte[] content, String pathInContainer) { + logger().info("Content [{}]: \n{}", pathInContainer, new String(content, StandardCharsets.UTF_8)); + copyFileToContainer(Transferable.of(content, 0777), pathInContainer); + } + + @Override + protected Logger logger() { + return dockerLog; + } + + @Override + public void start() { + log.info("Starting {} ...", getClass().getSimpleName()); + super.start(); + } + + @Override + public void stop() { + log.info("Stopping {}...", getClass().getSimpleName()); + super.stop(); + } +} diff --git a/extensions/observability-devservices/testlibs/devresource-common/pom.xml b/extensions/observability-devservices/testlibs/devresource-common/pom.xml new file mode 100644 index 0000000000000..3185155b20277 --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-common/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + quarkus-observability-testlibs + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devresource-common + Quarkus - Observability Dev Services - Dev Resource Common + + + + org.eclipse.microprofile.config + microprofile-config-api + + + org.jboss.logging + jboss-logging + + + io.quarkus + quarkus-observability-devservices-common + + + io.quarkus + quarkus-test-common + + + io.quarkus + quarkus-core-deployment + + + + + \ No newline at end of file diff --git a/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/Container.java b/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/Container.java new file mode 100644 index 0000000000000..e591fb9555a51 --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/Container.java @@ -0,0 +1,21 @@ +package io.quarkus.observability.devresource; + +import java.io.Closeable; +import java.time.Duration; + +import io.quarkus.observability.common.config.ContainerConfig; + +/** + * Simple container abstraction, e.g. similar to GenericContainer + */ +public interface Container { + void start(); + + void stop(); + + String getContainerId(); + + void withStartupTimeout(Duration duration); + + Closeable closeableCallback(String serviceName); +} diff --git a/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResourceLifecycleManager.java b/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResourceLifecycleManager.java new file mode 100644 index 0000000000000..be32e44084607 --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResourceLifecycleManager.java @@ -0,0 +1,83 @@ +package io.quarkus.observability.devresource; + +import java.util.Map; + +import io.quarkus.observability.common.config.ContainerConfig; +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +/** + * Extends {@link io.quarkus.test.common.QuarkusTestResourceLifecycleManager} + * so that classes implement both interfaces at the same time - simplifying testing. + */ +public interface DevResourceLifecycleManager extends QuarkusTestResourceLifecycleManager { + + // Put order constants here -- order by dependency + + int METRICS = 5000; + int SCRAPER = 7500; + int GRAFANA = 10000; + int JAEGER = 20000; + int OTEL = 20000; + + //---- + + /** + * Get resource's config from main observability configuration. + * + * @param configuration main observability configuration + * @return module's config + */ + T config(ModulesConfiguration configuration); + + /** + * Should we enable / start this dev resource. + * e.g. we could already have actual service running + * Each impl should provide its own reason on why it disabled dev service. + * + * @return true if ok to start new dev service, false otherwise + */ + default boolean enable() { + return true; + } + + /** + * Create container from config. + * + * @param config the config + * @return container id + */ + default Container container(T config) { + throw new IllegalStateException("Should be implemented!"); + } + + /** + * Create container from config. + * + * @param config the config + * @param root the all modules config + * @return container id + */ + default Container container(T config, ModulesConfiguration root) { + return container(config); + } + + /** + * Deduct current config from params. + * If port are too dynamic / configured, it's hard to deduct, + * since configuration is not part of the devservice state. + * e.g. different ports then usual - Grafana UI is 3000, if you do not use 3000, + * it's hard or impossible to know which port belongs to certain property. + * + * @return A map of system properties that should be set for the running dev-mode app + */ + Map config(int privatePort, String host, int publicPort); + + /** + * Called even before {@link #start()} so that the implementation can prepare itself + * to be used as dev resource (as opposed to test resource which uses a different + * init() method). + */ + default void initDev() { + } +} diff --git a/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResources.java b/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResources.java new file mode 100644 index 0000000000000..0dbd79ed0579e --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResources.java @@ -0,0 +1,88 @@ +package io.quarkus.observability.devresource; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; + +/** + * A registry of dev resources. + */ +@SuppressWarnings("rawtypes") +public class DevResources { + private static final Logger log = Logger.getLogger(DevResources.class); + + private static List resources; + private static Map map; + + /** + * @return list of found dev resources. + */ + public static synchronized List resources() { + if (resources == null) { + log.info("Activating dev resources"); + + resources = ServiceLoader + .load(DevResourceLifecycleManager.class, Thread.currentThread().getContextClassLoader()) + .stream() + .map(ServiceLoader.Provider::get) + .sorted(Comparator.comparing(DevResourceLifecycleManager::order)) + .collect(Collectors.toList()); + + log.infof("Found dev resources: %s", resources); + } + return resources; + } + + /** + * Ensures all dev resources are started and returns a map of config properties. + * + * @return a map of config properties to be returned by {@link DevResourcesConfigSource} + */ + static synchronized Map ensureStarted() { + if (map == null) { + try { + for (var res : resources()) { + res.initDev(); + } + } catch (Exception e) { + log.error("Exception initializing dev resource manager", e); + throw e; + } + try { + var map = new HashMap(); + for (var res : resources()) { + var resMap = res.start(); + log.infof("Dev resource [%s] contributed config: %s", res.getClass().getSimpleName(), resMap); + map.putAll(resMap); + } + DevResources.map = Collections.unmodifiableMap(map); + } catch (Exception e) { + log.error("Exception starting dev resource", e); + throw e; + } + } + return map; + } + + /** + * Stops all dev resources. + */ + public static synchronized void stop() { + if (map != null) { + for (var i = resources().listIterator(resources().size()); i.hasPrevious();) { + try { + i.previous().stop(); + } catch (Exception e) { + log.warn("Exception stopping dev resource", e); + } + } + map = null; + } + } +} diff --git a/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResourcesConfigSource.java b/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResourcesConfigSource.java new file mode 100644 index 0000000000000..b3458c5be2be2 --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-common/src/main/java/io/quarkus/observability/devresource/DevResourcesConfigSource.java @@ -0,0 +1,28 @@ +package io.quarkus.observability.devresource; + +import java.util.Set; + +import org.eclipse.microprofile.config.spi.ConfigSource; + +public class DevResourcesConfigSource implements ConfigSource { + @Override + public Set getPropertyNames() { + return DevResources.ensureStarted().keySet(); + } + + @Override + public String getValue(String propertyName) { + return DevResources.ensureStarted().get(propertyName); + } + + @Override + public String getName() { + return "DevResourcesConfigSource"; + } + + @Override + public int getOrdinal() { + // greater than any default Microprofile ConfigSource + return 500; + } +} diff --git a/extensions/observability-devservices/testlibs/devresource-lgtm/pom.xml b/extensions/observability-devservices/testlibs/devresource-lgtm/pom.xml new file mode 100644 index 0000000000000..9a89f891abe9b --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-lgtm/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + quarkus-observability-testlibs + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devresource-lgtm + Quarkus - Observability Dev Services - OTel LGTM Dev Resource + + + + io.quarkus + quarkus-observability-devresource-testcontainers + + + io.quarkus + quarkus-observability-testcontainers + + + + \ No newline at end of file diff --git a/extensions/observability-devservices/testlibs/devresource-lgtm/src/main/java/io/quarkus/observability/devresource/lgtm/LgtmResource.java b/extensions/observability-devservices/testlibs/devresource-lgtm/src/main/java/io/quarkus/observability/devresource/lgtm/LgtmResource.java new file mode 100644 index 0000000000000..6273380d805ba --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-lgtm/src/main/java/io/quarkus/observability/devresource/lgtm/LgtmResource.java @@ -0,0 +1,54 @@ +package io.quarkus.observability.devresource.lgtm; + +import java.util.Map; + +import io.quarkus.observability.common.ContainerConstants; +import io.quarkus.observability.common.config.LgtmConfig; +import io.quarkus.observability.common.config.ModulesConfiguration; +import io.quarkus.observability.devresource.Container; +import io.quarkus.observability.devresource.DevResourceLifecycleManager; +import io.quarkus.observability.devresource.testcontainers.ContainerResource; +import io.quarkus.observability.testcontainers.LgtmContainer; + +public class LgtmResource extends ContainerResource { + + @Override + public LgtmConfig config(ModulesConfiguration configuration) { + return configuration.lgtm(); + } + + @Override + public Container container(LgtmConfig config, ModulesConfiguration root) { + return set(new LgtmContainer(config)); + } + + @Override + public Map config(int privatePort, String host, int publicPort) { + switch (privatePort) { + case ContainerConstants.GRAFANA_PORT: + return Map.of("quarkus.grafana.url", String.format("%s:%s", host, publicPort)); + case ContainerConstants.OTEL_GRPC_EXPORTER_PORT: + case ContainerConstants.OTEL_HTTP_EXPORTER_PORT: + return Map.of("quarkus.otel-collector.url", String.format("%s:%s", host, publicPort)); + } + return Map.of(); + } + + @Override + protected LgtmContainer defaultContainer() { + return new LgtmContainer(); + } + + @Override + public Map doStart() { + String host = container.getHost(); + return Map.of( + "quarkus.grafana.url", String.format("%s:%s", host, container.getGrafanaPort()), + "quarkus.otel-collector.url", String.format("%s:%s", host, container.getOtlpPort())); + } + + @Override + public int order() { + return DevResourceLifecycleManager.GRAFANA; + } +} diff --git a/extensions/observability-devservices/testlibs/devresource-lgtm/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager b/extensions/observability-devservices/testlibs/devresource-lgtm/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager new file mode 100644 index 0000000000000..73702014d3425 --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-lgtm/src/main/resources/META-INF/services/io.quarkus.observability.devresource.DevResourceLifecycleManager @@ -0,0 +1 @@ +io.quarkus.observability.devresource.lgtm.LgtmResource diff --git a/extensions/observability-devservices/testlibs/devresource-testcontainers/pom.xml b/extensions/observability-devservices/testlibs/devresource-testcontainers/pom.xml new file mode 100644 index 0000000000000..1f943c5afaeeb --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-testcontainers/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + quarkus-observability-testlibs + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-devresource-testcontainers + Quarkus - Observability Dev Services - Dev Resource Testcontainers + + + + io.quarkus + quarkus-devservices-common + + + io.quarkus + quarkus-observability-devresource-common + + + org.testcontainers + testcontainers + + + junit + junit + + + + + io.quarkus + quarkus-junit4-mock + + + \ No newline at end of file diff --git a/extensions/observability-devservices/testlibs/devresource-testcontainers/src/main/java/io/quarkus/observability/devresource/testcontainers/ContainerResource.java b/extensions/observability-devservices/testlibs/devresource-testcontainers/src/main/java/io/quarkus/observability/devresource/testcontainers/ContainerResource.java new file mode 100644 index 0000000000000..5ad400f90a920 --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-testcontainers/src/main/java/io/quarkus/observability/devresource/testcontainers/ContainerResource.java @@ -0,0 +1,45 @@ +package io.quarkus.observability.devresource.testcontainers; + +import java.util.Map; + +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.observability.common.config.ContainerConfig; +import io.quarkus.observability.devresource.Container; +import io.quarkus.observability.devresource.DevResourceLifecycleManager; + +/** + * A container resource abstraction + */ +public abstract class ContainerResource, C extends ContainerConfig> + implements DevResourceLifecycleManager { + + protected T container; + protected Container wrapper; + + protected Container set(T container) { + this.container = container; + this.wrapper = new TestcontainerContainer<>(container); + return this.wrapper; + } + + @Override + public Map start() { + if (container == null) { + set(defaultContainer()); + } + container.start(); + return doStart(); + } + + @Override + public void stop() { + if (container != null) { + container.stop(); + } + } + + protected abstract T defaultContainer(); + + protected abstract Map doStart(); +} diff --git a/extensions/observability-devservices/testlibs/devresource-testcontainers/src/main/java/io/quarkus/observability/devresource/testcontainers/TestcontainerContainer.java b/extensions/observability-devservices/testlibs/devresource-testcontainers/src/main/java/io/quarkus/observability/devresource/testcontainers/TestcontainerContainer.java new file mode 100644 index 0000000000000..9799d3a4a4cac --- /dev/null +++ b/extensions/observability-devservices/testlibs/devresource-testcontainers/src/main/java/io/quarkus/observability/devresource/testcontainers/TestcontainerContainer.java @@ -0,0 +1,47 @@ +package io.quarkus.observability.devresource.testcontainers; + +import java.io.Closeable; +import java.time.Duration; +import java.util.Objects; + +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.devservices.common.ContainerShutdownCloseable; +import io.quarkus.observability.common.config.ContainerConfig; +import io.quarkus.observability.devresource.Container; + +/** + * Container impl / wrapper for Testcontainer's GenericContainer + */ +public class TestcontainerContainer, T extends ContainerConfig> implements Container { + private final GenericContainer container; + + public TestcontainerContainer(GenericContainer container) { + this.container = Objects.requireNonNull(container); + } + + @Override + public void start() { + container.start(); + } + + @Override + public void stop() { + container.stop(); + } + + @Override + public String getContainerId() { + return container.getContainerId(); + } + + @Override + public void withStartupTimeout(Duration duration) { + container.withStartupTimeout(duration); + } + + @Override + public Closeable closeableCallback(String serviceName) { + return new ContainerShutdownCloseable(container, serviceName); + } +} diff --git a/extensions/observability-devservices/testlibs/pom.xml b/extensions/observability-devservices/testlibs/pom.xml new file mode 100644 index 0000000000000..a7c4b4df2445e --- /dev/null +++ b/extensions/observability-devservices/testlibs/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + quarkus-observability-devservices-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-observability-testlibs + pom + Quarkus - Observability Dev Services - Test Libraries + + + devresource-common + devresource-testcontainers + devresource-lgtm + + + \ No newline at end of file diff --git a/extensions/pom.xml b/extensions/pom.xml index ccd152cf03acc..cbd5443006e62 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -52,6 +52,7 @@ micrometer-registry-prometheus opentelemetry info + observability-devservices resteasy-classic diff --git a/integration-tests/observability-lgtm/pom.xml b/integration-tests/observability-lgtm/pom.xml new file mode 100644 index 0000000000000..0abc8789677cb --- /dev/null +++ b/integration-tests/observability-lgtm/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-observability-lgtm + Quarkus - Integration Tests - Observability LGTM + + + + io.quarkus + quarkus-observability-devservices-lgtm + + + io.quarkus + quarkus-rest + + + + io.quarkiverse.micrometer.registry + quarkus-micrometer-registry-otlp + 3.2.4 + + + io.quarkus + quarkus-micrometer + + + io.quarkus + quarkus-opentelemetry + + + com.fasterxml.jackson.core + jackson-databind + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-opentelemetry-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-micrometer-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + diff --git a/integration-tests/observability-lgtm/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java b/integration-tests/observability-lgtm/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java new file mode 100644 index 0000000000000..17684efd1e944 --- /dev/null +++ b/integration-tests/observability-lgtm/src/main/java/io/quarkus/observability/example/SimpleEndpoint.java @@ -0,0 +1,48 @@ +package io.quarkus.observability.example; + +import java.security.SecureRandom; +import java.util.Random; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.logging.Logger; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; + +@Path("/api") +public class SimpleEndpoint { + private static final Logger log = Logger.getLogger(SimpleEndpoint.class); + + @Inject + MeterRegistry registry; + + Random random = new SecureRandom(); + double[] arr = new double[1]; + + @PostConstruct + public void start() { + String key = System.getProperty("tag-key", "test"); + Gauge.builder("xvalue", arr, a -> arr[0]) + .baseUnit("X") + .description("Some random x") + .tag(key, "x") + .register(registry); + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/poke") + public String poke(@QueryParam("f") int f) { + log.infof("Poke %s", f); + double x = random.nextDouble() * f; + arr[0] = x; + return "poke:" + x; + } +} diff --git a/integration-tests/observability-lgtm/src/main/resources/application.properties b/integration-tests/observability-lgtm/src/main/resources/application.properties new file mode 100644 index 0000000000000..b88a2d3f9bb17 --- /dev/null +++ b/integration-tests/observability-lgtm/src/main/resources/application.properties @@ -0,0 +1,17 @@ +quarkus.log.category."io.quarkus.observability".level=DEBUG +quarkus.log.category."io.quarkus.devservices".level=DEBUG + +#micrometer +quarkus.micrometer.export.otlp.enabled=true +quarkus.micrometer.export.otlp.publish=true +quarkus.micrometer.export.otlp.step=PT5S +quarkus.micrometer.export.otlp.default-registry=true +%dev.quarkus.micrometer.export.otlp.url=http://${quarkus.otel-collector.url}/v1/metrics +%prod.quarkus.micrometer.export.otlp.url=http://localhost:4318/v1/metrics + +#opentelemetry +quarkus.otel.exporter.otlp.traces.protocol=http/protobuf +%dev.quarkus.otel.exporter.otlp.traces.endpoint=http://${quarkus.otel-collector.url} +%prod.quarkus.otel.exporter.otlp.traces.endpoint=http://localhost:4318 + +#quarkus.observability.lgtm.image-name=grafana/otel-lgtm diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmLifecycleTest.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmLifecycleTest.java new file mode 100644 index 0000000000000..8660c82c95b93 --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmLifecycleTest.java @@ -0,0 +1,17 @@ +package io.quarkus.observability.test; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.observability.devresource.lgtm.LgtmResource; +import io.quarkus.observability.test.support.QuarkusTestResourceTestProfile; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@QuarkusTestResource(value = LgtmResource.class, restrictToAnnotatedClass = true) +@TestProfile(QuarkusTestResourceTestProfile.class) +@DisabledOnOs(OS.WINDOWS) +public class LgtmLifecycleTest extends LgtmTestBase { +} diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmResourcesIT.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmResourcesIT.java new file mode 100644 index 0000000000000..fe869cf62b010 --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmResourcesIT.java @@ -0,0 +1,11 @@ +package io.quarkus.observability.test; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +@DisabledOnOs(OS.WINDOWS) +public class LgtmResourcesIT extends LgtmResourcesTest { +} diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmResourcesTest.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmResourcesTest.java new file mode 100644 index 0000000000000..6cd232235b38f --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmResourcesTest.java @@ -0,0 +1,14 @@ +package io.quarkus.observability.test; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.observability.test.support.DevResourcesTestProfile; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(DevResourcesTestProfile.class) +@DisabledOnOs(OS.WINDOWS) +public class LgtmResourcesTest extends LgtmTestBase { +} diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmServicesTest.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmServicesTest.java new file mode 100644 index 0000000000000..33b7e0c13da7c --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmServicesTest.java @@ -0,0 +1,11 @@ +package io.quarkus.observability.test; + +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@DisabledOnOs(OS.WINDOWS) +public class LgtmServicesTest extends LgtmTestBase { +} diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmTestBase.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmTestBase.java new file mode 100644 index 0000000000000..6223f25763584 --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/LgtmTestBase.java @@ -0,0 +1,33 @@ +package io.quarkus.observability.test; + +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; +import org.junit.jupiter.api.Test; + +import io.quarkus.observability.test.support.GrafanaClient; +import io.restassured.RestAssured; + +public abstract class LgtmTestBase { + private final Logger log = Logger.getLogger(getClass()); + + @ConfigProperty(name = "quarkus.grafana.url") + String url; + + @Test + public void testTracing() { + log.info("Testing Grafana ..."); + String response = RestAssured.get("/api/poke?f=100").body().asString(); + log.info("Response: " + response); + GrafanaClient client = new GrafanaClient("http://" + url, "admin", "admin"); + Awaitility.await().atMost(61, TimeUnit.SECONDS).until( + client::user, + u -> "admin".equals(u.login)); + Awaitility.await().atMost(61, TimeUnit.SECONDS).until( + () -> client.query("xvalue_X"), + result -> !result.data.result.isEmpty()); + } + +} diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/DevResourcesTestProfile.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/DevResourcesTestProfile.java new file mode 100644 index 0000000000000..1110a11073098 --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/DevResourcesTestProfile.java @@ -0,0 +1,14 @@ +package io.quarkus.observability.test.support; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class DevResourcesTestProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.observability.dev-resources", "true", + "quarkus.observability.enabled", "false"); + } +} diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/GrafanaClient.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/GrafanaClient.java new file mode 100644 index 0000000000000..40f31cc689589 --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/GrafanaClient.java @@ -0,0 +1,92 @@ +package io.quarkus.observability.test.support; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Base64; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class GrafanaClient { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final String url; + private final String username; + private final String password; + + public GrafanaClient(String url, String username, String password) { + this.url = url; + this.username = username; + this.password = password; + } + + private void handle( + String path, + Function method, + HttpResponse.BodyHandler bodyHandler, + BiConsumer, T> consumer) { + try { + String credentials = username + ":" + password; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + + HttpClient httpClient = HttpClient.newHttpClient(); + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(url + path)) + .header("Authorization", "Basic " + encodedCredentials); + HttpRequest request = method.apply(builder).build(); + + HttpResponse response = httpClient.send(request, bodyHandler); + int code = response.statusCode(); + if (code < 200 || code > 299) { + throw new IllegalStateException("Bad response: " + code + " >> " + response.body()); + } + consumer.accept(response, response.body()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + + public User user() { + AtomicReference ref = new AtomicReference<>(); + handle( + "/api/user", + HttpRequest.Builder::GET, + HttpResponse.BodyHandlers.ofString(), + (r, b) -> { + try { + User user = MAPPER.readValue(b, User.class); + ref.set(user); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + }); + return ref.get(); + } + + public QueryResult query(String query) { + AtomicReference ref = new AtomicReference<>(); + handle( + "/api/datasources/proxy/1/api/v1/query?query=" + query, + HttpRequest.Builder::GET, + HttpResponse.BodyHandlers.ofString(), + (r, b) -> { + try { + QueryResult result = MAPPER.readValue(b, QueryResult.class); + ref.set(result); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + }); + return ref.get(); + } +} diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/QuarkusTestResourceTestProfile.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/QuarkusTestResourceTestProfile.java new file mode 100644 index 0000000000000..b60772d61d550 --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/QuarkusTestResourceTestProfile.java @@ -0,0 +1,14 @@ +package io.quarkus.observability.test.support; + +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class QuarkusTestResourceTestProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.observability.dev-resources", "false", + "quarkus.observability.enabled", "false"); + } +} diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/QueryResult.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/QueryResult.java new file mode 100644 index 0000000000000..77f24fa7e5a26 --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/QueryResult.java @@ -0,0 +1,73 @@ +package io.quarkus.observability.test.support; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class QueryResult { + public String status; + public Data data; + + // getters and setters + + @Override + public String toString() { + return "QueryResult{" + + "status='" + status + '\'' + + ", data=" + data + + '}'; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Data { + public String resultType; + public List result; + + // getters and setters + + @Override + public String toString() { + return "Data{" + + "resultType='" + resultType + '\'' + + ", result=" + result + + '}'; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ResultItem { + public Metric metric; + public List value; + + // getters and setters + + @Override + public String toString() { + return "ResultItem{" + + "metric=" + metric + + ", value=" + value + + '}'; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Metric { + @JsonProperty("__name__") + public String name; + public String job; + public String test; + + // getters and setters + + @Override + public String toString() { + return "Metric{" + + "name='" + name + '\'' + + ", job='" + job + '\'' + + ", test='" + test + '\'' + + '}'; + } + } +} diff --git a/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/User.java b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/User.java new file mode 100644 index 0000000000000..f617cd2b23bcf --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/java/io/quarkus/observability/test/support/User.java @@ -0,0 +1,14 @@ +package io.quarkus.observability.test.support; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class User { + @JsonProperty + public int id; + @JsonProperty + public String email; + @JsonProperty + public String login; +} diff --git a/integration-tests/observability-lgtm/src/test/resources/application.properties b/integration-tests/observability-lgtm/src/test/resources/application.properties new file mode 100644 index 0000000000000..d3f0cfca1f442 --- /dev/null +++ b/integration-tests/observability-lgtm/src/test/resources/application.properties @@ -0,0 +1,16 @@ +# Disable default binders +quarkus.micrometer.binder-enabled-default=false + +quarkus.log.category."io.quarkus.observability".level=DEBUG +quarkus.log.category."io.quarkus.devservices".level=DEBUG + +#micrometer +quarkus.micrometer.export.otlp.enabled=true +quarkus.micrometer.export.otlp.publish=true +quarkus.micrometer.export.otlp.step=PT5S +quarkus.micrometer.export.otlp.default-registry=true +quarkus.micrometer.export.otlp.url=http://${quarkus.otel-collector.url}/v1/metrics + +#opentelemetry +quarkus.otel.exporter.otlp.traces.protocol=http/protobuf +quarkus.otel.exporter.otlp.traces.endpoint=http://${quarkus.otel-collector.url} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index a17b168d5cd73..879e46b8c534d 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -245,6 +245,7 @@ flyway liquibase liquibase-mongodb + observability-lgtm oidc oidc-client oidc-client-reactive From c70c45ee37914ab1c2610437b9c0ce89de0f1908 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 18 Apr 2024 21:11:56 +0200 Subject: [PATCH 0020/2353] WebSockets Next: configuration updates - remove unimplemented timeout - add compression support/level - add max message size - also invoke error handlers for connection exceptions - resolves #39590 --- .../WriteErrorClosedConnectionTest.java | 70 +++++++++++++++++++ .../maxmessagesize/MaxMessageSizeTest.java | 63 +++++++++++++++++ .../SubprotocolNotAvailableTest.java | 13 ++++ .../next/WebSocketsRuntimeConfig.java | 27 +++++-- .../next/runtime/ContextSupport.java | 2 +- .../WebSocketHttpServerOptionsCustomizer.java | 13 +++- .../next/runtime/WebSocketServerRecorder.java | 15 ++++ 7 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/errors/WriteErrorClosedConnectionTest.java create mode 100644 extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/maxmessagesize/MaxMessageSizeTest.java diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/errors/WriteErrorClosedConnectionTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/errors/WriteErrorClosedConnectionTest.java new file mode 100644 index 0000000000000..ec46f533c4564 --- /dev/null +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/errors/WriteErrorClosedConnectionTest.java @@ -0,0 +1,70 @@ +package io.quarkus.websockets.next.test.errors; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.inject.Inject; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; + +public class WriteErrorClosedConnectionTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Echo.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("echo") + URI testUri; + + @Test + void testError() { + WSClient client = WSClient.create(vertx).connect(testUri); + client.sendAndAwait(Buffer.buffer("1")); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> client.isClosed()); + assertTrue(Echo.ERROR_HANDLER_CALLED.get()); + } + + @WebSocket(path = "/echo") + public static class Echo { + + static final AtomicBoolean ERROR_HANDLER_CALLED = new AtomicBoolean(); + + @OnBinaryMessage + Uni process(Buffer message, WebSocketConnection connection) { + // This should result in a failure because the connection is closed + // but we still try to write a binary message + return connection.close().replaceWith(message); + } + + @OnError + void runtimeProblem(Throwable t, WebSocketConnection connection) { + if (connection.isOpen()) { + throw new IllegalStateException(); + } + ERROR_HANDLER_CALLED.set(true); + } + + } + +} diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/maxmessagesize/MaxMessageSizeTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/maxmessagesize/MaxMessageSizeTest.java new file mode 100644 index 0000000000000..2ffe0778d69f7 --- /dev/null +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/maxmessagesize/MaxMessageSizeTest.java @@ -0,0 +1,63 @@ +package io.quarkus.websockets.next.test.maxmessagesize; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnError; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; + +public class MaxMessageSizeTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Echo.class, WSClient.class); + }).overrideConfigKey("quarkus.websockets-next.max-message-size", "10"); + + @Inject + Vertx vertx; + + @TestHTTPResource("/echo") + URI echoUri; + + @Test + void testMaxMessageSize() { + WSClient client = WSClient.create(vertx).connect(echoUri); + String msg = "foo".repeat(10); + String reply = client.sendAndAwaitReply(msg).toString(); + assertNotEquals(msg, reply); + assertTrue(Echo.ISE_THROWN.get()); + } + + @WebSocket(path = "/echo") + public static class Echo { + + static final AtomicBoolean ISE_THROWN = new AtomicBoolean(); + + @OnTextMessage + String process(String message) { + return message; + } + + @OnError + String onError(IllegalStateException ise) { + ISE_THROWN.set(true); + return ise.getMessage(); + } + + } + +} diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java index 9ef02fe878268..9a79b8d12fda0 100644 --- a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java @@ -5,11 +5,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URI; +import java.time.Duration; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicBoolean; +import jakarta.enterprise.context.Destroyed; +import jakarta.enterprise.context.SessionScoped; +import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; +import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -43,18 +48,26 @@ void testConnectionRejected() { Throwable cause = e.getCause(); assertTrue(cause instanceof WebSocketClientHandshakeException); assertFalse(Endpoint.OPEN_CALLED.get()); + // Wait until the CDI singleton context is destroyed + // Otherwise the test app is shut down before the WebSocketSessionContext is ended properly + Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> Endpoint.SESSION_CONTEXT_DESTROYED.get()); } @WebSocket(path = "/endpoint") public static class Endpoint { static final AtomicBoolean OPEN_CALLED = new AtomicBoolean(); + static final AtomicBoolean SESSION_CONTEXT_DESTROYED = new AtomicBoolean(); @OnOpen void open() { OPEN_CALLED.set(true); } + static void sessionContextDestroyed(@Observes @Destroyed(SessionScoped.class) Object event) { + SESSION_CONTEXT_DESTROYED.set(true); + } + } } diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java index ff38df72391d6..e1c76dc33dde3 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java @@ -1,12 +1,14 @@ package io.quarkus.websockets.next; -import java.time.Duration; import java.util.List; import java.util.Optional; +import java.util.OptionalInt; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.vertx.core.http.HttpServerOptions; @ConfigMapping(prefix = "quarkus.websockets-next") @ConfigRoot(phase = ConfigPhase.RUN_TIME) @@ -14,16 +16,27 @@ public interface WebSocketsRuntimeConfig { /** * See The WebSocket Protocol - * - * @return the supported subprotocols */ Optional> supportedSubprotocols(); /** - * TODO Not implemented yet. - * - * The default timeout to complete processing of a message. + * Compression Extensions for WebSocket are supported by default. + *

+ * See also RFC 7692 */ - Optional timeout(); + @WithDefault("true") + boolean perMessageCompressionSupported(); + + /** + * The compression level must be a value between 0 and 9. The default value is + * {@value HttpServerOptions#DEFAULT_WEBSOCKET_COMPRESSION_LEVEL}. + */ + OptionalInt compressionLevel(); + + /** + * The maximum size of a message in bytes. The default values is + * {@value HttpServerOptions#DEFAULT_MAX_WEBSOCKET_MESSAGE_SIZE}. + */ + OptionalInt maxMessageSize(); } diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java index 6edb66693f906..9e4b7a4e81525 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/ContextSupport.java @@ -69,7 +69,7 @@ void endSession() { } ContextState currentRequestContextState() { - return requestContext.getState(); + return requestContext.getStateIfActive(); } static Context createNewDuplicatedContext(Context context, WebSocketConnection connection) { diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java index 5018b1aee2b35..5233fd4a1cc34 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java @@ -17,12 +17,23 @@ public class WebSocketHttpServerOptionsCustomizer implements HttpServerOptionsCu @Override public void customizeHttpServer(HttpServerOptions options) { - config.supportedSubprotocols().orElse(List.of()).forEach(options::addWebSocketSubProtocol); + customize(options); } @Override public void customizeHttpsServer(HttpServerOptions options) { + customize(options); + } + + private void customize(HttpServerOptions options) { config.supportedSubprotocols().orElse(List.of()).forEach(options::addWebSocketSubProtocol); + options.setPerMessageWebSocketCompressionSupported(config.perMessageCompressionSupported()); + if (config.compressionLevel().isPresent()) { + options.setWebSocketCompressionLevel(config.compressionLevel().getAsInt()); + } + if (config.maxMessageSize().isPresent()) { + options.setMaxWebSocketMessageSize(config.maxMessageSize().getAsInt()); + } } } diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java index a0b98d13b1209..c53d15645b01d 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketServerRecorder.java @@ -234,6 +234,21 @@ public void handle(Void event) { }); } }); + + ws.exceptionHandler(new Handler() { + @Override + public void handle(Throwable t) { + ContextSupport.createNewDuplicatedContext(context, connection).runOnContext(new Handler() { + @Override + public void handle(Void event) { + endpoint.doOnError(t).subscribe().with( + v -> LOG.debugf("Error [%s] processed: %s", t.getClass(), connection), + t -> LOG.errorf(t, "Unhandled error occured: %s", t.toString(), + connection)); + } + }); + } + }); }); } }; From cb39e07838770ec54f6edf9c4d5e54d019d63978 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 19 Apr 2024 14:46:54 +0200 Subject: [PATCH 0021/2353] Qute: add qute integration-test module to native Windows CI category --- .github/native-tests.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/native-tests.json b/.github/native-tests.json index dbe32b8def327..bf7de09a2cc2b 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -133,9 +133,9 @@ "os-name": "ubuntu-latest" }, { - "category": "Windows - RESTEasy Jackson", - "timeout": 25, - "test-modules": "resteasy-jackson", + "category": "Windows support", + "timeout": 50, + "test-modules": "resteasy-jackson, qute", "os-name": "windows-latest" }, { From 9e05bd904d27cc4f74c609b986136100a56f115f Mon Sep 17 00:00:00 2001 From: Alasdair Preston Date: Fri, 19 Apr 2024 14:35:59 +0100 Subject: [PATCH 0022/2353] Remove split package ignored list as no entries remain --- .../arc/deployment/SplitPackageProcessor.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java index f68b4256900b6..998c040af65d4 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SplitPackageProcessor.java @@ -39,16 +39,6 @@ public class SplitPackageProcessor { private static final Logger LOGGER = Logger.getLogger(SplitPackageProcessor.class); - private static final Predicate IGNORE_PACKAGE = new Predicate<>() { - - @Override - public boolean test(String packageName) { - // Remove the elements from this list when the original issue is fixed - // so that we can detect further issues. - return packageName.startsWith("io.fabric8.kubernetes"); - } - }; - @BuildStep void splitPackageDetection(ApplicationArchivesBuildItem archivesBuildItem, ArcConfig config, @@ -82,9 +72,6 @@ void splitPackageDetection(ApplicationArchivesBuildItem archivesBuildItem, // - "com.me.app.sub" found in [archiveA, archiveB] StringBuilder splitPackagesWarning = new StringBuilder(); for (String packageName : packageToArchiveMap.keySet()) { - if (IGNORE_PACKAGE.test(packageName)) { - continue; - } // skip packages based on pre-built predicates boolean skipEvaluation = false; From 6014d5119451e6dfa9e22bd7b19b9ebeabf2477d Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Tue, 16 Apr 2024 23:16:33 +0200 Subject: [PATCH 0023/2353] Refactor Qute template scanning --- .../qute/deployment/QuteProcessor.java | 172 +++++------------- 1 file changed, 43 insertions(+), 129 deletions(-) diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 7353f30506eaa..fbff8958db717 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -38,7 +38,6 @@ import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; import jakarta.inject.Singleton; @@ -93,12 +92,15 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.pkg.NativeConfig; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.fs.util.ZipUtils; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.maven.dependency.Dependency; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.DependencyFlags; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.panache.common.deployment.PanacheEntityClassesBuildItem; +import io.quarkus.paths.FilteredPathTree; +import io.quarkus.paths.PathFilter; +import io.quarkus.paths.PathTree; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.Engine; import io.quarkus.qute.EngineBuilder; @@ -2124,9 +2126,6 @@ void collectTemplates(ApplicationArchivesBuildItem applicationArchives, QuteConfig config, TemplateRootsBuildItem templateRoots) throws IOException { - Set allApplicationArchives = applicationArchives.getAllApplicationArchives(); - List extensionArtifacts = curateOutcome.getApplicationModel().getDependencies().stream() - .filter(Dependency::isRuntimeExtensionArtifact).collect(Collectors.toList()); // Make sure the new templates are watched as well watchedPaths.produce(HotDeploymentWatchedFileBuildItem.builder().setLocationPredicate(new Predicate() { @@ -2141,80 +2140,39 @@ public boolean test(String path) { } }).build()); - for (ResolvedDependency artifact : extensionArtifacts) { - if (isApplicationArchive(artifact, allApplicationArchives)) { - // Skip extension archives that are also application archives - continue; - } - for (Path resolvedPath : artifact.getResolvedPaths()) { - if (Files.isDirectory(resolvedPath)) { - scanRootPath(resolvedPath, config, templateRoots, watchedPaths, templatePaths, - nativeImageResources); - } else { - try (FileSystem artifactFs = ZipUtils.newFileSystem(resolvedPath)) { - // Iterate over template roots, such as "templates", and collect the included templates - for (String templateRoot : templateRoots) { - Path artifactBasePath = artifactFs.getPath(templateRoot); - if (Files.exists(artifactBasePath)) { - LOGGER.debugf("Found template root in extension artifact: %s", resolvedPath); - scanDirectory(artifactBasePath, artifactBasePath, templateRoot + "/", watchedPaths, - templatePaths, - nativeImageResources, - config); - } - } - } catch (IOException e) { - LOGGER.warnf(e, "Unable to create the file system from the path: %s", resolvedPath); - } - } + final Set allApplicationArchives = applicationArchives.getAllApplicationArchives(); + final Set appArtifactKeys = new HashSet<>(allApplicationArchives.size()); + for (var archive : allApplicationArchives) { + appArtifactKeys.add(archive.getKey()); + } + for (ResolvedDependency artifact : curateOutcome.getApplicationModel() + .getDependencies(DependencyFlags.RUNTIME_EXTENSION_ARTIFACT)) { + // Skip extension archives that are also application archives + if (!appArtifactKeys.contains(artifact.getKey())) { + scanPathTree(artifact.getContentTree(), templateRoots, watchedPaths, templatePaths, nativeImageResources, + config); } } for (ApplicationArchive archive : allApplicationArchives) { - archive.accept(tree -> { - for (Path root : tree.getRoots()) { - // Note that we cannot use ApplicationArchive.getChildPath(String) here because we would not be able to detect - // a wrong directory name on case-insensitive file systems - scanRootPath(root, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); - } - }); + archive.accept( + tree -> scanPathTree(tree, templateRoots, watchedPaths, templatePaths, nativeImageResources, config)); } } - private void scanRootPath(Path rootPath, QuteConfig config, TemplateRootsBuildItem templateRoots, + private void scanPathTree(PathTree pathTree, TemplateRootsBuildItem templateRoots, BuildProducer watchedPaths, BuildProducer templatePaths, - BuildProducer nativeImageResources) { - scanRootPath(rootPath, rootPath, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); - } - - private void scanRootPath(Path rootPath, Path path, QuteConfig config, TemplateRootsBuildItem templateRoots, - BuildProducer watchedPaths, - BuildProducer templatePaths, - BuildProducer nativeImageResources) { - if (!Files.isDirectory(path)) { - return; - } - try (Stream paths = Files.list(path)) { - for (Path file : paths.collect(Collectors.toList())) { - if (Files.isDirectory(file)) { - // Iterate over the directories in the root - // "/io", "/META-INF", "/templates", "/web", etc. - Path relativePath = rootPath.relativize(file); - if (templateRoots.isRoot(relativePath)) { - LOGGER.debugf("Found templates root dir: %s", file); - // The base path is an OS-specific template root path relative to the scanned root path - String basePath = relativePath.toString() + relativePath.getFileSystem().getSeparator(); - scanDirectory(file, file, basePath, watchedPaths, templatePaths, - nativeImageResources, - config); - } else if (templateRoots.maybeRoot(relativePath)) { - // Scan the path recursively because the template root may be nested, for example "/web/public" - scanRootPath(rootPath, file, config, templateRoots, watchedPaths, templatePaths, nativeImageResources); - } + BuildProducer nativeImageResources, + QuteConfig config) { + for (String templateRoot : templateRoots) { + pathTree.accept(templateRoot, visit -> { + if (visit != null) { + // if template root is found in this tree then walk over its subtree + scanTemplateRootSubtree( + new FilteredPathTree(pathTree, PathFilter.forIncludes(List.of(templateRoot + "/**"))), + visit.getPath(), watchedPaths, templatePaths, nativeImageResources, config); } - } - } catch (IOException e) { - throw new UncheckedIOException(e); + }); } } @@ -3427,55 +3385,24 @@ private static void produceTemplateBuildItems(BuildProducer watchedPaths, BuildProducer templatePaths, BuildProducer nativeImageResources, - QuteConfig config) - throws IOException { - try (Stream files = Files.list(directory)) { - Iterator iter = files.iterator(); - while (iter.hasNext()) { - Path filePath = iter.next(); - /* - * Fix for https://github.com/quarkusio/quarkus/issues/25751 where running tests in Eclipse - * sometimes produces `/templates/tags` (absolute) files listed for `templates` (relative) - * directories, so we work around this - */ - if (!directory.isAbsolute() - && filePath.isAbsolute() - && filePath.getRoot() != null) { - filePath = filePath.getRoot().relativize(filePath); - } - if (Files.isRegularFile(filePath)) { - LOGGER.debugf("Found template: %s", filePath); - Path relativePath = root.relativize(filePath); - String templatePath = toOsAgnosticPath(relativePath); - if (config.templatePathExclude.matcher(templatePath).matches()) { - LOGGER.debugf("Template file excluded: %s", filePath); - continue; - } - produceTemplateBuildItems(templatePaths, watchedPaths, nativeImageResources, - basePath + relativePath.toString(), - templatePath, - filePath, config); - } else if (Files.isDirectory(filePath)) { - LOGGER.debugf("Scan directory: %s", filePath); - scanDirectory(root, filePath, basePath, watchedPaths, templatePaths, nativeImageResources, config); - } + QuteConfig config) { + pathTree.walk(visit -> { + if (Files.isRegularFile(visit.getPath())) { + LOGGER.debugf("Found template: %s", visit.getPath()); + String templatePath = toOsAgnosticPath(templateRoot.relativize(visit.getPath())); + if (config.templatePathExclude.matcher(templatePath).matches()) { + LOGGER.debugf("Template file excluded: %s", visit.getPath()); + return; + } + produceTemplateBuildItems(templatePaths, watchedPaths, nativeImageResources, + visit.getRelativePath(visit.getPath().getFileSystem().getSeparator()), + templatePath, visit.getPath(), config); } - } + }); } private static String toOsAgnosticPath(String path, FileSystem fs) { @@ -3518,19 +3445,6 @@ private void checkDuplicatePaths(List templatePaths) { } } - private boolean isApplicationArchive(ResolvedDependency dependency, Set applicationArchives) { - for (ApplicationArchive archive : applicationArchives) { - if (archive.getKey() == null) { - continue; - } - if (dependency.getGroupId().equals(archive.getKey().getGroupId()) - && dependency.getArtifactId().equals(archive.getKey().getArtifactId())) { - return true; - } - } - return false; - } - static String readTemplateContent(Path path, Charset defaultCharset) { try { return Files.readString(path, defaultCharset); From 202ea6e107da27118052ad2feb9de95690525f7f Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 19 Apr 2024 15:39:25 +0200 Subject: [PATCH 0024/2353] Qute: use the relative resource path for NativeImageResourceBuildItem - previously, we used incorrectly an OS-specific relative path --- .../quarkus/qute/deployment/QuteProcessor.java | 16 +++++++--------- .../templateroot/AdditionalTemplateRootTest.java | 5 +---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index fbff8958db717..43cb4659a1634 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -3355,31 +3355,29 @@ public static String getName(InjectionPointInfo injectionPoint) { * @param templatePaths * @param watchedPaths * @param nativeImageResources - * @param osSpecificResourcePath The OS-specific resource path, i.e. templates\nested\foo.html + * @param resourcePath The relative resource path, including the template root * @param templatePath The path relative to the template root; using the {@code /} path separator * @param originalPath * @param config */ private static void produceTemplateBuildItems(BuildProducer templatePaths, BuildProducer watchedPaths, - BuildProducer nativeImageResources, String osSpecificResourcePath, + BuildProducer nativeImageResources, String resourcePath, String templatePath, Path originalPath, QuteConfig config) { if (templatePath.isEmpty()) { return; } - // OS-agnostic full path, i.e. templates/foo.html - String osAgnosticResourcePath = toOsAgnosticPath(osSpecificResourcePath, originalPath.getFileSystem()); LOGGER.debugf("Produce template build items [templatePath: %s, osSpecificResourcePath: %s, originalPath: %s", templatePath, - osSpecificResourcePath, + resourcePath, originalPath); boolean restartNeeded = true; if (config.devMode.noRestartTemplates.isPresent()) { - restartNeeded = !config.devMode.noRestartTemplates.get().matcher(osAgnosticResourcePath).matches(); + restartNeeded = !config.devMode.noRestartTemplates.get().matcher(resourcePath).matches(); } - watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(osAgnosticResourcePath, restartNeeded)); - nativeImageResources.produce(new NativeImageResourceBuildItem(osSpecificResourcePath)); + watchedPaths.produce(new HotDeploymentWatchedFileBuildItem(resourcePath, restartNeeded)); + nativeImageResources.produce(new NativeImageResourceBuildItem(resourcePath)); templatePaths.produce( new TemplatePathBuildItem(templatePath, originalPath, readTemplateContent(originalPath, config.defaultCharset))); @@ -3399,7 +3397,7 @@ private void scanTemplateRootSubtree(PathTree pathTree, Path templateRoot, return; } produceTemplateBuildItems(templatePaths, watchedPaths, nativeImageResources, - visit.getRelativePath(visit.getPath().getFileSystem().getSeparator()), + visit.getRelativePath("/"), templatePath, visit.getPath(), config); } }); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java index 9095a01599387..37b3606d7a096 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/templateroot/AdditionalTemplateRootTest.java @@ -51,11 +51,8 @@ public void execute(BuildContext context) { List items = context.consumeMulti(NativeImageResourceBuildItem.class); for (NativeImageResourceBuildItem item : items) { if (item.getResources().contains("web/public/hello.txt") - || item.getResources().contains("web\\public\\hello.txt") || item.getResources().contains("templates/hi.txt") - || item.getResources().contains("templates\\hi.txt") - || item.getResources().contains("templates/nested/hoho.txt") - || item.getResources().contains("templates\\nested\\hoho.txt")) { + || item.getResources().contains("templates/nested/hoho.txt")) { found++; } } From dfbf690464285df7e90e2696ae941032e3cd746a Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Fri, 19 Apr 2024 12:01:22 -0300 Subject: [PATCH 0025/2353] Bump com.gradle:develocity-maven-extension to 1.21.2 --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 25380d5a91342..8de42849403a8 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -2,7 +2,7 @@ com.gradle develocity-maven-extension - 1.21 + 1.21.2 com.gradle From 861ca775be70c065f679a78404ed2fe1b89e5461 Mon Sep 17 00:00:00 2001 From: "Vinicius A. Santos" Date: Fri, 19 Apr 2024 11:59:23 -0300 Subject: [PATCH 0026/2353] Update file log handler description Remove duplicated statement on the "File log handler" section --- docs/src/main/asciidoc/logging.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/main/asciidoc/logging.adoc b/docs/src/main/asciidoc/logging.adoc index 84bef6c0cc3d1..4c0068b89ac8c 100644 --- a/docs/src/main/asciidoc/logging.adoc +++ b/docs/src/main/asciidoc/logging.adoc @@ -446,7 +446,6 @@ To log events to a file on the application's host, use the Quarkus file log hand The file log handler is disabled by default, so you must first enable it. The Quarkus file log handler supports log file rotation. -Log file rotation ensures effective log file management over time by maintaining a specified number of backup log files, while also keeping the primary log file up-to-date and manageable. Log file rotation ensures effective log file management over time by maintaining a specified number of backup log files, while keeping the primary log file up-to-date and manageable. From 7624880d9297c4934fd09df003398828e7907a2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 21:33:37 +0000 Subject: [PATCH 0027/2353] Bump io.smallrye.beanbag:smallrye-beanbag-maven from 1.4.0 to 1.4.1 Bumps [io.smallrye.beanbag:smallrye-beanbag-maven](https://github.com/smallrye/smallrye-beanbag) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/smallrye/smallrye-beanbag/releases) - [Commits](https://github.com/smallrye/smallrye-beanbag/compare/1.4.0...1.4.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] --- 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 87bd3e65cc0fb..47c5123683b40 100644 --- a/independent-projects/bootstrap/pom.xml +++ b/independent-projects/bootstrap/pom.xml @@ -74,7 +74,7 @@ 2.0 3.5.1 2.3.0 - 1.4.0 + 1.4.1 8.6 0.0.10 0.1.3 From 432d9dd391d5d7fe6dc91918eb072be82d4a6c39 Mon Sep 17 00:00:00 2001 From: asjervanasten Date: Sun, 21 Apr 2024 13:09:44 +0200 Subject: [PATCH 0028/2353] Updates quarkusdocs to replace deprecated injectMock Fixes https://github.com/quarkusio/quarkus/issues/40171 --- docs/src/main/asciidoc/hibernate-orm-panache.adoc | 4 ++-- docs/src/main/asciidoc/rest-client.adoc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/main/asciidoc/hibernate-orm-panache.adoc b/docs/src/main/asciidoc/hibernate-orm-panache.adoc index 12d76c52dd4f2..9726e5eafe168 100644 --- a/docs/src/main/asciidoc/hibernate-orm-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-orm-panache.adoc @@ -1242,8 +1242,8 @@ If you need to mock entity instance methods, such as `persist()` you can do it b [source,java] ---- +import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; import org.hibernate.Session; import org.hibernate.query.Query; import org.junit.jupiter.api.Assertions; @@ -1322,8 +1322,8 @@ You can write your mocking test like this: [source,java] ---- +import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 3f06f8291c7e0..ec8ad272d4dde 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -1768,8 +1768,8 @@ import org.eclipse.microprofile.rest.client.inject.RestClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; @QuarkusTest public class InjectMockTest { From 2a2bb840a921f9cc3b80a9c792212e738deaa646 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Sun, 21 Apr 2024 16:17:25 +0200 Subject: [PATCH 0029/2353] Allow concurrent invocation of blocking gRPC services by removing global ordering Fix https://github.com/quarkusio/quarkus/issues/40155 Previously, the code utilized `executeBlocking` with `ordered=true` to maintain event order. However, this approach enforced global order instead of per-call order. This commit corrects the behavior, ensuring per-call order preservation using the BlockingExecutionHandler lock. --- .../MultiThreadedBlockingImplTest.java | 88 +++++++++++++++++++ .../proto/blocking/BlockingGrpcService.proto | 19 ++++ .../blocking/BlockingServerInterceptor.java | 2 +- 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/MultiThreadedBlockingImplTest.java create mode 100644 extensions/grpc/deployment/src/test/proto/blocking/BlockingGrpcService.proto diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/MultiThreadedBlockingImplTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/MultiThreadedBlockingImplTest.java new file mode 100644 index 0000000000000..7f80c59068859 --- /dev/null +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/MultiThreadedBlockingImplTest.java @@ -0,0 +1,88 @@ +package io.quarkus.grpc.server.blocking; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.acme.Request; +import org.acme.Response; +import org.acme.StandardBlockingGrpcServiceGrpc; +import org.jboss.logging.Logger; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.grpc.stub.StreamObserver; +import io.quarkus.grpc.GrpcClient; +import io.quarkus.grpc.GrpcService; +import io.quarkus.grpc.runtime.devmode.GrpcServices; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.Blocking; +import io.vertx.core.impl.ConcurrentHashSet; + +public class MultiThreadedBlockingImplTest { + + private static final Logger logger = Logger.getLogger(GrpcServices.class); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addPackage(StandardBlocking.class.getPackage()) + .addPackage( + StandardBlockingGrpcServiceGrpc.StandardBlockingGrpcServiceImplBase.class.getPackage())); + + @GrpcClient + StandardBlockingGrpcServiceGrpc.StandardBlockingGrpcServiceBlockingStub client; + + static ExecutorService executor = Executors.newCachedThreadPool(); + + @AfterAll + static void cleanup() { + executor.shutdown(); + } + + @Test + void testTheBlockingCallsCanBeDispatchedOnMultipleThreads() throws InterruptedException { + int count = 100; + ConcurrentHashSet threads = new ConcurrentHashSet<>(); + CountDownLatch latch = new CountDownLatch(count); + for (int i = 0; i < count; i++) { + int id = i; + executor.submit(() -> { + threads.add(invokeService(id)); + latch.countDown(); + }); + } + + Assertions.assertTrue(latch.await(10, TimeUnit.SECONDS)); + Assertions.assertTrue(threads.size() > 1); + } + + String invokeService(int id) { + return client.invoke(Request.newBuilder().setId(id).build()).getThread(); + } + + @GrpcService + @Blocking + static class StandardBlocking extends StandardBlockingGrpcServiceGrpc.StandardBlockingGrpcServiceImplBase { + @Override + public void invoke(Request request, StreamObserver responseObserver) { + try { + Thread.sleep(Duration.ofSeconds(2).toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + responseObserver.onNext(Response.newBuilder() + .setId(request.getId()).setThread(Thread.currentThread().getName()).build()); + responseObserver.onCompleted(); + } + } +} diff --git a/extensions/grpc/deployment/src/test/proto/blocking/BlockingGrpcService.proto b/extensions/grpc/deployment/src/test/proto/blocking/BlockingGrpcService.proto new file mode 100644 index 0000000000000..b3521ce857420 --- /dev/null +++ b/extensions/grpc/deployment/src/test/proto/blocking/BlockingGrpcService.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "org.acme"; + +package hello; + +service StandardBlockingGrpcService { + rpc Invoke (Request) returns (Response) {} +} + +message Request { + int32 id = 1; +} + +message Response { + int32 id = 1; + string thread = 2; +} \ No newline at end of file diff --git a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/blocking/BlockingServerInterceptor.java b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/blocking/BlockingServerInterceptor.java index 047d67adce374..3a467c4e320e1 100644 --- a/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/blocking/BlockingServerInterceptor.java +++ b/extensions/grpc/runtime/src/main/java/io/quarkus/grpc/runtime/supports/blocking/BlockingServerInterceptor.java @@ -208,7 +208,7 @@ private void executeBlockingWithRequestContext(Consumer { + vertx.executeBlocking(blockingHandler, false).onComplete(p -> { Consumer> next = incomingEvents.poll(); if (next != null) { executeBlockingWithRequestContext(next); From 2bea0cdb53e9229547d646d8fae726c9878cde91 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Wed, 17 Apr 2024 18:44:20 +0200 Subject: [PATCH 0030/2353] Renamed ApplicationDependencyModelResolver to IncubatingModelResolver, exposed dependencies of ResolverDependency --- ...itionMatchesConditionalDependencyTest.java | 38 ++ .../GradleApplicationModelBuilder.java | 6 +- .../io/quarkus/maven/DependencyTreeMojo.java | 4 +- .../main/java/io/quarkus/maven/DevMojo.java | 4 +- .../maven/QuarkusBootstrapProvider.java | 27 +- .../app-with-conditional-deps-1.jar.prod | 26 +- .../app-with-conditional-graph-1.jar.prod | 56 +-- .../src/test/resources/test-app-1.jar.dev | 24 +- .../src/test/resources/test-app-1.jar.prod | 22 +- .../src/test/resources/test-app-1.jar.test | 26 +- .../quarkus/bootstrap/model/AppArtifact.java | 8 + .../bootstrap/model/AppDependency.java | 8 + .../model/ApplicationModelBuilder.java | 14 +- .../model/DefaultApplicationModel.java | 2 +- .../model/MutableJarApplicationModel.java | 2 +- .../quarkus/maven/dependency/Dependency.java | 6 +- .../ResolvedArtifactDependency.java | 17 +- .../maven/dependency/ResolvedDependency.java | 3 + .../dependency/ResolvedDependencyBuilder.java | 35 +- .../bootstrap/BootstrapAppModelFactory.java | 25 +- .../resolver/ResolverSetupCleanup.java | 4 +- .../resolver/BootstrapAppModelResolver.java | 74 ++-- .../maven/BuildDependencyGraphVisitor.java | 11 +- ...> IncubatingApplicationModelResolver.java} | 331 +++++++++--------- 24 files changed, 466 insertions(+), 307 deletions(-) rename independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/{ApplicationDependencyModelResolver.java => IncubatingApplicationModelResolver.java} (82%) diff --git a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/DependencyConditionMatchesConditionalDependencyTest.java b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/DependencyConditionMatchesConditionalDependencyTest.java index 1f993a28bcc42..04435597762f5 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/DependencyConditionMatchesConditionalDependencyTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/conditionaldeps/DependencyConditionMatchesConditionalDependencyTest.java @@ -1,8 +1,15 @@ package io.quarkus.deployment.conditionaldeps; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; + +import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.TsArtifact; import io.quarkus.bootstrap.resolver.TsQuarkusExt; +import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; import io.quarkus.deployment.runnerjar.BootstrapFromOriginalJarTestBase; +import io.quarkus.maven.dependency.ResolvedDependency; public class DependencyConditionMatchesConditionalDependencyTest extends BootstrapFromOriginalJarTestBase { @@ -38,6 +45,37 @@ protected TsArtifact composeApplication() { .addDependency(extA); } + @Override + protected void assertAppModel(ApplicationModel appModel) { + var extensions = new HashMap(); + for (var d : appModel.getDependencies()) { + extensions.put(d.getArtifactId(), d); + } + assertThat(extensions).hasSize(8); + + if (IncubatingApplicationModelResolver.isIncubatingEnabled(null)) { + var extA = extensions.get("ext-a"); + assertThat(extA.getDependencies()).isEmpty(); + var extADeployment = extensions.get("ext-a-deployment"); + assertThat(extADeployment.getDependencies()).containsExactly(extA); + + var extB = extensions.get("ext-b"); + assertThat(extB.getDependencies()).isEmpty(); + var extBDeployment = extensions.get("ext-b-deployment"); + assertThat(extBDeployment.getDependencies()).containsExactly(extB); + + var extD = extensions.get("ext-d"); + assertThat(extD.getDependencies()).containsExactly(extB); + var extDDeployment = extensions.get("ext-d-deployment"); + assertThat(extDDeployment.getDependencies()).containsExactlyInAnyOrder(extD, extBDeployment); + + var extC = extensions.get("ext-c"); + assertThat(extC.getDependencies()).containsExactly(extD); + var extCDeployment = extensions.get("ext-c-deployment"); + assertThat(extCDeployment.getDependencies()).containsExactlyInAnyOrder(extC, extDDeployment); + } + } + @Override protected String[] expectedExtensionDependencies() { return new String[] { diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java index 1b7a09dc783c8..06b61927c99fd 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/tooling/GradleApplicationModelBuilder.java @@ -114,7 +114,7 @@ public Object buildAll(String modelName, ModelParameter parameter, Project proje } } - final ResolvedDependency appArtifact = getProjectArtifact(project, workspaceDiscovery); + final ResolvedDependencyBuilder appArtifact = getProjectArtifact(project, workspaceDiscovery); final ApplicationModelBuilder modelBuilder = new ApplicationModelBuilder() .setAppArtifact(appArtifact) .addReloadableWorkspaceModule(appArtifact.getKey()) @@ -159,7 +159,7 @@ private static void addCompileOnly(Project project, ApplicationDeploymentClasspa } } - public static ResolvedDependency getProjectArtifact(Project project, boolean workspaceDiscovery) { + public static ResolvedDependencyBuilder getProjectArtifact(Project project, boolean workspaceDiscovery) { final ResolvedDependencyBuilder appArtifact = ResolvedDependencyBuilder.newInstance() .setGroupId(project.getGroup().toString()) .setArtifactId(project.getName()) @@ -206,7 +206,7 @@ public static ResolvedDependency getProjectArtifact(Project project, boolean wor collectDestinationDirs(mainModule.getMainSources().getSourceDirs(), paths); collectDestinationDirs(mainModule.getMainSources().getResourceDirs(), paths); - return appArtifact.setWorkspaceModule(mainModule).setResolvedPaths(paths.build()).build(); + return appArtifact.setWorkspaceModule(mainModule).setResolvedPaths(paths.build()); } private static void collectDestinationDirs(Collection sources, final PathList.Builder paths) { diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java index 6e027daff769d..38a5e0e5aa588 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DependencyTreeMojo.java @@ -22,9 +22,9 @@ import org.eclipse.aether.repository.RemoteRepository; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; -import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.bootstrap.resolver.maven.DependencyLoggingConfig; +import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.maven.components.QuarkusWorkspaceProvider; import io.quarkus.maven.dependency.ArtifactCoords; @@ -148,7 +148,7 @@ private void logTree(final Consumer log) throws MojoExecutionException { } } modelResolver.setIncubatingModelResolver( - ApplicationDependencyModelResolver.isIncubatingEnabled(project.getProperties())); + IncubatingApplicationModelResolver.isIncubatingEnabled(project.getProperties())); modelResolver.setDepLogConfig(DependencyLoggingConfig.builder() .setMessageConsumer(log) .setVerbose(verbose) diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index 05bd7d3b81622..696742ca1e9da 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -96,9 +96,9 @@ import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.model.PathsCollection; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; -import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContextConfig; +import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.util.BootstrapUtils; import io.quarkus.bootstrap.workspace.ArtifactSources; @@ -1361,7 +1361,7 @@ private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk, String bootstrap .setDevMode(true) .setTest(LaunchMode.TEST.equals(getLaunchModeClasspath())) .setCollectReloadableDependencies(!noDeps) - .setIncubatingModelResolver(ApplicationDependencyModelResolver.isIncubatingEnabled(project.getProperties())) + .setIncubatingModelResolver(IncubatingApplicationModelResolver.isIncubatingEnabled(project.getProperties())) .resolveModel(mvnCtx.getCurrentProject().getAppArtifact()); } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java index d83cb4fa9f46b..1177a4b2b933b 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java @@ -37,16 +37,16 @@ import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; -import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; +import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.maven.components.ManifestSection; import io.quarkus.maven.components.QuarkusWorkspaceProvider; import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.Dependency; -import io.quarkus.maven.dependency.ResolvedArtifactDependency; +import io.quarkus.maven.dependency.ResolvedDependencyBuilder; import io.quarkus.runtime.LaunchMode; import io.smallrye.common.expression.Expression; @@ -208,12 +208,12 @@ private CuratedApplication doBootstrap(QuarkusBootstrapMojo mojo, LaunchMode mod final BootstrapAppModelResolver modelResolver = new BootstrapAppModelResolver(artifactResolver(mojo, mode)) .setIncubatingModelResolver( - ApplicationDependencyModelResolver.isIncubatingEnabled(mojo.mavenProject().getProperties())) + IncubatingApplicationModelResolver.isIncubatingEnabled(mojo.mavenProject().getProperties())) .setDevMode(mode == LaunchMode.DEVELOPMENT) .setTest(mode == LaunchMode.TEST) .setCollectReloadableDependencies(mode == LaunchMode.DEVELOPMENT || mode == LaunchMode.TEST); - final ArtifactCoords appArtifact = appArtifact(mojo); + final ResolvedDependencyBuilder appArtifact = getApplicationArtifactBuilder(mojo); Set reloadableModules = Set.of(); if (mode == LaunchMode.NORMAL) { // collect reloadable artifacts for remote-dev @@ -367,7 +367,7 @@ protected ArtifactCoords managingProject(QuarkusBootstrapMojo mojo) { artifact.getVersion()); } - private ArtifactCoords appArtifact(QuarkusBootstrapMojo mojo) + private ResolvedDependencyBuilder getApplicationArtifactBuilder(QuarkusBootstrapMojo mojo) throws MojoExecutionException { String appArtifactCoords = mojo.appArtifactCoords(); if (appArtifactCoords == null) { @@ -388,9 +388,13 @@ private ArtifactCoords appArtifact(QuarkusBootstrapMojo mojo) } } } - return new ResolvedArtifactDependency(projectArtifact.getGroupId(), projectArtifact.getArtifactId(), - projectArtifact.getClassifier(), projectArtifact.getArtifactHandler().getExtension(), - projectArtifact.getVersion(), projectFile.toPath()); + return ResolvedDependencyBuilder.newInstance() + .setGroupId(projectArtifact.getGroupId()) + .setArtifactId(projectArtifact.getArtifactId()) + .setClassifier(projectArtifact.getClassifier()) + .setType(projectArtifact.getArtifactHandler().getExtension()) + .setVersion(projectArtifact.getVersion()) + .setResolvedPath(projectFile.toPath()); } final String[] coordsArr = appArtifactCoords.split(":"); @@ -429,7 +433,12 @@ private ArtifactCoords appArtifact(QuarkusBootstrapMojo mojo) } } - return ArtifactCoords.of(groupId, artifactId, classifier, type, version); + return ResolvedDependencyBuilder.newInstance() + .setGroupId(groupId) + .setArtifactId(artifactId) + .setClassifier(classifier) + .setType(type) + .setVersion(version); } @Override diff --git a/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod b/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod index 5780caf8fafca..3a23ee5ac0c15 100644 --- a/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod +++ b/devtools/maven/src/test/resources/app-with-conditional-deps-1.jar.prod @@ -1,15 +1,15 @@ [info] Quarkus application PROD mode build dependency tree: [info] io.quarkus.bootstrap.test:app-with-conditional-deps:pom:1 -[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:quarkus-tomato:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:jar:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella-deployment:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:quarkus-mozzarella:jar:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:quarkus-basil-deployment:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:quarkus-basil:jar:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:quarkus-salad-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:quarkus-salad:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:quarkus-caprese:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:quarkus-caprese-deployment:jar:1 (compile) -[info] └─ io.quarkus.bootstrap.test:quarkus-oil:jar:1 (compile) \ No newline at end of file +[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-tomato:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella-deployment:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:quarkus-mozzarella:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:quarkus-basil-deployment:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:quarkus-basil:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:quarkus-salad-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-salad:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:quarkus-caprese:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:quarkus-caprese-deployment:1 (compile) +[info] └─ io.quarkus.bootstrap.test:quarkus-oil:1 (compile) \ No newline at end of file diff --git a/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod b/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod index 77508e6965d59..45cee715ba446 100644 --- a/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod +++ b/devtools/maven/src/test/resources/app-with-conditional-graph-1.jar.prod @@ -1,30 +1,30 @@ [info] Quarkus application PROD mode build dependency tree: [info] io.quarkus.bootstrap.test:app-with-conditional-graph:pom:1 -[info] ├─ io.quarkus.bootstrap.test:quarkus-basil::jar:1 (compile) [+] -[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella::jar:1 (compile) [+] -[info] ├─ io.quarkus.bootstrap.test:quarkus-salad::jar:1 (compile) [+] -[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato::jar:1 (compile) [+] -[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:quarkus-tomato:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] -[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] -[info] │ └─ io.quarkus.bootstrap.test:quarkus-mozzarella:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] -[info] ├─ io.quarkus.bootstrap.test:quarkus-basil-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] -[info] │ └─ io.quarkus.bootstrap.test:quarkus-basil:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] -[info] ├─ io.quarkus.bootstrap.test:quarkus-salad-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] -[info] │ ├─ io.quarkus.bootstrap.test:quarkus-salad:jar:1 (compile) -[info] │ │ ├─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] -[info] │ │ └─ io.quarkus.bootstrap.test:quarkus-caprese:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext::jar:1 (compile) [+] -[info] │ └─ io.quarkus.bootstrap.test:quarkus-caprese-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:quarkus-caprese::jar:1 (compile) [+] -[info] │ ├─ io.quarkus.bootstrap.test:quarkus-oil::jar:1 (compile) [+] -[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment::jar:1 (compile) [+] -[info] └─ io.quarkus.bootstrap.test:quarkus-oil:jar:1 (compile) \ No newline at end of file +[info] ├─ io.quarkus.bootstrap.test:quarkus-basil:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-salad:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-tomato-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-tomato:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-mozzarella-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:quarkus-mozzarella:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-basil-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:quarkus-basil:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) [+] +[info] ├─ io.quarkus.bootstrap.test:quarkus-salad-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-core-ext-deployment:1 (compile) [+] +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-salad:1 (compile) +[info] │ │ ├─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) [+] +[info] │ │ └─ io.quarkus.bootstrap.test:quarkus-caprese:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:quarkus-caprese-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-caprese:1 (compile) [+] +[info] │ ├─ io.quarkus.bootstrap.test:quarkus-oil:1 (compile) [+] +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:1 (compile) [+] +[info] └─ io.quarkus.bootstrap.test:quarkus-oil:1 (compile) \ No newline at end of file diff --git a/devtools/maven/src/test/resources/test-app-1.jar.dev b/devtools/maven/src/test/resources/test-app-1.jar.dev index d43162a11db94..7c23ed5541ddd 100644 --- a/devtools/maven/src/test/resources/test-app-1.jar.dev +++ b/devtools/maven/src/test/resources/test-app-1.jar.dev @@ -1,14 +1,14 @@ [info] Quarkus application DEV mode build dependency tree: [info] io.quarkus.bootstrap.test:test-app:pom:1 -[info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:jar:classifier:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:test-ext2:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:test-ext1:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-ext1-deployment:jar:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:optional:jar:1 (compile optional) -[info] ├─ io.quarkus.bootstrap.test:test-ext3-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:test-ext3:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:jar:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:provided:jar:1 (provided) -[info] └─ io.quarkus.bootstrap.test:runtime:jar:1 (runtime) +[info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:classifier:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-ext2:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-ext1:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-ext1-deployment:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:optional:1 (compile optional) +[info] ├─ io.quarkus.bootstrap.test:test-ext3-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-ext3:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:provided:1 (provided) +[info] └─ io.quarkus.bootstrap.test:runtime:1 (runtime) diff --git a/devtools/maven/src/test/resources/test-app-1.jar.prod b/devtools/maven/src/test/resources/test-app-1.jar.prod index bf7d0a9836aae..7a252ceaa05da 100644 --- a/devtools/maven/src/test/resources/test-app-1.jar.prod +++ b/devtools/maven/src/test/resources/test-app-1.jar.prod @@ -1,13 +1,13 @@ [info] Quarkus application PROD mode build dependency tree: [info] io.quarkus.bootstrap.test:test-app:pom:1 -[info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:jar:classifier:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:test-ext2:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:test-ext1:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-ext1-deployment:jar:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:optional:jar:1 (compile optional) -[info] ├─ io.quarkus.bootstrap.test:test-ext3-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:test-ext3:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:jar:1 (compile) -[info] └─ io.quarkus.bootstrap.test:runtime:jar:1 (runtime) +[info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:classifier:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-ext2:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-ext1:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-ext1-deployment:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:optional:1 (compile optional) +[info] ├─ io.quarkus.bootstrap.test:test-ext3-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-ext3:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:1 (compile) +[info] └─ io.quarkus.bootstrap.test:runtime:1 (runtime) diff --git a/devtools/maven/src/test/resources/test-app-1.jar.test b/devtools/maven/src/test/resources/test-app-1.jar.test index 832397770b902..411f5600b0673 100644 --- a/devtools/maven/src/test/resources/test-app-1.jar.test +++ b/devtools/maven/src/test/resources/test-app-1.jar.test @@ -1,15 +1,15 @@ [info] Quarkus application TEST mode build dependency tree: [info] io.quarkus.bootstrap.test:test-app:pom:1 -[info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:jar:classifier:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:test-ext2:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:test-ext1:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-ext1-deployment:jar:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:optional:jar:1 (compile optional) -[info] ├─ io.quarkus.bootstrap.test:test-ext3-deployment:jar:1 (compile) -[info] │ ├─ io.quarkus.bootstrap.test:test-ext3:jar:1 (compile) -[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:jar:1 (compile) -[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:jar:1 (compile) -[info] ├─ io.quarkus.bootstrap.test:provided:jar:1 (provided) -[info] ├─ io.quarkus.bootstrap.test:runtime:jar:1 (runtime) -[info] └─ io.quarkus.bootstrap.test:test:jar:1 (test) +[info] ├─ io.quarkus.bootstrap.test:artifact-with-classifier:classifier:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:test-ext2-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-ext2:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-ext1:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-ext1-deployment:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:optional:1 (compile optional) +[info] ├─ io.quarkus.bootstrap.test:test-ext3-deployment:1 (compile) +[info] │ ├─ io.quarkus.bootstrap.test:test-ext3:1 (compile) +[info] │ │ └─ io.quarkus.bootstrap.test:test-core-ext:1 (compile) +[info] │ └─ io.quarkus.bootstrap.test:test-core-ext-deployment:1 (compile) +[info] ├─ io.quarkus.bootstrap.test:provided:1 (provided) +[info] ├─ io.quarkus.bootstrap.test:runtime:1 (runtime) +[info] └─ io.quarkus.bootstrap.test:test:1 (test) diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifact.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifact.java index 23d5751479c88..c52eacf3cdf7f 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifact.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppArtifact.java @@ -2,8 +2,11 @@ import java.io.Serializable; import java.nio.file.Path; +import java.util.Collection; +import java.util.List; import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.paths.PathCollection; import io.quarkus.paths.PathList; @@ -121,4 +124,9 @@ public String getScope() { public int getFlags() { return flags; } + + @Override + public Collection getDependencies() { + return List.of(); + } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppDependency.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppDependency.java index 4c74856d5f9b3..803b09b1df05e 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppDependency.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/AppDependency.java @@ -1,8 +1,11 @@ package io.quarkus.bootstrap.model; import java.io.Serializable; +import java.util.Collection; +import java.util.List; import java.util.Objects; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.DependencyFlags; import io.quarkus.maven.dependency.ResolvedDependency; @@ -126,4 +129,9 @@ public ArtifactKey getKey() { public PathCollection getResolvedPaths() { return artifact.getResolvedPaths(); } + + @Override + public Collection getDependencies() { + return List.of(); + } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java index 95784e1755be8..c4e0331313581 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/ApplicationModelBuilder.java @@ -35,7 +35,7 @@ public class ApplicationModelBuilder { private static final Logger log = Logger.getLogger(ApplicationModelBuilder.class); - ResolvedDependency appArtifact; + ResolvedDependencyBuilder appArtifact; final Map dependencies = new LinkedHashMap<>(); final Collection parentFirstArtifacts = new ConcurrentLinkedDeque<>(); @@ -56,11 +56,15 @@ public ApplicationModelBuilder() { .build()); } - public ApplicationModelBuilder setAppArtifact(ResolvedDependency appArtifact) { + public ApplicationModelBuilder setAppArtifact(ResolvedDependencyBuilder appArtifact) { this.appArtifact = appArtifact; return this; } + public ResolvedDependencyBuilder getApplicationArtifact() { + return appArtifact; + } + public ApplicationModelBuilder setPlatformImports(PlatformImports platformImports) { this.platformImports = platformImports; return this; @@ -77,10 +81,14 @@ public ApplicationModelBuilder addDependency(ResolvedDependencyBuilder dep) { } public ApplicationModelBuilder addDependencies(Collection deps) { - deps.forEach(d -> addDependency(d)); + deps.forEach(this::addDependency); return this; } + public boolean hasDependency(ArtifactKey key) { + return dependencies.containsKey(key); + } + public ResolvedDependencyBuilder getDependency(ArtifactKey key) { return dependencies.get(key); } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java index b03ebc134dbcb..1796b1ceff51a 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/DefaultApplicationModel.java @@ -26,7 +26,7 @@ public class DefaultApplicationModel implements ApplicationModel, Serializable { private final Map> excludedResources; public DefaultApplicationModel(ApplicationModelBuilder builder) { - this.appArtifact = builder.appArtifact; + this.appArtifact = builder.appArtifact.build(); this.dependencies = builder.buildDependencies(); this.platformImports = builder.platformImports; this.capabilityContracts = List.copyOf(builder.extensionCapabilities); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableJarApplicationModel.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableJarApplicationModel.java index b705dac6dde86..ebceb7d97089f 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableJarApplicationModel.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/MutableJarApplicationModel.java @@ -59,7 +59,7 @@ public String getUserProvidersDirectory() { public ApplicationModel getAppModel(Path root) { final ApplicationModelBuilder model = new ApplicationModelBuilder(); - model.setAppArtifact(appArtifact.getDep(root).build()); + model.setAppArtifact(appArtifact.getDep(root)); for (SerializedDep i : dependencies) { model.addDependency(i.getDep(root)); } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/Dependency.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/Dependency.java index 8fe5601ca64dc..2a95f37474117 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/Dependency.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/Dependency.java @@ -8,15 +8,15 @@ public interface Dependency extends ArtifactCoords { String SCOPE_COMPILE = "compile"; String SCOPE_IMPORT = "import"; - public static Dependency of(String groupId, String artifactId) { + static Dependency of(String groupId, String artifactId) { return new ArtifactDependency(groupId, artifactId, null, ArtifactCoords.TYPE_JAR, null); } - public static Dependency of(String groupId, String artifactId, String version) { + static Dependency of(String groupId, String artifactId, String version) { return new ArtifactDependency(groupId, artifactId, null, ArtifactCoords.TYPE_JAR, version); } - public static Dependency pomImport(String groupId, String artifactId, String version) { + static Dependency pomImport(String groupId, String artifactId, String version) { return new ArtifactDependency(groupId, artifactId, null, ArtifactCoords.TYPE_POM, version, SCOPE_IMPORT, false); } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedArtifactDependency.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedArtifactDependency.java index e8788cd70f53b..25964852a2b78 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedArtifactDependency.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedArtifactDependency.java @@ -2,6 +2,8 @@ import java.io.Serializable; import java.nio.file.Path; +import java.util.Collection; +import java.util.List; import java.util.Objects; import io.quarkus.bootstrap.workspace.WorkspaceModule; @@ -14,7 +16,8 @@ public class ResolvedArtifactDependency extends ArtifactDependency implements Re private static final long serialVersionUID = 4038042391733012566L; private PathCollection paths; - private WorkspaceModule module; + private final WorkspaceModule module; + private final Collection deps; private volatile transient PathTree contentTree; public ResolvedArtifactDependency(ArtifactCoords coords) { @@ -34,17 +37,22 @@ public ResolvedArtifactDependency(String groupId, String artifactId, String clas PathCollection resolvedPath) { super(groupId, artifactId, classifier, type, version); this.paths = resolvedPath; + this.module = null; + this.deps = List.of(); } public ResolvedArtifactDependency(ArtifactCoords coords, PathCollection resolvedPaths) { super(coords); this.paths = resolvedPaths; + this.module = null; + this.deps = List.of(); } public ResolvedArtifactDependency(ResolvedDependencyBuilder builder) { - super(builder); + super((AbstractDependencyBuilder) builder); this.paths = builder.getResolvedPaths(); this.module = builder.getWorkspaceModule(); + this.deps = builder.getDependencies(); } @Override @@ -66,6 +74,11 @@ public PathTree getContentTree() { return contentTree == null ? contentTree = ResolvableDependency.super.getContentTree() : contentTree; } + @Override + public Collection getDependencies() { + return deps; + } + @Override public int hashCode() { final int prime = 31; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedDependency.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedDependency.java index e22e23a961671..03701e7c46b3e 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedDependency.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedDependency.java @@ -1,6 +1,7 @@ package io.quarkus.maven.dependency; import java.nio.file.Path; +import java.util.Collection; import io.quarkus.bootstrap.workspace.ArtifactSources; import io.quarkus.bootstrap.workspace.WorkspaceModule; @@ -15,6 +16,8 @@ public interface ResolvedDependency extends Dependency { PathCollection getResolvedPaths(); + Collection getDependencies(); + default boolean isResolved() { final PathCollection paths = getResolvedPaths(); return paths != null && !paths.isEmpty(); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedDependencyBuilder.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedDependencyBuilder.java index 551898e1f7d8f..1f6d115bcc84a 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedDependencyBuilder.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/maven/dependency/ResolvedDependencyBuilder.java @@ -1,12 +1,16 @@ package io.quarkus.maven.dependency; import java.nio.file.Path; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; import io.quarkus.bootstrap.workspace.WorkspaceModule; import io.quarkus.paths.PathCollection; import io.quarkus.paths.PathList; -public class ResolvedDependencyBuilder extends AbstractDependencyBuilder { +public class ResolvedDependencyBuilder extends AbstractDependencyBuilder + implements ResolvedDependency { public static ResolvedDependencyBuilder newInstance() { return new ResolvedDependencyBuilder(); @@ -15,7 +19,9 @@ public static ResolvedDependencyBuilder newInstance() { PathCollection resolvedPaths; WorkspaceModule workspaceModule; private volatile ArtifactCoords coords; + private Set deps = Set.of(); + @Override public PathCollection getResolvedPaths() { return resolvedPaths; } @@ -30,6 +36,7 @@ public ResolvedDependencyBuilder setResolvedPaths(PathCollection resolvedPaths) return this; } + @Override public WorkspaceModule getWorkspaceModule() { return workspaceModule; } @@ -46,6 +53,32 @@ public ArtifactCoords getArtifactCoords() { return coords == null ? coords = ArtifactCoords.of(groupId, artifactId, classifier, type, version) : coords; } + public ResolvedDependencyBuilder addDependency(ArtifactCoords coords) { + if (coords != null) { + if (deps.isEmpty()) { + deps = new HashSet<>(); + } + deps.add(coords); + } + return this; + } + + public ResolvedDependencyBuilder addDependencies(Collection deps) { + if (!deps.isEmpty()) { + if (this.deps.isEmpty()) { + this.deps = new HashSet<>(deps); + } else { + this.deps.addAll(deps); + } + } + return this; + } + + @Override + public Collection getDependencies() { + return deps; + } + @Override public ResolvedDependency build() { return new ResolvedArtifactDependency(this); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java index e7b1c5a1e4c82..b89b39355fcc0 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java @@ -24,6 +24,7 @@ import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContextConfig; +import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; import io.quarkus.bootstrap.resolver.maven.workspace.LocalWorkspace; @@ -152,24 +153,34 @@ public AppModelResolver getAppModelResolver() { } else { mvn = mavenArtifactResolver; } - - return bootstrapAppModelResolver = new BootstrapAppModelResolver(mvn) - .setTest(test) - .setDevMode(devMode); + return bootstrapAppModelResolver = initAppModelResolver(mvn); } MavenArtifactResolver mvn = mavenArtifactResolver; if (mvn == null) { mvn = new MavenArtifactResolver(createBootstrapMavenContext()); } - return bootstrapAppModelResolver = new BootstrapAppModelResolver(mvn) - .setTest(test) - .setDevMode(devMode); + return bootstrapAppModelResolver = initAppModelResolver(mvn); } catch (Exception e) { throw new RuntimeException("Failed to create application model resolver for " + projectRoot, e); } } + private BootstrapAppModelResolver initAppModelResolver(MavenArtifactResolver artifactResolver) { + var appModelResolver = new BootstrapAppModelResolver(artifactResolver) + .setTest(test) + .setDevMode(devMode); + var project = artifactResolver.getMavenContext().getCurrentProject(); + if (project != null) { + appModelResolver.setIncubatingModelResolver( + IncubatingApplicationModelResolver.isIncubatingEnabled( + project.getModelBuildingResult() == null + ? project.getRawModel().getProperties() + : project.getModelBuildingResult().getEffectiveModel().getProperties())); + } + return appModelResolver; + } + private BootstrapMavenContext createBootstrapMavenContext() throws AppModelResolverException { if (mvnContext != null) { return mvnContext; diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java index 5a5d2666f48e5..d8861642c2171 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/resolver/ResolverSetupCleanup.java @@ -18,8 +18,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; +import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; import io.quarkus.bootstrap.util.IoUtils; @@ -149,7 +149,7 @@ protected boolean isBootstrapForTestMode() { protected BootstrapAppModelResolver newAppModelResolver(LocalProject currentProject) throws Exception { final BootstrapAppModelResolver appModelResolver = new BootstrapAppModelResolver(newArtifactResolver(currentProject)); - appModelResolver.setIncubatingModelResolver(ApplicationDependencyModelResolver.isIncubatingEnabled(null)); + appModelResolver.setIncubatingModelResolver(IncubatingApplicationModelResolver.isIncubatingEnabled(null)); if (isBootstrapForTestMode()) { appModelResolver.setTest(true); } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java index 037ad003fea3f..050846b40ae72 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java @@ -34,10 +34,10 @@ import io.quarkus.bootstrap.BootstrapDependencyProcessingException; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.model.ApplicationModelBuilder; -import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyModelResolver; import io.quarkus.bootstrap.resolver.maven.ApplicationDependencyTreeResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.DependencyLoggingConfig; +import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.util.DependencyUtils; import io.quarkus.bootstrap.workspace.ArtifactSources; @@ -127,8 +127,21 @@ public void relink(ArtifactCoords artifact, Path path) throws AppModelResolverEx } @Override - public ResolvedDependency resolve(ArtifactCoords artifact) throws AppModelResolverException { - return resolve(artifact, toAetherArtifact(artifact), mvn.getRepositories()); + public ResolvedDependency resolve(ArtifactCoords coords) throws AppModelResolverException { + final ResolvedDependency resolvedArtifact = ResolvedDependency.class.isAssignableFrom(coords.getClass()) + ? (ResolvedDependency) coords + : null; + if (resolvedArtifact != null + && (resolvedArtifact.getWorkspaceModule() != null || mvn.getProjectModuleResolver() == null)) { + return resolvedArtifact; + } + final WorkspaceModule resolvedModule = mvn.getProjectModuleResolver() == null ? null + : mvn.getProjectModuleResolver().getProjectModule(coords.getGroupId(), coords.getArtifactId(), + coords.getVersion()); + if (resolvedArtifact != null && resolvedModule == null) { + return resolvedArtifact; + } + return resolve(coords, toAetherArtifact(coords), mvn.getRepositories()).build(); } @Override @@ -215,15 +228,14 @@ public ApplicationModel resolveModel(WorkspaceModule module) final Artifact mainArtifact = new DefaultArtifact(module.getId().getGroupId(), module.getId().getArtifactId(), null, ArtifactCoords.TYPE_JAR, module.getId().getVersion()); - final ResolvedDependency mainDep = ResolvedDependencyBuilder.newInstance() + final ResolvedDependencyBuilder mainDep = ResolvedDependencyBuilder.newInstance() .setGroupId(mainArtifact.getGroupId()) .setArtifactId(mainArtifact.getArtifactId()) .setClassifier(mainArtifact.getClassifier()) .setType(mainArtifact.getExtension()) .setVersion(mainArtifact.getVersion()) .setResolvedPaths(resolvedPaths.build()) - .setWorkspaceModule(module) - .build(); + .setWorkspaceModule(module); final Map managedMap = new HashMap<>(); for (io.quarkus.maven.dependency.Dependency d : module.getDirectDependencyConstraints()) { @@ -276,7 +288,7 @@ private ApplicationModel doResolveModel(ArtifactCoords coords, } List aggregatedRepos = mvn.aggregateRepositories(managedRepos, mvn.getRepositories()); - final ResolvedDependency appArtifact = resolve(coords, mvnArtifact, aggregatedRepos); + final ResolvedDependencyBuilder appArtifact = resolve(coords, mvnArtifact, aggregatedRepos); mvnArtifact = toAetherArtifact(appArtifact); final ArtifactDescriptorResult appArtifactDescr = resolveDescriptor(mvnArtifact, aggregatedRepos); @@ -319,7 +331,7 @@ private Set getExcludedScopes() { return Set.of(JavaScopes.PROVIDED, JavaScopes.TEST); } - private ApplicationModel buildAppModel(ResolvedDependency appArtifact, + private ApplicationModel buildAppModel(ResolvedDependencyBuilder appArtifact, Artifact artifact, List directDeps, List repos, Set reloadableModules, List managedDeps) throws AppModelResolverException { @@ -353,7 +365,7 @@ private ApplicationModel buildAppModel(ResolvedDependency appArtifact, start = System.currentTimeMillis(); } if (incubatingModelResolver) { - ApplicationDependencyModelResolver.newInstance() + IncubatingApplicationModelResolver.newInstance() .setArtifactResolver(mvn) .setApplicationModelBuilder(appBuilder) .setCollectReloadableModules(collectReloadableDeps && reloadableModules.isEmpty()) @@ -382,45 +394,43 @@ private ApplicationModel buildAppModel(ResolvedDependency appArtifact, return appBuilder.build(); } - private io.quarkus.maven.dependency.ResolvedDependency resolve(ArtifactCoords appArtifact, Artifact mvnArtifact, + private io.quarkus.maven.dependency.ResolvedDependencyBuilder resolve(ArtifactCoords coords, Artifact artifact, List aggregatedRepos) throws BootstrapMavenException { - final ResolvedDependency resolvedArtifact = ResolvedDependency.class.isAssignableFrom(appArtifact.getClass()) - ? (ResolvedDependency) appArtifact - : null; - if (resolvedArtifact != null - && (resolvedArtifact.getWorkspaceModule() != null || mvn.getProjectModuleResolver() == null)) { - return resolvedArtifact; + final ResolvedDependencyBuilder depBuilder; + if (ResolvedDependencyBuilder.class.isAssignableFrom(coords.getClass())) { + depBuilder = (ResolvedDependencyBuilder) coords; + } else { + depBuilder = ResolvedDependencyBuilder.newInstance().setCoords(coords); + if (coords instanceof ResolvedDependency resolved) { + depBuilder.setResolvedPaths(resolved.getResolvedPaths()); + } } - final WorkspaceModule resolvedModule = mvn.getProjectModuleResolver() == null ? null - : mvn.getProjectModuleResolver().getProjectModule(appArtifact.getGroupId(), appArtifact.getArtifactId(), - appArtifact.getVersion()); - if (resolvedArtifact != null && resolvedModule == null) { - return resolvedArtifact; + WorkspaceModule wsModule = depBuilder.getWorkspaceModule(); + if (wsModule == null && mvn.getProjectModuleResolver() != null) { + wsModule = mvn.getProjectModuleResolver().getProjectModule(coords.getGroupId(), coords.getArtifactId(), + coords.getVersion()); + depBuilder.setWorkspaceModule(wsModule); } - PathCollection resolvedPaths = null; - if ((devmode || test) && resolvedModule != null) { - final ArtifactSources artifactSources = resolvedModule.getSources(appArtifact.getClassifier()); + PathCollection resolvedPaths = depBuilder.getResolvedPaths(); + if ((devmode || test) && wsModule != null) { + final ArtifactSources artifactSources = wsModule.getSources(coords.getClassifier()); if (artifactSources != null) { final PathList.Builder pathBuilder = PathList.builder(); collectSourceDirs(pathBuilder, artifactSources.getSourceDirs()); collectSourceDirs(pathBuilder, artifactSources.getResourceDirs()); if (!pathBuilder.isEmpty()) { resolvedPaths = pathBuilder.build(); + depBuilder.setResolvedPaths(resolvedPaths); } } } - if (resolvedPaths == null) { - if (resolvedArtifact == null || resolvedArtifact.getResolvedPaths() == null) { - resolvedPaths = PathList.of(resolve(mvnArtifact, aggregatedRepos).getArtifact().getFile().toPath()); - } else { - resolvedPaths = resolvedArtifact.getResolvedPaths(); - } + if (resolvedPaths == null || resolvedPaths.isEmpty()) { + depBuilder.setResolvedPaths(PathList.of(resolve(artifact, aggregatedRepos).getArtifact().getFile().toPath())); } - return ResolvedDependencyBuilder.newInstance().setCoords(appArtifact).setWorkspaceModule(resolvedModule) - .setResolvedPaths(resolvedPaths).build(); + return depBuilder; } private static void collectSourceDirs(final PathList.Builder pathBuilder, Collection resources) { diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BuildDependencyGraphVisitor.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BuildDependencyGraphVisitor.java index 025aa5413c781..2ca34bb6e91c0 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BuildDependencyGraphVisitor.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BuildDependencyGraphVisitor.java @@ -15,6 +15,7 @@ import org.eclipse.aether.graph.DependencyNode; import io.quarkus.bootstrap.model.ApplicationModelBuilder; +import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.DependencyFlags; public class BuildDependencyGraphVisitor { @@ -112,7 +113,15 @@ private void consume(DependencyNode node) { buf.append('\u2514').append('\u2500').append(' '); } } - buf.append(node.getArtifact()); + var a = node.getArtifact(); + buf.append(a.getGroupId()).append(":").append(a.getArtifactId()).append(":"); + if (!a.getClassifier().isEmpty()) { + buf.append(a.getClassifier()).append(":"); + } + if (!ArtifactCoords.TYPE_JAR.equals(a.getExtension())) { + buf.append(a.getExtension()).append(":"); + } + buf.append(a.getVersion()); if (!depth.isEmpty()) { buf.append(" (").append(node.getDependency().getScope()); if (node.getDependency().isOptional()) { diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java similarity index 82% rename from independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java rename to independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java index 9a3b9e1855b4e..0cc742766e0c7 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/ApplicationDependencyModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/IncubatingApplicationModelResolver.java @@ -17,14 +17,13 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Phaser; import java.util.function.BiConsumer; import org.eclipse.aether.DefaultRepositorySystemSession; @@ -62,12 +61,13 @@ import io.quarkus.maven.dependency.ArtifactCoords; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.DependencyFlags; +import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.maven.dependency.ResolvedDependencyBuilder; import io.quarkus.paths.PathTree; -public class ApplicationDependencyModelResolver { +public class IncubatingApplicationModelResolver { - private static final Logger log = Logger.getLogger(ApplicationDependencyModelResolver.class); + private static final Logger log = Logger.getLogger(IncubatingApplicationModelResolver.class); private static final String QUARKUS_RUNTIME_ARTIFACT = "quarkus.runtime"; private static final String QUARKUS_EXTENSION_DEPENDENCY = "quarkus.ext"; @@ -95,8 +95,8 @@ public static boolean isIncubatingEnabled(Properties projectProperties) { return Boolean.parseBoolean(value); } - public static ApplicationDependencyModelResolver newInstance() { - return new ApplicationDependencyModelResolver(); + public static IncubatingApplicationModelResolver newInstance() { + return new IncubatingApplicationModelResolver(); } private final ExtensionInfo EXT_INFO_NONE = new ExtensionInfo(); @@ -105,8 +105,6 @@ public static ApplicationDependencyModelResolver newInstance() { private final Map allExtensions = new ConcurrentHashMap<>(); private List conditionalDepsToProcess = new ArrayList<>(); - private final Map> artifactDeps = new HashMap<>(); - private final Collection errors = new ConcurrentLinkedDeque<>(); private MavenArtifactResolver resolver; @@ -116,22 +114,22 @@ public static ApplicationDependencyModelResolver newInstance() { private DependencyLoggingConfig depLogging; private List collectCompileOnly; - public ApplicationDependencyModelResolver setArtifactResolver(MavenArtifactResolver resolver) { + public IncubatingApplicationModelResolver setArtifactResolver(MavenArtifactResolver resolver) { this.resolver = resolver; return this; } - public ApplicationDependencyModelResolver setApplicationModelBuilder(ApplicationModelBuilder appBuilder) { + public IncubatingApplicationModelResolver setApplicationModelBuilder(ApplicationModelBuilder appBuilder) { this.appBuilder = appBuilder; return this; } - public ApplicationDependencyModelResolver setCollectReloadableModules(boolean collectReloadableModules) { + public IncubatingApplicationModelResolver setCollectReloadableModules(boolean collectReloadableModules) { this.collectReloadableModules = collectReloadableModules; return this; } - public ApplicationDependencyModelResolver setDependencyLogging(DependencyLoggingConfig depLogging) { + public IncubatingApplicationModelResolver setDependencyLogging(DependencyLoggingConfig depLogging) { this.depLogging = depLogging; return this; } @@ -143,7 +141,7 @@ public ApplicationDependencyModelResolver setDependencyLogging(DependencyLogging * @param collectCompileOnly compile-only dependencies to add to the model * @return self */ - public ApplicationDependencyModelResolver setCollectCompileOnly(List collectCompileOnly) { + public IncubatingApplicationModelResolver setCollectCompileOnly(List collectCompileOnly) { this.collectCompileOnly = collectCompileOnly; return this; } @@ -206,13 +204,11 @@ private List activateConditionalDeps() { private void processDeploymentDeps(DependencyNode root) { var app = new AppDep(root); - var futures = new ArrayList>(); - app.scheduleChildVisits(futures, AppDep::scheduleDeploymentVisit); - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - if (logErrors()) { - throw new RuntimeException( - "Failed to process Quarkus application deployment dependencies, please see the errors logged above for more details."); - } + var phaser = new Phaser(1); + app.scheduleChildVisits(phaser, AppDep::scheduleDeploymentVisit); + phaser.arriveAndAwaitAdvance(); + assertNoErrors(); + appBuilder.getApplicationArtifact().addDependencies(app.allDeps); for (var d : app.children) { d.addToModel(); } @@ -222,74 +218,85 @@ private void processDeploymentDeps(DependencyNode root) { } } - private boolean logErrors() { + private void assertNoErrors() { if (!errors.isEmpty()) { - log.error("The following errors were encountered while processing Quarkus application dependencies:"); + var sb = new StringBuilder( + "The following errors were encountered while processing Quarkus application dependencies:"); + log.error(sb); var i = 1; for (var error : errors) { - log.error(i++ + ")", error); + var prefix = i++ + ")"; + log.error(prefix, error); + sb.append(System.lineSeparator()).append(prefix).append(" ").append(error.getLocalizedMessage()); + for (var e : error.getStackTrace()) { + sb.append(System.lineSeparator()).append(e); + if (e.getClassName().contains("io.quarkus")) { + break; + } + } } - return true; + throw new RuntimeException(sb.toString()); } - return false; } private void injectDeployment(List activatedConditionalDeps) { - final List> futures = new ArrayList<>(topExtensionDeps.size() - + activatedConditionalDeps.size()); - for (ExtensionDependency extDep : topExtensionDeps) { - futures.add(CompletableFuture.supplyAsync(() -> { - var resolvedDep = appBuilder.getDependency(getKey(extDep.info.deploymentArtifact)); - if (resolvedDep == null) { + final ConcurrentLinkedDeque injectQueue = new ConcurrentLinkedDeque<>(); + { + var phaser = new Phaser(1); + for (ExtensionDependency extDep : topExtensionDeps) { + phaser.register(); + CompletableFuture.runAsync(() -> { + var resolvedDep = appBuilder.getDependency(getKey(extDep.info.deploymentArtifact)); try { - extDep.collectDeploymentDeps(); - return () -> extDep.injectDeploymentNode(null); + if (resolvedDep == null) { + extDep.collectDeploymentDeps(); + injectQueue.add(() -> extDep.injectDeploymentNode(null)); + } else { + // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath + // in which case we also clear the reloadable flag on it, in case it's coming from the workspace + resolvedDep.clearFlag(DependencyFlags.RELOADABLE); + } } catch (BootstrapDependencyProcessingException e) { errors.add(e); + } finally { + phaser.arriveAndDeregister(); } - } else { - // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath - // in which case we also clear the reloadable flag on it, in case it's coming from the workspace - resolvedDep.clearFlag(DependencyFlags.RELOADABLE); - } - return null; - })); + }); + } + // non-conditional deployment branches should be added before the activated conditional ones to have consistent + // dependency graph structures + phaser.arriveAndAwaitAdvance(); + assertNoErrors(); } - // non-conditional deployment branches should be added before the activated conditional ones to have consistent - // dependency graph structures - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - if (errors.isEmpty() && !activatedConditionalDeps.isEmpty()) { + if (!activatedConditionalDeps.isEmpty()) { + var phaser = new Phaser(1); for (ConditionalDependency cd : activatedConditionalDeps) { - futures.add(CompletableFuture.supplyAsync(() -> { - var resolvedDep = appBuilder.getDependency(getKey(cd.appDep.ext.info.deploymentArtifact)); - if (resolvedDep == null) { - var extDep = cd.getExtensionDependency(); - try { + phaser.register(); + CompletableFuture.runAsync(() -> { + var resolvedDep = appBuilder.getDependency(getKey(cd.conditionalDep.ext.info.deploymentArtifact)); + try { + if (resolvedDep == null) { + var extDep = cd.getExtensionDependency(); extDep.collectDeploymentDeps(); - return () -> extDep.injectDeploymentNode(cd.appDep.ext.getParentDeploymentNode()); - } catch (BootstrapDependencyProcessingException e) { - errors.add(e); + injectQueue.add(() -> extDep.injectDeploymentNode(cd.conditionalDep.ext.getParentDeploymentNode())); + } else { + // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath + // in which case we also clear the reloadable flag on it, in case it's coming from the workspace + resolvedDep.clearFlag(DependencyFlags.RELOADABLE); } - } else { - // if resolvedDep isn't null, it means the deployment artifact is on the runtime classpath - // in which case we also clear the reloadable flag on it, in case it's coming from the workspace - resolvedDep.clearFlag(DependencyFlags.RELOADABLE); + } catch (BootstrapDependencyProcessingException e) { + errors.add(e); + } finally { + phaser.arriveAndDeregister(); } - return null; - })); + }); } - } - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - if (logErrors()) { - throw new RuntimeException( - "Failed to process Quarkus application deployment dependencies, please see the errors logged above for more details."); + phaser.arriveAndAwaitAdvance(); + assertNoErrors(); } - for (var future : futures) { - var ext = future.getNow(null); - if (ext != null) { - ext.run(); - } + for (var inject : injectQueue) { + inject.run(); } } @@ -370,14 +377,15 @@ private void collectPlatformProperties() throws AppModelResolverException { for (Dependency d : managedDeps) { final Artifact artifact = d.getArtifact(); final String extension = artifact.getExtension(); - final String artifactId = artifact.getArtifactId(); if ("json".equals(extension) - && artifactId.endsWith(BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX)) { - platformReleases.addPlatformDescriptor(artifact.getGroupId(), artifactId, artifact.getClassifier(), extension, + && artifact.getArtifactId().endsWith(BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX)) { + platformReleases.addPlatformDescriptor(artifact.getGroupId(), artifact.getArtifactId(), + artifact.getClassifier(), extension, artifact.getVersion()); } else if ("properties".equals(extension) - && artifactId.endsWith(BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX)) { - platformReleases.addPlatformProperties(artifact.getGroupId(), artifactId, artifact.getClassifier(), extension, + && artifact.getArtifactId().endsWith(BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX)) { + platformReleases.addPlatformProperties(artifact.getGroupId(), artifact.getArtifactId(), + artifact.getClassifier(), extension, artifact.getVersion(), resolver.resolve(artifact).getArtifact().getFile().toPath()); } } @@ -385,12 +393,12 @@ private void collectPlatformProperties() throws AppModelResolverException { } private void clearReloadableFlag(ResolvedDependencyBuilder dep) { - final Set deps = artifactDeps.get(dep.getArtifactCoords()); - if (deps == null || deps.isEmpty()) { + final Collection deps = dep.getDependencies(); + if (deps.isEmpty()) { return; } - for (ArtifactKey key : deps) { - final ResolvedDependencyBuilder child = appBuilder.getDependency(key); + for (ArtifactCoords coords : deps) { + final ResolvedDependencyBuilder child = appBuilder.getDependency(coords.getKey()); if (child == null || child.isFlagSet(DependencyFlags.VISITED)) { continue; } @@ -444,20 +452,18 @@ private boolean isRuntimeArtifact(ArtifactKey key) { } private void processRuntimeDeps(DependencyNode root) { - final AppDep app = new AppDep(root); - app.walkingFlags = COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS; + final AppDep appRoot = new AppDep(root); + appRoot.walkingFlags = COLLECT_TOP_EXTENSION_RUNTIME_NODES | COLLECT_DIRECT_DEPS; if (collectReloadableModules) { - app.walkingFlags |= COLLECT_RELOADABLE_MODULES; + appRoot.walkingFlags |= COLLECT_RELOADABLE_MODULES; } - var futures = new ArrayList>(); - app.scheduleChildVisits(futures, AppDep::scheduleRuntimeVisit); - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - if (logErrors()) { - throw new RuntimeException( - "Failed to process Quarkus application runtime dependencies, please see the errors logged above for more details."); - } - app.setChildFlags(); + final Phaser phaser = new Phaser(1); + appRoot.scheduleChildVisits(phaser, AppDep::scheduleRuntimeVisit); + phaser.arriveAndAwaitAdvance(); + assertNoErrors(); + appBuilder.getApplicationArtifact().addDependencies(appRoot.allDeps); + appRoot.setChildFlags(); } private class AppDep { @@ -467,17 +473,20 @@ private class AppDep { byte walkingFlags; ResolvedDependencyBuilder resolvedDep; final List children; + final List allDeps; AppDep(DependencyNode node) { this.parent = null; this.node = node; this.children = new ArrayList<>(node.getChildren().size()); + this.allDeps = new ArrayList<>(node.getChildren().size()); } AppDep(AppDep parent, DependencyNode node) { this.parent = parent; this.node = node; this.children = new ArrayList<>(node.getChildren().size()); + this.allDeps = new ArrayList<>(node.getChildren().size()); } void addToModel() { @@ -486,41 +495,48 @@ void addToModel() { } // this node is added after its children to stay compatible with the legacy impl if (resolvedDep != null) { + resolvedDep.addDependencies(allDeps); appBuilder.addDependency(resolvedDep); } } - void scheduleDeploymentVisit(List> futures) { - futures.add(CompletableFuture.runAsync(() -> { + void scheduleDeploymentVisit(Phaser phaser) { + phaser.register(); + CompletableFuture.runAsync(() -> { try { visitDeploymentDependency(); } catch (Throwable e) { errors.add(e); + } finally { + phaser.arriveAndDeregister(); } - })); - scheduleChildVisits(futures, AppDep::scheduleDeploymentVisit); + }); + scheduleChildVisits(phaser, AppDep::scheduleDeploymentVisit); } void visitDeploymentDependency() { - var dep = appBuilder.getDependency(getKey(node.getArtifact())); - if (dep == null) { + if (!appBuilder.hasDependency(getKey(node.getArtifact()))) { try { - resolvedDep = newDependencyBuilder(node, resolver).setFlags(DependencyFlags.DEPLOYMENT_CP); + resolvedDep = newDependencyBuilder(node, resolver) + .setFlags(DependencyFlags.DEPLOYMENT_CP); } catch (BootstrapMavenException e) { throw new RuntimeException(e); } } } - void scheduleRuntimeVisit(List> futures) { - futures.add(CompletableFuture.runAsync(() -> { + void scheduleRuntimeVisit(Phaser phaser) { + phaser.register(); + CompletableFuture.runAsync(() -> { try { visitRuntimeDependency(); } catch (Throwable t) { errors.add(t); + } finally { + phaser.arriveAndDeregister(); } - })); - scheduleChildVisits(futures, AppDep::scheduleRuntimeVisit); + }); + scheduleChildVisits(phaser, AppDep::scheduleRuntimeVisit); } void visitRuntimeDependency() { @@ -558,23 +574,21 @@ void visitRuntimeDependency() { } } - void scheduleChildVisits(List> futures, - BiConsumer>> childVisitor) { + void scheduleChildVisits(Phaser phaser, BiConsumer childVisitor) { var childNodes = node.getChildren(); List filtered = null; - var depKeys = artifactDeps.computeIfAbsent(getCoords(node.getArtifact()), key -> new HashSet<>(childNodes.size())); for (int i = 0; i < childNodes.size(); ++i) { var childNode = childNodes.get(i); var winner = getWinner(childNode); if (winner == null) { - depKeys.add(getKey(childNode.getArtifact())); + allDeps.add(getCoords(childNode.getArtifact())); var child = new AppDep(this, childNode); children.add(child); if (filtered != null) { filtered.add(childNode); } } else { - depKeys.add(getKey(winner.getArtifact())); + allDeps.add(getCoords(winner.getArtifact())); if (filtered == null) { filtered = new ArrayList<>(childNodes.size()); for (int j = 0; j < i; ++j) { @@ -587,7 +601,7 @@ void scheduleChildVisits(List> futures, node.setChildren(filtered); } for (var child : children) { - childVisitor.accept(child, futures); + childVisitor.accept(child, phaser); } } @@ -599,6 +613,7 @@ void setChildFlags() { void setFlags(byte walkingFlags) { + resolvedDep.addDependencies(allDeps); if (ext != null) { var parentExtDep = parent; while (parentExtDep != null) { @@ -611,11 +626,14 @@ void setFlags(byte walkingFlags) { ext.info.ensureActivated(); } - if (appBuilder.getDependency(resolvedDep.getKey()) == null) { + var existingDep = appBuilder.getDependency(resolvedDep.getKey()); + if (existingDep == null) { appBuilder.addDependency(resolvedDep); if (ext != null) { managedDeps.add(new Dependency(ext.info.deploymentArtifact, JavaScopes.COMPILE)); } + } else if (existingDep != resolvedDep) { + throw new IllegalStateException(node.getArtifact() + " is already in the model"); } this.walkingFlags = walkingFlags; @@ -635,7 +653,6 @@ void setFlags(byte walkingFlags) { } clearWalkingFlag(COLLECT_DIRECT_DEPS); - setChildFlags(); } @@ -723,7 +740,7 @@ private void collectConditionalDependencies() } final ConditionalDependency conditionalDep = new ConditionalDependency(conditionalInfo, this); conditionalDepsToProcess.add(conditionalDep); - conditionalDep.appDep.collectConditionalDependencies(); + conditionalDep.conditionalDep.collectConditionalDependencies(); } } } @@ -1019,7 +1036,7 @@ private boolean replaceDirectDepBranch(DependencyNode parentNode, boolean replac private class ConditionalDependency { - final AppDep appDep; + final AppDep conditionalDep; private boolean activated; private ConditionalDependency(ExtensionInfo info, AppDep parent) { @@ -1030,12 +1047,12 @@ private ConditionalDependency(ExtensionInfo info, AppDep parent) { new BootstrapArtifactVersion(info.runtimeArtifact.getVersion()))); rtNode.setRepositories(parent.ext.runtimeNode.getRepositories()); - appDep = new AppDep(parent, rtNode); - appDep.ext = new ExtensionDependency(info, rtNode, parent.ext.exclusions); + conditionalDep = new AppDep(parent, rtNode); + conditionalDep.ext = new ExtensionDependency(info, rtNode, parent.ext.exclusions); } ExtensionDependency getExtensionDependency() { - return appDep.ext; + return conditionalDep.ext; } void activate() { @@ -1044,7 +1061,7 @@ void activate() { } activated = true; final ExtensionDependency extDep = getExtensionDependency(); - final DependencyNode originalNode = collectDependencies(appDep.ext.info.runtimeArtifact, extDep.exclusions, + final DependencyNode originalNode = collectDependencies(conditionalDep.ext.info.runtimeArtifact, extDep.exclusions, extDep.runtimeNode.getRepositories()); final DefaultDependencyNode rtNode = (DefaultDependencyNode) extDep.runtimeNode; rtNode.setRepositories(originalNode.getRepositories()); @@ -1057,39 +1074,28 @@ void activate() { currentChildren.addAll(originalNode.getChildren()); } - appDep.walkingFlags = COLLECT_DIRECT_DEPS; + conditionalDep.walkingFlags = COLLECT_DIRECT_DEPS; if (collectReloadableModules) { - appDep.walkingFlags |= COLLECT_RELOADABLE_MODULES; + conditionalDep.walkingFlags |= COLLECT_RELOADABLE_MODULES; } - var futures = new ArrayList>(); - appDep.scheduleRuntimeVisit(futures); - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - if (logErrors()) { - throw new RuntimeException( - "Failed to process Quarkus application conditional dependencies, please see the errors logged above for more details."); - } - - appDep.setFlags(appDep.walkingFlags); - - var parentExtDep = appDep.parent; - parentExtDep.children.add(appDep); - while (parentExtDep != null) { - if (parentExtDep.ext != null) { - parentExtDep.ext.addExtensionDependency(appDep.ext); - break; - } - parentExtDep = parentExtDep.parent; + var phaser = new Phaser(1); + conditionalDep.scheduleRuntimeVisit(phaser); + phaser.arriveAndAwaitAdvance(); + assertNoErrors(); + conditionalDep.setFlags(conditionalDep.walkingFlags); + if (conditionalDep.parent.resolvedDep == null) { + conditionalDep.parent.allDeps.add(conditionalDep.resolvedDep.getArtifactCoords()); + } else { + conditionalDep.parent.resolvedDep.addDependency(conditionalDep.resolvedDep.getArtifactCoords()); } - appDep.ext.info.ensureActivated(); - - appDep.parent.ext.runtimeNode.getChildren().add(rtNode); + conditionalDep.parent.ext.runtimeNode.getChildren().add(rtNode); } boolean isSatisfied() { - if (appDep.ext.info.dependencyCondition == null) { + if (conditionalDep.ext.info.dependencyCondition == null) { return true; } - for (ArtifactKey key : appDep.ext.info.dependencyCondition) { + for (ArtifactKey key : conditionalDep.ext.info.dependencyCondition) { if (!isRuntimeArtifact(key)) { return false; } @@ -1125,19 +1131,18 @@ private class AppDepLogger { private AppDepLogger() { } - void log(AppDep root) { - logInternal(root); - - final int childrenTotal = root.children.size(); + void log(AppDep dep) { + logInternal(dep); + final int childrenTotal = dep.node.getChildren().size(); if (childrenTotal > 0) { if (childrenTotal == 1) { depth.add(false); - log(root.children.get(0)); + log(dep.children.get(0)); } else { depth.add(true); int i = 0; while (i < childrenTotal) { - log(root.children.get(i++)); + log(dep.children.get(i++)); if (i == childrenTotal - 1) { depth.set(depth.size() - 1, false); } @@ -1166,25 +1171,26 @@ private void logInternal(AppDep dep) { buf.append('\u2514').append('\u2500').append(' '); } } - buf.append(dep.node.getArtifact()); + var resolvedDep = getResolvedDependency(getKey(dep.node.getArtifact())); + buf.append(resolvedDep.toCompactCoords()); if (!depth.isEmpty()) { - appendFlags(buf, getResolvedDependency(getKey(dep.node.getArtifact()))); + appendFlags(buf, resolvedDep); } depLogging.getMessageConsumer().accept(buf.toString()); if (depLogging.isGraph()) { - var depKeys = artifactDeps.get(getCoords(dep.node.getArtifact())); - if (depKeys != null && !depKeys.isEmpty() && depKeys.size() != dep.children.size()) { - final Map versions = new HashMap<>(dep.children.size()); + var deps = resolvedDep.getDependencies(); + if (!deps.isEmpty() && deps.size() != dep.children.size()) { + final Map versions = new HashMap<>(dep.children.size()); for (var c : dep.children) { - versions.put(getKey(c.node.getArtifact()), c.node.getArtifact().getVersion()); + versions.put(getCoords(c.node.getArtifact()), null); } - var list = new ArrayList(depKeys.size() - dep.children.size()); - for (var key : depKeys) { - if (!versions.containsKey(key)) { - var d = getResolvedDependency(key); - var sb = new StringBuilder().append(d.toGACTVString()); - appendFlags(sb, d); + var list = new ArrayList(deps.size() - dep.children.size()); + for (var coords : deps) { + if (!versions.containsKey(coords)) { + var childDep = getResolvedDependency(coords.getKey()); + var sb = new StringBuilder().append(childDep.toCompactCoords()); + appendFlags(sb, childDep); list.add(sb.append(" [+]").toString()); } } @@ -1225,7 +1231,7 @@ private void logInternal(AppDep dep) { } } - private void appendFlags(StringBuilder sb, ResolvedDependencyBuilder d) { + private void appendFlags(StringBuilder sb, ResolvedDependency d) { sb.append(" (").append(d.getScope()); if (d.isFlagSet(DependencyFlags.OPTIONAL)) { sb.append(" optional"); @@ -1247,11 +1253,14 @@ private void appendFlags(StringBuilder sb, ResolvedDependencyBuilder d) { } private ResolvedDependencyBuilder getResolvedDependency(ArtifactKey key) { - var d = appBuilder.getDependency(key); - if (d == null) { - throw new IllegalArgumentException(key + " is not found among application dependencies"); + var resolvedDep = appBuilder.getDependency(key); + if (resolvedDep == null) { + if (appBuilder.getApplicationArtifact().getKey().equals(key)) { + return appBuilder.getApplicationArtifact(); + } + throw new IllegalArgumentException("Failed to locate " + key + " among application dependencies"); } - return d; + return resolvedDep; } } } From a58c5a0d7e52a7849c8a3d053624b6fcc99880de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Phillip=20Kr=C3=BCger?= Date: Mon, 22 Apr 2024 10:32:10 +1000 Subject: [PATCH 0031/2353] Update codeblock to 1.0.13 --- bom/dev-ui/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/dev-ui/pom.xml b/bom/dev-ui/pom.xml index c461772d70c91..a8c23cc5f4a11 100644 --- a/bom/dev-ui/pom.xml +++ b/bom/dev-ui/pom.xml @@ -28,7 +28,7 @@ 1.7.5 1.7.0 5.5.0 - 1.0.12 + 1.0.13 1.8.3 2.4.0 From a40c1c2c3f017b01130e0ab9833da6b8662d14fa Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 22 Apr 2024 11:35:58 +0300 Subject: [PATCH 0032/2353] Turn beans from info extension into ApplicationScoped This is done purely to aid testing Closes: #40152 --- .../java/io/quarkus/info/deployment/InfoProcessor.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoProcessor.java b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoProcessor.java index c8882b07d9f1d..9e62736b27be6 100644 --- a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoProcessor.java +++ b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoProcessor.java @@ -13,7 +13,7 @@ import java.util.TimeZone; import java.util.stream.Collectors; -import jakarta.inject.Singleton; +import jakarta.enterprise.context.ApplicationScoped; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -122,7 +122,7 @@ void gitInfo(InfoBuildTimeConfig config, valuesProducer.produce(new InfoBuildTimeValuesBuildItem("git", data)); beanProducer.produce(SyntheticBeanBuildItem.configure(GitInfo.class) .supplier(recorder.gitInfoSupplier(branch, latestCommitId, latestCommitTime)) - .scope(Singleton.class) + .scope(ApplicationScoped.class) .setRuntimeInit() .done()); } catch (Exception e) { @@ -229,7 +229,7 @@ void buildInfo(CurateOutcomeBuildItem curateOutcomeBuildItem, valuesProducer.produce(new InfoBuildTimeValuesBuildItem("build", data)); beanProducer.produce(SyntheticBeanBuildItem.configure(BuildInfo.class) .supplier(recorder.buildInfoSupplier(group, artifact, version, time, quarkusVersion)) - .scope(Singleton.class) + .scope(ApplicationScoped.class) .setRuntimeInit() .done()); } @@ -251,7 +251,7 @@ void osInfo(InfoRecorder recorder, valuesProducer.produce(new InfoBuildTimeContributorBuildItem(recorder.osInfoContributor())); beanProducer.produce(SyntheticBeanBuildItem.configure(OsInfo.class) .supplier(recorder.osInfoSupplier()) - .scope(Singleton.class) + .scope(ApplicationScoped.class) .setRuntimeInit() .done()); } @@ -264,7 +264,7 @@ void javaInfo(InfoRecorder recorder, valuesProducer.produce(new InfoBuildTimeContributorBuildItem(recorder.javaInfoContributor())); beanProducer.produce(SyntheticBeanBuildItem.configure(JavaInfo.class) .supplier(recorder.javaInfoSupplier()) - .scope(Singleton.class) + .scope(ApplicationScoped.class) .setRuntimeInit() .done()); } From 8df1abee5ad919f9948d3b9731e691252f76c273 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 22 Apr 2024 10:34:38 +0200 Subject: [PATCH 0033/2353] WebSockets Next: produce ExecutionModelAnnotationsAllowedBuildItem - so that callback methods can be annotated with Blocking, NonBlocking and RunOnVirtualThread --- .../next/deployment/WebSocketDotNames.java | 7 +++ .../deployment/WebSocketServerProcessor.java | 36 ++++++++++--- .../BlockingAnnotationTest.java | 54 +++++++++++++++++++ .../NonBlockingAnnotationTest.java | 53 ++++++++++++++++++ 4 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/BlockingAnnotationTest.java create mode 100644 extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/NonBlockingAnnotationTest.java diff --git a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketDotNames.java b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketDotNames.java index 72b18bc9f29cb..98dfd77f50cad 100644 --- a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketDotNames.java +++ b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketDotNames.java @@ -1,5 +1,7 @@ package io.quarkus.websockets.next.deployment; +import java.util.List; + import org.jboss.jandex.DotName; import io.quarkus.websockets.next.OnBinaryMessage; @@ -12,6 +14,7 @@ import io.quarkus.websockets.next.WebSocket; import io.quarkus.websockets.next.WebSocketConnection; import io.smallrye.common.annotation.Blocking; +import io.smallrye.common.annotation.NonBlocking; import io.smallrye.common.annotation.RunOnVirtualThread; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; @@ -33,6 +36,7 @@ final class WebSocketDotNames { static final DotName MULTI = DotName.createSimple(Multi.class); static final DotName RUN_ON_VIRTUAL_THREAD = DotName.createSimple(RunOnVirtualThread.class); static final DotName BLOCKING = DotName.createSimple(Blocking.class); + static final DotName NON_BLOCKING = DotName.createSimple(NonBlocking.class); static final DotName STRING = DotName.createSimple(String.class); static final DotName BUFFER = DotName.createSimple(Buffer.class); static final DotName JSON_OBJECT = DotName.createSimple(JsonObject.class); @@ -41,4 +45,7 @@ final class WebSocketDotNames { static final DotName PATH_PARAM = DotName.createSimple(PathParam.class); static final DotName HANDSHAKE_REQUEST = DotName.createSimple(WebSocketConnection.HandshakeRequest.class); static final DotName THROWABLE = DotName.createSimple(Throwable.class); + + static final List CALLBACK_ANNOTATIONS = List.of(ON_OPEN, ON_CLOSE, ON_BINARY_MESSAGE, ON_TEXT_MESSAGE, + ON_PONG_MESSAGE, ON_ERROR); } diff --git a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java index 18df7a083df1d..9c6f52d139ad3 100644 --- a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java +++ b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,6 +37,7 @@ import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.TransformedAnnotationsBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; @@ -47,6 +49,7 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.CatchBlockCreator; import io.quarkus.gizmo.ClassCreator; @@ -117,6 +120,18 @@ void unremovableBeans(BuildProducer unremovableBeans) unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(TextMessageCodec.class)); } + @BuildStep + ExecutionModelAnnotationsAllowedBuildItem executionModelAnnotations( + TransformedAnnotationsBuildItem transformedAnnotations) { + return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() { + @Override + public boolean test(MethodInfo method) { + return Annotations.containsAny(transformedAnnotations.getAnnotations(method), + WebSocketDotNames.CALLBACK_ANNOTATIONS); + } + }); + } + @BuildStep public void collectEndpoints(BeanArchiveIndexBuildItem beanArchiveIndex, BeanDiscoveryFinishedBuildItem beanDiscoveryFinished, @@ -1006,7 +1021,8 @@ private List findErrorHandlers(IndexView index, ClassInfo beanClass, C List errorHandlers = new ArrayList<>(); for (AnnotationInstance annotation : annotations) { MethodInfo method = annotation.target().asMethod(); - Callback callback = new Callback(annotation, method, executionModel(method), callbackArguments, + Callback callback = new Callback(annotation, method, executionModel(method, transformedAnnotations), + callbackArguments, transformedAnnotations, endpointPath, index); long errorArguments = callback.arguments.stream().filter(ca -> ca instanceof ErrorCallbackArgument).count(); if (errorArguments != 1) { @@ -1052,7 +1068,8 @@ private Callback findCallback(IndexView index, ClassInfo beanClass, DotName anno } else if (annotations.size() == 1) { AnnotationInstance annotation = annotations.get(0); MethodInfo method = annotation.target().asMethod(); - Callback callback = new Callback(annotation, method, executionModel(method), callbackArguments, + Callback callback = new Callback(annotation, method, executionModel(method, transformedAnnotations), + callbackArguments, transformedAnnotations, endpointPath, index); long messageArguments = callback.arguments.stream().filter(ca -> ca instanceof MessageCallbackArgument).count(); if (callback.acceptsMessage()) { @@ -1081,13 +1098,16 @@ private Callback findCallback(IndexView index, ClassInfo beanClass, DotName anno String.format("There can be only one callback annotated with %s declared on %s", annotationName, beanClass)); } - ExecutionModel executionModel(MethodInfo method) { - if (hasBlockingSignature(method)) { - return method.hasDeclaredAnnotation(WebSocketDotNames.RUN_ON_VIRTUAL_THREAD) ? ExecutionModel.VIRTUAL_THREAD - : ExecutionModel.WORKER_THREAD; + ExecutionModel executionModel(MethodInfo method, TransformedAnnotationsBuildItem transformedAnnotations) { + if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.RUN_ON_VIRTUAL_THREAD)) { + return ExecutionModel.VIRTUAL_THREAD; + } else if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.BLOCKING)) { + return ExecutionModel.WORKER_THREAD; + } else if (transformedAnnotations.hasAnnotation(method, WebSocketDotNames.NON_BLOCKING)) { + return ExecutionModel.EVENT_LOOP; + } else { + return hasBlockingSignature(method) ? ExecutionModel.WORKER_THREAD : ExecutionModel.EVENT_LOOP; } - return method.hasDeclaredAnnotation(WebSocketDotNames.BLOCKING) ? ExecutionModel.WORKER_THREAD - : ExecutionModel.EVENT_LOOP; } boolean hasBlockingSignature(MethodInfo method) { diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/BlockingAnnotationTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/BlockingAnnotationTest.java new file mode 100644 index 0000000000000..b31cb1d540daf --- /dev/null +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/BlockingAnnotationTest.java @@ -0,0 +1,54 @@ +package io.quarkus.websockets.next.test.executionmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.smallrye.common.annotation.Blocking; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Vertx; + +public class BlockingAnnotationTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Endpoint.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("endpoint") + URI endUri; + + @Test + void testEndoint() { + try (WSClient client = new WSClient(vertx).connect(endUri)) { + assertEquals("evenloop:false,worker:true", client.sendAndAwaitReply("foo").toString()); + } + } + + @WebSocket(path = "/endpoint") + public static class Endpoint { + + @Blocking + @OnTextMessage + Uni message(String ignored) { + return Uni.createFrom().item("evenloop:" + Context.isOnEventLoopThread() + ",worker:" + Context.isOnWorkerThread()); + } + + } + +} diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/NonBlockingAnnotationTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/NonBlockingAnnotationTest.java new file mode 100644 index 0000000000000..3c4da547354be --- /dev/null +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/executionmodel/NonBlockingAnnotationTest.java @@ -0,0 +1,53 @@ +package io.quarkus.websockets.next.test.executionmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.smallrye.common.annotation.NonBlocking; +import io.vertx.core.Context; +import io.vertx.core.Vertx; + +public class NonBlockingAnnotationTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Endpoint.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("endpoint") + URI endUri; + + @Test + void testEndoint() { + try (WSClient client = new WSClient(vertx).connect(endUri)) { + assertEquals("evenloop:true,worker:false", client.sendAndAwaitReply("foo").toString()); + } + } + + @WebSocket(path = "/endpoint") + public static class Endpoint { + + @NonBlocking + @OnTextMessage + String message(String ignored) { + return "evenloop:" + Context.isOnEventLoopThread() + ",worker:" + Context.isOnWorkerThread(); + } + + } + +} From 1acdb64020e1908cadce99769c9de859588f3908 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 22 Apr 2024 12:04:05 +0200 Subject: [PATCH 0034/2353] WebSockets Next: avoid unnecessary bean lookups - i.e. obtain the contextual reference when endpoint is created --- .../deployment/WebSocketServerProcessor.java | 24 +++++++++++-------- .../next/runtime/WebSocketEndpoint.java | 7 ++++++ .../next/runtime/WebSocketEndpointBase.java | 21 ++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java index 18df7a083df1d..1ccd5ae685638 100644 --- a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java +++ b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java @@ -398,7 +398,7 @@ private void validateOnClose(Callback callback) { * } * * public Uni doOnTextMessage(String message) { - * Uni uni = ((Echo) super.beanInstance("MTd91f3oxHtG8gnznR7XcZBCLdE")).echo((String) message); + * Uni uni = ((Echo) super.beanInstance().echo((String) message); * if (uni != null) { * // The lambda is implemented as a generated function: Echo_WebSocketEndpoint$$function$$1 * return uni.chain(m -> sendText(m, false)); @@ -408,7 +408,7 @@ private void validateOnClose(Callback callback) { * } * * public Uni doOnTextMessage(Object message) { - * Object bean = super.beanInstance("egBJQ7_QAFkQlYXSTKE0XlN3wow"); + * Object bean = super.beanInstance(); * try { * String ret = ((EchoEndpoint) bean).echo((String) message); * return ret != null ? super.sendText(ret, false) : Uni.createFrom().voidItem(); @@ -430,6 +430,10 @@ private void validateOnClose(Callback callback) { * public WebSocketEndpoint.ExecutionModel onTextMessageExecutionModel() { * return ExecutionModel.EVENT_LOOP; * } + * + * public String beanIdentifier() { + * return "egBJQ7_QAFkQlYXSTKE0XlN3wow"; + * } * } * * @@ -470,13 +474,15 @@ private String generateEndpoint(WebSocketEndpointBuildItem endpoint, MethodCreator executionMode = endpointCreator.getMethodCreator("executionMode", WebSocket.ExecutionMode.class); executionMode.returnValue(executionMode.load(endpoint.executionMode)); + MethodCreator beanIdentifier = endpointCreator.getMethodCreator("beanIdentifier", String.class); + beanIdentifier.returnValue(beanIdentifier.load(endpoint.bean.getIdentifier())); + if (endpoint.onOpen != null) { Callback callback = endpoint.onOpen; MethodCreator doOnOpen = endpointCreator.getMethodCreator("doOnOpen", Uni.class, Object.class); - // Foo foo = beanInstance("foo"); + // Foo foo = beanInstance(); ResultHandle beanInstance = doOnOpen.invokeVirtualMethod( - MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "beanInstance", Object.class, String.class), - doOnOpen.getThis(), doOnOpen.load(endpoint.bean.getIdentifier())); + MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "beanInstance", Object.class), doOnOpen.getThis()); // Call the business method TryBlock tryBlock = onErrorTryBlock(doOnOpen, doOnOpen.getThis()); ResultHandle[] args = callback.generateArguments(tryBlock.getThis(), tryBlock, transformedAnnotations, index); @@ -500,8 +506,7 @@ private String generateEndpoint(WebSocketEndpointBuildItem endpoint, MethodCreator doOnClose = endpointCreator.getMethodCreator("doOnClose", Uni.class, Object.class); // Foo foo = beanInstance("foo"); ResultHandle beanInstance = doOnClose.invokeVirtualMethod( - MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "beanInstance", Object.class, String.class), - doOnClose.getThis(), doOnClose.load(endpoint.bean.getIdentifier())); + MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "beanInstance", Object.class), doOnClose.getThis()); // Call the business method TryBlock tryBlock = onErrorTryBlock(doOnClose, doOnClose.getThis()); ResultHandle[] args = callback.generateArguments(tryBlock.getThis(), tryBlock, transformedAnnotations, index); @@ -648,10 +653,9 @@ private void generateOnMessage(ClassCreator endpointCreator, WebSocketEndpointBu methodParameterType); TryBlock tryBlock = onErrorTryBlock(doOnMessage, doOnMessage.getThis()); - // Foo foo = beanInstance("foo"); + // Foo foo = beanInstance(); ResultHandle beanInstance = tryBlock.invokeVirtualMethod( - MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "beanInstance", Object.class, String.class), - tryBlock.getThis(), tryBlock.load(endpoint.bean.getIdentifier())); + MethodDescriptor.ofMethod(WebSocketEndpointBase.class, "beanInstance", Object.class), tryBlock.getThis()); ResultHandle[] args = callback.generateArguments(tryBlock.getThis(), tryBlock, transformedAnnotations, index); // Call the business method ResultHandle ret = tryBlock.invokeVirtualMethod(MethodDescriptor.of(callback.method), beanInstance, diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpoint.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpoint.java index 5ad60e04a69dd..a5dfb70a86076 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpoint.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpoint.java @@ -79,6 +79,13 @@ default ExecutionModel onCloseExecutionModel() { Uni doOnError(Throwable t); + /** + * + * @return the identifier of the bean with callbacks + * @see io.quarkus.arc.InjectableBean#getIdentifier() + */ + String beanIdentifier(); + enum ExecutionModel { WORKER_THREAD, VIRTUAL_THREAD, diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java index 051362461babe..261de140f1683 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketEndpointBase.java @@ -5,10 +5,14 @@ import java.util.function.Consumer; import java.util.function.Function; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Singleton; + import org.jboss.logging.Logger; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableContext.ContextState; import io.quarkus.virtual.threads.VirtualThreadsRecorder; import io.quarkus.websockets.next.WebSocket.ExecutionMode; @@ -43,6 +47,9 @@ public abstract class WebSocketEndpointBase implements WebSocketEndpoint { private final ContextSupport contextSupport; + private final InjectableBean bean; + private final Object beanInstance; + public WebSocketEndpointBase(WebSocketConnection connection, Codecs codecs, WebSocketsRuntimeConfig config, ContextSupport contextSupport) { this.connection = connection; @@ -51,6 +58,16 @@ public WebSocketEndpointBase(WebSocketConnection connection, Codecs codecs, this.config = config; this.container = Arc.container(); this.contextSupport = contextSupport; + InjectableBean bean = container.bean(beanIdentifier()); + if (bean.getScope().equals(ApplicationScoped.class) + || bean.getScope().equals(Singleton.class)) { + // For certain scopes, we can optimize and obtain the contextual reference immediately + this.bean = null; + this.beanInstance = container.instance(bean).get(); + } else { + this.bean = bean; + this.beanInstance = null; + } } @Override @@ -238,6 +255,10 @@ public void handle(Void event) { return UniHelper.toUni(promise.future()); } + public Object beanInstance() { + return beanInstance != null ? beanInstance : container.instance(bean).get(); + } + public Object beanInstance(String identifier) { return container.instance(container.bean(identifier)).get(); } From 2585bb0c5a8e8e4e758d4d6e49d98b13380267f6 Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Mon, 22 Apr 2024 14:18:37 +0300 Subject: [PATCH 0035/2353] Fix resource registration for native compilation NativeImageResourceBuildItem and ServiceProviderBuildItem contain a path of the resource we should include and not a pattern or a glob. As a result, `Pattern.quote` is the right method to use in order to produce a pattern that would match the path. The patch also remove the wrong addition of the paths "as is" to the includes json array. Fix up of b7f49dd9d952a46fde3f0abaabcc08b1a2e5f067 --- .../deployment/steps/NativeImageResourceConfigStep.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageResourceConfigStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageResourceConfigStep.java index 84a002343b29e..936eb56a03614 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageResourceConfigStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageResourceConfigStep.java @@ -4,6 +4,7 @@ import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.regex.Pattern; import io.quarkus.builder.Json; import io.quarkus.builder.Json.JsonArrayBuilder; @@ -16,7 +17,6 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; -import io.quarkus.util.GlobUtil; public class NativeImageResourceConfigStep { @@ -35,14 +35,13 @@ void generateResourceConfig(BuildProducer resourceCo for (NativeImageResourceBuildItem i : resources) { for (String path : i.getResources()) { JsonObjectBuilder pat = Json.object(); - pat.put("pattern", GlobUtil.toRegexPattern(path)); + pat.put("pattern", Pattern.quote(path)); includes.add(pat); } - addListToJsonArray(includes, i.getResources()); } for (ServiceProviderBuildItem i : serviceProviderBuildItems) { - includes.add(Json.object().put("pattern", GlobUtil.toRegexPattern(i.serviceDescriptorFile()))); + includes.add(Json.object().put("pattern", Pattern.quote(i.serviceDescriptorFile()))); } for (NativeImageResourcePatternsBuildItem resourcePatternsItem : resourcePatterns) { From 70781c827ab93539a63f692d7c15ddc6403ef453 Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Mon, 22 Apr 2024 13:59:37 +0200 Subject: [PATCH 0036/2353] Fix a compilation problem in the SmallRye GraphQL guide --- docs/src/main/asciidoc/smallrye-graphql.adoc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/src/main/asciidoc/smallrye-graphql.adoc b/docs/src/main/asciidoc/smallrye-graphql.adoc index 01284ee079384..9537d30858049 100644 --- a/docs/src/main/asciidoc/smallrye-graphql.adoc +++ b/docs/src/main/asciidoc/smallrye-graphql.adoc @@ -721,17 +721,18 @@ public class Film implements SearchResult { // ... } -public class Hero implements Character, SearchResult { +public interface Character implements SearchResult { // ... } -public class Ally implements Character, SearchResult { +public class Hero implements Character { // ... } ----- -TIP: We can also leverage inheritance and have `interface Character extends SearchResult`. -This will result in all implementations of `Character` being added as members of the `SearchResult` union in the schema. +public class Ally implements Character { + // ... +} +---- Update `GalaxyService` to provide search: From 22d1052049a375ed5a0318b1de691b36d9b10144 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Mon, 22 Apr 2024 13:38:40 +0100 Subject: [PATCH 0037/2353] Bump BouncyCastle version to 1.78.1 --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 49e76c95bd93f..c2610aa2430a2 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -15,7 +15,7 @@ 2.0.2 - 1.77 + 1.78.1 1.0.2.4 1.0.18 5.0.0 From b6285a65398d48f5ba3a873d827441ad8540fa68 Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Tue, 2 Apr 2024 16:04:25 +0200 Subject: [PATCH 0038/2353] Infinispan Cache Extension * Deprecates custom annotations * Implements the cache extension SPI --- .github/native-tests.json | 4 +- bom/application/pom.xml | 10 + devtools/bom-descriptor-json/pom.xml | 13 + docs/pom.xml | 13 + .../asciidoc/cache-infinispan-reference.adoc | 138 +++++++ .../asciidoc/infinispan-client-reference.adoc | 41 +- .../infinispan-cache/deployment/pom.xml | 94 +++++ .../deployment/InfinispanCacheProcessor.java | 52 +++ .../src/main/resources/application.properties | 2 + .../cache/infinispan/InfinispanCacheTest.java | 355 ++++++++++++++++++ ...y-application-infinispan-client.properties | 0 extensions/infinispan-cache/pom.xml | 20 + extensions/infinispan-cache/runtime/pom.xml | 79 ++++ .../runtime/CompositeCacheKeyMarshaller.java | 47 +++ .../runtime/CompositeKeyMarshallerBean.java | 30 ++ .../runtime/InfinispanCacheBuildRecorder.java | 70 ++++ .../runtime/InfinispanCacheImpl.java | 181 +++++++++ .../runtime/InfinispanCacheInfo.java | 23 ++ .../runtime/InfinispanCacheInfoBuilder.java | 42 +++ .../runtime/InfinispanCacheRuntimeConfig.java | 23 ++ .../InfinispanCachesBuildTimeConfig.java | 19 + .../runtime/InfinispanCachesConfig.java | 29 ++ .../resources/META-INF/quarkus-extension.yaml | 14 + .../infinispan/client/CacheInvalidate.java | 3 + .../infinispan/client/CacheInvalidateAll.java | 3 + .../infinispan/client/CacheResult.java | 3 + extensions/pom.xml | 1 + integration-tests/infinispan-cache/pom.xml | 144 +++++++ .../cache/infinispan/ExpensiveResource.java | 53 +++ .../cache/infinispan/RestClientResource.java | 88 +++++ .../cache/infinispan/SunriseRestClient.java | 52 +++ .../infinispan/SunriseRestServerResource.java | 41 ++ .../src/main/resources/application.properties | 1 + .../quarkus/it/cache/infinispan/CacheIT.java | 7 + .../it/cache/infinispan/CacheTest.java | 33 ++ .../InfinspanCacheClientTestCase.java | 87 +++++ integration-tests/pom.xml | 1 + 37 files changed, 1789 insertions(+), 27 deletions(-) create mode 100644 docs/src/main/asciidoc/cache-infinispan-reference.adoc create mode 100644 extensions/infinispan-cache/deployment/pom.xml create mode 100644 extensions/infinispan-cache/deployment/src/main/java/io/quarkus/cache/infinispan/deployment/InfinispanCacheProcessor.java create mode 100644 extensions/infinispan-cache/deployment/src/main/resources/application.properties create mode 100644 extensions/infinispan-cache/deployment/src/test/java/io/quarkus/cache/infinispan/InfinispanCacheTest.java create mode 100644 extensions/infinispan-cache/deployment/src/test/resources/empty-application-infinispan-client.properties create mode 100644 extensions/infinispan-cache/pom.xml create mode 100644 extensions/infinispan-cache/runtime/pom.xml create mode 100644 extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/CompositeCacheKeyMarshaller.java create mode 100644 extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/CompositeKeyMarshallerBean.java create mode 100644 extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheBuildRecorder.java create mode 100644 extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheImpl.java create mode 100644 extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheInfo.java create mode 100644 extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheInfoBuilder.java create mode 100644 extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheRuntimeConfig.java create mode 100644 extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCachesBuildTimeConfig.java create mode 100644 extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCachesConfig.java create mode 100644 extensions/infinispan-cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 integration-tests/infinispan-cache/pom.xml create mode 100644 integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/ExpensiveResource.java create mode 100644 integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/RestClientResource.java create mode 100644 integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestClient.java create mode 100644 integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestServerResource.java create mode 100644 integration-tests/infinispan-cache/src/main/resources/application.properties create mode 100644 integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheIT.java create mode 100644 integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheTest.java create mode 100644 integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/InfinspanCacheClientTestCase.java diff --git a/.github/native-tests.json b/.github/native-tests.json index dbe32b8def327..8885824d48d51 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -86,8 +86,8 @@ }, { "category": "Cache", - "timeout": 65, - "test-modules": "infinispan-cache-jpa, infinispan-client, cache, redis-cache", + "timeout": 75, + "test-modules": "infinispan-cache-jpa, infinispan-client, cache, redis-cache, infinispan-cache", "os-name": "ubuntu-latest" }, { diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 382ec161b8619..9d9b7e1efc987 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1265,6 +1265,16 @@ quarkus-infinispan-client-deployment ${project.version} + + io.quarkus + quarkus-infinispan-cache + ${project.version} + + + io.quarkus + quarkus-infinispan-cache-deployment + ${project.version} + io.quarkus quarkus-jdbc-db2 diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 426891e36c7da..22bf08a97b911 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -889,6 +889,19 @@ + + io.quarkus + quarkus-infinispan-cache + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-infinispan-client diff --git a/docs/pom.xml b/docs/pom.xml index e70a317eb71e5..0c68d57da90a6 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -905,6 +905,19 @@ + + io.quarkus + quarkus-infinispan-cache-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-infinispan-client-deployment diff --git a/docs/src/main/asciidoc/cache-infinispan-reference.adoc b/docs/src/main/asciidoc/cache-infinispan-reference.adoc new file mode 100644 index 0000000000000..4b62a29739d54 --- /dev/null +++ b/docs/src/main/asciidoc/cache-infinispan-reference.adoc @@ -0,0 +1,138 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Infinispan Cache +:extension-status: preview +include::_attributes.adoc[] +:categories: data +:summary: Use Infinispan as the Quarkus cache backend +:topics: infinispan,cache,data +:extensions: io.quarkus:quarkus-infinispan-cache,io.quarkus:quarkus-infinispan-client + +By default, Quarkus Cache uses Caffeine as backend. +It's possible to use Infinispan instead. + +include::{includes}/extension-status.adoc[] + +== Infinispan as cache backend + +When using Infinispan as the backend for Quarkus cache, each cached item will be stored in Infinispan: + +- The backend uses the __ Infinispan client (unless configured differently), so ensure its configuration is +set up accordingly (or use the xref:infinispan-dev-services.adoc[Infinispan Dev Service]) +- Both the key and the value are marshalled using Protobuf with Protostream. + +== Use the Infinispan backend + +First, add the `quarkus-infinispan-cache` extension to your project: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + quarkus-infinispan-cache + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:quarkus-infinispan-cache") +---- + +Then, use the `@CacheResult` and other cache annotations as detailed in the xref:cache.adoc[Quarkus Cache guide]: + +[source, java] +---- +@GET +@Path("/{keyElement1}/{keyElement2}/{keyElement3}") +@CacheResult(cacheName = "expensiveResourceCache") +public ExpensiveResponse getExpensiveResponse(@PathParam("keyElement1") @CacheKey String keyElement1, + @PathParam("keyElement2") @CacheKey String keyElement2, @PathParam("keyElement3") @CacheKey String keyElement3, + @QueryParam("foo") String foo) { + invocations.incrementAndGet(); + ExpensiveResponse response = new ExpensiveResponse(); + response.setResult(keyElement1 + " " + keyElement2 + " " + keyElement3 + " too!"); + return response; +} + +@POST +@CacheInvalidateAll(cacheName = "expensiveResourceCache") +public void invalidateAll() { + +} +---- + +[[infinispan-cache-configuration-reference]] +== Configure the Infinispan backend + +The Infinispan backend uses the `` Infinispan client. +Refer to the xref:infinispan-client-reference.adoc[Infinispan reference] for configuring the access to Infinispan. + +TIP: In dev mode, you can use the xref:infinispan-dev-services.adoc[Infinispan Dev Service]. + +If you want to use another Infinispan for your cache, configure the `client-name` as follows: + +[source, properties] +---- +quarkus.cache.infinispan.client-name=another +---- + +== Marshalling + +When interacting with Infinispan in Quarkus, you can easily marshal and unmarshal +Java simple types when writing to or reading from the cache. However, when dealing +with Plain Old Java Objects (POJOs), users of Infinispan need to provide the marshalling +schema. + +[source, java] +---- +@Proto +public record ExpensiveResponse(String result) { +} + +@ProtoSchema(includeClasses = { ExpensiveResponse.class }) +interface Schema extends GeneratedSchema { +} +---- + +Read more about it in the xref:infinispan-client-reference.adoc[Infinispan reference] in the Annotation +based serialization section. + +== Expiration + +You have the option to configure two properties for data expiration: *lifespan* and *max-idle*. + +=== Lifespan + +In Infinispan, *lifespan* refers to a configuration parameter that determines the maximum time an +entry (or an object) can remain in the cache since it was created or last accessed before it is +considered expired and removed from the cache. + +When you configure the *lifespan* parameter for entries in an Infinispan cache, +you specify a time duration. After an entry has been added to the cache or accessed +(read or written), it starts its lifespan countdown. If the time since the entry +was created or last accessed exceeds the specified "lifespan" duration, the entry +is considered expired and becomes eligible for eviction from the cache. + +[source, properties] +---- +quarkus.cache.infinispan.my-cache.lifespan=10s +---- + +=== Max Idle +When you configure the *max-idle* parameter for entries in an Infinispan cache, you specify a time +duration. After an entry has been accessed (read or written) in the cache, if there are no subsequent +accesses to that entry within the specified duration, it is considered idle. Once the idle time +exceeds the *max-idle* duration, the entry is considered expired and eligible for eviction from +the cache. + +[source, properties] +---- +quarkus.cache.infinispan.my-cache.max-idle=100s +---- + +include::{generated-dir}/config/quarkus-cache-infinispan.adoc[opts=optional, leveloffset=+1] \ No newline at end of file diff --git a/docs/src/main/asciidoc/infinispan-client-reference.adoc b/docs/src/main/asciidoc/infinispan-client-reference.adoc index 665217686267d..8e00c704a1b28 100644 --- a/docs/src/main/asciidoc/infinispan-client-reference.adoc +++ b/docs/src/main/asciidoc/infinispan-client-reference.adoc @@ -318,36 +318,19 @@ some additional steps that are detailed here. Let's say we have the following us .Author.java [source,java] ---- -public class Author { - private final String name; - private final String surname; - - public Author(String name, String surname) { - this.name = Objects.requireNonNull(name); - this.surname = Objects.requireNonNull(surname); - } - // Getter/Setter/equals/hashCode/toString omitted +public record Author(String name, String surname) { } ---- .Book.java [source,java] ---- -public class Book { - private final String title; - private final String description; - private final int publicationYear; - private final Set authors; - private final BigDecimal price; - - public Book(String title, String description, int publicationYear, Set authors, BigDecimal price) { - this.title = Objects.requireNonNull(title); - this.description = Objects.requireNonNull(description); - this.publicationYear = publicationYear; - this.authors = Objects.requireNonNull(authors); - this.price = price; - } - // Getter/Setter/equals/hashCode/toString omitted +public record Book(String title, + String description, + int publicationYear, + Set authors, + Type bookType, + BigDecimal price) { } ---- @@ -711,7 +694,15 @@ https://infinispan.org/docs/stable/titles/rest/rest.html#rest_v2_protobuf_schema https://infinispan.org/docs/stable/titles/encoding/encoding.html#registering-sci-remote-caches_marshalling[Hot Rod Java Client]. [[infinispan-annotations-api]] -== Caching using annotations +=== Caching using annotations + +[IMPORTANT] +==== +Infinispan Caching annotations are deprecated *in this extension* and will be removed. +Use or replace your annotations by using the xref:cache-infinispan-reference.adoc[Infinispan Cache extension]. +Update your import statements to use the annotations from `io.quarkus.cache` package instead of +`io.quarkus.infinispan.client`. +==== The Infinispan Client extension offers a set of annotations that can be used in a CDI managed bean to enable caching abilities with Infinispan. diff --git a/extensions/infinispan-cache/deployment/pom.xml b/extensions/infinispan-cache/deployment/pom.xml new file mode 100644 index 0000000000000..9577d042bdd15 --- /dev/null +++ b/extensions/infinispan-cache/deployment/pom.xml @@ -0,0 +1,94 @@ + + + + quarkus-infinispan-cache-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-infinispan-cache-deployment + Quarkus - Infinispan - Cache - Deployment + + + + io.quarkus + quarkus-infinispan-client-deployment + + + io.quarkus + quarkus-cache-deployment + + + io.quarkus + quarkus-infinispan-cache + + + io.quarkus + quarkus-junit5-internal + test + + + org.assertj + assertj-core + test + + + io.rest-assured + rest-assured + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + maven-surefire-plugin + + true + + + + + + + + test-infinispan + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + false + + + + + + + \ No newline at end of file diff --git a/extensions/infinispan-cache/deployment/src/main/java/io/quarkus/cache/infinispan/deployment/InfinispanCacheProcessor.java b/extensions/infinispan-cache/deployment/src/main/java/io/quarkus/cache/infinispan/deployment/InfinispanCacheProcessor.java new file mode 100644 index 0000000000000..a51cbb19cf5fd --- /dev/null +++ b/extensions/infinispan-cache/deployment/src/main/java/io/quarkus/cache/infinispan/deployment/InfinispanCacheProcessor.java @@ -0,0 +1,52 @@ +package io.quarkus.cache.infinispan.deployment; + +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; + +import org.infinispan.client.hotrod.RemoteCacheManager; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.cache.CompositeCacheKey; +import io.quarkus.cache.deployment.spi.CacheManagerInfoBuildItem; +import io.quarkus.cache.infinispan.runtime.CompositeKeyMarshallerBean; +import io.quarkus.cache.infinispan.runtime.InfinispanCacheBuildRecorder; +import io.quarkus.cache.infinispan.runtime.InfinispanCachesBuildTimeConfig; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.infinispan.client.deployment.InfinispanClientNameBuildItem; +import io.quarkus.infinispan.client.runtime.InfinispanClientUtil; + +public class InfinispanCacheProcessor { + + @BuildStep + @Record(RUNTIME_INIT) + CacheManagerInfoBuildItem cacheManagerInfo(BuildProducer syntheticBeanBuildItemBuildProducer, + InfinispanCacheBuildRecorder recorder) { + return new CacheManagerInfoBuildItem(recorder.getCacheManagerSupplier()); + } + + @BuildStep + void ensureAdditionalBeans(BuildProducer additionalBeans) { + additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CompositeKeyMarshallerBean.class)); + } + + @BuildStep + UnremovableBeanBuildItem ensureBeanLookupAvailable() { + return UnremovableBeanBuildItem.beanTypes(RemoteCacheManager.class); + } + + @BuildStep + InfinispanClientNameBuildItem requestedInfinispanClientBuildItem(InfinispanCachesBuildTimeConfig buildConfig) { + return new InfinispanClientNameBuildItem( + buildConfig.clientName.orElse(InfinispanClientUtil.DEFAULT_INFINISPAN_CLIENT_NAME)); + } + + @BuildStep + void nativeImage(BuildProducer producer) { + producer.produce(ReflectiveClassBuildItem.builder(CompositeCacheKey.class).methods(true).build()); + } + +} diff --git a/extensions/infinispan-cache/deployment/src/main/resources/application.properties b/extensions/infinispan-cache/deployment/src/main/resources/application.properties new file mode 100644 index 0000000000000..a01fe24813cbd --- /dev/null +++ b/extensions/infinispan-cache/deployment/src/main/resources/application.properties @@ -0,0 +1,2 @@ +# To override "quarkus.cache.type" io.quarkus.cache.runtime.CacheBuildConfig#type() +quarkus.cache.type=infinispan diff --git a/extensions/infinispan-cache/deployment/src/test/java/io/quarkus/cache/infinispan/InfinispanCacheTest.java b/extensions/infinispan-cache/deployment/src/test/java/io/quarkus/cache/infinispan/InfinispanCacheTest.java new file mode 100644 index 0000000000000..0fe55a3210876 --- /dev/null +++ b/extensions/infinispan-cache/deployment/src/test/java/io/quarkus/cache/infinispan/InfinispanCacheTest.java @@ -0,0 +1,355 @@ +package io.quarkus.cache.infinispan; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.inject.Inject; + +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.commons.jdkspecific.ThreadCreator; +import org.infinispan.commons.util.NullValue; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.cache.Cache; +import io.quarkus.cache.CompositeCacheKey; +import io.quarkus.cache.infinispan.runtime.InfinispanCacheImpl; +import io.quarkus.cache.infinispan.runtime.InfinispanCacheInfo; +import io.quarkus.infinispan.client.Remote; +import io.quarkus.logging.Log; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Uni; + +public class InfinispanCacheTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .withConfigurationResource("empty-application-infinispan-client.properties"); + + private static final String CACHE_NAME = "cache"; + + private static final ThreadFactory defaultThreadFactory = getTestThreadFactory("ForkThread"); + private static final ExecutorService testExecutor = ThreadCreator.createBlockingExecutorService() + .orElseGet(() -> new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue<>(), + defaultThreadFactory)); + @Inject + @Remote(CACHE_NAME) + RemoteCache remoteCache; + + @BeforeEach + void clear() { + try { + remoteCache.clear(); + } catch (Exception ignored) { + // ignored. + } + } + + @AfterAll + public static void shutdown() { + testExecutor.shutdown(); + } + + @Test + public void testGetName() { + Cache cache = getCache(); + assertThat(cache.getName()).isEqualTo(CACHE_NAME); + } + + @Test + public void testGetDefaultKey() { + Cache cache = getCache(); + assertThat(cache.getDefaultKey()).isEqualTo("default-key"); + } + + @Test + public void testGetWithLifespan() throws Exception { + Cache cache = getCache(2, -1); + String id = generateId(); + String value = awaitUni(cache.get(id, key -> "one")); + assertThat(value).isEqualTo("one"); + value = awaitUni(cache.get(id, key -> "two")); + assertThat(value).isEqualTo("one"); + assertThat(remoteCache.get(id)).isEqualTo("one"); + // Wait lifespan expiration + await().atMost(Duration.ofSeconds(3)).until(() -> remoteCache.get(id) == null); + // key has expired + assertThat(remoteCache.get(id)).isNull(); + value = awaitUni(cache.get(id, key -> "two")); + assertThat(value).isEqualTo("two"); + assertThat(remoteCache.get(id)).isEqualTo("two"); + // Wait lifespan expiration + await().atMost(Duration.ofSeconds(3)).until(() -> remoteCache.get(id) == null); + assertThat(remoteCache.get(id)).isNull(); + } + + @Test + public void testGetWithWithMaxidle() { + Cache cache = getCache(-1, 3); + String id = generateId(); + String value = awaitUni(cache.get(id, key -> "one")); + assertThat(value).isEqualTo("one"); + value = awaitUni(cache.get(id, key -> "two")); + assertThat(value).isEqualTo("one"); + assertThat(remoteCache.get(id)).isEqualTo("one"); + // Wait maxidle expiration + await().pollDelay(Duration.ofSeconds(3)).untilAsserted(() -> assertThat(true).isTrue()); + // key has expired + assertThat(remoteCache.get(id)).isNull(); + value = awaitUni(cache.get(id, key -> "two")); + assertThat(value).isEqualTo("two"); + assertThat(remoteCache.get(id)).isEqualTo("two"); + // Wait maxidle expiration + await().pollDelay(Duration.ofSeconds(3)).untilAsserted(() -> assertThat(true).isTrue()); + assertThat(remoteCache.get(id)).isNull(); + } + + @Test + public void testGetWithNullValues() { + Cache cache = getCache(); + String id = generateId(); + String value = awaitUni(cache.get(id, key -> null)); + assertThat(value).isEqualTo(null); + assertThat(remoteCache.get(id)).isEqualTo(NullValue.NULL); + } + + protected Future fork(Callable c) { + return testExecutor.submit(new CallableWrapper<>(c)); + } + + private static class CallableWrapper implements Callable { + private final Callable c; + + CallableWrapper(Callable c) { + this.c = c; + } + + @Override + public T call() throws Exception { + try { + Log.trace("Started fork callable.."); + T result = c.call(); + Log.debug("Exiting fork callable."); + return result; + } catch (Exception e) { + Log.warn("Exiting fork callable due to exception", e); + throw e; + } + } + } + + protected static ThreadFactory getTestThreadFactory(final String prefix) { + final String className = InfinispanCacheTest.class.getSimpleName(); + + return new ThreadFactory() { + private final AtomicInteger counter = new AtomicInteger(0); + + @Override + public Thread newThread(Runnable r) { + String threadName = prefix + "-" + counter.incrementAndGet() + "," + className; + return new Thread(r, threadName); + } + }; + } + + @Test + public void testGetWithParallelCalls() throws Exception { + CyclicBarrier barrier = new CyclicBarrier(2); + Cache cache = getCache(); + String id = generateId(); + Future thread1 = fork(() -> cache.get(id, key -> { + try { + // In order to avoid it to be a flaky test, first call is to make sure we are inside the lambda. + // The second to wait inside the lambda until we issue the second request on line 193 + barrier.await(10, TimeUnit.SECONDS); + barrier.await(10, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + return "thread1"; + }).await().atMost(Duration.ofSeconds(10))); + + // Ensure first retrieval is in lambda before continuing + barrier.await(10, TimeUnit.SECONDS); + + Future thread2 = fork(() -> cache.get(id, key -> "thread2").await().atMost(Duration.ofSeconds(10))); + + barrier.await(1, TimeUnit.SECONDS); + + String valueObtainedByThread1 = thread1.get(10, TimeUnit.SECONDS); + String valueObtainedByThread2 = thread2.get(10, TimeUnit.SECONDS); + + assertThat(remoteCache.get(id)).isEqualTo("thread1"); + assertThat(valueObtainedByThread1).isEqualTo("thread1"); + assertThat(valueObtainedByThread2).isEqualTo("thread1"); + } + + @Test + public void testGetAsyncWithLifespan() { + Cache cache = getCache(2, -1); + String id = generateId(); + String value = awaitUni(cache.getAsync(id, key -> Uni.createFrom().item("one"))); + assertThat(value).isEqualTo("one"); + value = awaitUni(cache.getAsync(id, key -> Uni.createFrom().item("two"))); + assertThat(value).isEqualTo("one"); + assertThat(remoteCache.get(id)).isEqualTo("one"); + // Wait lifespan expiration + await().atMost(Duration.ofSeconds(3)).until(() -> remoteCache.get(id) == null); + // key has expired + assertThat(remoteCache.get(id)).isNull(); + value = awaitUni(cache.getAsync(id, key -> Uni.createFrom().item("two"))); + assertThat(value).isEqualTo("two"); + assertThat(remoteCache.get(id)).isEqualTo("two"); + // Wait lifespan expiration + await().atMost(Duration.ofSeconds(3)).until(() -> remoteCache.get(id) == null); + assertThat(remoteCache.get(id)).isNull(); + } + + @Test + public void testGetAsyncWithWithMaxidle() { + Cache cache = getCache(-1, 3); + String id = generateId(); + String value = awaitUni(cache.getAsync(id, key -> Uni.createFrom().item("one"))); + assertThat(value).isEqualTo("one"); + value = awaitUni(cache.getAsync(id, key -> Uni.createFrom().item("two"))); + assertThat(value).isEqualTo("one"); + assertThat(remoteCache.get(id)).isEqualTo("one"); + // Wait maxidle expiration + await().pollDelay(Duration.ofSeconds(3)).untilAsserted(() -> assertThat(true).isTrue()); + // key has expired + assertThat(remoteCache.get(id)).isNull(); + value = awaitUni(cache.getAsync(id, key -> Uni.createFrom().item("two"))); + assertThat(value).isEqualTo("two"); + assertThat(remoteCache.get(id)).isEqualTo("two"); + // Wait maxidle expiration + await().pollDelay(Duration.ofSeconds(3)).untilAsserted(() -> assertThat(true).isTrue()); + assertThat(remoteCache.get(id)).isNull(); + } + + @Test + public void testGetAsyncWithNullValues() { + Cache cache = getCache(); + String id = generateId(); + String value = awaitUni(cache.getAsync(id, key -> Uni.createFrom().nullItem())); + assertThat(value).isEqualTo(null); + assertThat(remoteCache.get(id)).isEqualTo(NullValue.NULL); + } + + @Test + public void testGetAsyncWithParallelCalls() throws Exception { + CyclicBarrier barrier = new CyclicBarrier(2); + Cache cache = getCache(); + String id = generateId(); + Future> thread1 = fork(() -> cache.getAsync(id, key -> { + try { + barrier.await(10, TimeUnit.SECONDS); + barrier.await(10, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException(e); + } + return Uni.createFrom().item("thread1"); + })); + + // Ensure first retrieval is in lambda before continuing + barrier.await(10, TimeUnit.SECONDS); + + Future> thread2 = fork(() -> cache.getAsync(id, key -> Uni.createFrom().item("thread2"))); + + barrier.await(1, TimeUnit.SECONDS); + + String valueObtainedByThread1 = awaitUni(thread1.get(10, TimeUnit.SECONDS)); + String valueObtainedByThread2 = awaitUni(thread2.get(10, TimeUnit.SECONDS)); + + assertThat(remoteCache.get(id)).isEqualTo("thread1"); + assertThat(valueObtainedByThread1).isEqualTo("thread1"); + assertThat(valueObtainedByThread2).isEqualTo("thread1"); + } + + @Test + public void testInvalidate() { + Cache cache = getCache(); + String id = generateId(); + awaitUni(cache.get(id, key -> "value")); + assertThat(remoteCache.size()).isOne(); + assertThat(remoteCache.get(id)).isEqualTo("value"); + awaitUni(cache.invalidate(id)); + assertThat(remoteCache.size()).isZero(); + } + + @Test + public void testInvalidateIf() { + Cache cache = getCache(); + String id1 = generateId(); + String id2 = generateId(); + awaitUni(cache.get(id1, key -> "value")); + awaitUni(cache.get(id2, key -> null)); + assertThat(remoteCache.get(id1)).isEqualTo("value"); + assertThat(remoteCache.get(id2)).isEqualTo(NullValue.NULL); + + awaitUni(cache.invalidateIf(k -> k.equals(id2))); + + assertThat(remoteCache.containsKey(id1)).isTrue(); + assertThat(remoteCache.containsKey(id2)).isFalse(); + } + + @Test + public void testInvalidateAll() { + Cache cache = getCache(); + for (int i = 0; i < 10; i++) { + awaitUni(cache.get(generateId(), key -> "value")); + } + assertThat(remoteCache.size()).isEqualTo(10); + awaitUni(cache.invalidateAll()); + assertThat(remoteCache.size()).isZero(); + } + + @Test + public void testGetWithCompositeCacheKey() { + Cache cache = getCache(); + CompositeCacheKey compositeId = new CompositeCacheKey("id1", "id2"); + awaitUni(cache.get(compositeId, key -> "value")); + assertThat(remoteCache.get(compositeId)).isEqualTo("value"); + } + + private static String generateId() { + return UUID.randomUUID().toString(); + } + + private Cache getCache() { + InfinispanCacheInfo info = new InfinispanCacheInfo(); + info.name = CACHE_NAME; + info.lifespan = Optional.empty(); + info.maxIdle = Optional.empty(); + return new InfinispanCacheImpl(info, remoteCache); + } + + private Cache getCache(int lifespan, int maxidle) { + InfinispanCacheInfo info = new InfinispanCacheInfo(); + info.name = CACHE_NAME; + info.lifespan = Optional.of(Duration.ofSeconds(lifespan)); + info.maxIdle = Optional.of(Duration.ofSeconds(maxidle)); + return new InfinispanCacheImpl(info, remoteCache); + } + + private static T awaitUni(Uni uni) { + return uni.await().atMost(Duration.ofSeconds(10)); + } +} diff --git a/extensions/infinispan-cache/deployment/src/test/resources/empty-application-infinispan-client.properties b/extensions/infinispan-cache/deployment/src/test/resources/empty-application-infinispan-client.properties new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/extensions/infinispan-cache/pom.xml b/extensions/infinispan-cache/pom.xml new file mode 100644 index 0000000000000..f49b737d37859 --- /dev/null +++ b/extensions/infinispan-cache/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-infinispan-cache-parent + Quarkus - Infinispan - Cache + pom + + deployment + runtime + + diff --git a/extensions/infinispan-cache/runtime/pom.xml b/extensions/infinispan-cache/runtime/pom.xml new file mode 100644 index 0000000000000..5ae742d0b1b13 --- /dev/null +++ b/extensions/infinispan-cache/runtime/pom.xml @@ -0,0 +1,79 @@ + + + + quarkus-infinispan-cache-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-infinispan-cache + Quarkus - Infinispan - Cache - Runtime + Implements quarkus-cache SPI + + + io.quarkus + quarkus-infinispan-client + + + io.quarkus + quarkus-cache + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + maven-surefire-plugin + + true + + + + + + + + test-infinispan + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + false + + + + + + + diff --git a/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/CompositeCacheKeyMarshaller.java b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/CompositeCacheKeyMarshaller.java new file mode 100644 index 0000000000000..1a0d7e0929c0f --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/CompositeCacheKeyMarshaller.java @@ -0,0 +1,47 @@ +package io.quarkus.cache.infinispan.runtime; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.infinispan.protostream.MessageMarshaller; +import org.infinispan.protostream.WrappedMessage; + +import io.quarkus.cache.CompositeCacheKey; + +/** + * {@link CompositeCacheKey } protostream marshaller class + */ +public class CompositeCacheKeyMarshaller implements MessageMarshaller { + public static final String PACKAGE = "io.quarkus.cache.infinispan.internal"; + public static final String NAME = "CompositeCacheKey"; + public static final String FULL_NAME = PACKAGE + "." + NAME; + public static final String KEYS = "keys"; + + @Override + public CompositeCacheKey readFrom(ProtoStreamReader reader) throws IOException { + Object[] compositeKeys = reader.readCollection(KEYS, new ArrayList<>(), WrappedMessage.class).stream() + .map(we -> we.getValue()).collect(Collectors.toList()).toArray(); + return new CompositeCacheKey(compositeKeys); + } + + @Override + public void writeTo(ProtoStreamWriter writer, CompositeCacheKey compositeCacheKey) throws IOException { + List wrappedMessages = Arrays.stream(compositeCacheKey.getKeyElements()) + .map(e -> new WrappedMessage(e)) + .collect(Collectors.toList()); + writer.writeCollection(KEYS, wrappedMessages, WrappedMessage.class); + } + + @Override + public Class getJavaClass() { + return CompositeCacheKey.class; + } + + @Override + public String getTypeName() { + return FULL_NAME; + } +} diff --git a/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/CompositeKeyMarshallerBean.java b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/CompositeKeyMarshallerBean.java new file mode 100644 index 0000000000000..9e979d37b82a2 --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/CompositeKeyMarshallerBean.java @@ -0,0 +1,30 @@ +package io.quarkus.cache.infinispan.runtime; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +import org.infinispan.protostream.MessageMarshaller; +import org.infinispan.protostream.schema.Schema; +import org.infinispan.protostream.schema.Type; + +/** + * Produces the schema marshaller and protoschema to marshall {@link io.quarkus.cache.CompositeCacheKey} + */ +@ApplicationScoped +public class CompositeKeyMarshallerBean { + + @Produces + public Schema compositeKeySchema() { + return new Schema.Builder("io.quarkus.cache.infinispan.internal.cache.proto") + .packageName(CompositeCacheKeyMarshaller.PACKAGE) + .addImport("org/infinispan/protostream/message-wrapping.proto") + .addMessage(CompositeCacheKeyMarshaller.NAME) + .addRepeatedField(Type.create("org.infinispan.protostream.WrappedMessage"), CompositeCacheKeyMarshaller.KEYS, 1) + .build(); + } + + @Produces + public MessageMarshaller compositeKeyMarshaller() { + return new CompositeCacheKeyMarshaller(); + } +} diff --git a/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheBuildRecorder.java b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheBuildRecorder.java new file mode 100644 index 0000000000000..19ad5ace35291 --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheBuildRecorder.java @@ -0,0 +1,70 @@ +package io.quarkus.cache.infinispan.runtime; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.jboss.logging.Logger; + +import io.quarkus.cache.Cache; +import io.quarkus.cache.CacheManager; +import io.quarkus.cache.CacheManagerInfo; +import io.quarkus.cache.runtime.CacheManagerImpl; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class InfinispanCacheBuildRecorder { + + private static final Logger LOGGER = Logger.getLogger(InfinispanCacheBuildRecorder.class); + + private final InfinispanCachesBuildTimeConfig buildConfig; + private final RuntimeValue infinispanCacheConfigRV; + + public InfinispanCacheBuildRecorder(InfinispanCachesBuildTimeConfig buildConfig, + RuntimeValue infinispanCacheConfigRV) { + this.buildConfig = buildConfig; + this.infinispanCacheConfigRV = infinispanCacheConfigRV; + } + + public CacheManagerInfo getCacheManagerSupplier() { + return new CacheManagerInfo() { + @Override + public boolean supports(Context context) { + return context.cacheEnabled() && "infinispan".equals(context.cacheType()); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Supplier get(Context context) { + return new Supplier() { + @Override + public CacheManager get() { + Set cacheInfos = InfinispanCacheInfoBuilder.build(context.cacheNames(), + buildConfig, + infinispanCacheConfigRV.getValue()); + if (cacheInfos.isEmpty()) { + return new CacheManagerImpl(Collections.emptyMap()); + } else { + // The number of caches is known at build time so we can use fixed initialCapacity and loadFactor for the caches map. + Map caches = new HashMap<>(cacheInfos.size() + 1, 1.0F); + for (InfinispanCacheInfo cacheInfo : cacheInfos) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf( + "Building Infinispan cache [%s] with [lifespan=%s], [maxIdle=%s]", + cacheInfo.name, cacheInfo.lifespan, cacheInfo.maxIdle); + } + + InfinispanCacheImpl cache = new InfinispanCacheImpl(cacheInfo, buildConfig.clientName); + caches.put(cacheInfo.name, cache); + } + return new CacheManagerImpl(caches); + } + } + }; + } + }; + } +} diff --git a/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheImpl.java b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheImpl.java new file mode 100644 index 0000000000000..a92f6f4e70fd7 --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheImpl.java @@ -0,0 +1,181 @@ +package io.quarkus.cache.infinispan.runtime; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Flow; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.impl.protocol.Codec27; +import org.infinispan.commons.util.NullValue; +import org.infinispan.commons.util.concurrent.CompletionStages; +import org.reactivestreams.FlowAdapters; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.cache.Cache; +import io.quarkus.cache.runtime.AbstractCache; +import io.quarkus.infinispan.client.runtime.InfinispanClientProducer; +import io.quarkus.infinispan.client.runtime.InfinispanClientUtil; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +/** + * This class is an internal Quarkus cache implementation using Infinispan. + * Do not use it explicitly from your Quarkus application. + */ +public class InfinispanCacheImpl extends AbstractCache implements Cache { + + private final RemoteCache remoteCache; + private final InfinispanCacheInfo cacheInfo; + private final Map computationResults = new ConcurrentHashMap<>(); + private final long lifespan; + private final long maxIdle; + + public InfinispanCacheImpl(InfinispanCacheInfo cacheInfo, RemoteCache remoteCache) { + this.cacheInfo = cacheInfo; + this.remoteCache = remoteCache; + this.lifespan = cacheInfo.lifespan.map(l -> l.toMillis()).orElse(-1L); + this.maxIdle = cacheInfo.maxIdle.map(m -> m.toMillis()).orElse(-1L); + } + + public InfinispanCacheImpl(InfinispanCacheInfo cacheInfo, + Optional infinispanClientName) { + this(cacheInfo, determineInfinispanClient(infinispanClientName, cacheInfo.name)); + } + + private static RemoteCache determineInfinispanClient(Optional infinispanCacheName, String cacheName) { + ArcContainer container = Arc.container(); + InfinispanClientProducer producer = container.instance(InfinispanClientProducer.class).get(); + return producer.getRemoteCache(infinispanCacheName.orElse(InfinispanClientUtil.DEFAULT_INFINISPAN_CLIENT_NAME), + cacheName); + } + + @Override + public String getName() { + return Objects.requireNonNullElse(cacheInfo.name, "default-infinispan-cache"); + } + + @Override + public Object getDefaultKey() { + return "default-key"; + } + + private Object encodeNull(Object value) { + return value != null ? value : NullValue.NULL; + } + + private T decodeNull(Object value) { + return value != NullValue.NULL ? (T) value : null; + } + + @Override + public Uni get(K key, Function valueLoader) { + return Uni.createFrom() + .completionStage(() -> CompletionStages.handleAndCompose(remoteCache.getAsync(key), (v1, ex1) -> { + if (ex1 != null) { + return CompletableFuture.failedFuture(ex1); + } + + if (v1 != null) { + return CompletableFuture.completedFuture(decodeNull(v1)); + } + + CompletableFuture resultAsync = new CompletableFuture<>(); + CompletableFuture computedValue = computationResults.putIfAbsent(key, resultAsync); + if (computedValue != null) { + return computedValue; + } + V newValue = valueLoader.apply(key); + remoteCache + .putIfAbsentAsync(key, encodeNull(newValue), lifespan, TimeUnit.MILLISECONDS, maxIdle, + TimeUnit.MILLISECONDS) + .whenComplete((existing, ex2) -> { + if (ex2 != null) { + resultAsync.completeExceptionally((Throwable) ex2); + } else if (existing == null) { + resultAsync.complete(newValue); + } else { + resultAsync.complete(decodeNull(existing)); + } + computationResults.remove(key); + }); + return resultAsync; + })); + } + + @Override + public Uni getAsync(K key, Function> valueLoader) { + return Uni.createFrom().completionStage(CompletionStages.handleAndCompose(remoteCache.getAsync(key), (v1, ex1) -> { + if (ex1 != null) { + return CompletableFuture.failedFuture(ex1); + } + + if (v1 != null) { + return CompletableFuture.completedFuture(decodeNull(v1)); + } + + CompletableFuture resultAsync = new CompletableFuture<>(); + CompletableFuture computedValue = computationResults.putIfAbsent(key, resultAsync); + if (computedValue != null) { + return computedValue; + } + valueLoader.apply(key).convert().toCompletionStage() + .whenComplete((newValue, ex2) -> { + if (ex2 != null) { + resultAsync.completeExceptionally(ex2); + computationResults.remove(key); + } else { + remoteCache.putIfAbsentAsync(key, encodeNull(newValue), lifespan, TimeUnit.MILLISECONDS, maxIdle, + TimeUnit.MILLISECONDS).whenComplete((existing, ex3) -> { + if (ex3 != null) { + resultAsync.completeExceptionally((Throwable) ex3); + } else if (existing == null) { + resultAsync.complete(newValue); + } else { + resultAsync.complete(decodeNull(existing)); + } + computationResults.remove(key); + }); + } + }); + return resultAsync; + })); + } + + @Override + public Uni invalidate(Object key) { + return Uni.createFrom().completionStage(() -> remoteCache.removeAsync(key)); + } + + @Override + public Uni invalidateAll() { + return Uni.createFrom().completionStage(() -> remoteCache.clearAsync()); + } + + @Override + public Uni invalidateIf(Predicate predicate) { + Flow.Publisher entriesPublisher = FlowAdapters + .toFlowPublisher(remoteCache.publishEntries(Codec27.EMPTY_VALUE_CONVERTER, null, null, 512)); + return Uni.createFrom().multi(Multi.createFrom().publisher(entriesPublisher) + .map(e -> ((Map.Entry) e).getKey()) + .filter(key -> predicate.test(key)) + .onItem() + .call(key -> Uni.createFrom().completionStage(remoteCache.removeAsync(key)))) + .replaceWithVoid(); + } + + @Override + public T as(Class type) { + if (type.getTypeName().equals(InfinispanCacheImpl.class.getTypeName())) { + return (T) this; + } + + throw new IllegalArgumentException("Class type not supported : " + type); + } +} diff --git a/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheInfo.java b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheInfo.java new file mode 100644 index 0000000000000..60c936de92969 --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheInfo.java @@ -0,0 +1,23 @@ +package io.quarkus.cache.infinispan.runtime; + +import java.time.Duration; +import java.util.Optional; + +public class InfinispanCacheInfo { + + /** + * The cache name + */ + public String name; + + /** + * The default lifespan of the item stored in the cache + */ + public Optional lifespan = Optional.empty(); + + /** + * The default max-idle of the item stored in the cache + */ + public Optional maxIdle = Optional.empty(); + +} diff --git a/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheInfoBuilder.java b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheInfoBuilder.java new file mode 100644 index 0000000000000..7519e082563a7 --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheInfoBuilder.java @@ -0,0 +1,42 @@ +package io.quarkus.cache.infinispan.runtime; + +import java.util.Collections; +import java.util.Set; + +import io.quarkus.runtime.configuration.HashSetFactory; + +public class InfinispanCacheInfoBuilder { + + public static Set build(Set cacheNames, InfinispanCachesBuildTimeConfig buildTimeConfig, + InfinispanCachesConfig runtimeConfig) { + if (cacheNames.isEmpty()) { + return Collections.emptySet(); + } else { + Set result = HashSetFactory. getInstance().apply(cacheNames.size()); + + for (String cacheName : cacheNames) { + + InfinispanCacheInfo cacheInfo = new InfinispanCacheInfo(); + cacheInfo.name = cacheName; + + InfinispanCacheRuntimeConfig defaultRuntimeConfig = runtimeConfig.defaultConfig; + InfinispanCacheRuntimeConfig namedRuntimeConfig = runtimeConfig.cachesConfig.get(cacheInfo.name); + + if (namedRuntimeConfig != null && namedRuntimeConfig.lifespan.isPresent()) { + cacheInfo.lifespan = namedRuntimeConfig.lifespan; + } else if (defaultRuntimeConfig.lifespan.isPresent()) { + cacheInfo.lifespan = defaultRuntimeConfig.lifespan; + } + + if (namedRuntimeConfig != null && namedRuntimeConfig.maxIdle.isPresent()) { + cacheInfo.maxIdle = namedRuntimeConfig.maxIdle; + } else if (defaultRuntimeConfig.maxIdle.isPresent()) { + cacheInfo.maxIdle = defaultRuntimeConfig.maxIdle; + } + + result.add(cacheInfo); + } + return result; + } + } +} diff --git a/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheRuntimeConfig.java b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheRuntimeConfig.java new file mode 100644 index 0000000000000..51b22ff1ed9c7 --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCacheRuntimeConfig.java @@ -0,0 +1,23 @@ +package io.quarkus.cache.infinispan.runtime; + +import java.time.Duration; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class InfinispanCacheRuntimeConfig { + /** + * The default lifespan of the item stored in the cache + */ + @ConfigItem + public Optional lifespan; + + /** + * The default max-idle of the item stored in the cache + */ + @ConfigItem + public Optional maxIdle; + +} diff --git a/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCachesBuildTimeConfig.java b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCachesBuildTimeConfig.java new file mode 100644 index 0000000000000..66596bc5170a6 --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCachesBuildTimeConfig.java @@ -0,0 +1,19 @@ +package io.quarkus.cache.infinispan.runtime; + +import static io.quarkus.runtime.annotations.ConfigPhase.BUILD_AND_RUN_TIME_FIXED; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = BUILD_AND_RUN_TIME_FIXED, name = "cache.infinispan") +public class InfinispanCachesBuildTimeConfig { + + /** + * The name of the named Infinispan client to be used for communicating with Infinispan. + * If not set, use the default Infinispan client. + */ + @ConfigItem + public Optional clientName; +} diff --git a/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCachesConfig.java b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCachesConfig.java new file mode 100644 index 0000000000000..c2533936392c8 --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/java/io/quarkus/cache/infinispan/runtime/InfinispanCachesConfig.java @@ -0,0 +1,29 @@ +package io.quarkus.cache.infinispan.runtime; + +import static io.quarkus.runtime.annotations.ConfigPhase.RUN_TIME; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = RUN_TIME, name = "cache.infinispan") +public class InfinispanCachesConfig { + + /** + * Default configuration applied to all Infinispan caches (lowest precedence) + */ + @ConfigItem(name = ConfigItem.PARENT) + public InfinispanCacheRuntimeConfig defaultConfig; + + /** + * Additional configuration applied to a specific Infinispan cache (highest precedence) + */ + @ConfigItem(name = ConfigItem.PARENT) + @ConfigDocMapKey("cache-name") + @ConfigDocSection + Map cachesConfig; + +} diff --git a/extensions/infinispan-cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/infinispan-cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..ba4cb54091c21 --- /dev/null +++ b/extensions/infinispan-cache/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,14 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Infinispan Cache" +metadata: + keywords: + - "infinispan" + - "cache" + guide: "https://quarkus.io/guides/cache-infinispan-reference" + categories: + - "data" + - "reactive" + status: "preview" + config: + - "quarkus.cache.infinispan" diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java index d3056b4ef2c6b..3cdfc250512c6 100644 --- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidate.java @@ -18,11 +18,14 @@ * This annotation can be combined with {@link CacheResult} annotation on a single method. Caching operations will always * be executed in the same order: {@link CacheInvalidateAll} first, then {@link CacheInvalidate} and finally * {@link CacheResult}. + * + * @deprecated Use Infinispan Cache Extension */ @InterceptorBinding @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Repeatable(List.class) +@Deprecated(forRemoval = true) public @interface CacheInvalidate { /** diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java index e25b9311ccd36..e8572eb1fda8e 100644 --- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheInvalidateAll.java @@ -18,11 +18,14 @@ * This annotation can be combined with {@link CacheResult} annotation on a single method. Caching operations will always * be executed in the same order: {@link CacheInvalidateAll} first, then {@link CacheInvalidate} and finally * {@link CacheResult}. + * + * @deprecated Use Infinispan Cache Extension */ @InterceptorBinding @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Repeatable(List.class) +@Deprecated(forRemoval = true) public @interface CacheInvalidateAll { /** diff --git a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java index 6c1ac7a1e5c8e..95970dd85cd26 100644 --- a/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java +++ b/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/CacheResult.java @@ -28,10 +28,13 @@ * annotations on a single method. Caching operations will always be executed in the same order: {@link CacheInvalidateAll} * first, then {@link CacheInvalidate} and finally {@link CacheResult}. *

+ * + * @deprecated Use Infinispan Cache Extension */ @InterceptorBinding @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) +@Deprecated(forRemoval = true) public @interface CacheResult { /** diff --git a/extensions/pom.xml b/extensions/pom.xml index ccd152cf03acc..aa266acf915e5 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -150,6 +150,7 @@ infinispan-client + infinispan-cache caffeine diff --git a/integration-tests/infinispan-cache/pom.xml b/integration-tests/infinispan-cache/pom.xml new file mode 100644 index 0000000000000..670df99e995ea --- /dev/null +++ b/integration-tests/infinispan-cache/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-integration-test-infinispan-cache + Quarkus - Integration Tests - Infinispan Cache + + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-rest-client-jackson + + + io.quarkus + quarkus-infinispan-cache + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + io.quarkus + quarkus-infinispan-cache-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-client-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/main/resources + true + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + + test-infinispan + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + false + + + + + + + + diff --git a/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/ExpensiveResource.java b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/ExpensiveResource.java new file mode 100644 index 0000000000000..326c01a3c7b45 --- /dev/null +++ b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/ExpensiveResource.java @@ -0,0 +1,53 @@ +package io.quarkus.it.cache.infinispan; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; + +import org.infinispan.protostream.GeneratedSchema; +import org.infinispan.protostream.annotations.Proto; +import org.infinispan.protostream.annotations.ProtoSchema; + +import io.quarkus.cache.CacheInvalidateAll; +import io.quarkus.cache.CacheKey; +import io.quarkus.cache.CacheResult; + +@Path("/expensive-resource") +public class ExpensiveResource { + + private final AtomicInteger invocations = new AtomicInteger(0); + + @GET + @Path("/{keyElement1}/{keyElement2}/{keyElement3}") + @CacheResult(cacheName = "expensiveResourceCache") + public ExpensiveResponse getExpensiveResponse(@PathParam("keyElement1") @CacheKey String keyElement1, + @PathParam("keyElement2") @CacheKey String keyElement2, @PathParam("keyElement3") @CacheKey String keyElement3, + @QueryParam("foo") String foo) { + invocations.incrementAndGet(); + return new ExpensiveResponse(keyElement1 + " " + keyElement2 + " " + keyElement3 + " too!"); + } + + @POST + @CacheInvalidateAll(cacheName = "expensiveResourceCache") + public void invalidateAll() { + + } + + @GET + @Path("/invocations") + public int getInvocations() { + return invocations.get(); + } + + @Proto + public record ExpensiveResponse(String result) { + } + + @ProtoSchema(includeClasses = { ExpensiveResponse.class }) + interface Schema extends GeneratedSchema { + } +} diff --git a/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/RestClientResource.java b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/RestClientResource.java new file mode 100644 index 0000000000000..09d8ea5978660 --- /dev/null +++ b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/RestClientResource.java @@ -0,0 +1,88 @@ +package io.quarkus.it.cache.infinispan; + +import java.util.Set; +import java.util.function.Function; + +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.HttpHeaders; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.resteasy.reactive.RestResponse; + +import io.quarkus.runtime.BlockingOperationControl; +import io.smallrye.mutiny.Uni; + +@Path("rest-client") +public class RestClientResource { + + @RestClient + SunriseRestClient sunriseRestClient; + + @Inject + HttpHeaders headers; // used in order to make sure that @RequestScoped beans continue to work despite the cache coming into play + + @GET + @Path("time/{city}") + public RestResponse getSunriseTime(@RestPath String city, @RestQuery String date) { + Set incomingHeadersBeforeRestCall = headers.getRequestHeaders().keySet(); + String restResponse = sunriseRestClient.getSunriseTime(city, date); + Set incomingHeadersAfterRestCall = headers.getRequestHeaders().keySet(); + return RestResponse.ResponseBuilder + .ok(restResponse) + .header("before", String.join(", ", incomingHeadersBeforeRestCall)) + .header("after", String.join(", ", incomingHeadersAfterRestCall)) + .header("blockingAllowed", BlockingOperationControl.isBlockingAllowed()) + .build(); + } + + @GET + @Path("async/time/{city}") + public Uni> getAsyncSunriseTime(@RestPath String city, @RestQuery String date) { + Set incomingHeadersBeforeRestCall = headers.getRequestHeaders().keySet(); + return sunriseRestClient.getAsyncSunriseTime(city, date).onItem().transform(new Function<>() { + @Override + public RestResponse apply(String restResponse) { + Set incomingHeadersAfterRestCall = headers.getRequestHeaders().keySet(); + return RestResponse.ResponseBuilder + .ok(restResponse) + .header("before", String.join(", ", incomingHeadersBeforeRestCall)) + .header("after", String.join(", ", incomingHeadersAfterRestCall)) + .header("blockingAllowed", BlockingOperationControl.isBlockingAllowed()) + .build(); + } + }); + } + + @GET + @Path("invocations") + public Integer getSunriseTimeInvocations() { + return sunriseRestClient.getSunriseTimeInvocations(); + } + + @DELETE + @Path("invalidate/{city}") + public Uni> invalidate(@RestPath String city, @RestQuery String notPartOfTheCacheKey, + @RestQuery String date) { + return sunriseRestClient.invalidate(city, notPartOfTheCacheKey, date).onItem().transform( + new Function<>() { + @Override + public RestResponse apply(Void unused) { + return RestResponse.ResponseBuilder. create(RestResponse.Status.NO_CONTENT) + .header("blockingAllowed", BlockingOperationControl.isBlockingAllowed()) + .header("incoming", String.join(", ", headers.getRequestHeaders().keySet())) + .build(); + } + }); + } + + @DELETE + @Path("invalidate") + public void invalidateAll() { + sunriseRestClient.invalidateAll(); + } +} diff --git a/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestClient.java b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestClient.java new file mode 100644 index 0000000000000..f8ee388c9c2ee --- /dev/null +++ b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestClient.java @@ -0,0 +1,52 @@ +package io.quarkus.it.cache.infinispan; + +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; + +import io.quarkus.cache.CacheInvalidate; +import io.quarkus.cache.CacheInvalidateAll; +import io.quarkus.cache.CacheKey; +import io.quarkus.cache.CacheResult; +import io.smallrye.mutiny.Uni; + +@RegisterRestClient +@Path("sunrise") +public interface SunriseRestClient { + + String CACHE_NAME = "sunrise-cache"; + + @GET + @Path("time/{city}") + @CacheResult(cacheName = CACHE_NAME) + String getSunriseTime(@RestPath String city, @RestQuery String date); + + @GET + @Path("time/{city}") + @CacheResult(cacheName = CACHE_NAME) + Uni getAsyncSunriseTime(@RestPath String city, @RestQuery String date); + + @GET + @Path("invocations") + Integer getSunriseTimeInvocations(); + + /* + * The following methods wouldn't make sense in a real-life application but it's not relevant here. We only need to check if + * the caching annotations work as intended with the rest-client extension. + */ + + @DELETE + @Path("invalidate/{city}") + @CacheInvalidate(cacheName = CACHE_NAME) + Uni invalidate(@CacheKey @RestPath String city, @RestQuery String notPartOfTheCacheKey, + @CacheKey @RestPath String date); + + @DELETE + @Path("invalidate") + @CacheInvalidateAll(cacheName = CACHE_NAME) + void invalidateAll(); +} diff --git a/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestServerResource.java b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestServerResource.java new file mode 100644 index 0000000000000..fb21d7c9e7e91 --- /dev/null +++ b/integration-tests/infinispan-cache/src/main/java/io/quarkus/it/cache/infinispan/SunriseRestServerResource.java @@ -0,0 +1,41 @@ +package io.quarkus.it.cache.infinispan; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; + +@ApplicationScoped +@Path("sunrise") +public class SunriseRestServerResource { + + private int sunriseTimeInvocations; + + @GET + @Path("time/{city}") + public String getSunriseTime(@RestPath String city, @RestQuery String date) { + sunriseTimeInvocations++; + return "2020-12-20T10:15:30"; + } + + @GET + @Path("invocations") + public Integer getSunriseTimeInvocations() { + return sunriseTimeInvocations; + } + + @DELETE + @Path("invalidate/{city}") + public void invalidate(@RestPath String city, @RestQuery String notPartOfTheCacheKey, @RestQuery String date) { + // Do nothing. We only need to test the caching annotation on the client side. + } + + @DELETE + @Path("invalidate") + public void invalidateAll() { + // Do nothing. We only need to test the caching annotation on the client side. + } +} diff --git a/integration-tests/infinispan-cache/src/main/resources/application.properties b/integration-tests/infinispan-cache/src/main/resources/application.properties new file mode 100644 index 0000000000000..25525ca3c8e4f --- /dev/null +++ b/integration-tests/infinispan-cache/src/main/resources/application.properties @@ -0,0 +1 @@ +io.quarkus.it.cache.infinispan.SunriseRestClient/mp-rest/url=${test.url} diff --git a/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheIT.java b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheIT.java new file mode 100644 index 0000000000000..de4fb795de42b --- /dev/null +++ b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.cache.infinispan; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class CacheIT extends CacheTest { +} diff --git a/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheTest.java b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheTest.java new file mode 100644 index 0000000000000..b5a73d4558a6f --- /dev/null +++ b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/CacheTest.java @@ -0,0 +1,33 @@ +package io.quarkus.it.cache.infinispan; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class CacheTest { + + @Test + public void testCache() { + runExpensiveRequest(); + runExpensiveRequest(); + runExpensiveRequest(); + when().get("/expensive-resource/invocations").then().statusCode(200).body(is("1")); + + when() + .post("/expensive-resource") + .then() + .statusCode(204); + } + + private void runExpensiveRequest() { + when() + .get("/expensive-resource/I/love/Quarkus?foo=bar") + .then() + .statusCode(200) + .body("result", is("I love Quarkus too!")); + } +} diff --git a/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/InfinspanCacheClientTestCase.java b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/InfinspanCacheClientTestCase.java new file mode 100644 index 0000000000000..ccfa3c8dd5fc8 --- /dev/null +++ b/integration-tests/infinispan-cache/src/test/java/io/quarkus/it/cache/infinispan/InfinspanCacheClientTestCase.java @@ -0,0 +1,87 @@ +package io.quarkus.it.cache.infinispan; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.Headers; + +@QuarkusTest +@DisplayName("Tests the integration between the infinispan cache and the rest-client extensions") +public class InfinspanCacheClientTestCase { + + private static final String CITY = "Toulouse"; + private static final String TODAY = "2020-12-20"; + + @Test + public void test() { + assertInvocations("0"); + getSunriseTimeInvocations(); + assertInvocations("1"); + getSunriseTimeInvocations(); + assertInvocations("1"); + getAsyncSunriseTimeInvocations(); + assertInvocations("1"); + invalidate(); + getSunriseTimeInvocations(); + assertInvocations("2"); + invalidateAll(); + getSunriseTimeInvocations(); + assertInvocations("3"); + } + + private void assertInvocations(String expectedInvocations) { + given() + .when() + .get("/rest-client/invocations") + .then() + .statusCode(200) + .body(equalTo(expectedInvocations)); + } + + private void getSunriseTimeInvocations() { + doGetSunriseTimeInvocations("/rest-client/time/{city}", true); + } + + private void getAsyncSunriseTimeInvocations() { + doGetSunriseTimeInvocations("/rest-client/async/time/{city}", false); + } + + private void doGetSunriseTimeInvocations(String path, Boolean blockingAllowed) { + Headers headers = given() + .queryParam("date", TODAY) + .when() + .get(path, CITY) + .then() + .statusCode(200) + .extract().headers(); + assertEquals(headers.get("before").getValue(), headers.get("after").getValue()); + assertEquals(blockingAllowed.toString(), headers.get("blockingAllowed").getValue()); + } + + private void invalidate() { + Headers headers = given() + .queryParam("date", TODAY) + .queryParam("notPartOfTheCacheKey", "notPartOfTheCacheKey") + .when() + .delete("/rest-client/invalidate/{city}", CITY) + .then() + .statusCode(204) + .extract().headers(); + assertNotNull(headers.get("incoming").getValue()); + assertEquals("false", headers.get("blockingAllowed").getValue()); + } + + private void invalidateAll() { + given() + .when() + .delete("/rest-client/invalidate") + .then() + .statusCode(204); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index a17b168d5cd73..e963d51266bd8 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -176,6 +176,7 @@ hibernate-validator-resteasy-reactive common-jpa-entities infinispan-client + infinispan-cache devtools devtools-registry-client gradle From 757537b21b8c859c075349abd706642e0e315982 Mon Sep 17 00:00:00 2001 From: Foivos Zakkak Date: Mon, 22 Apr 2024 15:06:49 +0300 Subject: [PATCH 0039/2353] Add documentation to NativeImageResourceBuildItem's constructors --- .../builditem/nativeimage/NativeImageResourceBuildItem.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceBuildItem.java index 56ecff462a4d0..f58313c940ae5 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/NativeImageResourceBuildItem.java @@ -7,7 +7,11 @@ import io.quarkus.builder.item.MultiBuildItem; /** - * A build item that indicates that a static resource should be included in the native image + * A build item that indicates that a static resource should be included in the native image. + *

+ * A static resource is a file that is not processed by the build steps, but is included in the native image as-is. + * The resource path passed to the constructor is a {@code /}-separated path name (with the same semantics as the parameters + * passed to {@link java.lang.ClassLoader#getResources(String)}. *

* Related build items: *