diff --git a/bom/application/pom.xml b/bom/application/pom.xml index ec654056624cf..35a68796cabd8 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1984,6 +1984,16 @@ quarkus-resteasy-reactive-jackson-common-deployment ${project.version} + + io.quarkus + quarkus-resteasy-reactive-links + ${project.version} + + + io.quarkus + quarkus-resteasy-reactive-links-deployment + ${project.version} + io.quarkus quarkus-reactive-datasource 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 ac132aa88a2b2..e30ef0bb77037 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -94,6 +94,7 @@ public enum Feature { RESTEASY_REACTIVE_JAXRS_CLIENT, RESTEASY_REACTIVE_JSONB, RESTEASY_REACTIVE_JACKSON, + RESTEASY_REACTIVE_LINKS, REST_CLIENT, REST_CLIENT_JACKSON, REST_CLIENT_JAXB, diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 418d967d4a233..eb98c7bebd7f2 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -2009,6 +2009,19 @@ + + io.quarkus + quarkus-resteasy-reactive-links + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-resteasy-reactive-qute @@ -2560,4 +2573,4 @@ - \ No newline at end of file + diff --git a/docs/pom.xml b/docs/pom.xml index 2c23f7c03f003..5688899ad6059 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1969,6 +1969,19 @@ + + io.quarkus + quarkus-resteasy-reactive-links-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-resteasy-reactive-qute-deployment diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/deployment/pom.xml b/extensions/panache/quarkus-resteasy-reactive-problem/deployment/pom.xml new file mode 100644 index 0000000000000..0ded82a136e8d --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/deployment/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + io.quarkus + quarkus-resteasy-reactive-problem-parent + 999-SNAPSHOT + + quarkus-resteasy-reactive-problem-deployment + Quarkus Resteasy Reactive Problem - Deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-resteasy-reactive-problem + ${project.version} + + + io.quarkus + quarkus-junit5-internal + test + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/deployment/src/main/java/io/quarkus/resteasy/reactive/problem/deployment/QuarkusResteasyReactiveProblemProcessor.java b/extensions/panache/quarkus-resteasy-reactive-problem/deployment/src/main/java/io/quarkus/resteasy/reactive/problem/deployment/QuarkusResteasyReactiveProblemProcessor.java new file mode 100644 index 0000000000000..2bb609980e13e --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/deployment/src/main/java/io/quarkus/resteasy/reactive/problem/deployment/QuarkusResteasyReactiveProblemProcessor.java @@ -0,0 +1,14 @@ +package io.quarkus.resteasy.reactive.problem.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class QuarkusResteasyReactiveProblemProcessor { + + private static final String FEATURE = "quarkus-resteasy-reactive-problem"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } +} diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemDevModeTest.java b/extensions/panache/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemDevModeTest.java new file mode 100644 index 0000000000000..c754355c39254 --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemDevModeTest.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.reactive.problem.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; + +public class QuarkusResteasyReactiveProblemDevModeTest { + + // Start hot reload (DevMode) test with your extension loaded + @RegisterExtension + static final QuarkusDevModeTest devModeTest = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnDevModeTest() { + // Write your dev mode tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-hot-reload for more information + Assertions.assertTrue(true, "Add dev mode assertions to " + getClass().getName()); + } +} diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemTest.java b/extensions/panache/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemTest.java new file mode 100644 index 0000000000000..4056ed49eb8d7 --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemTest.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.reactive.problem.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class QuarkusResteasyReactiveProblemTest { + + // Start unit test with your extension loaded + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnUnitTest() { + // Write your unit tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-extensions for more information + Assertions.assertTrue(true, "Add some assertions to " + getClass().getName()); + } +} diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/pom.xml b/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/pom.xml new file mode 100644 index 0000000000000..013cbbf231fd3 --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + io.quarkus + quarkus-resteasy-reactive-problem-parent + 999-SNAPSHOT + + quarkus-resteasy-reactive-problem-integration-tests + Quarkus Resteasy Reactive Problem - Integration Tests + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-reactive-problem + ${project.version} + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + + diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/main/java/io/quarkus/resteasy/reactive/problem/it/QuarkusResteasyReactiveProblemResource.java b/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/main/java/io/quarkus/resteasy/reactive/problem/it/QuarkusResteasyReactiveProblemResource.java new file mode 100644 index 0000000000000..6b2414a142218 --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/main/java/io/quarkus/resteasy/reactive/problem/it/QuarkusResteasyReactiveProblemResource.java @@ -0,0 +1,32 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package io.quarkus.resteasy.reactive.problem.it; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/quarkus-resteasy-reactive-problem") +@ApplicationScoped +public class QuarkusResteasyReactiveProblemResource { + // add some rest methods here + + @GET + public String hello() { + return "Hello quarkus-resteasy-reactive-problem"; + } +} diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/main/resources/application.properties b/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/main/resources/application.properties new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/test/java/io/quarkus/resteasy/reactive/problem/it/NativeQuarkusResteasyReactiveProblemResourceIT.java b/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/test/java/io/quarkus/resteasy/reactive/problem/it/NativeQuarkusResteasyReactiveProblemResourceIT.java new file mode 100644 index 0000000000000..d9a13937ccab1 --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/test/java/io/quarkus/resteasy/reactive/problem/it/NativeQuarkusResteasyReactiveProblemResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.resteasy.reactive.problem.it; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class NativeQuarkusResteasyReactiveProblemResourceIT extends QuarkusResteasyReactiveProblemResourceTest { +} diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/test/java/io/quarkus/resteasy/reactive/problem/it/QuarkusResteasyReactiveProblemResourceTest.java b/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/test/java/io/quarkus/resteasy/reactive/problem/it/QuarkusResteasyReactiveProblemResourceTest.java new file mode 100644 index 0000000000000..d40f4f040afa5 --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/integration-tests/src/test/java/io/quarkus/resteasy/reactive/problem/it/QuarkusResteasyReactiveProblemResourceTest.java @@ -0,0 +1,21 @@ +package io.quarkus.resteasy.reactive.problem.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class QuarkusResteasyReactiveProblemResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/quarkus-resteasy-reactive-problem") + .then() + .statusCode(200) + .body(is("Hello quarkus-resteasy-reactive-problem")); + } +} diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/pom.xml b/extensions/panache/quarkus-resteasy-reactive-problem/pom.xml new file mode 100644 index 0000000000000..2e05d96b13c3a --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + io.quarkus + quarkus-resteasy-reactive-problem-parent + 999-SNAPSHOT + pom + Quarkus Resteasy Reactive Problem - Parent + + deployment + runtime + + + 3.8.1 + ${surefire-plugin.version} + true + 1.8 + 1.8 + UTF-8 + UTF-8 + 999-SNAPSHOT + 3.0.0-M5 + + + + + io.quarkus + quarkus-bom + ${quarkus.version} + pom + import + + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.version} + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + ${settings.localRepository} + + + + + maven-failsafe-plugin + ${failsafe-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + ${settings.localRepository} + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + + + diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/runtime/pom.xml b/extensions/panache/quarkus-resteasy-reactive-problem/runtime/pom.xml new file mode 100644 index 0000000000000..7f9dc57de0c0e --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/runtime/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + io.quarkus + quarkus-resteasy-reactive-problem-parent + 999-SNAPSHOT + + quarkus-resteasy-reactive-problem + Quarkus Resteasy Reactive Problem - Runtime + + + io.quarkus + quarkus-arc + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + ${quarkus.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/extensions/panache/quarkus-resteasy-reactive-problem/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/panache/quarkus-resteasy-reactive-problem/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..22fb2756cf9cd --- /dev/null +++ b/extensions/panache/quarkus-resteasy-reactive-problem/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +name: Quarkus Resteasy Reactive Problem +#description: Quarkus Resteasy Reactive Problem ... +metadata: +# keywords: +# - quarkus-resteasy-reactive-problem +# guide: ... +# categories: +# - "miscellaneous" +# status: "preview" \ No newline at end of file diff --git a/extensions/resteasy-reactive/pom.xml b/extensions/resteasy-reactive/pom.xml index fb1b8a6789553..e7db538b82c12 100644 --- a/extensions/resteasy-reactive/pom.xml +++ b/extensions/resteasy-reactive/pom.xml @@ -24,6 +24,7 @@ quarkus-resteasy-reactive-servlet rest-client-reactive rest-client-reactive-jackson + quarkus-resteasy-reactive-links diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/pom.xml new file mode 100644 index 0000000000000..d22a41c9656d3 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/pom.xml @@ -0,0 +1,58 @@ + + + + quarkus-resteasy-reactive-links-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-resteasy-reactive-links-deployment + Quarkus - RESTEasy Reactive - Links - Deployment + + + + io.quarkus + quarkus-resteasy-reactive-links + + + io.quarkus + quarkus-resteasy-reactive-deployment + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + true + + + + + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/GetterAccessorImplementor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/GetterAccessorImplementor.java new file mode 100644 index 0000000000000..b672f97ba0a33 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/GetterAccessorImplementor.java @@ -0,0 +1,29 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static io.quarkus.gizmo.MethodDescriptor.ofMethod; + +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.reactive.links.runtime.GetterAccessor; + +class GetterAccessorImplementor { + + /** + * Implements a {@link GetterAccessor} that knows how to access a specific getter method of a specific type. + */ + void implement(ClassOutput classOutput, GetterMetadata getterMetadata) { + ClassCreator classCreator = ClassCreator.builder() + .classOutput(classOutput).className(getterMetadata.getGetterAccessorName()) + .interfaces(GetterAccessor.class) + .build(); + MethodCreator methodCreator = classCreator.getMethodCreator("get", Object.class, Object.class); + ResultHandle value = methodCreator.invokeVirtualMethod( + ofMethod(getterMetadata.getEntityType(), getterMetadata.getGetterName(), getterMetadata.getFieldType()), + methodCreator.getMethodParam(0)); + methodCreator.returnValue(value); + methodCreator.close(); + classCreator.close(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/GetterImplementor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/GetterImplementor.java new file mode 100644 index 0000000000000..670c0c8498990 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/GetterImplementor.java @@ -0,0 +1,54 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static io.quarkus.gizmo.DescriptorUtils.objectToDescriptor; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ALOAD; +import static org.objectweb.asm.Opcodes.GETFIELD; +import static org.objectweb.asm.Opcodes.IRETURN; + +import java.util.function.BiFunction; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import io.quarkus.gizmo.Gizmo; + +class GetterImplementor extends ClassVisitor { + + private final GetterMetadata getterMetadata; + + static BiFunction getVisitorFunction(GetterMetadata getterMetadata) { + return (className, classVisitor) -> new GetterImplementor(classVisitor, getterMetadata); + } + + GetterImplementor(ClassVisitor outputClassVisitor, GetterMetadata getterMetadata) { + super(Gizmo.ASM_API_VERSION, outputClassVisitor); + this.getterMetadata = getterMetadata; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, + String[] exceptions) { + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public void visitEnd() { + generateGetter(); + super.visitEnd(); + } + + private void generateGetter() { + String owner = Type.getType(objectToDescriptor(getterMetadata.getEntityType())).getInternalName(); + String fieldDescriptor = objectToDescriptor(getterMetadata.getFieldType()); + String getterDescriptor = "()" + fieldDescriptor; + + MethodVisitor mv = super.visitMethod(ACC_PUBLIC, getterMetadata.getGetterName(), getterDescriptor, null, null); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, owner, getterMetadata.getFieldName(), fieldDescriptor); + mv.visitInsn(Type.getType(fieldDescriptor).getOpcode(IRETURN)); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/GetterMetadata.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/GetterMetadata.java new file mode 100644 index 0000000000000..733d7d23a11c5 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/GetterMetadata.java @@ -0,0 +1,63 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.util.Objects; + +import org.jboss.jandex.FieldInfo; + +class GetterMetadata { + + private static final String GETTER_PREFIX = "resteasy_links_get_"; + + private static final String ACCESSOR_SUFFIX = "$_resteasy_links"; + + private final String entityType; + + private final String fieldType; + + private final String fieldName; + + GetterMetadata(FieldInfo fieldInfo) { + this.entityType = fieldInfo.declaringClass().toString(); + this.fieldType = fieldInfo.type().name().toString(); + this.fieldName = fieldInfo.name(); + } + + public String getEntityType() { + return entityType; + } + + public String getFieldType() { + return fieldType; + } + + public String getFieldName() { + return fieldName; + } + + public String getGetterName() { + return GETTER_PREFIX + fieldName; + } + + public String getGetterAccessorName() { + return entityType + ACCESSOR_SUFFIX + getGetterName(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GetterMetadata that = (GetterMetadata) o; + return entityType.equals(that.entityType) + && fieldType.equals(that.fieldType) + && fieldName.equals(that.fieldName); + } + + @Override + public int hashCode() { + return Objects.hash(entityType, fieldType, fieldName); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java new file mode 100644 index 0000000000000..12c0c3eb196e2 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java @@ -0,0 +1,138 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.core.UriBuilder; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; +import org.jboss.resteasy.reactive.common.model.MethodParameter; +import org.jboss.resteasy.reactive.common.model.ResourceClass; +import org.jboss.resteasy.reactive.common.model.ResourceMethod; +import org.jboss.resteasy.reactive.common.util.URLUtils; + +import io.quarkus.resteasy.reactive.links.RestLink; +import io.quarkus.resteasy.reactive.links.runtime.LinkInfo; +import io.quarkus.resteasy.reactive.links.runtime.LinksContainer; + +final class LinksContainerFactory { + + private static final DotName REST_LINK_ANNOTATION = DotName.createSimple(RestLink.class.getName()); + + private final IndexView index; + + LinksContainerFactory(IndexView index) { + this.index = index; + } + + /** + * Find the resource methods that are marked with a {@link RestLink} annotations and add them to a links container. + */ + LinksContainer getLinksContainer(List resourceClasses) { + LinksContainer linksContainer = new LinksContainer(); + + for (ResourceClass resourceClass : resourceClasses) { + for (ResourceMethod resourceMethod : resourceClass.getMethods()) { + MethodInfo resourceMethodInfo = getResourceMethodInfo(resourceClass, resourceMethod); + AnnotationInstance restLinkAnnotation = resourceMethodInfo.annotation(REST_LINK_ANNOTATION); + if (restLinkAnnotation != null) { + LinkInfo linkInfo = getLinkInfo(resourceClass, resourceMethod, resourceMethodInfo, + restLinkAnnotation); + linksContainer.put(linkInfo); + } + } + } + + return linksContainer; + } + + private LinkInfo getLinkInfo(ResourceClass resourceClass, ResourceMethod resourceMethod, + MethodInfo resourceMethodInfo, AnnotationInstance restLinkAnnotation) { + String rel = getAnnotationValue(restLinkAnnotation, "rel", resourceMethod.getName()); + String entityType = getAnnotationValue(restLinkAnnotation, "entityType", deductEntityType(resourceMethodInfo)); + String path = UriBuilder.fromPath(resourceClass.getPath()).path(resourceMethod.getPath()).toTemplate(); + while (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + Set pathParameters = getPathParameters(path); + + return new LinkInfo(rel, entityType, path, pathParameters); + } + + /** + * If a method return type is parameterized and has a single argument (e.g. List), then use that argument as an + * entity type. Otherwise, use the return type. + */ + private String deductEntityType(MethodInfo methodInfo) { + if (methodInfo.returnType().kind() == Type.Kind.PARAMETERIZED_TYPE) { + if (methodInfo.returnType().asParameterizedType().arguments().size() == 1) { + return methodInfo.returnType().asParameterizedType().arguments().get(0).name().toString(); + } + } + return methodInfo.returnType().name().toString(); + } + + /** + * Extract parameters from a path string + */ + private Set getPathParameters(String path) { + Set names = new HashSet<>(); + URLUtils.parsePathParameters(path, names); + Set trimmedNames = new HashSet<>(names.size()); + for (String name : names) { + trimmedNames.add(name.trim()); + } + return trimmedNames; + } + + /** + * Find a {@link MethodInfo} for a given resource method + */ + private MethodInfo getResourceMethodInfo(ResourceClass resourceClass, ResourceMethod resourceMethod) { + ClassInfo classInfo = index.getClassByName(DotName.createSimple(resourceClass.getClassName())); + for (MethodInfo methodInfo : classInfo.methods()) { + if (isSameMethod(methodInfo, resourceMethod)) { + return methodInfo; + } + } + throw new RuntimeException(String.format("Could not find method info for resource '%s.%s'", + resourceClass.getClassName(), resourceMethod.getName())); + } + + /** + * Check if the given {@link MethodInfo} and {@link ResourceMethod} defined the same underlying method + */ + private boolean isSameMethod(MethodInfo resourceMethodInfo, ResourceMethod resourceMethod) { + if (!resourceMethodInfo.name().equals(resourceMethod.getName())) { + return false; + } + if (resourceMethodInfo.parameters().size() != resourceMethod.getParameters().length) { + return false; + } + + List parameterTypes = resourceMethodInfo.parameters(); + MethodParameter[] parameters = resourceMethod.getParameters(); + for (int i = 0; i < parameters.length; i++) { + if (!parameterTypes.get(i).name().equals(DotName.createSimple(parameters[i].type))) { + return false; + } + } + + return true; + } + + private String getAnnotationValue(AnnotationInstance annotationInstance, String name, String defaultValue) { + AnnotationValue value = annotationInstance.value(name); + if (value == null || value.asString().equals("")) { + return defaultValue; + } + return value.asString(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java new file mode 100644 index 0000000000000..3f8983032c57e --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java @@ -0,0 +1,147 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.GeneratedClassGizmoAdaptor; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.resteasy.reactive.links.RestLinksResponseFilter; +import io.quarkus.resteasy.reactive.links.runtime.GetterAccessorsContainer; +import io.quarkus.resteasy.reactive.links.runtime.GetterAccessorsContainerRecorder; +import io.quarkus.resteasy.reactive.links.runtime.LinkInfo; +import io.quarkus.resteasy.reactive.links.runtime.LinksContainer; +import io.quarkus.resteasy.reactive.links.runtime.LinksProviderRecorder; +import io.quarkus.resteasy.reactive.links.runtime.RestLinksProviderProducer; +import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveDeploymentInfoBuildItem; +import io.quarkus.resteasy.reactive.spi.CustomContainerResponseFilterBuildItem; +import io.quarkus.runtime.RuntimeValue; + +final class LinksProcessor { + + private final GetterAccessorImplementor getterAccessorImplementor = new GetterAccessorImplementor(); + + @BuildStep + void feature(BuildProducer feature) { + feature.produce(new FeatureBuildItem(Feature.RESTEASY_REACTIVE_LINKS)); + } + + @BuildStep + @Record(STATIC_INIT) + void initializeLinksProvider(CombinedIndexBuildItem indexBuildItem, + ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem, + BuildProducer bytecodeTransformersProducer, + BuildProducer generatedClassesProducer, + GetterAccessorsContainerRecorder getterAccessorsContainerRecorder, + LinksProviderRecorder linksProviderRecorder) { + IndexView index = indexBuildItem.getIndex(); + ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClassesProducer, true); + + // Initialize links container + LinksContainer linksContainer = getLinksContainer(index, deploymentInfoBuildItem); + // Implement getters to access link path parameter values + RuntimeValue getterAccessorsContainer = implementPathParameterValueGetters( + index, classOutput, linksContainer, getterAccessorsContainerRecorder, bytecodeTransformersProducer); + + linksProviderRecorder.setGetterAccessorsContainer(getterAccessorsContainer); + linksProviderRecorder.setLinksContainer(linksContainer); + } + + @BuildStep + AdditionalBeanBuildItem registerRestLinksProviderProducer() { + return AdditionalBeanBuildItem.unremovableOf(RestLinksProviderProducer.class); + } + + @BuildStep + CustomContainerResponseFilterBuildItem registerRestLinksResponseFilter() { + return new CustomContainerResponseFilterBuildItem(RestLinksResponseFilter.class.getName()); + } + + private LinksContainer getLinksContainer(IndexView index, + ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem) { + LinksContainerFactory linksContainerFactory = new LinksContainerFactory(index); + return linksContainerFactory.getLinksContainer( + deploymentInfoBuildItem.getDeploymentInfo().getResourceClasses()); + } + + /** + * For each path parameter implement a getter method in a class that holds its value. + * Then implement a getter accessor class that knows how to access that getter method to avoid using reflection later. + */ + private RuntimeValue implementPathParameterValueGetters(IndexView index, + ClassOutput classOutput, LinksContainer linksContainer, + GetterAccessorsContainerRecorder getterAccessorsContainerRecorder, + BuildProducer bytecodeTransformersProducer) { + RuntimeValue getterAccessorsContainer = getterAccessorsContainerRecorder.newContainer(); + Set implementedGetters = new HashSet<>(); + + for (List linkInfos : linksContainer.getLinksMap().values()) { + for (LinkInfo linkInfo : linkInfos) { + String entityType = linkInfo.getEntityType(); + for (String parameterName : linkInfo.getPathParameters()) { + // We implement a getter inside a class that has the required field. + // We later map that getter's accessor with a entity type. + // If a field is inside a parent class, the getter accessor will be mapped to each subclass which + // has REST links that need access to that field. + FieldInfo fieldInfo = getFieldInfo(index, DotName.createSimple(entityType), parameterName); + GetterMetadata getterMetadata = new GetterMetadata(fieldInfo); + if (!implementedGetters.contains(getterMetadata)) { + implementGetterWithAccessor(classOutput, bytecodeTransformersProducer, getterMetadata); + implementedGetters.add(getterMetadata); + } + + getterAccessorsContainerRecorder.addAccessor(getterAccessorsContainer, + entityType, parameterName, getterMetadata.getGetterAccessorName()); + } + } + } + + return getterAccessorsContainer; + } + + /** + * Implement a field getter inside a class and create an accessor class which knows how to access it. + */ + private void implementGetterWithAccessor(ClassOutput classOutput, + BuildProducer bytecodeTransformersProducer, + GetterMetadata getterMetadata) { + bytecodeTransformersProducer.produce(new BytecodeTransformerBuildItem( + getterMetadata.getEntityType(), GetterImplementor.getVisitorFunction(getterMetadata))); + getterAccessorImplementor.implement(classOutput, getterMetadata); + } + + /** + * Find a field info by name inside a class. + * This is a recursive method that looks through the class hierarchy until the field throws an error if it's not. + */ + private FieldInfo getFieldInfo(IndexView index, DotName className, String fieldName) { + ClassInfo classInfo = index.getClassByName(className); + if (classInfo == null) { + throw new RuntimeException(String.format("Class '%s' was not found", className)); + } + FieldInfo fieldInfo = classInfo.field(fieldName); + if (fieldInfo != null) { + return fieldInfo; + } + if (classInfo.superName() != null) { + return getFieldInfo(index, classInfo.superName(), fieldName); + } + throw new RuntimeException(String.format("Class '%s' field '%s' was not found", className, fieldName)); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractEntity.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractEntity.java new file mode 100644 index 0000000000000..861ad58348303 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractEntity.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +public abstract class AbstractEntity { + + private int id; + + private String slug; + + public AbstractEntity() { + } + + protected AbstractEntity(int id, String slug) { + this.id = id; + this.slug = slug; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java new file mode 100644 index 0000000000000..b5ed6b73ca37f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksInjectionTest.java @@ -0,0 +1,103 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static io.restassured.RestAssured.when; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import javax.ws.rs.core.Link; +import javax.ws.rs.core.UriBuilder; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class RestLinksInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)); + + @TestHTTPResource("records") + String recordsUrl; + + @TestHTTPResource("records/without-links") + String recordsWithoutLinksUrl; + + @Test + void shouldGetById() { + List firstRecordLinks = when().get(recordsUrl + "/1") + .thenReturn() + .getHeaders() + .getValues("Link"); + assertThat(firstRecordLinks).containsOnly( + Link.fromUri(recordsUrl).rel("list").build().toString(), + Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(), + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/1")).rel("self").build().toString(), + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("getBySlug").build().toString()); + + List secondRecordLinks = when().get(recordsUrl + "/2") + .thenReturn() + .getHeaders() + .getValues("Link"); + assertThat(secondRecordLinks).containsOnly( + Link.fromUri(recordsUrl).rel("list").build().toString(), + Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(), + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/2")).rel("self").build().toString(), + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/second")) + .rel("getBySlug") + .build() + .toString()); + } + + @Test + void shouldGetBySlug() { + List firstRecordLinks = when().get(recordsUrl + "/first") + .thenReturn() + .getHeaders() + .getValues("Link"); + assertThat(firstRecordLinks).containsOnly( + Link.fromUri(recordsUrl).rel("list").build().toString(), + Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(), + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/1")).rel("self").build().toString(), + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("getBySlug").build().toString()); + + List secondRecordLinks = when().get(recordsUrl + "/second") + .thenReturn() + .getHeaders() + .getValues("Link"); + assertThat(secondRecordLinks).containsOnly( + Link.fromUri(recordsUrl).rel("list").build().toString(), + Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(), + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/2")).rel("self").build().toString(), + Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/second")) + .rel("getBySlug") + .build() + .toString()); + } + + @Test + void shouldGetAll() { + List links = when().get(recordsUrl) + .thenReturn() + .getHeaders() + .getValues("Link"); + assertThat(links).containsOnly( + Link.fromUri(recordsUrl).rel("list").build().toString(), + Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString()); + } + + @Test + void shouldGetAllWithoutLinks() { + List links = when().get(recordsWithoutLinksUrl) + .thenReturn() + .getHeaders() + .getValues("Link"); + assertThat(links).isEmpty(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecord.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecord.java new file mode 100644 index 0000000000000..1da9e9cf8ec52 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecord.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +public class TestRecord extends AbstractEntity { + + private String value; + + public TestRecord() { + } + + public TestRecord(int id, String slug, String value) { + super(id, slug); + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java new file mode 100644 index 0000000000000..d5102e3fdbd08 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java @@ -0,0 +1,67 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.resteasy.reactive.links.InjectRestLinks; +import io.quarkus.resteasy.reactive.links.RestLink; +import io.quarkus.resteasy.reactive.links.RestLinkType; + +@Path("/records") +public class TestResource { + + private static final AtomicInteger ID_COUNTER = new AtomicInteger(0); + + private static final List RECORDS = new LinkedList<>(Arrays.asList( + new TestRecord(ID_COUNTER.incrementAndGet(), "first", "First value"), + new TestRecord(ID_COUNTER.incrementAndGet(), "second", "Second value"))); + + @GET + @Produces(MediaType.APPLICATION_JSON) + @RestLink(entityType = TestRecord.class, rel = "list") + @InjectRestLinks + public List getAll() { + return RECORDS; + } + + @GET + @Path("/without-links") + @Produces(MediaType.APPLICATION_JSON) + @RestLink + public List getAllWithoutLinks() { + return RECORDS; + } + + @GET + @Path("/{id: \\d+}") + @Produces(MediaType.APPLICATION_JSON) + @RestLink(entityType = TestRecord.class, rel = "self") + @InjectRestLinks(RestLinkType.INSTANCE) + public TestRecord getById(@PathParam("id") int id) { + return RECORDS.stream() + .filter(record -> record.getId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/{slug: [a-zA-Z-]+}") + @Produces(MediaType.APPLICATION_JSON) + @RestLink + @InjectRestLinks(RestLinkType.INSTANCE) + public TestRecord getBySlug(@PathParam("slug") String slug) { + return RECORDS.stream() + .filter(record -> record.getSlug().equals(slug)) + .findFirst() + .orElseThrow(NotFoundException::new); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/pom.xml new file mode 100644 index 0000000000000..621f1d660d263 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/pom.xml @@ -0,0 +1,21 @@ + + + + quarkus-resteasy-reactive-parent-aggregator + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-resteasy-reactive-links-parent + Quarkus - RESTEasy Reactive - Links + pom + + + deployment + runtime + + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml new file mode 100644 index 0000000000000..c18b1adab4798 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml @@ -0,0 +1,43 @@ + + + + quarkus-resteasy-reactive-links-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-resteasy-reactive-links + Quarkus - RESTEasy Reactive - Links - Runtime + Quarkus RESTEast Reactive resource linking support + + + + io.quarkus + quarkus-resteasy-reactive + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java new file mode 100644 index 0000000000000..ca8ca0f0ed289 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java @@ -0,0 +1,13 @@ +package io.quarkus.resteasy.reactive.links; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface InjectRestLinks { + + RestLinkType value() default RestLinkType.TYPE; +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java new file mode 100644 index 0000000000000..6f6b836a3d9ce --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java @@ -0,0 +1,15 @@ +package io.quarkus.resteasy.reactive.links; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface RestLink { + + String rel() default ""; + + Class entityType() default Object.class; +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java new file mode 100644 index 0000000000000..5a23ed33ca821 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java @@ -0,0 +1,6 @@ +package io.quarkus.resteasy.reactive.links; + +public enum RestLinkType { + TYPE, + INSTANCE +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java new file mode 100644 index 0000000000000..ea0a90c331810 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java @@ -0,0 +1,12 @@ +package io.quarkus.resteasy.reactive.links; + +import java.util.Collection; + +import javax.ws.rs.core.Link; + +public interface RestLinksProvider { + + Collection getTypeLinks(Class elementType); + + Collection getInstanceLinks(T instance); +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksResponseFilter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksResponseFilter.java new file mode 100644 index 0000000000000..09b275d0d7a15 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksResponseFilter.java @@ -0,0 +1,76 @@ +package io.quarkus.resteasy.reactive.links; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Link; + +import org.jboss.resteasy.reactive.server.ServerResponseFilter; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; + +public class RestLinksResponseFilter { + + private final RestLinksProvider linksProvider; + + public RestLinksResponseFilter(RestLinksProvider linksProvider) { + this.linksProvider = linksProvider; + } + + @ServerResponseFilter + public void filter(ResourceInfo resourceInfo, ContainerResponseContext responseContext) { + if (!(resourceInfo instanceof ResteasyReactiveResourceInfo)) { + return; + } + for (Link link : getLinks((ResteasyReactiveResourceInfo) resourceInfo, responseContext)) { + responseContext.getHeaders().add("Link", link); + } + } + + private Collection getLinks(ResteasyReactiveResourceInfo resourceInfo, + ContainerResponseContext responseContext) { + InjectRestLinks injectRestLinksAnnotation = getInjectRestLinksAnnotation(resourceInfo); + if (injectRestLinksAnnotation == null) { + return Collections.emptyList(); + } + + if (injectRestLinksAnnotation.value() == RestLinkType.INSTANCE && responseContext.hasEntity()) { + return linksProvider.getInstanceLinks(responseContext.getEntity()); + } + + return linksProvider.getTypeLinks(getEntityType(resourceInfo, responseContext)); + } + + private InjectRestLinks getInjectRestLinksAnnotation(ResteasyReactiveResourceInfo resourceInfo) { + if (resourceInfo.getMethodAnnotationNames().contains(InjectRestLinks.class.getName())) { + for (Annotation annotation : resourceInfo.getAnnotations()) { + if (annotation instanceof InjectRestLinks) { + return (InjectRestLinks) annotation; + } + } + } + if (resourceInfo.getClassAnnotationNames().contains(InjectRestLinks.class.getName())) { + for (Annotation annotation : resourceInfo.getClassAnnotations()) { + if (annotation instanceof InjectRestLinks) { + return (InjectRestLinks) annotation; + } + } + } + return null; + } + + private Class getEntityType(ResteasyReactiveResourceInfo resourceInfo, + ContainerResponseContext responseContext) { + for (Annotation annotation : resourceInfo.getAnnotations()) { + if (annotation instanceof RestLink) { + Class entityType = ((RestLink) annotation).entityType(); + if (entityType != Object.class) { + return entityType; + } + } + } + return responseContext.getEntityClass(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/GetterAccessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/GetterAccessor.java new file mode 100644 index 0000000000000..4028f7830fccd --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/GetterAccessor.java @@ -0,0 +1,12 @@ +package io.quarkus.resteasy.reactive.links.runtime; + +/** + * An accessor that knows how to access a specific getter method of a specific type. + */ +public interface GetterAccessor { + + /** + * Access a getter on a given instance and return a response. + */ + Object get(Object instance); +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/GetterAccessorsContainer.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/GetterAccessorsContainer.java new file mode 100644 index 0000000000000..db1f858272912 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/GetterAccessorsContainer.java @@ -0,0 +1,26 @@ +package io.quarkus.resteasy.reactive.links.runtime; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class that allows us to easily find a {@code GetterAccessor} based on a type and a field name. + */ +public class GetterAccessorsContainer { + + private final Map> getterAccessors = new HashMap<>(); + + public GetterAccessor get(String className, String fieldName) { + return getterAccessors.get(className).get(fieldName); + } + + public void put(String className, String fieldName, GetterAccessor getterAccessor) { + if (!getterAccessors.containsKey(className)) { + getterAccessors.put(className, new HashMap<>()); + } + Map getterAccessorsByField = getterAccessors.get(className); + if (!getterAccessorsByField.containsKey(fieldName)) { + getterAccessorsByField.put(fieldName, getterAccessor); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/GetterAccessorsContainerRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/GetterAccessorsContainerRecorder.java new file mode 100644 index 0000000000000..6424690f18537 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/GetterAccessorsContainerRecorder.java @@ -0,0 +1,33 @@ +package io.quarkus.resteasy.reactive.links.runtime; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class GetterAccessorsContainerRecorder { + + /** + * Create new getter accessors container. + */ + public RuntimeValue newContainer() { + return new RuntimeValue<>(new GetterAccessorsContainer()); + } + + /** + * Add a getter accessor to a container. + */ + public void addAccessor(RuntimeValue container, String className, String fieldName, + String accessorName) { + try { + // Create a new accessor object early + GetterAccessor accessor = (GetterAccessor) Thread.currentThread() + .getContextClassLoader() + .loadClass(accessorName) + .getDeclaredConstructor() + .newInstance(); + container.getValue().put(className, fieldName, accessor); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize " + accessorName + ": " + e.getMessage()); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java new file mode 100644 index 0000000000000..06c8543c77eb9 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java @@ -0,0 +1,40 @@ +package io.quarkus.resteasy.reactive.links.runtime; + +import java.util.Set; + +import io.quarkus.runtime.annotations.RecordableConstructor; + +public final class LinkInfo { + + private final String rel; + + private final String entityType; + + private final String path; + + private final Set pathParameters; + + @RecordableConstructor + public LinkInfo(String rel, String entityType, String path, Set pathParameters) { + this.rel = rel; + this.entityType = entityType; + this.path = path; + this.pathParameters = pathParameters; + } + + public String getRel() { + return rel; + } + + public String getEntityType() { + return entityType; + } + + public String getPath() { + return path; + } + + public Set getPathParameters() { + return pathParameters; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinksContainer.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinksContainer.java new file mode 100644 index 0000000000000..fd2d1d4f1ecb2 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinksContainer.java @@ -0,0 +1,43 @@ +package io.quarkus.resteasy.reactive.links.runtime; + +import java.util.Collections; +import java.util.List; + +import javax.ws.rs.core.MultivaluedMap; + +import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; + +import io.quarkus.runtime.annotations.RecordableConstructor; + +/** + * A container holding links mapped by an entity which they represent. + */ +public final class LinksContainer { + + /** + * Links mapped by their entity type. + * In order to be recorded this field name has to match the constructor parameter name and have a getter. + */ + private final MultivaluedMap linksMap; + + public LinksContainer() { + linksMap = new MultivaluedTreeMap<>(); + } + + @RecordableConstructor + public LinksContainer(MultivaluedMap linksMap) { + this.linksMap = linksMap; + } + + public List getForClass(Class c) { + return linksMap.getOrDefault(c.getName(), Collections.emptyList()); + } + + public void put(LinkInfo linkInfo) { + linksMap.add(linkInfo.getEntityType(), linkInfo); + } + + public MultivaluedMap getLinksMap() { + return MultivaluedTreeMap.clone(linksMap); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinksProviderRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinksProviderRecorder.java new file mode 100644 index 0000000000000..286f8c4bb7b44 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinksProviderRecorder.java @@ -0,0 +1,16 @@ +package io.quarkus.resteasy.reactive.links.runtime; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class LinksProviderRecorder { + + public void setLinksContainer(LinksContainer linksContainer) { + RestLinksProviderImpl.setLinksContainer(linksContainer); + } + + public void setGetterAccessorsContainer(RuntimeValue getterAccessorsContainer) { + RestLinksProviderImpl.setGetterAccessorsContainer(getterAccessorsContainer.getValue()); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java new file mode 100644 index 0000000000000..7a0354d4a6d74 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java @@ -0,0 +1,81 @@ +package io.quarkus.resteasy.reactive.links.runtime; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import javax.ws.rs.core.Link; +import javax.ws.rs.core.UriInfo; + +import io.quarkus.resteasy.reactive.links.RestLinksProvider; + +final class RestLinksProviderImpl implements RestLinksProvider { + + private static LinksContainer linksContainer; + + private static GetterAccessorsContainer getterAccessorsContainer; + + private final UriInfo uriInfo; + + static void setLinksContainer(LinksContainer context) { + RestLinksProviderImpl.linksContainer = context; + } + + static void setGetterAccessorsContainer(GetterAccessorsContainer getterAccessorsContainer) { + RestLinksProviderImpl.getterAccessorsContainer = getterAccessorsContainer; + } + + RestLinksProviderImpl(UriInfo uriInfo) { + this.uriInfo = uriInfo; + } + + @Override + public Collection getTypeLinks(Class elementType) { + verifyInit(); + + List links = new LinkedList<>(); + for (LinkInfo linkInfo : linksContainer.getForClass(elementType)) { + if (linkInfo.getPathParameters().size() == 0) { + links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) + .rel(linkInfo.getRel()) + .build()); + } + } + return links; + } + + @Override + public Collection getInstanceLinks(T instance) { + verifyInit(); + + List links = new LinkedList<>(); + for (LinkInfo linkInfo : linksContainer.getForClass(instance.getClass())) { + links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) + .rel(linkInfo.getRel()) + .build(getPathParameterValues(linkInfo, instance))); + } + return links; + } + + private Object[] getPathParameterValues(LinkInfo linkInfo, Object instance) { + List values = new ArrayList<>(linkInfo.getPathParameters().size()); + for (String name : linkInfo.getPathParameters()) { + GetterAccessor accessor = getterAccessorsContainer.get(linkInfo.getEntityType(), name); + if (accessor == null) { + throw new RuntimeException("Could not get '" + name + "' value"); + } + values.add(accessor.get(instance)); + } + return values.toArray(); + } + + private void verifyInit() { + if (linksContainer == null) { + throw new IllegalStateException("Links context has not been initialized"); + } + if (getterAccessorsContainer == null) { + throw new IllegalStateException("Getter accessors container has not been initialized"); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderProducer.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderProducer.java new file mode 100644 index 0000000000000..a2119687f984e --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderProducer.java @@ -0,0 +1,20 @@ +package io.quarkus.resteasy.reactive.links.runtime; + +import javax.enterprise.context.Dependent; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Produces; +import javax.ws.rs.core.UriInfo; + +import io.quarkus.arc.DefaultBean; +import io.quarkus.resteasy.reactive.links.RestLinksProvider; + +@Dependent +public final class RestLinksProviderProducer { + + @Produces + @RequestScoped + @DefaultBean + public RestLinksProvider restLinksProvider(UriInfo uriInfo) { + return new RestLinksProviderImpl(uriInfo); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..26fa8c254aadb --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,12 @@ +--- +name: "RESTEasy Reactive Links" +metadata: + short-name: "resteasy-reactive-links" + keywords: + - "rest" + - "jaxrs" + - "links" + categories: + - "web" + - "reactive" + status: "experimental" diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/pom.xml new file mode 100644 index 0000000000000..ad5e69f0c2e87 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + quarkus-resteasy-reactive-problem-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-resteasy-reactive-problem-deployment + Quarkus - RESTEasy Reactive - Problem - Deployment + + + + io.quarkus + quarkus-resteasy-reactive-problem + ${project.version} + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-resteasy-reactive-common-deployment + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/src/main/java/io/quarkus/resteasy/reactive/problem/deployment/QuarkusResteasyReactiveProblemProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/src/main/java/io/quarkus/resteasy/reactive/problem/deployment/QuarkusResteasyReactiveProblemProcessor.java new file mode 100644 index 0000000000000..2bb609980e13e --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/src/main/java/io/quarkus/resteasy/reactive/problem/deployment/QuarkusResteasyReactiveProblemProcessor.java @@ -0,0 +1,14 @@ +package io.quarkus.resteasy.reactive.problem.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class QuarkusResteasyReactiveProblemProcessor { + + private static final String FEATURE = "quarkus-resteasy-reactive-problem"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemDevModeTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemDevModeTest.java new file mode 100644 index 0000000000000..25fdef13680e7 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemDevModeTest.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.reactive.problem.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; + +public class QuarkusResteasyReactiveProblemDevModeTest { + + // Start hot reload (DevMode) test with your extension loaded + @RegisterExtension + static final QuarkusDevModeTest devModeTest = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnDevModeTest() { + // Write your dev mode tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-hot-reload for more information + Assertions.assertTrue(true, "Add dev mode assertions to " + getClass().getName()); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemTest.java new file mode 100644 index 0000000000000..9bb7430c84a5a --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/deployment/src/test/java/io/quarkus/resteasy/reactive/problem/test/QuarkusResteasyReactiveProblemTest.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.reactive.problem.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class QuarkusResteasyReactiveProblemTest { + + // Start unit test with your extension loaded + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnUnitTest() { + // Write your unit tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-extensions for more information + Assertions.assertTrue(true, "Add some assertions to " + getClass().getName()); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/pom.xml new file mode 100644 index 0000000000000..7c403ffc4a768 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/pom.xml @@ -0,0 +1,21 @@ + + + + quarkus-resteasy-reactive-parent-aggregator + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-resteasy-reactive-problem-parent + Quarkus - RESTEasy Reactive - Problem + pom + + + deployment + runtime + + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/runtime/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/runtime/pom.xml new file mode 100644 index 0000000000000..ace7323e3c26d --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/runtime/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + quarkus-resteasy-reactive-problem-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-resteasy-reactive-problem + Quarkus - RESTEasy Reactive - Problem - Runtime + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy-reactive-common + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..22fb2756cf9cd --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-problem/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +name: Quarkus Resteasy Reactive Problem +#description: Quarkus Resteasy Reactive Problem ... +metadata: +# keywords: +# - quarkus-resteasy-reactive-problem +# guide: ... +# categories: +# - "miscellaneous" +# status: "preview" \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveDeploymentInfoBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveDeploymentInfoBuildItem.java new file mode 100644 index 0000000000000..c4bc3ec4cad46 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveDeploymentInfoBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.resteasy.reactive.server.deployment; + +import org.jboss.resteasy.reactive.server.core.DeploymentInfo; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class ResteasyReactiveDeploymentInfoBuildItem extends SimpleBuildItem { + + private final DeploymentInfo deploymentInfo; + + public ResteasyReactiveDeploymentInfoBuildItem(DeploymentInfo deploymentInfo) { + this.deploymentInfo = deploymentInfo; + } + + public DeploymentInfo getDeploymentInfo() { + return deploymentInfo; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index f3c7233e6fe14..ccbefc71b2664 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -241,6 +241,7 @@ public void setupEndpoints(Capabilities capabilities, BeanArchiveIndexBuildItem List serverDefaultProducesHandlers, Optional requestContextFactoryBuildItem, Optional classLevelExceptionMappers, + BuildProducer quarkusRestDeploymentInfoBuildItemBuildProducer, BuildProducer quarkusRestDeploymentBuildItemBuildProducer, BuildProducer reflectiveClass, BuildProducer reflectiveHierarchy, @@ -510,7 +511,7 @@ private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName an // Handler used for both the default and non-default deployment path (specified as application path or resteasyConfig.path) // Routes use the order VertxHttpRecorder.DEFAULT_ROUTE_ORDER + 1 to ensure the default route is called before the resteasy one Class applicationClass = application == null ? Application.class : application.getClass(); - RuntimeValue deployment = recorder.createDeployment(new DeploymentInfo() + DeploymentInfo deploymentInfo = new DeploymentInfo() .setInterceptors(interceptors.sort()) .setConfig(new org.jboss.resteasy.reactive.common.ResteasyReactiveConfig( config.inputBufferSize.asLongValue(), config.singleDefaultProduces, config.defaultProduces)) @@ -525,7 +526,11 @@ private boolean hasAnnotation(MethodInfo method, short paramPosition, DotName an .setApplicationPath(applicationPath) .setResourceClasses(resourceClasses) .setLocatableResourceClasses(subResourceClasses) - .setParamConverterProviders(paramConverterProviders), + .setParamConverterProviders(paramConverterProviders); + quarkusRestDeploymentInfoBuildItemBuildProducer + .produce(new ResteasyReactiveDeploymentInfoBuildItem(deploymentInfo)); + + RuntimeValue deployment = recorder.createDeployment(deploymentInfo, beanContainerBuildItem.getValue(), shutdownContext, vertxConfig, requestContextFactoryBuildItem.map(RequestContextFactoryBuildItem::getFactory).orElse(null), initClassFactory); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 40814b4837c6c..12ac45305c693 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -4,6 +4,7 @@ import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -12,9 +13,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; import java.util.function.Supplier; @@ -144,8 +147,12 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, for (int i = 0; i < method.getParameters().length; ++i) { parameterClasses[i] = loadClass(method.getParameters()[i].declaredType); } + Set classAnnotationNames = new HashSet<>(); + for (Annotation annotation : resourceClass.getAnnotations()) { + classAnnotationNames.add(annotation.annotationType().getName()); + } ResteasyReactiveResourceInfo lazyMethod = new ResteasyReactiveResourceInfo(method.getName(), resourceClass, - parameterClasses, method.getMethodAnnotationNames()); + parameterClasses, classAnnotationNames, method.getMethodAnnotationNames()); RuntimeInterceptorDeployment.MethodInterceptorContext interceptorDeployment = runtimeInterceptorDeployment .forMethod(method, lazyMethod); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ResteasyReactiveResourceInfo.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ResteasyReactiveResourceInfo.java index 45d0e7187d277..205603e6a734f 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ResteasyReactiveResourceInfo.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/spi/ResteasyReactiveResourceInfo.java @@ -17,16 +17,19 @@ public class ResteasyReactiveResourceInfo implements ResourceInfo { private final String name; private final Class declaringClass; private final Class[] parameterTypes; + private final Set classAnnotationNames; private final Set methodAnnotationNames; + private volatile Annotation[] classAnnotations; private volatile Method method; private volatile Annotation[] annotations; private volatile Type returnType; public ResteasyReactiveResourceInfo(String name, Class declaringClass, Class[] parameterTypes, - Set methodAnnotationNames) { + Set classAnnotationNames, Set methodAnnotationNames) { this.name = name; this.declaringClass = declaringClass; this.parameterTypes = parameterTypes; + this.classAnnotationNames = classAnnotationNames; this.methodAnnotationNames = methodAnnotationNames; } @@ -38,6 +41,10 @@ public Class[] getParameterTypes() { return parameterTypes; } + public Set getClassAnnotationNames() { + return classAnnotationNames; + } + public Set getMethodAnnotationNames() { return methodAnnotationNames; } @@ -60,6 +67,13 @@ public Method getMethod() { return method; } + public Annotation[] getClassAnnotations() { + if (classAnnotations == null) { + classAnnotations = declaringClass.getAnnotations(); + } + return classAnnotations; + } + public Annotation[] getAnnotations() { if (annotations == null) { getMethod();