Skip to content

Commit 8c65abf

Browse files
committed
Introduce support for @JSONVIEW on the REST Client
Closes: #35909
1 parent 583c6fa commit 8c65abf

File tree

4 files changed

+212
-17
lines changed

4 files changed

+212
-17
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package io.quarkus.rest.client.reactive.jackson.test;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.net.URI;
6+
import java.util.UUID;
7+
8+
import jakarta.ws.rs.Consumes;
9+
import jakarta.ws.rs.GET;
10+
import jakarta.ws.rs.POST;
11+
import jakarta.ws.rs.Path;
12+
import jakarta.ws.rs.Produces;
13+
import jakarta.ws.rs.core.MediaType;
14+
import jakarta.ws.rs.core.Response;
15+
16+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
17+
import org.jboss.resteasy.reactive.RestPath;
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.extension.RegisterExtension;
20+
21+
import com.fasterxml.jackson.annotation.JsonView;
22+
23+
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
24+
import io.quarkus.test.QuarkusUnitTest;
25+
import io.quarkus.test.common.http.TestHTTPResource;
26+
import io.smallrye.mutiny.Uni;
27+
28+
public class JsonViewClientTest {
29+
30+
@RegisterExtension
31+
static final QuarkusUnitTest TEST = new QuarkusUnitTest().withEmptyApplication();
32+
33+
@TestHTTPResource
34+
URI uri;
35+
36+
@Test
37+
public void test() {
38+
UserClient client = QuarkusRestClientBuilder.newBuilder().baseUri(uri).build(UserClient.class);
39+
40+
User u = client.get(UUID.randomUUID().toString());
41+
assertThat(u.name).isEqualTo("bob");
42+
assertThat(u.id).isNull();
43+
44+
// Ensure JsonView also applies when resource is wrapped in Uni<>
45+
User u2 = client.getUni(UUID.randomUUID().toString()).await().indefinitely();
46+
assertThat(u2.name).isEqualTo("bob");
47+
assertThat(u2.id).isNull();
48+
49+
User toCreate = new User();
50+
toCreate.id = "should-be-ignored";
51+
toCreate.name = "alice";
52+
Response resp = client.create(toCreate);
53+
assertThat(resp.getStatus()).isEqualTo(200);
54+
}
55+
56+
public static class Views {
57+
public static class Public {
58+
}
59+
60+
public static class Private {
61+
}
62+
}
63+
64+
public static class User {
65+
@JsonView(Views.Private.class)
66+
public String id;
67+
68+
@JsonView(Views.Public.class)
69+
public String name;
70+
}
71+
72+
@Path("/users")
73+
public static class UserResource {
74+
@GET
75+
@Path("/{id}")
76+
@Produces(MediaType.APPLICATION_JSON)
77+
@JsonView(Views.Public.class)
78+
public User get(@RestPath String id) {
79+
User u = new User();
80+
u.id = id;
81+
u.name = "bob";
82+
return u;
83+
}
84+
85+
@POST
86+
@Consumes(MediaType.APPLICATION_JSON)
87+
public Response create(User user) {
88+
// Only the fields allowed by the client's view should have been serialized
89+
// Server receives what client sent; for simplicity just reflect back OK
90+
if (user.id != null) {
91+
return Response.status(Response.Status.BAD_REQUEST).entity("id must be null").build();
92+
}
93+
if (user.name == null) {
94+
return Response.status(Response.Status.BAD_REQUEST).entity("name must be set").build();
95+
}
96+
return Response.ok().build();
97+
}
98+
}
99+
100+
@Path("/users")
101+
@RegisterRestClient
102+
public interface UserClient {
103+
@GET
104+
@Path("/{id}")
105+
@Produces(MediaType.APPLICATION_JSON)
106+
@JsonView(Views.Public.class)
107+
User get(@RestPath String id);
108+
109+
@GET
110+
@Path("/{id}")
111+
@Produces(MediaType.APPLICATION_JSON)
112+
@JsonView(Views.Public.class)
113+
Uni<User> getUni(@RestPath String id);
114+
115+
@POST
116+
@Consumes(MediaType.APPLICATION_JSON)
117+
Response create(@JsonView(Views.Public.class) User user);
118+
}
119+
}

extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.io.InputStream;
77
import java.lang.annotation.Annotation;
88
import java.lang.reflect.Type;
9+
import java.util.Optional;
910
import java.util.concurrent.ConcurrentHashMap;
1011
import java.util.concurrent.ConcurrentMap;
1112
import java.util.function.Function;
@@ -42,17 +43,18 @@ public ClientJacksonMessageBodyReader(ObjectMapper mapper) {
4243
@Override
4344
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
4445
MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
45-
return doRead(type, genericType, mediaType, entityStream, null);
46+
return doRead(type, genericType, mediaType, annotations, entityStream, null);
4647
}
4748

48-
private Object doRead(Class<Object> type, Type genericType, MediaType mediaType, InputStream entityStream,
49+
private Object doRead(Class<Object> type, Type genericType, MediaType mediaType, Annotation[] annotations,
50+
InputStream entityStream,
4951
RestClientRequestContext context)
5052
throws IOException {
5153
try {
5254
if (entityStream instanceof EmptyInputStream) {
5355
return null;
5456
}
55-
ObjectReader reader = getEffectiveReader(mediaType, context);
57+
ObjectReader reader = getEffectiveReader(mediaType, annotations, context);
5658
return reader.forType(reader.getTypeFactory().constructType(genericType != null ? genericType : type))
5759
.readValue(entityStream);
5860

@@ -68,20 +70,31 @@ public Object readFrom(Class<Object> type, Type genericType,
6870
MultivaluedMap<String, String> httpHeaders,
6971
InputStream entityStream,
7072
RestClientRequestContext context) throws java.io.IOException, jakarta.ws.rs.WebApplicationException {
71-
return doRead(type, genericType, mediaType, entityStream, context);
73+
return doRead(type, genericType, mediaType, annotations, entityStream, context);
7274
}
7375

74-
private ObjectReader getEffectiveReader(MediaType responseMediaType, RestClientRequestContext context) {
76+
private ObjectReader getEffectiveReader(MediaType responseMediaType, Annotation[] annotations,
77+
RestClientRequestContext context) {
7578
ObjectMapper effectiveMapper = getObjectMapperFromContext(responseMediaType, context);
7679
if (effectiveMapper == null) {
7780
return defaultReader;
7881
}
7982

80-
return objectReaderMap.computeIfAbsent(effectiveMapper, new Function<>() {
83+
return applyJsonViewIfPresent(objectReaderMap.computeIfAbsent(effectiveMapper, new Function<>() {
8184
@Override
8285
public ObjectReader apply(ObjectMapper objectMapper) {
8386
return objectMapper.reader();
8487
}
85-
});
88+
}), annotations);
8689
}
90+
91+
private static ObjectReader applyJsonViewIfPresent(ObjectReader reader, Annotation[] annotations) {
92+
Optional<Class<?>> maybeView = JacksonUtil.matchingView(annotations);
93+
if (maybeView.isPresent()) {
94+
return reader.withView(maybeView.get());
95+
} else {
96+
return reader;
97+
}
98+
}
99+
87100
}

extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package io.quarkus.rest.client.reactive.jackson.runtime.serialisers;
22

33
import static io.quarkus.rest.client.reactive.jackson.runtime.serialisers.JacksonUtil.getObjectMapperFromContext;
4+
import static io.quarkus.rest.client.reactive.jackson.runtime.serialisers.JacksonUtil.matchingView;
45
import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.createDefaultWriter;
56
import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.doLegacyWrite;
67

78
import java.io.IOException;
89
import java.io.OutputStream;
910
import java.lang.annotation.Annotation;
11+
import java.lang.reflect.Parameter;
1012
import java.lang.reflect.Type;
13+
import java.util.Optional;
1114
import java.util.concurrent.ConcurrentHashMap;
1215
import java.util.concurrent.ConcurrentMap;
1316
import java.util.function.Function;
@@ -41,27 +44,69 @@ public boolean isWriteable(Class type, Type genericType, Annotation[] annotation
4144
@Override
4245
public void writeTo(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
4346
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
44-
doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType, null));
47+
doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType, annotations, null));
4548
}
4649

4750
@Override
4851
public void writeTo(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
4952
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream,
5053
RestClientRequestContext context) throws IOException, WebApplicationException {
51-
doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType, context));
54+
doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType, annotations, context));
5255
}
5356

