Skip to content

Commit

Permalink
Merge pull request #25493 from Sgitario/rr_links_rel_good
Browse files Browse the repository at this point in the history
Resteasy Reactive Links: Improve "rel" deduction rules
  • Loading branch information
geoand authored May 11, 2022
2 parents bc72595 + 2ee3256 commit 8f01ea6
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
package io.quarkus.resteasy.reactive.links.deployment;

import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETABLE_FUTURE;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETION_STAGE;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.REST_RESPONSE;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.UNI;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.UriBuilder;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.Type;
import org.jboss.resteasy.reactive.common.model.ResourceMethod;
import org.jboss.resteasy.reactive.common.util.URLUtils;
Expand All @@ -20,29 +32,36 @@

final class LinksContainerFactory {

private static final String LIST = "list";
private static final String SELF = "self";
private static final String REMOVE = "remove";
private static final String UPDATE = "update";
private static final String ADD = "add";

/**
* Find the resource methods that are marked with a {@link RestLink} annotations and add them to a links container.
*/
LinksContainer getLinksContainer(List<ResteasyReactiveResourceMethodEntriesBuildItem.Entry> entries) {
LinksContainer getLinksContainer(List<ResteasyReactiveResourceMethodEntriesBuildItem.Entry> entries, IndexView index) {
LinksContainer linksContainer = new LinksContainer();

for (ResteasyReactiveResourceMethodEntriesBuildItem.Entry entry : entries) {
MethodInfo resourceMethodInfo = entry.getMethodInfo();
AnnotationInstance restLinkAnnotation = resourceMethodInfo.annotation(DotNames.REST_LINK_ANNOTATION);
if (restLinkAnnotation != null) {
LinkInfo linkInfo = getLinkInfo(entry.getResourceMethod(), resourceMethodInfo,
restLinkAnnotation, entry.getBasicResourceClassInfo().getPath());
restLinkAnnotation, entry.getBasicResourceClassInfo().getPath(), index);
linksContainer.put(linkInfo);
}
}

return linksContainer;
}

private LinkInfo getLinkInfo(ResourceMethod resourceMethod,
MethodInfo resourceMethodInfo, AnnotationInstance restLinkAnnotation, String resourceClassPath) {
String rel = getAnnotationValue(restLinkAnnotation, "rel", resourceMethod.getName());
String entityType = getAnnotationValue(restLinkAnnotation, "entityType", deductEntityType(resourceMethodInfo));
private LinkInfo getLinkInfo(ResourceMethod resourceMethod, MethodInfo resourceMethodInfo,
AnnotationInstance restLinkAnnotation, String resourceClassPath, IndexView index) {
Type returnType = getNonAsyncReturnType(resourceMethodInfo.returnType());
String rel = getAnnotationValue(restLinkAnnotation, "rel", deductRel(resourceMethod, returnType, index));
String entityType = getAnnotationValue(restLinkAnnotation, "entityType", deductEntityType(returnType));
String path = UriBuilder.fromPath(resourceClassPath).path(resourceMethod.getPath()).toTemplate();
while (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
Expand All @@ -52,17 +71,48 @@ private LinkInfo getLinkInfo(ResourceMethod resourceMethod,
return new LinkInfo(rel, entityType, path, pathParameters);
}

/**
* When the "rel" property is not set, it will be resolved as follows:
* - "list" for GET methods returning a Collection.
* - "self" for GET methods returning a non-Collection.
* - "remove" for DELETE methods.
* - "update" for PUT methods.
* - "add" for POST methods.
* <p>
* Otherwise, it will return the method name.
*
* @param resourceMethod the resource method definition.
* @return the deducted rel property.
*/
private String deductRel(ResourceMethod resourceMethod, Type returnType, IndexView index) {
String httpMethod = resourceMethod.getHttpMethod();
boolean isCollection = isCollection(returnType, index);
if (HttpMethod.GET.equals(httpMethod) && isCollection) {
return LIST;
} else if (HttpMethod.GET.equals(httpMethod)) {
return SELF;
} else if (HttpMethod.DELETE.equals(httpMethod)) {
return REMOVE;
} else if (HttpMethod.PUT.equals(httpMethod)) {
return UPDATE;
} else if (HttpMethod.POST.equals(httpMethod)) {
return ADD;
}

return resourceMethod.getName();
}

/**
* If a method return type is parameterized and has a single argument (e.g. List), then use that argument as an
* entity type. Otherwise, use the return type.
*/
private String deductEntityType(MethodInfo methodInfo) {
if (methodInfo.returnType().kind() == Type.Kind.PARAMETERIZED_TYPE) {
if (methodInfo.returnType().asParameterizedType().arguments().size() == 1) {
return methodInfo.returnType().asParameterizedType().arguments().get(0).name().toString();
private String deductEntityType(Type returnType) {
if (returnType.kind() == Type.Kind.PARAMETERIZED_TYPE) {
if (returnType.asParameterizedType().arguments().size() == 1) {
return returnType.asParameterizedType().arguments().get(0).name().toString();
}
}
return methodInfo.returnType().name().toString();
return returnType.name().toString();
}

/**
Expand All @@ -85,4 +135,38 @@ private String getAnnotationValue(AnnotationInstance annotationInstance, String
}
return value.asString();
}

private boolean isCollection(Type type, IndexView index) {
if (type.kind() == Type.Kind.PRIMITIVE) {
return false;
}
ClassInfo classInfo = index.getClassByName(type.name());
if (classInfo == null) {
return false;
}
return classInfo.interfaceNames().stream().anyMatch(DotName.createSimple(Collection.class.getName())::equals);
}

private Type getNonAsyncReturnType(Type returnType) {
switch (returnType.kind()) {
case ARRAY:
case CLASS:
case PRIMITIVE:
case VOID:
return returnType;
case PARAMETERIZED_TYPE:
// NOTE: same code in RuntimeResourceDeployment.getNonAsyncReturnType
ParameterizedType parameterizedType = returnType.asParameterizedType();
if (COMPLETION_STAGE.equals(parameterizedType.name())
|| COMPLETABLE_FUTURE.equals(parameterizedType.name())
|| UNI.equals(parameterizedType.name())
|| MULTI.equals(parameterizedType.name())
|| REST_RESPONSE.equals(parameterizedType.name())) {
return parameterizedType.arguments().get(0);
}
return returnType;
default:
}
return returnType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import io.quarkus.resteasy.reactive.links.runtime.RestLinksProviderProducer;
import io.quarkus.resteasy.reactive.links.runtime.hal.HalServerResponseFilter;
import io.quarkus.resteasy.reactive.links.runtime.hal.ResteasyReactiveHalService;
import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveDeploymentInfoBuildItem;
import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveResourceMethodEntriesBuildItem;
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
import io.quarkus.resteasy.reactive.spi.CustomContainerResponseFilterBuildItem;
Expand All @@ -56,7 +55,6 @@ MethodScannerBuildItem linksSupport() {
@BuildStep
@Record(STATIC_INIT)
void initializeLinksProvider(JaxRsResourceIndexBuildItem indexBuildItem,
ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem,
ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntriesBuildItem,
BuildProducer<BytecodeTransformerBuildItem> bytecodeTransformersProducer,
BuildProducer<GeneratedClassBuildItem> generatedClassesProducer,
Expand All @@ -66,7 +64,7 @@ void initializeLinksProvider(JaxRsResourceIndexBuildItem indexBuildItem,
ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClassesProducer, true);

// Initialize links container
LinksContainer linksContainer = getLinksContainer(deploymentInfoBuildItem, resourceMethodEntriesBuildItem);
LinksContainer linksContainer = getLinksContainer(resourceMethodEntriesBuildItem, index);
// Implement getters to access link path parameter values
RuntimeValue<GetterAccessorsContainer> getterAccessorsContainer = implementPathParameterValueGetters(
index, classOutput, linksContainer, getterAccessorsContainerRecorder, bytecodeTransformersProducer);
Expand Down Expand Up @@ -98,11 +96,10 @@ void addHalSupport(Capabilities capabilities, BuildProducer<CustomContainerRespo
}
}

private LinksContainer getLinksContainer(ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem,
ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntriesBuildItem) {
private LinksContainer getLinksContainer(ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntriesBuildItem,
IndexView index) {
LinksContainerFactory linksContainerFactory = new LinksContainerFactory();
return linksContainerFactory.getLinksContainer(
resourceMethodEntriesBuildItem.getEntries());
return linksContainerFactory.getLinksContainer(resourceMethodEntriesBuildItem.getEntries(), index);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,20 @@ void shouldGetById() {
.getValues("Link");
assertThat(firstRecordLinks).containsOnly(
Link.fromUri(recordsUrl).rel("list").build().toString(),
Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(),
Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString(),
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/1")).rel("self").build().toString(),
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("getBySlug").build().toString());
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("get-by-slug").build().toString());

List<String> secondRecordLinks = when().get(recordsUrl + "/2")
.thenReturn()
.getHeaders()
.getValues("Link");
assertThat(secondRecordLinks).containsOnly(
Link.fromUri(recordsUrl).rel("list").build().toString(),
Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(),
Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString(),
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/2")).rel("self").build().toString(),
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/second"))
.rel("getBySlug")
.rel("get-by-slug")
.build()
.toString());
}
Expand All @@ -61,20 +61,20 @@ void shouldGetBySlug() {
.getValues("Link");
assertThat(firstRecordLinks).containsOnly(
Link.fromUri(recordsUrl).rel("list").build().toString(),
Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(),
Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString(),
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/1")).rel("self").build().toString(),
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("getBySlug").build().toString());
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/first")).rel("get-by-slug").build().toString());

List<String> secondRecordLinks = when().get(recordsUrl + "/second")
.thenReturn()
.getHeaders()
.getValues("Link");
assertThat(secondRecordLinks).containsOnly(
Link.fromUri(recordsUrl).rel("list").build().toString(),
Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString(),
Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString(),
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/2")).rel("self").build().toString(),
Link.fromUriBuilder(UriBuilder.fromUri(recordsUrl).path("/second"))
.rel("getBySlug")
.rel("get-by-slug")
.build()
.toString());
}
Expand All @@ -87,7 +87,7 @@ void shouldGetAll() {
.getValues("Link");
assertThat(links).containsOnly(
Link.fromUri(recordsUrl).rel("list").build().toString(),
Link.fromUri(recordsWithoutLinksUrl).rel("getAllWithoutLinks").build().toString());
Link.fromUri(recordsWithoutLinksUrl).rel("list-without-links").build().toString());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class TestResource {

@GET
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
@RestLink(entityType = TestRecord.class, rel = "list")
@RestLink(entityType = TestRecord.class)
@InjectRestLinks
public Uni<List<TestRecord>> getAll() {
return Uni.createFrom().item(RECORDS).onItem().delayIt().by(Duration.ofMillis(100));
Expand All @@ -40,15 +40,15 @@ public Uni<List<TestRecord>> getAll() {
@GET
@Path("/without-links")
@Produces(MediaType.APPLICATION_JSON)
@RestLink
@RestLink(rel = "list-without-links")
public List<TestRecord> getAllWithoutLinks() {
return RECORDS;
}

@GET
@Path("/{id: \\d+}")
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
@RestLink(entityType = TestRecord.class, rel = "self")
@RestLink(entityType = TestRecord.class)
@InjectRestLinks(RestLinkType.INSTANCE)
public TestRecord getById(@PathParam("id") int id) {
return RECORDS.stream()
Expand All @@ -60,7 +60,7 @@ public TestRecord getById(@PathParam("id") int id) {
@GET
@Path("/{slug: [a-zA-Z-]+}")
@Produces(MediaType.APPLICATION_JSON)
@RestLink
@RestLink(rel = "get-by-slug")
@InjectRestLinks(RestLinkType.INSTANCE)
public TestRecord getBySlug(@PathParam("slug") String slug) {
return RECORDS.stream()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package org.jboss.resteasy.reactive.common.util.types;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import org.jboss.resteasy.reactive.RestResponse;

/**
* Type conversions and generic type manipulations
Expand Down Expand Up @@ -168,6 +172,35 @@ private static Type[] extractTypes(Map<TypeVariable<?>, Type> typeVarMap, Type g
}
}

public static Type getEffectiveReturnType(Type returnType) {
if (returnType instanceof Class)
return returnType;
if (returnType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType) returnType;
Type firstTypeArgument = type.getActualTypeArguments()[0];
if (type.getRawType() == CompletionStage.class) {
return getEffectiveReturnType(firstTypeArgument);
}
if (type.getRawType() == Uni.class) {
return getEffectiveReturnType(firstTypeArgument);
}
if (type.getRawType() == Multi.class) {
return getEffectiveReturnType(firstTypeArgument);
}
if (type.getRawType() == RestResponse.class) {
return getEffectiveReturnType(firstTypeArgument);
}
return returnType;
}
if (returnType instanceof WildcardType) {
Type[] bounds = ((WildcardType) returnType).getLowerBounds();
if (bounds.length > 0)
return getRawType(bounds[0]);
return getRawType(((WildcardType) returnType).getUpperBounds()[0]);
}
throw new UnsupportedOperationException("Endpoint return type not supported yet: " + returnType);
}

public static Class<?> getRawType(Type type) {
if (type instanceof Class)
return (Class<?>) type;
Expand Down
Loading

0 comments on commit 8f01ea6

Please sign in to comment.