From 325a75810b30d7146a691541829a6f62c4279d31 Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 11 May 2022 10:14:20 +0200 Subject: [PATCH 1/2] Resteasy Reactive Links: Improve "rel" deduction rules - The new deduction logic is based on what Resteasy Classic is doing: * - `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. * - Otherwise, the rest method name. --- .../deployment/LinksContainerFactory.java | 106 ++++++++++++++++-- .../links/deployment/LinksProcessor.java | 11 +- .../deployment/RestLinksInjectionTest.java | 18 +-- .../links/deployment/TestResource.java | 8 +- 4 files changed, 112 insertions(+), 31 deletions(-) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java index 853827a7e0877..b85bb99f3d4d6 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java @@ -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; @@ -20,10 +32,16 @@ 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 entries) { + LinksContainer getLinksContainer(List entries, IndexView index) { LinksContainer linksContainer = new LinksContainer(); for (ResteasyReactiveResourceMethodEntriesBuildItem.Entry entry : entries) { @@ -31,7 +49,7 @@ LinksContainer getLinksContainer(List + * 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(); } /** @@ -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; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java index 35a8647ce554c..0fec6704edecc 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java @@ -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; @@ -56,7 +55,6 @@ MethodScannerBuildItem linksSupport() { @BuildStep @Record(STATIC_INIT) void initializeLinksProvider(JaxRsResourceIndexBuildItem indexBuildItem, - ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem, ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntriesBuildItem, BuildProducer bytecodeTransformersProducer, BuildProducer generatedClassesProducer, @@ -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 = implementPathParameterValueGetters( index, classOutput, linksContainer, getterAccessorsContainerRecorder, bytecodeTransformersProducer); @@ -98,11 +96,10 @@ void addHalSupport(Capabilities capabilities, BuildProducer secondRecordLinks = when().get(recordsUrl + "/2") .thenReturn() @@ -45,10 +45,10 @@ void shouldGetById() { .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()); } @@ -61,9 +61,9 @@ 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 secondRecordLinks = when().get(recordsUrl + "/second") .thenReturn() @@ -71,10 +71,10 @@ void shouldGetBySlug() { .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()); } @@ -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 diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java index 2baa751f3bb16..3ffb09372ab05 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java @@ -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> getAll() { return Uni.createFrom().item(RECORDS).onItem().delayIt().by(Duration.ofMillis(100)); @@ -40,7 +40,7 @@ public Uni> getAll() { @GET @Path("/without-links") @Produces(MediaType.APPLICATION_JSON) - @RestLink + @RestLink(rel = "list-without-links") public List getAllWithoutLinks() { return RECORDS; } @@ -48,7 +48,7 @@ public List getAllWithoutLinks() { @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() @@ -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() From 2ee32563a31fdd1f0ea9916c9567f08c20e0d954 Mon Sep 17 00:00:00 2001 From: Jose Date: Wed, 11 May 2022 12:40:51 +0200 Subject: [PATCH 2/2] Add `getEffectiveReturnType` into `Types` --- .../reactive/common/util/types/Types.java | 33 +++++++++++++ .../startup/RuntimeResourceDeployment.java | 47 +------------------ 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java index 8167c40588a4c..ce6837b7115e4 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/types/Types.java @@ -1,5 +1,7 @@ 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; @@ -7,6 +9,8 @@ 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 @@ -168,6 +172,35 @@ private static Type[] extractTypes(Map, 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; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index d8b8aa906779a..e155cc0c16a5a 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -1,14 +1,12 @@ package org.jboss.resteasy.reactive.server.core.startup; import static org.jboss.resteasy.reactive.common.util.DeploymentUtils.loadClass; +import static org.jboss.resteasy.reactive.common.util.types.Types.getEffectiveReturnType; +import static org.jboss.resteasy.reactive.common.util.types.Types.getRawType; -import io.smallrye.mutiny.Multi; -import io.smallrye.mutiny.Uni; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.lang.reflect.WildcardType; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -19,7 +17,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; import java.util.function.Supplier; import javax.ws.rs.RuntimeType; @@ -28,7 +25,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.MessageBodyWriter; import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.RestResponse; import org.jboss.resteasy.reactive.common.ResteasyReactiveConfig; import org.jboss.resteasy.reactive.common.model.MethodParameter; import org.jboss.resteasy.reactive.common.model.ParameterType; @@ -617,43 +613,4 @@ public Map buildParamIndexMap(URITemplate classPathTemplate, UR return pathParameterIndexes; } - private Class getRawType(Type type) { - if (type instanceof Class) - return (Class) type; - if (type instanceof ParameterizedType) { - ParameterizedType ptype = (ParameterizedType) type; - return (Class) ptype.getRawType(); - } - throw new UnsupportedOperationException("Endpoint return type not supported yet: " + type); - } - - private 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); - } - }