54-
protected ObjectWriter getEffectiveWriter(MediaType responseMediaType, RestClientRequestContext context) {
57+
protected ObjectWriter getEffectiveWriter(MediaType responseMediaType, Annotation[] annotations,
58+
RestClientRequestContext context) {
59+
ObjectWriter result = defaultWriter;
5560
ObjectMapper objectMapper = getObjectMapperFromContext(responseMediaType, context);
56-
if (objectMapper == null) {
57-
return defaultWriter;
61+
if (objectMapper != null) {
62+
result = objectWriterMap.computeIfAbsent(objectMapper, new Function<>() {
63+
@Override
64+
public ObjectWriter apply(ObjectMapper objectMapper) {
65+
return createDefaultWriter(objectMapper);
66+
}
67+
});
68+
}
69+
return applyJsonViewIfPresent(result, effectiveView(annotations, context));
70+
}
71+
72+
private Optional<Class<?>> effectiveView(Annotation[] annotations, RestClientRequestContext context) {
73+
Optional<Class<?>> fromAnnotations = matchingView(annotations);
74+
if (fromAnnotations.isPresent()) {
75+
return fromAnnotations;
5876
}
5977

60-
return objectWriterMap.computeIfAbsent(objectMapper, new Function<>() {
61-
@Override
62-
public ObjectWriter apply(ObjectMapper objectMapper) {
63-
return createDefaultWriter(objectMapper);
78+
// now check the method parameters for a @JsonView on the body parameter
79+
if (context != null && context.getInvokedMethod() != null) {
80+
Parameter[] parameters = context.getInvokedMethod().getParameters();
81+
if (parameters != null) {
82+
for (Parameter parameter : parameters) {
83+
Annotation[] paramAnnotations = parameter.getAnnotations();
84+
boolean isBodyParameter = true;
85+
for (Annotation paramAnnotation : paramAnnotations) {
86+
String paramTypeClassName = paramAnnotation.annotationType().getName();
87+
// TODO: this should be centralized somewhere
88+
if (paramTypeClassName.startsWith("jakarta.ws.rs")
89+
|| paramTypeClassName.startsWith("io.quarkus.rest.client")
90+
|| paramTypeClassName.startsWith("org.jboss.resteasy.reactive")) {
91+
isBodyParameter = false;
92+
break;
93+
}
94+
}
95+
if (isBodyParameter) {
96+
return matchingView(paramAnnotations);
97+
}
98+
}
6499
}
65-
});
100+
}
101+
102+
return Optional.empty();
103+
}
104+
105+
private static ObjectWriter applyJsonViewIfPresent(ObjectWriter writer, Optional<Class<?>> maybeView) {
106+
if (maybeView.isPresent()) {
107+
return writer.withView(maybeView.get());
108+
} else {
109+
return writer;
110+
}
66111
}
67112
}

extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/JacksonUtil.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.quarkus.rest.client.reactive.jackson.runtime.serialisers;
22

3+
import java.lang.annotation.Annotation;
4+
import java.util.Optional;
35
import java.util.concurrent.ConcurrentHashMap;
46
import java.util.concurrent.ConcurrentMap;
57
import java.util.function.Function;
@@ -10,6 +12,7 @@
1012

1113
import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext;
1214

15+
import com.fasterxml.jackson.annotation.JsonView;
1316
import com.fasterxml.jackson.databind.ObjectMapper;
1417

1518
final class JacksonUtil {
@@ -53,4 +56,19 @@ private static Providers getProviders(RestClientRequestContext context) {
5356

5457
return null;
5558
}
59+
60+
static Optional<Class<?>> matchingView(Annotation[] annotations) {
61+
if (annotations == null) {
62+
return Optional.empty();
63+
}
64+
for (Annotation annotation : annotations) {
65+
if (annotation.annotationType() == JsonView.class) {
66+
Class<?>[] views = ((JsonView) annotation).value();
67+
if (views != null && views.length > 0 && views[0] != null) {
68+
return Optional.of(views[0]);
69+
}
70+
}
71+
}
72+
return Optional.empty();
73+
}
5674
}

0 commit comments

Comments
 (0)