Skip to content

Commit 3025c93

Browse files
authored
[client] support generics with jackson (#553)
1 parent c8b75e9 commit 3025c93

File tree

17 files changed

+340
-98
lines changed

17 files changed

+340
-98
lines changed

http-client/src/main/java/io/avaje/http/client/JacksonBodyAdapter.java

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package io.avaje.http.client;
22

3-
import com.fasterxml.jackson.annotation.JsonInclude;
4-
import com.fasterxml.jackson.core.JsonProcessingException;
5-
import com.fasterxml.jackson.databind.*;
6-
import com.fasterxml.jackson.databind.type.CollectionType;
7-
83
import java.io.IOException;
94
import java.io.UncheckedIOException;
5+
import java.lang.reflect.Type;
106
import java.util.List;
117
import java.util.concurrent.ConcurrentHashMap;
128

9+
import com.fasterxml.jackson.annotation.JsonInclude;
10+
import com.fasterxml.jackson.core.JsonProcessingException;
11+
import com.fasterxml.jackson.databind.DeserializationFeature;
12+
import com.fasterxml.jackson.databind.ObjectMapper;
13+
import com.fasterxml.jackson.databind.ObjectReader;
14+
import com.fasterxml.jackson.databind.ObjectWriter;
15+
import com.fasterxml.jackson.databind.SerializationFeature;
16+
import com.fasterxml.jackson.databind.type.CollectionType;
17+
1318
/**
1419
* Jackson BodyAdapter to read and write beans as JSON.
1520
*
@@ -26,9 +31,9 @@ public final class JacksonBodyAdapter implements BodyAdapter {
2631

2732
private final ObjectMapper mapper;
2833

29-
private final ConcurrentHashMap<Class<?>, BodyWriter<?>> beanWriterCache = new ConcurrentHashMap<>();
30-
private final ConcurrentHashMap<Class<?>, BodyReader<?>> beanReaderCache = new ConcurrentHashMap<>();
31-
private final ConcurrentHashMap<Class<?>, BodyReader<?>> listReaderCache = new ConcurrentHashMap<>();
34+
private final ConcurrentHashMap<Type, BodyWriter<?>> beanWriterCache = new ConcurrentHashMap<>();
35+
private final ConcurrentHashMap<Type, BodyReader<?>> beanReaderCache = new ConcurrentHashMap<>();
36+
private final ConcurrentHashMap<Type, BodyReader<?>> listReaderCache = new ConcurrentHashMap<>();
3237

3338
/**
3439
* Create passing the ObjectMapper to use.
@@ -72,6 +77,30 @@ public <T> BodyReader<T> beanReader(Class<T> cls) {
7277
});
7378
}
7479

80+
@SuppressWarnings("unchecked")
81+
@Override
82+
public <T> BodyWriter<T> beanWriter(Type cls) {
83+
return (BodyWriter<T>) beanWriterCache.computeIfAbsent(cls, aClass -> {
84+
try {
85+
return new JWriter<>(mapper.writerFor(mapper.getTypeFactory().constructType(cls)));
86+
} catch (Exception e) {
87+
throw new RuntimeException(e);
88+
}
89+
});
90+
}
91+
92+
@SuppressWarnings("unchecked")
93+
@Override
94+
public <T> BodyReader<T> beanReader(Type cls) {
95+
return (BodyReader<T>) beanReaderCache.computeIfAbsent(cls, aClass -> {
96+
try {
97+
return new JReader<>(mapper.readerFor(mapper.getTypeFactory().constructType(cls)));
98+
} catch (Exception e) {
99+
throw new RuntimeException(e);
100+
}
101+
});
102+
}
103+
75104
@SuppressWarnings("unchecked")
76105
@Override
77106
public <T> BodyReader<List<T>> listReader(Class<T> cls) {
@@ -86,7 +115,22 @@ public <T> BodyReader<List<T>> listReader(Class<T> cls) {
86115
});
87116
}
88117

89-
private static class JReader<T> implements BodyReader<T> {
118+
@SuppressWarnings("unchecked")
119+
@Override
120+
public <T> BodyReader<List<T>> listReader(Type type) {
121+
return (BodyReader<List<T>>) listReaderCache.computeIfAbsent(type, aType -> {
122+
try {
123+
var javaType = mapper.getTypeFactory().constructType(aType);
124+
final CollectionType collectionType = mapper.getTypeFactory().constructCollectionType(List.class, javaType);
125+
final ObjectReader reader = mapper.readerFor(collectionType);
126+
return new JReader<>(reader);
127+
} catch (Exception e) {
128+
throw new RuntimeException(e);
129+
}
130+
});
131+
}
132+
133+
private static final class JReader<T> implements BodyReader<T> {
90134

91135
private final ObjectReader reader;
92136

@@ -113,7 +157,7 @@ public T read(BodyContent bodyContent) {
113157
}
114158
}
115159

116-
private static class JWriter<T> implements BodyWriter<T> {
160+
private static final class JWriter<T> implements BodyWriter<T> {
117161

118162
private final ObjectWriter writer;
119163

http-generator-client/src/main/java/io/avaje/http/generator/client/ClientMethodWriter.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
* Write code to register Web route for a given controller method.
2222
*/
2323
final class ClientMethodWriter {
24-
2524
private static final KnownResponse KNOWN_RESPONSE = new KnownResponse();
2625
private static final String BODY_HANDLER = "java.net.http.HttpResponse.BodyHandler";
2726
private static final String COMPLETABLE_FUTURE = "java.util.concurrent.CompletableFuture";
@@ -33,19 +32,21 @@ final class ClientMethodWriter {
3332
private final UType returnType;
3433
private MethodParam bodyHandlerParam;
3534
private String methodGenericParams = "";
36-
private final boolean useJsonb;
35+
private static final boolean useJsonb = APContext.typeElement("io.avaje.jsonb.Types") != null;
36+
private static final boolean useJackson = APContext.typeElement("com.fasterxml.jackson.core.type.TypeReference") != null;
37+
private static final boolean useInject = APContext.typeElement("io.avaje.inject.spi.GenericType") != null;
38+
3739
private final Optional<RequestTimeoutPrism> timeout;
3840
private final boolean useConfig;
3941
private final Map<String, String> segmentPropertyMap;
4042
private final Set<String> propertyConstants;
4143
private final List<Entry<String, String>> presetHeaders;
4244

43-
ClientMethodWriter(MethodReader method, Append writer, boolean useJsonb, Set<String> propertyConstants) {
45+
ClientMethodWriter(MethodReader method, Append writer, Set<String> propertyConstants) {
4446
this.method = method;
4547
this.writer = writer;
4648
this.webMethod = method.webMethod();
4749
this.returnType = Util.parseType(method.returnType());
48-
this.useJsonb = useJsonb;
4950
this.timeout = method.timeout();
5051
this.useConfig = ProcessingContext.typeElement("io.avaje.config.Config") != null;
5152

@@ -73,6 +74,13 @@ final class ClientMethodWriter {
7374
}
7475

7576
void addImportTypes(ControllerReader reader) {
77+
if (useJsonb) {
78+
reader.addImportType("io.avaje.jsonb.Types");
79+
} else if (useJackson) {
80+
reader.addImportType("com.fasterxml.jackson.core.type.TypeReference");
81+
} else if (useInject) {
82+
reader.addImportType("io.avaje.inject.spi.GenericType");
83+
}
7684
reader.addImportTypes(returnType.importTypes());
7785
method.throwsList().stream()
7886
.map(UType::parse)
@@ -240,13 +248,18 @@ private void writeResponse(UType type) {
240248
}
241249

242250
void writeGeneric(UType type) {
243-
if (useJsonb && type.isGeneric()) {
244-
final var params = type.importTypes().stream()
245-
.skip(1)
246-
.map(Util::shortName)
247-
.collect(Collectors.joining(".class, "));
251+
if (type.isGeneric() && useJsonb) {
252+
final var params =
253+
type.importTypes().stream()
254+
.skip(1)
255+
.map(Util::shortName)
256+
.collect(Collectors.joining(".class, "));
248257

249258
writer.append("Types.newParameterizedType(%s.class, %s.class)", Util.shortName(type.mainType()), params);
259+
} else if (type.isGeneric() && useJackson) {
260+
writer.append("new TypeReference<%s>() {}.getType()", type.shortType());
261+
} else if (type.isGeneric() && useInject) {
262+
writer.append("new GenericType<%s>() {}.getType()", type.shortType());
250263
} else {
251264
writer.append("%s.class", Util.shortName(type.mainType()));
252265
}

http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ public class ClientProcessor extends AbstractProcessor {
3030

3131
private final ComponentMetaData metaData = new ComponentMetaData();
3232

33-
private boolean useJsonB;
34-
3533
private SimpleComponentWriter componentWriter;
3634

3735
private boolean readModuleInfo;
@@ -48,7 +46,6 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
4846
APContext.init(processingEnv);
4947
ProcessingContext.init(processingEnv, new ClientPlatformAdapter(), false);
5048
this.componentWriter = new SimpleComponentWriter(metaData);
51-
useJsonB = ProcessingContext.useJsonb();
5249
}
5350

5451
@Override
@@ -103,7 +100,7 @@ private void writeClient(Element controller) {
103100

104101
protected String writeClientAdapter(ControllerReader reader) throws IOException {
105102
var suffix = ClientSuffix.fromInterface(reader.beanType().getQualifiedName().toString());
106-
return new ClientWriter(reader, suffix, useJsonB).write();
103+
return new ClientWriter(reader, suffix).write();
107104
}
108105

109106
private void initialiseComponent() {

http-generator-client/src/main/java/io/avaje/http/generator/client/ClientWriter.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,14 @@ final class ClientWriter extends BaseControllerWriter {
2424
private static final String AT_GENERATED = "@Generated(\"avaje-http-client-generator\")";
2525

2626
private final List<ClientMethodWriter> methodList = new ArrayList<>();
27-
private final boolean useJsonb;
2827
private final Set<String> propertyConstants = new HashSet<>();
2928
private final String suffix;
3029

31-
ClientWriter(ControllerReader reader, String suffix, boolean useJsonB) throws IOException {
30+
ClientWriter(ControllerReader reader, String suffix) throws IOException {
3231
super(reader, suffix);
3332
this.suffix = suffix;
3433
reader.addImportType(HTTP_CLIENT);
35-
this.useJsonb = useJsonB;
3634
readMethods();
37-
if (useJsonB) reader.addImportType("io.avaje.jsonb.Types");
3835
}
3936

4037
@Override
@@ -50,7 +47,7 @@ protected String initPackageName(String originName) {
5047
private void readMethods() {
5148
for (final MethodReader method : reader.methods()) {
5249
if (method.isWebMethod()) {
53-
final var methodWriter = new ClientMethodWriter(method, writer, useJsonb, propertyConstants);
50+
final var methodWriter = new ClientMethodWriter(method, writer, propertyConstants);
5451
methodWriter.addImportTypes(reader);
5552
methodList.add(methodWriter);
5653
}

tests/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
<modules>
2525
<module>test-javalin</module>
2626
<module>test-javalin-jsonb</module>
27-
<module>test-client</module>
2827
<module>test-sigma</module>
2928
</modules>
3029

@@ -35,6 +34,7 @@
3534
<jdk>[21,)</jdk>
3635
</activation>
3736
<modules>
37+
<module>test-client</module>
3838
<module>test-nima</module>
3939
<module>test-jex</module>
4040
<module>test-nima-jsonb</module>

tests/test-client/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<artifactId>test-client</artifactId>
1313

1414
<properties>
15+
<maven.compiler.release>21</maven.compiler.release>
1516
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1617
</properties>
1718

@@ -54,6 +55,19 @@
5455
<version>1.0</version>
5556
</dependency>
5657

58+
<dependency>
59+
<groupId>io.avaje</groupId>
60+
<artifactId>avaje-jex</artifactId>
61+
<version>3.0-RC14</version>
62+
<scope>test</scope>
63+
</dependency>
64+
65+
<dependency>
66+
<groupId>io.avaje</groupId>
67+
<artifactId>avaje-http-client-generator</artifactId>
68+
<version>${project.version}</version>
69+
</dependency>
70+
5771
</dependencies>
5872

5973
</project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package example.github;
2+
3+
import java.util.List;
4+
5+
import io.avaje.http.api.Client;
6+
import io.avaje.http.api.Post;
7+
import io.avaje.http.client.HttpException;
8+
9+
@Client
10+
public interface Generic {
11+
12+
@Post("/generic")
13+
List<GenericData<Repo>> post(GenericData<Repo> repo) throws HttpException;
14+
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package example.github;
2+
3+
public class GenericData<T> {
4+
private T data;
5+
6+
public T getData() {
7+
return data;
8+
}
9+
10+
public void setData(T data) {
11+
this.data = data;
12+
}
13+
}
Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
package example.github;
22

3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
35
public class Repo {
4-
public long id;
5-
public String name;
6+
private long id;
7+
private String name;
8+
9+
public long getId() {
10+
return id;
11+
}
12+
13+
public void setId(long id) {
14+
this.id = id;
15+
}
16+
17+
public String getName() {
18+
return name;
19+
}
20+
21+
public void setName(String name) {
22+
this.name = name;
23+
}
624
}
Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package example.github;
22

3-
//import io.avaje.http.api.Get;
4-
//import io.avaje.http.api.Path;
5-
import io.avaje.http.client.HttpException;
6-
73
import java.util.List;
84

9-
//@Path("/")
5+
import io.avaje.http.api.Client;
6+
import io.avaje.http.api.Get;
7+
import io.avaje.http.client.HttpException;
8+
9+
@Client
1010
public interface Simple {
1111

12-
//@Get("users/{user}/repos")
12+
@Get("users/{user}/repos")
1313
List<Repo> listRepos(String user, String other) throws HttpException;
14-
1514
}

tests/test-client/src/main/java/example/github/httpclient/GeneratedHttpComponent.java

Lines changed: 0 additions & 15 deletions
This file was deleted.

tests/test-client/src/main/java/example/github/httpclient/Simple$HttpClient.java

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)