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

Resteasy Reactive Links: Improve "rel" deduction rules #25493

Merged
merged 2 commits into from
May 11, 2022
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
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
Sgitario marked this conversation as resolved.
Show resolved Hide resolved
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