diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index bd78ec95b0ad1..8b23992d27078 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1643,7 +1643,8 @@ Assuming a `Record` looks like: ---- public class Record { - // the class must contain/inherit either and `id` field or an `@Id` annotated field + // The class must contain/inherit either and `id` field, an `@Id` or `@RestLinkId` annotated field. + // When resolving the id the order of preference is: `@RestLinkId` > `@Id` > `id` field. private int id; public Record() { 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 fae3611ccbfab..375324b773f92 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 @@ -31,6 +31,7 @@ import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.gizmo.ClassOutput; import io.quarkus.resteasy.reactive.common.deployment.JaxRsResourceIndexBuildItem; +import io.quarkus.resteasy.reactive.links.RestLinkId; import io.quarkus.resteasy.reactive.links.runtime.GetterAccessorsContainer; import io.quarkus.resteasy.reactive.links.runtime.GetterAccessorsContainerRecorder; import io.quarkus.resteasy.reactive.links.runtime.LinkInfo; @@ -150,35 +151,7 @@ private RuntimeValue implementPathParameterValueGetter validateClassHasFieldId(index, entityType); for (String parameterName : linkInfo.getPathParameters()) { - 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 - // and see if any is annotated with any sort of @Id annotation - // N.B. as this module does not depend on any other module that could supply this @Id annotation - // (like Panache), we need this general lookup - FieldInfoSupplier byIdAnnotation = new FieldInfoSupplier( - c -> { - for (FieldInfo field : c.fields()) { - List annotationInstances = field.annotations(); - for (AnnotationInstance annotationInstance : annotationInstances) { - if (annotationInstance.name().toString().endsWith("persistence.Id")) { - return field; - } - } - } - return null; - }, - className, - index); - fieldInfo = byIdAnnotation.get(); - } - + FieldInfo fieldInfo = resolveField(index, parameterName, className); if (fieldInfo != null) { GetterMetadata getterMetadata = new GetterMetadata(fieldInfo); if (!implementedGetters.contains(getterMetadata)) { @@ -187,7 +160,8 @@ private RuntimeValue implementPathParameterValueGetter } getterAccessorsContainerRecorder.addAccessor(getterAccessorsContainer, - entityType, getterMetadata.getFieldName(), getterMetadata.getGetterAccessorName()); + entityType, parameterName, + getterMetadata.getGetterAccessorName()); } } } @@ -196,6 +170,52 @@ private RuntimeValue implementPathParameterValueGetter return getterAccessorsContainer; } + private FieldInfo resolveField(IndexView index, String parameterName, DotName className) { + FieldInfoSupplier byParamName = new FieldInfoSupplier(c -> c.field(parameterName), className, index); + + // check if we have field matching the name + FieldInfo fieldInfo = byParamName.get(); + if (parameterName.equals("id")) { + // this is a special case where we want to go through the fields of the class + // and see if any is annotated with any sort of @persistence.Id/@RestLinkId annotation + // N.B. as this module does not depend on any other module that could supply this @Id annotation + // (like Panache), we need this general lookup + // the order of preference for the annotations is @RestLinkId > @persistence.Id > id + FieldInfoSupplier byAnnotation = new FieldInfoSupplier( + c -> { + FieldInfo persistenceId = null; + for (FieldInfo field : c.fields()) { + // prefer RestLinId over Id + if (field.hasAnnotation(RestLinkId.class)) { + return field; + } + // keep the first found @persistence.Id annotation in case not @RestLinkId is found + if (fieldAnnotatedWith(field, "persistence.Id") && persistenceId == null) { + persistenceId = field; + } + } + return persistenceId; + }, + className, + index); + FieldInfo annotatedField = byAnnotation.get(); + if (annotatedField != null) { + fieldInfo = annotatedField; + } + } + return fieldInfo; + } + + private boolean fieldAnnotatedWith(FieldInfo field, String annotation) { + List annotationInstances = field.annotations(); + for (AnnotationInstance annotationInstance : annotationInstances) { + if (annotationInstance.name().toString().endsWith(annotation)) { + return true; + } + } + return false; + } + /** * Validates if the given classname contains a field `id` or annotated with `@Id` * @@ -227,15 +247,26 @@ private void validateRec(IndexView index, String entityType, ClassInfo classInfo .filter(a -> a.name().toString().endsWith("persistence.Id")) .toList(); + List fieldsAnnotatedWithRestLinkId = classInfo.fields().stream() + .flatMap(f -> f.annotations(RestLinkId.class).stream()) + .toList(); + + // @RestLinkId annotation count > 1 is not allowed + if (fieldsAnnotatedWithRestLinkId.size() > 1) { + throw new IllegalStateException("Cannot generate web links for the class " + entityType + + " because it has multiple fields annotated with `@RestLinkId`, where a maximum of one is allowed"); + } + // Id field found, break the loop - if (!fieldsNamedId.isEmpty() || !fieldsAnnotatedWithId.isEmpty()) + if (!fieldsNamedId.isEmpty() || !fieldsAnnotatedWithId.isEmpty() || !fieldsAnnotatedWithRestLinkId.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"); + " because it is either missing an `id` field, a field with an `@Id` annotation or a field with a `@RestLinkId annotation"); } // Id field not found but there's still hope diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java index 5245b1fcec275..99f71ebaac2b0 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java @@ -28,4 +28,66 @@ void shouldGetHalLinksForInstance() { assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); } + + @Test + void shouldGetHalLinksForIdAndPersistenceIdAndRestLinkId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-id-and-persistence-id-and-rest-link-id/100") + .thenReturn(); + + assertThat(response.body() + .jsonPath() + .getString("_links.self.href")).endsWith("/records/with-id-and-persistence-id-and-rest-link-id/100"); + } + + @Test + void shouldGetHalLinksForIdAndPersistenceId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-id-and-persistence-id/10") + .thenReturn(); + + assertThat(response.body() + .jsonPath() + .getString("_links.self.href")).endsWith("/records/with-id-and-persistence-id/10"); + } + + @Test + void shouldGetHalLinksForIdAndRestLinkId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-id-and-rest-link-id/100") + .thenReturn(); + + assertThat(response.body() + .jsonPath() + .getString("_links.self.href")).endsWith("/records/with-id-and-rest-link-id/100"); + } + + @Test + void shouldGetHalLinksForPersistenceIdAndRestLinkId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-persistence-id-and-rest-link-id/100") + .thenReturn(); + + assertThat(response.body() + .jsonPath() + .getString("_links.self.href")).endsWith("/records/with-persistence-id-and-rest-link-id/100"); + } + + @Test + void shouldGetHalLinksForPersistenceId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-persistence-id/10") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/records/with-persistence-id/10"); + } + + @Test + void shouldGetHalLinksForRestLinkId() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-rest-link-id/100") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/records/with-rest-link-id/100"); + } } 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 b37da8dcb601f..6a35906db9843 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,10 @@ public class HalLinksWithJacksonTest extends AbstractHalLinksTest { @RegisterExtension static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar - .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class)) + .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class, + TestRecordWithIdAndPersistenceIdAndRestLinkId.class, TestRecordWithIdAndRestLinkId.class, + TestRecordWithIdAndPersistenceId.class, TestRecordWithPersistenceId.class, + TestRecordWithRestLinkId.class, TestRecordWithPersistenceIdAndRestLinkId.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 778847fbdbdea..9cf3b61e7e91d 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,10 @@ public class HalLinksWithJsonbTest extends AbstractHalLinksTest { @RegisterExtension static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() .withApplicationRoot((jar) -> jar - .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class)) + .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class, + TestRecordWithIdAndPersistenceIdAndRestLinkId.class, TestRecordWithIdAndRestLinkId.class, + TestRecordWithIdAndPersistenceId.class, TestRecordWithPersistenceId.class, + TestRecordWithRestLinkId.class, TestRecordWithPersistenceIdAndRestLinkId.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 f8610662664fe..1efbc3f9d384a 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,10 @@ public class RestLinksInjectionTest { @RegisterExtension static final QuarkusUnitTest TEST = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class)); + .addClasses(AbstractId.class, AbstractEntity.class, TestRecord.class, TestResource.class, + TestRecordWithIdAndPersistenceIdAndRestLinkId.class, TestRecordWithIdAndRestLinkId.class, + TestRecordWithIdAndPersistenceId.class, TestRecordWithPersistenceId.class, + TestRecordWithRestLinkId.class, TestRecordWithPersistenceIdAndRestLinkId.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/RestLinksWithFailureInjectionMultipleRestLinkIdTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionMultipleRestLinkIdTest.java new file mode 100644 index 0000000000000..0b85bcbc39050 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/RestLinksWithFailureInjectionMultipleRestLinkIdTest.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +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 RestLinksWithFailureInjectionMultipleRestLinkIdTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot( + jar -> jar.addClasses(TestRecordMultipleRestLinkIds.class, TestResourceMultipleRestLinkIds.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.TestRecordMultipleRestLinkIds" + + " because it has multiple fields annotated with `@RestLinkId`, where a maximum of one is allowed"); + }); + + @Test + void validationFailed() { + // Should not be reached: verify + fail(); + } +} 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 index 72791f897f1b6..b338136024942 100644 --- 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 @@ -1,7 +1,7 @@ package io.quarkus.resteasy.reactive.links.deployment; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -17,13 +17,13 @@ public class RestLinksWithFailureInjectionTest { 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"); + "io.quarkus.resteasy.reactive.links.deployment.TestRecordNoId because it is " + + "either missing an `id` field, a field with an `@Id` annotation or a field with a `@RestLinkId annotation"); }); @Test void validationFailed() { // Should not be reached: verify - assertTrue(false); + fail(); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordMultipleRestLinkIds.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordMultipleRestLinkIds.java new file mode 100644 index 0000000000000..90706ba0a1d32 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordMultipleRestLinkIds.java @@ -0,0 +1,46 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; + +public class TestRecordMultipleRestLinkIds { + + @RestLinkId + private long idOne; + @RestLinkId + private long idTwo; + + private String name; + + public TestRecordMultipleRestLinkIds() { + } + + public TestRecordMultipleRestLinkIds(long idOne, long idTwo, String name) { + this.idOne = idOne; + this.idTwo = idTwo; + this.name = name; + } + + public long getIdOne() { + return idOne; + } + + public void setIdOne(long idOne) { + this.idOne = idOne; + } + + public long getIdTwo() { + return idTwo; + } + + public void setIdTwo(long idTwo) { + this.idTwo = idTwo; + } + + 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/TestRecordWithIdAndPersistenceId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceId.java new file mode 100644 index 0000000000000..aa991505e4045 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceId.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.deployment.persistence.Id; + +public class TestRecordWithIdAndPersistenceId { + + @Id + private int persistenceId; + private int id; + private String name; + + public TestRecordWithIdAndPersistenceId() { + } + + public TestRecordWithIdAndPersistenceId(int persistenceId, int id, String value) { + this.persistenceId = persistenceId; + this.id = id; + this.name = value; + } + + public int getPersistenceId() { + return persistenceId; + } + + public void setPersistenceId(int persistenceId) { + this.persistenceId = persistenceId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + 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/TestRecordWithIdAndPersistenceIdAndRestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceIdAndRestLinkId.java new file mode 100644 index 0000000000000..3f03638b8175b --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndPersistenceIdAndRestLinkId.java @@ -0,0 +1,56 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; +import io.quarkus.resteasy.reactive.links.deployment.persistence.Id; + +public class TestRecordWithIdAndPersistenceIdAndRestLinkId { + + @RestLinkId + private int restLinkId; + @Id + private int persistenceId; + private int id; + private String name; + + public TestRecordWithIdAndPersistenceIdAndRestLinkId() { + } + + public TestRecordWithIdAndPersistenceIdAndRestLinkId(int restLinkId, int persistenceId, int id, String name) { + this.restLinkId = restLinkId; + this.persistenceId = persistenceId; + this.id = id; + this.name = name; + } + + public int getRestLinkId() { + return restLinkId; + } + + public void setRestLinkId(int restLinkId) { + this.restLinkId = restLinkId; + } + + public int getPersistenceId() { + return persistenceId; + } + + public void setPersistenceId(int persistenceId) { + this.persistenceId = persistenceId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + 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/TestRecordWithIdAndRestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndRestLinkId.java new file mode 100644 index 0000000000000..d0de5c6c2b73f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithIdAndRestLinkId.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; + +public class TestRecordWithIdAndRestLinkId { + + @RestLinkId + private int restLinkId; + private int id; + private String name; + + public TestRecordWithIdAndRestLinkId() { + } + + public TestRecordWithIdAndRestLinkId(int restLinkId, int id, String value) { + this.restLinkId = restLinkId; + this.id = id; + this.name = value; + } + + public int getRestLinkId() { + return restLinkId; + } + + public void setRestLinkId(int restLinkId) { + this.restLinkId = restLinkId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + 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/TestRecordWithPersistenceId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceId.java new file mode 100644 index 0000000000000..8ab9924a5d6da --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceId.java @@ -0,0 +1,34 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.deployment.persistence.Id; + +public class TestRecordWithPersistenceId { + + @Id + private int persistenceId; + private String name; + + public TestRecordWithPersistenceId() { + } + + public TestRecordWithPersistenceId(int persistenceId, String value) { + this.persistenceId = persistenceId; + this.name = value; + } + + public int getPersistenceId() { + return persistenceId; + } + + public void setPersistenceId(int persistenceId) { + this.persistenceId = persistenceId; + } + + 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/TestRecordWithPersistenceIdAndRestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceIdAndRestLinkId.java new file mode 100644 index 0000000000000..bb12576819eb8 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithPersistenceIdAndRestLinkId.java @@ -0,0 +1,46 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; +import io.quarkus.resteasy.reactive.links.deployment.persistence.Id; + +public class TestRecordWithPersistenceIdAndRestLinkId { + + @RestLinkId + private int restLinkId; + @Id + private int persistenceId; + private String name; + + public TestRecordWithPersistenceIdAndRestLinkId() { + } + + public TestRecordWithPersistenceIdAndRestLinkId(int restLinkId, int persistenceId, String name) { + this.restLinkId = restLinkId; + this.persistenceId = persistenceId; + this.name = name; + } + + public int getRestLinkId() { + return restLinkId; + } + + public void setRestLinkId(int restLinkId) { + this.restLinkId = restLinkId; + } + + public int getPersistenceId() { + return persistenceId; + } + + public void setPersistenceId(int persistenceId) { + this.persistenceId = persistenceId; + } + + 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/TestRecordWithRestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithRestLinkId.java new file mode 100644 index 0000000000000..163a19e8551b2 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestRecordWithRestLinkId.java @@ -0,0 +1,34 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import io.quarkus.resteasy.reactive.links.RestLinkId; + +public class TestRecordWithRestLinkId { + + @RestLinkId + private int restLinkId; + private String name; + + public TestRecordWithRestLinkId() { + } + + public TestRecordWithRestLinkId(int restLinkId, String value) { + this.restLinkId = restLinkId; + this.name = value; + } + + public int getRestLinkId() { + return restLinkId; + } + + public void setRestLinkId(int restLinkId) { + this.restLinkId = restLinkId; + } + + 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/TestResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java index cbfd69aa57159..50255cd5f39cb 100644 --- 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 @@ -29,6 +29,34 @@ public class TestResource { new TestRecord(ID_COUNTER.incrementAndGet(), "first", "First value"), new TestRecord(ID_COUNTER.incrementAndGet(), "second", "Second value"))); + private static final List ID_AND_PERSISTENCE_ID_AND_REST_LINK_IDS = new LinkedList<>( + Arrays.asList( + new TestRecordWithIdAndPersistenceIdAndRestLinkId(100, 10, 1, "One"), + new TestRecordWithIdAndPersistenceIdAndRestLinkId(101, 11, 2, "Two"))); + + private static final List PERSISTENCE_ID_AND_REST_LINK_IDS = new LinkedList<>( + Arrays.asList( + new TestRecordWithPersistenceIdAndRestLinkId(100, 10, "One"), + new TestRecordWithPersistenceIdAndRestLinkId(101, 11, "Two"))); + + private static final List ID_AND_PERSISTENCE_ID_RECORDS = new LinkedList<>( + Arrays.asList( + new TestRecordWithIdAndPersistenceId(10, 1, "One"), + new TestRecordWithIdAndPersistenceId(11, 2, "Two"))); + + private static final List ID_AND_REST_LINK_ID_RECORDS = new LinkedList<>( + Arrays.asList( + new TestRecordWithIdAndRestLinkId(100, 1, "One"), + new TestRecordWithIdAndRestLinkId(101, 2, "Two"))); + + private static final List PERSISTENCE_ID_RECORDS = new LinkedList<>(Arrays.asList( + new TestRecordWithPersistenceId(10, "One"), + new TestRecordWithPersistenceId(11, "Two"))); + + private static final List REST_LINK_ID_RECORDS = new LinkedList<>(Arrays.asList( + new TestRecordWithRestLinkId(100, "One"), + new TestRecordWithRestLinkId(101, "Two"))); + @GET @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) @RestLink(entityType = TestRecord.class) @@ -80,4 +108,77 @@ public TestRecord getBySlugOrId(@PathParam("slugOrId") String slugOrId) { .findFirst() .orElseThrow(NotFoundException::new); } + + @GET + @Path("/with-id-and-persistence-id-and-rest-link-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithIdAndPersistenceIdAndRestLinkId.class) + @InjectRestLinks + public TestRecordWithIdAndPersistenceIdAndRestLinkId getWithIdAndPersistenceIdAndRestLinkId(@PathParam("id") int id) { + return ID_AND_PERSISTENCE_ID_AND_REST_LINK_IDS.stream() + .filter(record -> record.getRestLinkId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-persistence-id-and-rest-link-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithPersistenceIdAndRestLinkId.class) + @InjectRestLinks + public TestRecordWithPersistenceIdAndRestLinkId getWithPersistenceIdAndRestLinkId(@PathParam("id") int id) { + return PERSISTENCE_ID_AND_REST_LINK_IDS.stream() + .filter(record -> record.getRestLinkId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-id-and-persistence-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithIdAndPersistenceId.class) + @InjectRestLinks + public TestRecordWithIdAndPersistenceId getWithIdAndPersistenceId(@PathParam("id") int id) { + return ID_AND_PERSISTENCE_ID_RECORDS.stream() + .filter(record -> record.getPersistenceId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-id-and-rest-link-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithIdAndRestLinkId.class) + @InjectRestLinks + public TestRecordWithIdAndRestLinkId getWithIdAndRestLinkId(@PathParam("id") int id) { + return ID_AND_REST_LINK_ID_RECORDS.stream() + .filter(record -> record.getRestLinkId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-persistence-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithPersistenceId.class) + @InjectRestLinks + public TestRecordWithPersistenceId getWithPersistenceId(@PathParam("id") int id) { + return PERSISTENCE_ID_RECORDS.stream() + .filter(record -> record.getPersistenceId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + + @GET + @Path("/with-rest-link-id/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(entityType = TestRecordWithRestLinkId.class) + @InjectRestLinks + public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) { + return REST_LINK_ID_RECORDS.stream() + .filter(record -> record.getRestLinkId() == id) + .findFirst() + .orElseThrow(NotFoundException::new); + } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResourceMultipleRestLinkIds.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResourceMultipleRestLinkIds.java new file mode 100644 index 0000000000000..bb1bdfc449d0e --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResourceMultipleRestLinkIds.java @@ -0,0 +1,33 @@ +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.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +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.smallrye.mutiny.Uni; + +@Path("/recordsMultipleRestLinkIds") +@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) +public class TestResourceMultipleRestLinkIds { + + private static final List RECORDS = new LinkedList<>(Arrays.asList( + new TestRecordMultipleRestLinkIds(10, 20, "first_value"), + new TestRecordMultipleRestLinkIds(11, 22, "second_value"))); + + @GET + @RestLink(entityType = TestRecordMultipleRestLinkIds.class) + @InjectRestLinks + public Uni> getAll() { + return Uni.createFrom().item(RECORDS).onItem().delayIt().by(Duration.ofMillis(100)); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/persistence/Id.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/persistence/Id.java new file mode 100644 index 0000000000000..7879297ffd300 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/persistence/Id.java @@ -0,0 +1,7 @@ +package io.quarkus.resteasy.reactive.links.deployment.persistence; + +/** + * Dummy annotation to test the persistence.Id annotation functionality. + */ +public @interface Id { +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkId.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkId.java new file mode 100644 index 0000000000000..dd6164644a6f9 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkId.java @@ -0,0 +1,17 @@ +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; + +/** + * Mark a field on a resource to be a provider for 'id' parameters in associated Web links. + *

+ * The RestLinkId annotation can be used at field level. + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +public @interface RestLinkId { +}