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

[client] Support Generics with Jackson #553

Merged
merged 5 commits into from
Jan 26, 2025
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,15 +1,20 @@
package io.avaje.http.client;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.type.CollectionType;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.CollectionType;

/**
* Jackson BodyAdapter to read and write beans as JSON.
*
Expand All @@ -26,9 +31,9 @@ public final class JacksonBodyAdapter implements BodyAdapter {

private final ObjectMapper mapper;

private final ConcurrentHashMap<Class<?>, BodyWriter<?>> beanWriterCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Class<?>, BodyReader<?>> beanReaderCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Class<?>, BodyReader<?>> listReaderCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Type, BodyWriter<?>> beanWriterCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Type, BodyReader<?>> beanReaderCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Type, BodyReader<?>> listReaderCache = new ConcurrentHashMap<>();

/**
* Create passing the ObjectMapper to use.
Expand Down Expand Up @@ -72,6 +77,30 @@ public <T> BodyReader<T> beanReader(Class<T> cls) {
});
}

@SuppressWarnings("unchecked")
@Override
public <T> BodyWriter<T> beanWriter(Type cls) {
return (BodyWriter<T>) beanWriterCache.computeIfAbsent(cls, aClass -> {
try {
return new JWriter<>(mapper.writerFor(mapper.getTypeFactory().constructType(cls)));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}

@SuppressWarnings("unchecked")
@Override
public <T> BodyReader<T> beanReader(Type cls) {
return (BodyReader<T>) beanReaderCache.computeIfAbsent(cls, aClass -> {
try {
return new JReader<>(mapper.readerFor(mapper.getTypeFactory().constructType(cls)));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}

@SuppressWarnings("unchecked")
@Override
public <T> BodyReader<List<T>> listReader(Class<T> cls) {
Expand All @@ -86,7 +115,22 @@ public <T> BodyReader<List<T>> listReader(Class<T> cls) {
});
}

private static class JReader<T> implements BodyReader<T> {
@SuppressWarnings("unchecked")
@Override
public <T> BodyReader<List<T>> listReader(Type type) {
return (BodyReader<List<T>>) listReaderCache.computeIfAbsent(type, aType -> {
try {
var javaType = mapper.getTypeFactory().constructType(aType);
final CollectionType collectionType = mapper.getTypeFactory().constructCollectionType(List.class, javaType);
final ObjectReader reader = mapper.readerFor(collectionType);
return new JReader<>(reader);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}

private static final class JReader<T> implements BodyReader<T> {

private final ObjectReader reader;

Expand All @@ -113,7 +157,7 @@ public T read(BodyContent bodyContent) {
}
}

private static class JWriter<T> implements BodyWriter<T> {
private static final class JWriter<T> implements BodyWriter<T> {

private final ObjectWriter writer;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
* Write code to register Web route for a given controller method.
*/
final class ClientMethodWriter {

private static final KnownResponse KNOWN_RESPONSE = new KnownResponse();
private static final String BODY_HANDLER = "java.net.http.HttpResponse.BodyHandler";
private static final String COMPLETABLE_FUTURE = "java.util.concurrent.CompletableFuture";
Expand All @@ -33,19 +32,21 @@ final class ClientMethodWriter {
private final UType returnType;
private MethodParam bodyHandlerParam;
private String methodGenericParams = "";
private final boolean useJsonb;
private static final boolean useJsonb = APContext.typeElement("io.avaje.jsonb.Types") != null;
private static final boolean useJackson = APContext.typeElement("com.fasterxml.jackson.core.type.TypeReference") != null;
private static final boolean useInject = APContext.typeElement("io.avaje.inject.spi.GenericType") != null;

private final Optional<RequestTimeoutPrism> timeout;
private final boolean useConfig;
private final Map<String, String> segmentPropertyMap;
private final Set<String> propertyConstants;
private final List<Entry<String, String>> presetHeaders;

ClientMethodWriter(MethodReader method, Append writer, boolean useJsonb, Set<String> propertyConstants) {
ClientMethodWriter(MethodReader method, Append writer, Set<String> propertyConstants) {
this.method = method;
this.writer = writer;
this.webMethod = method.webMethod();
this.returnType = Util.parseType(method.returnType());
this.useJsonb = useJsonb;
this.timeout = method.timeout();
this.useConfig = ProcessingContext.typeElement("io.avaje.config.Config") != null;

Expand Down Expand Up @@ -73,6 +74,13 @@ final class ClientMethodWriter {
}

void addImportTypes(ControllerReader reader) {
if (useJsonb) {
reader.addImportType("io.avaje.jsonb.Types");
} else if (useJackson) {
reader.addImportType("com.fasterxml.jackson.core.type.TypeReference");
} else if (useInject) {
reader.addImportType("io.avaje.inject.spi.GenericType");
}
reader.addImportTypes(returnType.importTypes());
method.throwsList().stream()
.map(UType::parse)
Expand Down Expand Up @@ -240,13 +248,18 @@ private void writeResponse(UType type) {
}

void writeGeneric(UType type) {
if (useJsonb && type.isGeneric()) {
final var params = type.importTypes().stream()
.skip(1)
.map(Util::shortName)
.collect(Collectors.joining(".class, "));
if (type.isGeneric() && useJsonb) {
final var params =
type.importTypes().stream()
.skip(1)
.map(Util::shortName)
.collect(Collectors.joining(".class, "));

writer.append("Types.newParameterizedType(%s.class, %s.class)", Util.shortName(type.mainType()), params);
} else if (type.isGeneric() && useJackson) {
writer.append("new TypeReference<%s>() {}.getType()", type.shortType());
} else if (type.isGeneric() && useInject) {
writer.append("new GenericType<%s>() {}.getType()", type.shortType());
} else {
writer.append("%s.class", Util.shortName(type.mainType()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ public class ClientProcessor extends AbstractProcessor {

private final ComponentMetaData metaData = new ComponentMetaData();

private boolean useJsonB;

private SimpleComponentWriter componentWriter;

private boolean readModuleInfo;
Expand All @@ -48,7 +46,6 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
APContext.init(processingEnv);
ProcessingContext.init(processingEnv, new ClientPlatformAdapter(), false);
this.componentWriter = new SimpleComponentWriter(metaData);
useJsonB = ProcessingContext.useJsonb();
}

@Override
Expand Down Expand Up @@ -103,7 +100,7 @@ private void writeClient(Element controller) {

protected String writeClientAdapter(ControllerReader reader) throws IOException {
var suffix = ClientSuffix.fromInterface(reader.beanType().getQualifiedName().toString());
return new ClientWriter(reader, suffix, useJsonB).write();
return new ClientWriter(reader, suffix).write();
}

private void initialiseComponent() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,14 @@ final class ClientWriter extends BaseControllerWriter {
private static final String AT_GENERATED = "@Generated(\"avaje-http-client-generator\")";

private final List<ClientMethodWriter> methodList = new ArrayList<>();
private final boolean useJsonb;
private final Set<String> propertyConstants = new HashSet<>();
private final String suffix;

ClientWriter(ControllerReader reader, String suffix, boolean useJsonB) throws IOException {
ClientWriter(ControllerReader reader, String suffix) throws IOException {
super(reader, suffix);
this.suffix = suffix;
reader.addImportType(HTTP_CLIENT);
this.useJsonb = useJsonB;
readMethods();
if (useJsonB) reader.addImportType("io.avaje.jsonb.Types");
}

@Override
Expand All @@ -50,7 +47,7 @@ protected String initPackageName(String originName) {
private void readMethods() {
for (final MethodReader method : reader.methods()) {
if (method.isWebMethod()) {
final var methodWriter = new ClientMethodWriter(method, writer, useJsonb, propertyConstants);
final var methodWriter = new ClientMethodWriter(method, writer, propertyConstants);
methodWriter.addImportTypes(reader);
methodList.add(methodWriter);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
<modules>
<module>test-javalin</module>
<module>test-javalin-jsonb</module>
<module>test-client</module>
<module>test-sigma</module>
</modules>

Expand All @@ -35,6 +34,7 @@
<jdk>[21,)</jdk>
</activation>
<modules>
<module>test-client</module>
<module>test-nima</module>
<module>test-jex</module>
<module>test-nima-jsonb</module>
Expand Down
14 changes: 14 additions & 0 deletions tests/test-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<artifactId>test-client</artifactId>

<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

Expand Down Expand Up @@ -54,6 +55,19 @@
<version>1.0</version>
</dependency>

<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex</artifactId>
<version>3.0-RC14</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-http-client-generator</artifactId>
<version>${project.version}</version>
</dependency>

</dependencies>

</project>
15 changes: 15 additions & 0 deletions tests/test-client/src/main/java/example/github/Generic.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package example.github;

import java.util.List;

import io.avaje.http.api.Client;
import io.avaje.http.api.Post;
import io.avaje.http.client.HttpException;

@Client
public interface Generic {

@Post("/generic")
List<GenericData<Repo>> post(GenericData<Repo> repo) throws HttpException;

}
13 changes: 13 additions & 0 deletions tests/test-client/src/main/java/example/github/GenericData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package example.github;

public class GenericData<T> {
private T data;

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}
}
22 changes: 20 additions & 2 deletions tests/test-client/src/main/java/example/github/Repo.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
package example.github;

import com.fasterxml.jackson.annotation.JsonCreator;

public class Repo {
public long id;
public String name;
private long id;
private String name;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
13 changes: 6 additions & 7 deletions tests/test-client/src/main/java/example/github/Simple.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package example.github;

//import io.avaje.http.api.Get;
//import io.avaje.http.api.Path;
import io.avaje.http.client.HttpException;

import java.util.List;

//@Path("/")
import io.avaje.http.api.Client;
import io.avaje.http.api.Get;
import io.avaje.http.client.HttpException;

@Client
public interface Simple {

//@Get("users/{user}/repos")
@Get("users/{user}/repos")
List<Repo> listRepos(String user, String other) throws HttpException;

}

This file was deleted.

This file was deleted.

Loading
Loading