Skip to content

Commit b793849

Browse files
committed
Support type variables on sub resource interfaces for the rest client
Allows to use parameterized sub resource interfaces as returns type of sub resource locator methods. Any type variables used as return types for sub resource methods are then resolved.
1 parent d3d1131 commit b793849

File tree

2 files changed

+268
-8
lines changed

2 files changed

+268
-8
lines changed

extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.jboss.jandex.Type.Kind.CLASS;
66
import static org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE;
77
import static org.jboss.jandex.Type.Kind.PRIMITIVE;
8+
import static org.jboss.jandex.Type.Kind.TYPE_VARIABLE;
89
import static org.jboss.resteasy.reactive.client.impl.RestClientRequestContext.DEFAULT_CONTENT_TYPE_PROP;
910
import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.extractProducesConsumesValues;
1011
import static org.jboss.resteasy.reactive.common.processor.JandexUtil.isAssignableFrom;
@@ -83,6 +84,7 @@
8384
import org.jboss.jandex.ParameterizedType;
8485
import org.jboss.jandex.PrimitiveType;
8586
import org.jboss.jandex.Type;
87+
import org.jboss.jandex.TypeVariable;
8688
import org.jboss.logging.Logger;
8789
import org.jboss.resteasy.reactive.client.api.ClientMultipartForm;
8890
import org.jboss.resteasy.reactive.client.handlers.ClientObservabilityHandler;
@@ -930,7 +932,7 @@ A more full example of generated client (with sub-resource) can is at the bottom
930932
handleSubResourceMethod(enrichers, generatedClasses, interfaceClass, index, defaultMediaType,
931933
httpAnnotationToMethod, name, classContext, baseTarget, methodIndex, method,
932934
javaMethodParameters, jandexMethod, multipartResponseTypes, Collections.emptyList(),
933-
generatedSubResources);
935+
generatedSubResources, new HashMap<>());
934936
} else {
935937
FieldDescriptor methodField = classContext.createJavaMethodField(interfaceClass, jandexMethod,
936938
methodIndex);
@@ -1228,7 +1230,7 @@ A more full example of generated client (with sub-resource) can is at the bottom
12281230
handleReturn(interfaceClass, defaultMediaType, method.getHttpMethod(),
12291231
method.getConsumes(), jandexMethod, methodCreator, formParams,
12301232
bodyParameterIdx == null ? null : methodCreator.getMethodParam(bodyParameterIdx), builder,
1231-
multipart);
1233+
multipart, Collections.emptyMap());
12321234
}
12331235
}
12341236

