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