Skip to content

Commit

Permalink
Merge pull request #36722 from leo-bogastry/add-error-message-generat…
Browse files Browse the repository at this point in the history
…e-web-links

Add error message to quarkus-resteasy-reactive-links and improve documentation
  • Loading branch information
FroMage committed Jan 4, 2024
2 parents 76b2f29 + 1f1af0c commit 2e6fe10
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 22 deletions.
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"))
.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);
}
}

0 comments on commit 2e6fe10

Please sign in to comment.