@@ -1399,9 +1401,48 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
13991401
ClassRestClientContext ownerContext, ResultHandle ownerTarget, int methodIndex,
14001402
ResourceMethod method, String[] javaMethodParameters, MethodInfo jandexMethod,
14011403
Set<ClassInfo> multipartResponseTypes, List<SubResourceParameter> ownerSubResourceParameters,
1402-
Map<GeneratedSubResourceKey, String> generatedSubResources) {
1404+
Map<GeneratedSubResourceKey, String> generatedSubResources, Map<String, Type> ownerIdentifierToTypeVariable) {
1405+
1406+
Map<String, Type> identifierToTypeVariable = new HashMap<>();
14031407
Type returnType = jandexMethod.returnType();
1404-
if (returnType.kind() != CLASS) {
1408+
if (returnType.kind() == PARAMETERIZED_TYPE) {
1409+
1410+
ParameterizedType parameterizedReturnType = returnType.asParameterizedType();
1411+
ClassInfo returnClass = index.getClassByName(returnType.name());
1412+
ParameterizedType.Builder methodReturnTypeBuilder = ParameterizedType.builder(returnType.name());
1413+
for (int i = 0; i < parameterizedReturnType.arguments().size(); i++) {
1414+
Type paramReturnTypeArg = parameterizedReturnType.arguments().get(i);
1415+
Type resolvedType;
1416+
if (paramReturnTypeArg.kind() == TYPE_VARIABLE) {
1417+
// method returns another subresource, and one of the arguments is a type variable e.g. Wrapper<T>
1418+
resolvedType = ownerIdentifierToTypeVariable.get(paramReturnTypeArg.asTypeVariable().identifier());
1419+
if (resolvedType == null) {
1420+
throw new IllegalArgumentException(
1421+
"Type variable %s of the sub resource locator method's return type %s could not be resolved."
1422+
.formatted(paramReturnTypeArg.asTypeVariable().identifier(), jandexMethod));
1423+
}
1424+
} else {
1425+
// Subresource, but no type variable, e.g. Wrapper<String>
1426+
resolvedType = paramReturnTypeArg;
1427+
}
1428+
1429+
identifierToTypeVariable.put(returnClass.typeParameters().get(i).identifier(), resolvedType);
1430+
methodReturnTypeBuilder.addArgument(resolvedType);
1431+
}
1432+
1433+
// rewrite returnType to reflect the resolved type variable for the generatedSubResources cache
1434+
// i.e. Wrapper<String> instead of Wrapper<V>
1435+
returnType = methodReturnTypeBuilder.build();
1436+
} else if (returnType.kind() == TYPE_VARIABLE) {
1437+
TypeVariable typeVariable = returnType.asTypeVariable();
1438+
// rewrite returnType to reflect the resolved type variable for the generatedSubResources cache
1439+
// i.e. String instead of Type Variable V
1440+
returnType = identifierToTypeVariable.get(typeVariable.identifier());
1441+
if (returnType == null) {
1442+
return;
1443+
}
1444+
1445+
} else if (returnType.kind() != CLASS) {
14051446
// sort of sub-resource method that returns a thing that isn't a class
14061447
throw new IllegalArgumentException("Sub resource type is not a class: " + returnType.name().toString());
14071448
}
@@ -1854,7 +1895,7 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
18541895
handleReturn(subInterface, defaultMediaType,
18551896
getHttpMethod(jandexSubMethod, subMethod.getHttpMethod(), httpAnnotationToMethod),
18561897
consumes, jandexSubMethod, subMethodCreator, formParams, bodyParameterValue,
1857-
builder, multipart);
1898+
builder, multipart, identifierToTypeVariable);
18581899
} else {
18591900
// finding corresponding jandex method, used by enricher (MicroProfile enricher stores it in a field
18601901
// to later fill in context with corresponding java.lang.reflect.Method)
@@ -1867,7 +1908,7 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
18671908
handleSubResourceMethod(enrichers, generatedClasses, subInterface, index,
18681909
defaultMediaType, httpAnnotationToMethod, subName, subContext, subMethodTarget,
18691910
subMethodIndex, subMethod, subJavaMethodParameters, jandexSubMethod,
1870-
multipartResponseTypes, subParamFields, generatedSubResources);
1911+
multipartResponseTypes, subParamFields, generatedSubResources, identifierToTypeVariable);
18711912
}
18721913

