diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 8234d1200f09a..4ad51fddae5a5 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1636,7 +1636,34 @@ To enable Web Links support, add the `quarkus-resteasy-reactive-links` extension |=== -Importing this module will allow injecting web links into the response HTTP headers by just annotating your endpoint resources with the `@InjectRestLinks` annotation. To declare the web links that will be returned, you need to use the `@RestLink` annotation in the linked methods. An example of this could look like: +Importing this module will allow injecting web links into the response HTTP headers by just annotating your endpoint resources with the `@InjectRestLinks` annotation. To declare the web links that will be returned, you must use the `@RestLink` annotation in the linked methods. +Assuming a `Record` looks like: + +[source,java] +---- +public class Record { + + // the class must contain/inherit either and `id` field or an `@Id` annotated field + private int id; + + public Record() { + } + + protected Record(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} +---- + +An example of enabling Web Links support would look like: [source,java] ---- @@ -1654,7 +1681,7 @@ public class RecordsResource { @Path("/{id}") @RestLink(rel = "self") @InjectRestLinks(RestLinkType.INSTANCE) - public TestRecord get(@PathParam("id") int id) { + public Record get(@PathParam("id") int id) { // ... } @@ -1662,14 +1689,14 @@ public class RecordsResource { @Path("/{id}") @RestLink @InjectRestLinks(RestLinkType.INSTANCE) - public TestRecord update(@PathParam("id") int id) { + public Record update(@PathParam("id") int id) { // ... } @DELETE @Path("/{id}") @RestLink - public TestRecord delete(@PathParam("id") int id) { + public Record delete(@PathParam("id") int id) { // ... } } @@ -1771,7 +1798,7 @@ public class RecordsResource { @Path("/{id}") @RestLink(rel = "self") @InjectRestLinks(RestLinkType.INSTANCE) - public TestRecord get(@PathParam("id") int id) { + public Record get(@PathParam("id") int id) { // ... } } 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 index cf55570e97d97..fae3611ccbfab 100644 --- 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 @@ -145,15 +145,17 @@ private RuntimeValue implementPathParameterValueGetter for (List linkInfos : linksContainer.getLinksMap().values()) { for (LinkInfo linkInfo : linkInfos) { String entityType = linkInfo.getEntityType(); + DotName className = DotName.createSimple(entityType); + + validateClassHasFieldId(index, entityType); + for (String parameterName : linkInfo.getPathParameters()) { - DotName className = DotName.createSimple(entityType); FieldInfoSupplier byParamName = new FieldInfoSupplier(c -> c.field(parameterName), className, index); // We implement a getter inside a class that has the required field. // We later map that getter's accessor with an 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 = byParamName.get(); if ((fieldInfo == null) && parameterName.equals("id")) { // this is a special case where we want to go through the fields of the class @@ -194,6 +196,56 @@ private RuntimeValue implementPathParameterValueGetter return getterAccessorsContainer; } + /** + * Validates if the given classname contains a field `id` or annotated with `@Id` + * + * @throws IllegalStateException if the classname does not contain any sort of field identifier + */ + private void validateClassHasFieldId(IndexView index, String entityType) { + // create a new independent class name that we can override + DotName className = DotName.createSimple(entityType); + ClassInfo classInfo = index.getClassByName(className); + + if (classInfo == null) { + throw new RuntimeException(String.format("Class '%s' was not found", classInfo)); + } + validateRec(index, entityType, classInfo); + } + + /** + * Validates if the given classname contains a field `id` or annotated with `@Id` + * + * @throws IllegalStateException if the classname does not contain any sort of field identifier + */ + private void validateRec(IndexView index, String entityType, ClassInfo classInfo) { + List fieldsNamedId = classInfo.fields().stream() + .filter(f -> f.name().equals("id")) + .toList(); + + List fieldsAnnotatedWithId = classInfo.fields().stream() + .flatMap(f -> f.annotations().stream()) + .filter(a -> a.name().toString().endsWith("persistence.Id")) + .toList(); + + // Id field found, break the loop + if (!fieldsNamedId.isEmpty() || !fieldsAnnotatedWithId.isEmpty()) + return; + + // Id field not found and hope is gone + DotName superClassName = classInfo.superName(); + if (superClassName == null) { + throw new IllegalStateException("Cannot generate web links for the class " + entityType + + " because is either missing an `id` field or a field with an `@Id` annotation"); + } + + // Id field not found but there's still hope + classInfo = index.getClassByName(superClassName); + if (classInfo == null) { + throw new RuntimeException(String.format("Class '%s' was not found", classInfo)); + } + validateRec(index, entityType, classInfo); + } + /** * Implement a field getter inside a class and create an accessor class which knows how to access it. */ 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 index 861ad58348303..7ebfdec550a3b 100644 --- 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 @@ -1,8 +1,6 @@ package io.quarkus.resteasy.reactive.links.deployment; -public abstract class AbstractEntity { - - private int id; +public abstract class AbstractEntity extends AbstractId { private String slug; @@ -10,18 +8,10 @@ public AbstractEntity() { } protected AbstractEntity(int id, String slug) { - this.id = id; + super(id); this.slug = slug; } - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - public String getSlug() { return slug; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractId.java new file mode 100644 index 0000000000000..163d7dcb90731 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractId.java @@ -0,0 +1,21 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +public abstract class AbstractId { + + private int id; + + public AbstractId() { + } + + protected AbstractId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java index 16ac0de033a17..b37da8dcb601f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java @@ -12,7 +12,7 @@ public class HalLinksWithJacksonTest extends AbstractHalLinksTest { @RegisterExtension static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar - .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class)) .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-resteasy-reactive-jackson", Version.getVersion()), Dependency.of("io.quarkus", "quarkus-hal", Version.getVersion()))) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java index 45d6663f60ac7..778847fbdbdea 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java @@ -12,7 +12,7 @@ public class HalLinksWithJsonbTest extends AbstractHalLinksTest { @RegisterExtension static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar - .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class)) .setForcedDependencies(List.of( Dependency.of("io.quarkus", "quarkus-resteasy-reactive-jsonb", Version.getVersion()), Dependency.of("io.quarkus", "quarkus-hal", Version.getVersion()))) 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 index e7009c35a5c62..f8610662664fe 100644 --- 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 @@ -19,7 +19,7 @@ public class RestLinksInjectionTest { @RegisterExtension static final QuarkusUnitTest TEST = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)); + .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class)); @TestHTTPResource("records") String recordsUrl; diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionTest.java new file mode 100644 index 0000000000000..72791f897f1b6 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionTest.java @@ -0,0 +1,29 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.runtime.util.ExceptionUtil; +import io.quarkus.test.QuarkusUnitTest; + +public class RestLinksWithFailureInjectionTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(TestRecordNoId.class, TestResourceNoId.class)).assertException(t -> { + Throwable rootCause = ExceptionUtil.getRootCause(t); + assertThat(rootCause).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Cannot generate web links for the class " + + "io.quarkus.resteasy.reactive.links.deployment.TestRecordNoId because is either " + + "missing an `id` field or a field with an `@Id` annotation"); + }); + + @Test + void validationFailed() { + // Should not be reached: verify + assertTrue(false); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordNoId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordNoId.java new file mode 100644 index 0000000000000..a11d49fba2270 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordNoId.java @@ -0,0 +1,21 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +public class TestRecordNoId { + + private String name; + + public TestRecordNoId() { + } + + public TestRecordNoId(String value) { + this.name = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResourceNoId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResourceNoId.java new file mode 100644 index 0000000000000..6298e870a3ee2 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResourceNoId.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.time.Duration; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.common.util.RestMediaType; + +import io.quarkus.resteasy.reactive.links.InjectRestLinks; +import io.quarkus.resteasy.reactive.links.RestLink; +import io.quarkus.resteasy.reactive.links.RestLinkType; +import io.smallrye.mutiny.Uni; + +@Path("/recordsNoId") +@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) +public class TestResourceNoId { + + private static final List RECORDS = new LinkedList<>(Arrays.asList( + new TestRecordNoId("first_value"), + new TestRecordNoId("second_value"))); + + @GET + + @RestLink(entityType = TestRecordNoId.class) + @InjectRestLinks + public Uni> getAll() { + return Uni.createFrom().item(RECORDS).onItem().delayIt().by(Duration.ofMillis(100)); + } + + @GET + @Path("/by-name/{name}") + @RestLink(entityType = TestRecordNoId.class) + @InjectRestLinks(RestLinkType.INSTANCE) + public TestRecordNoId getByNothing(@PathParam("name") String name) { + return RECORDS.stream() + .filter(record -> record.getName().equals(name)) + .findFirst() + .orElseThrow(NotFoundException::new); + } +}