Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error message to quarkus-resteasy-reactive-links and improve documentation #36722

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions docs/src/main/asciidoc/resteasy-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
----
Expand All @@ -1654,22 +1681,22 @@ 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) {
// ...
}

@PUT
@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) {
// ...
}
}
Expand Down Expand Up @@ -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) {
// ...
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,17 @@ private RuntimeValue<GetterAccessorsContainer> implementPathParameterValueGetter
for (List<LinkInfo> 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
Expand Down Expand Up @@ -194,6 +196,56 @@ private RuntimeValue<GetterAccessorsContainer> 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<FieldInfo> fieldsNamedId = classInfo.fields().stream()
.filter(f -> f.name().equals("id"))
.toList();

List<AnnotationInstance> fieldsAnnotatedWithId = classInfo.fields().stream()
.flatMap(f -> f.annotations().stream())
.filter(a -> a.name().toString().endsWith("persistence.Id"))
leo-bogastry marked this conversation as resolved.
Show resolved Hide resolved
.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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
package io.quarkus.resteasy.reactive.links.deployment;

public abstract class AbstractEntity {

private int id;
public abstract class AbstractEntity extends AbstractId {

private String slug;

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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<TestRecordNoId> RECORDS = new LinkedList<>(Arrays.asList(
new TestRecordNoId("first_value"),
new TestRecordNoId("second_value")));

@GET

@RestLink(entityType = TestRecordNoId.class)
@InjectRestLinks
public Uni<List<TestRecordNoId>> 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);
}
}