Skip to content

Commit

Permalink
Validate that the class has either or inherits either and id field …
Browse files Browse the repository at this point in the history
…or an `@Id` annotated field
  • Loading branch information
leo-bogastry committed Nov 16, 2023
1 parent a16f65b commit 3469e69
Show file tree
Hide file tree
Showing 10 changed files with 72 additions and 61 deletions.
2 changes: 1 addition & 1 deletion docs/src/main/asciidoc/resteasy-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1626,7 +1626,7 @@ Assuming a `Record` looks like:
----
public class Record {
// id is a mandatory field for the generation of web links
// the class must contain/inherit either and `id` field or an `@Id` annotated field
private int id;
public Record() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OBJECT_NAME;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
Expand Down Expand Up @@ -146,14 +148,16 @@ private RuntimeValue<GetterAccessorsContainer> implementPathParameterValueGetter
for (LinkInfo linkInfo : linkInfos) {
String entityType = linkInfo.getEntityType();
DotName className = DotName.createSimple(entityType);

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
Expand Down Expand Up @@ -188,14 +192,49 @@ private RuntimeValue<GetterAccessorsContainer> implementPathParameterValueGetter
entityType, getterMetadata.getFieldName(), getterMetadata.getGetterAccessorName());
}
}

}
getterAccessorsContainerRecorder.validateContainer(getterAccessorsContainer);
}

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);
List<FieldInfo> classFields = index.getClassByName(className).fields();
List<AnnotationInstance> classAnnotations = index.getClassByName(className).annotations();

LinkedList<FieldInfo> allFields = new LinkedList<>(classFields);
LinkedList<AnnotationInstance> allAnnotations = new LinkedList<>(classAnnotations);

// also get fields and annotations from all super classes
while (index.getClassByName(className).superName() != null) {
DotName superClassName = index.getClassByName(className).superName();

List<FieldInfo> allSuperFields = index.getClassByName(superClassName).fields();
List<AnnotationInstance> allSuperAnnotations = index.getClassByName(superClassName).annotations();
allFields.addAll(allSuperFields);
allAnnotations.addAll(allSuperAnnotations);

className = superClassName;
}

List<FieldInfo> fieldsNamedId = allFields.stream().filter(f -> f.name().equals("id")).collect(Collectors.toList());
List<AnnotationInstance> fieldsAnnotatedWithId = allAnnotations.stream()
.filter(a -> a.name().toString().endsWith("persistence.Id"))
.collect(Collectors.toList());

if (fieldsNamedId.isEmpty() && fieldsAnnotatedWithId.isEmpty()) {
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");
}
}

/**
* 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
Expand Up @@ -16,8 +16,9 @@ public class RestLinksWithFailureInjectionTest {
.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(es) " +
"io.quarkus.resteasy.reactive.links.deployment.TestRecordNoId because it does not contain an `id` field");
.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
* Utility class that allows us to easily find a {@code GetterAccessor} based on a type and a field name.
Expand All @@ -24,13 +23,4 @@ public void put(String className, String fieldName, GetterAccessor getterAccesso
getterAccessorsByField.put(fieldName, getterAccessor);
}
}

public Set<String> classNameSet() {
return getterAccessors.keySet();
}

public boolean containsClassNameAndFieldName(String className, String fieldName) {
return getterAccessors.containsKey(className) &&
getterAccessors.get(className).containsKey(fieldName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,4 @@ public void addAccessor(RuntimeValue<GetterAccessorsContainer> container, String
throw new RuntimeException("Failed to initialize " + accessorName + ": " + e.getMessage());
}
}

/**
* Validates if all classes the container contain an accessor for the field name id
*
* @throws IllegalStateException if no accessor for classname.id is found
*/
public void validateContainer(RuntimeValue<GetterAccessorsContainer> container) {
boolean shouldThrow = false;
StringBuilder builder = new StringBuilder();
String mandatoryField = "id";

var allClassNames = container.getValue().classNameSet();
if (!allClassNames.isEmpty()) {
builder.append("Cannot generate web links for the class(es) ");
}

for (String className : allClassNames) {
boolean containsIdField = container.getValue().containsClassNameAndFieldName(className, mandatoryField);
if (!containsIdField) {
shouldThrow = true;
builder.append(className);
builder.append(" ");
}
}

if (shouldThrow) {
builder.append("because it does not contain an `id` field");
throw new IllegalStateException(builder.toString());
}
}
}

0 comments on commit 3469e69

Please sign in to comment.