18731914
}
@@ -2295,13 +2336,24 @@ private String getHttpMethod(MethodInfo subMethod, String defaultMethod, Map<Dot
22952336

22962337
private void handleReturn(ClassInfo restClientInterface, String defaultMediaType, String httpMethod, String[] consumes,
22972338
MethodInfo jandexMethod, MethodCreator methodCreator, ResultHandle formParams,
2298-
ResultHandle bodyValue, AssignableResultHandle builder, boolean multipart) {
2339+
ResultHandle bodyValue, AssignableResultHandle builder, boolean multipart,
2340+
Map<String, Type> identifierToTypeVariable) {
22992341
Type returnType = jandexMethod.returnType();
23002342
ReturnCategory returnCategory = ReturnCategory.BLOCKING;
23012343

2344+
if (returnType.kind() == TYPE_VARIABLE) {
2345+
TypeVariable typeVariable = returnType.asTypeVariable();
2346+
Type resolvedTypeVariable = identifierToTypeVariable.get(typeVariable.identifier());
2347+
if (resolvedTypeVariable != null) {
2348+
returnType = resolvedTypeVariable;
2349+
} else {
2350+
throw new RuntimeException("Type variable %s of the return type of method %s could not be resolved."
2351+
.formatted(typeVariable.identifier(), jandexMethod));
2352+
}
2353+
}
2354+
23022355
String simpleReturnType = returnType.name().toString();
23032356
ResultHandle genericReturnType = null;
2304-
23052357
if (returnType.kind() == PARAMETERIZED_TYPE) {
23062358
ParameterizedType paramType = returnType.asParameterizedType();
23072359
if (ASYNC_RETURN_TYPES.contains(paramType.name())) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package io.quarkus.rest.client.reactive.subresource;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.lang.annotation.Annotation;
8+
import java.lang.reflect.Type;
9+
import java.net.URI;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.Arrays;
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
import jakarta.ws.rs.GET;
16+
import jakarta.ws.rs.Path;
17+
import jakarta.ws.rs.WebApplicationException;
18+
import jakarta.ws.rs.core.MediaType;
19+
import jakarta.ws.rs.core.MultivaluedMap;
20+
import jakarta.ws.rs.ext.Provider;
21+
22+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
23+
import org.eclipse.microprofile.rest.client.inject.RestClient;
24+
import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext;
25+
import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyReader;
26+
import org.junit.jupiter.api.Assertions;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.extension.RegisterExtension;
29+
30+
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
31+
import io.quarkus.test.QuarkusUnitTest;
32+
33+
public class SubResourceGenericsTest {
34+
@RegisterExtension
35+
static final QuarkusUnitTest TEST = new QuarkusUnitTest()
36+
.withApplicationRoot((jar) -> jar
37+
.addClass(Resource.class)
38+
.addClass(ClientLocator.class)
39+
.addClass(TranslationSubResource.class)
40+
.addClass(EnglishSubResource.class)
41+
.addClass(ShortsSubResource.class)
42+
.addClass(HelloSubResource.class)
43+
.addClass(EachCellAsListElementClientMessageBodyReader.class)
44+
.addClass(CellsAsMapEntryClientMessageBodyReader.class));
45+
46+
@RestClient
47+
ClientLocator client;
48+
49+
@Test
50+
void testRestCalls() {
51+
{
52+
String result = client.string().subResource().subResource().subResource().get();
53+
assertThat(result).isEqualTo("Hello,World");
54+
}
55+
56+
{
57+
Long result = client.number().subResource().subResource().subResource().get();
58+
assertThat(result).isEqualTo(42L);
59+
}
60+
61+
{
62+
List<String> result = client.testListOnRoot();
63+
assertThat(result).contains("Hello", "World");
64+
}
65+
66+
{
67+
List<String> result = client.list().subResource().subResource().subResource().get();
68+
assertThat(result).contains("Hello", "World");
69+
}
70+
71+
{
72+
Map<String, String> result = client.map().subResource().subResource().subResource().get();
73+
assertThat(result).containsEntry("Hello", "World");
74+
}
75+
}
76+
77+
@Test
78+
void testFailureSubResourceLocatorMethodWithUnresolvedTypeVariable() {
79+
80+
try {
81+
QuarkusRestClientBuilder.newBuilder().baseUri(URI.create("http://localhost:8081"))
82+
.build(TranslationSubResource.class);
83+
} catch (Exception e) {
84+
assertThat(e.getMessage()).endsWith(
85+
"Failed to generate client for class interface io.quarkus.rest.client.reactive.subresource.SubResourceGenericsTest$TranslationSubResource : Type variable R of the sub resource locator method's return type io.quarkus.rest.client.reactive.subresource.SubResourceGenericsTest$EnglishSubResource<R> subResource() could not be resolved.");
86+
return;
87+
}
88+
Assertions.fail("Should have thrown an exception");
89+
}
90+
91+
@Test
92+
void testFailureRestClientMethodWithUnresolvedTypeVariable() {
93+
94+
try {
95+
QuarkusRestClientBuilder.newBuilder().baseUri(URI.create("http://localhost:8081")).build(HelloSubResource.class);
96+
} catch (Exception e) {
97+
assertThat(e.getMessage()).endsWith(
98+
"Failed to generate client for class interface io.quarkus.rest.client.reactive.subresource.SubResourceGenericsTest$HelloSubResource : Type variable V of the return type of method V get() could not be resolved.");
99+
return;
100+
}
101+
Assertions.fail("Should have thrown an exception");
102+
}
103+
104+
@Path("")
105+
public static class Resource {
106+
@Path("greetings/translations/english/shorts/hello")
107+
@GET
108+
public String greet() {
109+
return "Hello,World";
110+
}
111+
112+
@Path("greetings-count/translations/english/shorts/hello")
113+
@GET
114+
public Long greetingCount() {
115+
return 42L;
116+
}
117+
}
118+
119+
@RegisterRestClient(baseUri = "http://localhost:8081")
120+
@Path("")
121+
public interface ClientLocator {
122+
123+
@Path("greetings/translations")
124+
TranslationSubResource<String> string();
125+
126+
@Path("greetings-count/translations")
127+
TranslationSubResource<Long> number();
128+
129+
@Path("greetings/translations")
130+
TranslationSubResource<List<String>> list();
131+
132+
@Path("greetings/translations")
133+
TranslationSubResource<Map<String, String>> map();
134+
135+
@Path("greetings/translations/english/shorts/hello")
136+
@GET
137+
List<String> testListOnRoot();
138+
}
139+
140+
public interface TranslationSubResource<R> {
141+
@Path("english")
142+
EnglishSubResource<R> subResource();
143+
}
144+
145+
public interface EnglishSubResource<Z> {
146+
@Path("shorts")
147+
ShortsSubResource<Z> subResource();
148+
}
149+
150+
public interface ShortsSubResource<Y> {
151+
@Path("hello")
152+
HelloSubResource<Y> subResource();
153+
}
154+
155+
public interface HelloSubResource<V> {
156+
@GET
157+
V get();
158+
}
159+
160+
@Provider
161+
public static class EachCellAsListElementClientMessageBodyReader implements ClientMessageBodyReader<List<String>> {
162+
163+
@Override
164+
public List<String> readFrom(Class<List<String>> type, Type genericType, Annotation[] annotations, MediaType mediaType,
165+
MultivaluedMap<String, String> httpHeaders, InputStream entityStream, RestClientRequestContext context)
166+
throws IOException, WebApplicationException {
167+
return readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream);
168+
}
169+
170+
@Override
171+
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
172+
return genericType.getTypeName().equals("java.util.List<java.lang.String>");
173+
}
174+
175+
@Override
176+
public List<String> readFrom(Class<List<String>> type, Type genericType, Annotation[] annotations, MediaType mediaType,
177+
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
178+
throws IOException, WebApplicationException {
179+
String body = new String(entityStream.readAllBytes(), StandardCharsets.UTF_8);
180+
return Arrays.asList(body.split(","));
181+
}
182+
}
183+
184+
@Provider
185+
public static class CellsAsMapEntryClientMessageBodyReader implements ClientMessageBodyReader<Map<String, String>> {
186+
187+
@Override
188+
public Map<String, String> readFrom(Class<Map<String, String>> type, Type genericType, Annotation[] annotations,
189+
MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream,
190+
RestClientRequestContext context) throws IOException, WebApplicationException {
191+
return readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream);
192+
}
193+
194+
@Override
195+
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
196+
return genericType.getTypeName().equals("java.util.Map<java.lang.String, java.lang.String>");
197+
}
198+
199+
@Override
200+
public Map<String, String> readFrom(Class<Map<String, String>> type, Type genericType, Annotation[] annotations,
201+
MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
202+
throws IOException, WebApplicationException {
203+
String body = new String(entityStream.readAllBytes(), StandardCharsets.UTF_8);
204+
String[] split = body.split(",");
205+
return Map.of(split[0], split[1]);
206+
}
207+
}
208+
}

0 commit comments

Comments
 (0)