Code Design: JSON from Socket to custom handler class - java

I have an Socket-Server in Java. This socket will receive json-strings with an specific structure.
{
"command": "test",
"name": "Hallo Welt"
}
I can not change this structure. The value of "command" will declare the type of content.
After I receive this from the socket, I would like to call different handlers, to handle these different commands:
command "test" > TestHandler implements CommandHandler
command "foo" > FooHandler implements CommandHandler
How can I convert the json into a object and bind the object to the specific handler?
This is my current approach:
I have an model class called BaseCommand which contains a enum command field.
class BaseCommand {
public CommandType command;
}
class TestCommand extends BaseCommand {
public String name;
}
With GSON I parse the JSON to BaseCommand class.
After that I can read the command type.
I declare a ENUM to map the command types to the Handler:
enum CommandType {
test(TestHandler.class),
foo(FooHandler.class);
public final Class<? extends CommandHandler> handlerClass;
public CommandTypes(Class<? extends CommandHandler> handlerClass) {
this.handlerClass = handlerClass;
}
}
My handler's are implementing this interface:
public interface CommandHandler<T extends BaseCommand> {
void handle(T command);
}
Now I have the command type enum and through Google Guices MapBinder I can get the Handler instance to handle request. This works
// in class ...
private final Map<CommandType, CommandHandler> handlers;
#Inject ClassName(Map<CommandType, CommandHandler> handlers) {
this.handlers = handlers;
}
// in converter method
private void convert(String json) {
BaseCommand baseCommand = GSONHelper().fromJson(json, BaseCommand.class);
// How can I get the CommandModel?
// If the commandType is "test" how can I parse TestCommand automatically?
??? commandModel = GSONHelper().fromJson(json, ???);
handlers.get(baseCommand.command).handle(commandModel);
}
Does anyone know a solution for my problem?
Or a complete different approach for this?
best regards, Michael

How can I get the CommandModel?
If the commandType is "test" how can I parse TestCommand automatically?
You can use a TypeAdapterFactory to get the most appropriate type adapter in the most accurate and flexible way. The example below slightly differs from your classes naming, but I think it's not a big issue to you. So, let's assume you have the following command arguments DTO declarations:
abstract class AbstractCommandDto {
final String command = null;
}
final class HelloCommandDto
extends AbstractCommandDto {
final String name = null;
}
Now you can make a special TypeAdapterFactory to make a sort of looking-ahead to determine the incoming command by command arguments name. It may look complicated, but in fact TypeAdapterFactoryies are not that hard to implement. Note that JsonDeserializer might be another option for you, but then you lose automatic deserializing unless you delegate its deserialize() method to another backing Gson instance.
final class AbstractCommandDtoTypeAdapterFactory
implements TypeAdapterFactory {
// The factory handles no state and can be instantiated once
private static final TypeAdapterFactory abstractCommandDtoTypeAdapterFactory = new AbstractCommandDtoTypeAdapterFactory();
// Type tokens are used to define type information and are perfect value types so they can be instantiated once as well
private static final TypeToken<CommandProbingDto> abstractCommandProbingDtoTypeToken = new TypeToken<CommandProbingDto>() {
};
private static final TypeToken<HelloCommandDto> helloCommandDtoTypeToken = new TypeToken<HelloCommandDto>() {
};
private AbstractCommandDtoTypeAdapterFactory() {
}
static TypeAdapterFactory getAbstractCommandDtoTypeAdapterFactory() {
return abstractCommandDtoTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// First, check if the incoming type is AbstractCommandDto
if ( AbstractCommandDto.class.isAssignableFrom(typeToken.getRawType()) ) {
// If yes, then build a special type adapter for the concrete type
final TypeAdapter<AbstractCommandDto> abstractCommandDtoTypeAdapter = new AbstractCommandDtoTypeAdapter(
gson,
gson.getDelegateAdapter(this, abstractCommandProbingDtoTypeToken),
(commandName, jsonObject) -> deserialize(gson, commandName, jsonObject),
dto -> getTypeAdapter(gson, dto)
);
// Some cheating for javac...
#SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) abstractCommandDtoTypeAdapter;
return typeAdapter;
}
// If it's something else, just let Gson pick up the next type adapter
return null;
}
// Create an AbstractCommandDto instance out of a ready to use JsonObject (see the disadvantages about JSON trees below)
private AbstractCommandDto deserialize(final Gson gson, final String commandName, final JsonObject jsonObject) {
#SuppressWarnings("unchecked")
final TypeToken<AbstractCommandDto> typeToken = (TypeToken<AbstractCommandDto>) resolve(commandName);
final TypeAdapter<AbstractCommandDto> typeAdapter = gson.getDelegateAdapter(this, typeToken);
return typeAdapter.fromJsonTree(jsonObject);
}
private TypeAdapter<AbstractCommandDto> getTypeAdapter(final Gson gson, final AbstractCommandDto dto) {
#SuppressWarnings("unchecked")
final Class<AbstractCommandDto> clazz = (Class<AbstractCommandDto>) dto.getClass();
return gson.getDelegateAdapter(this, TypeToken.get(clazz));
}
// Or any other way to resolve the class. This is just for simplicity and can be even extract elsewhere from the type adapter factory class
private static TypeToken<? extends AbstractCommandDto> resolve(final String commandName)
throws IllegalArgumentException {
switch ( commandName ) {
case "hello":
return helloCommandDtoTypeToken;
default:
throw new IllegalArgumentException("Cannot handle " + commandName);
}
}
private static final class AbstractCommandDtoTypeAdapter
extends TypeAdapter<AbstractCommandDto> {
private final Gson gson;
private final TypeAdapter<CommandProbingDto> probingTypeAdapter;
private final BiFunction<? super String, ? super JsonObject, ? extends AbstractCommandDto> commandNameToCommand;
private final Function<? super AbstractCommandDto, ? extends TypeAdapter<AbstractCommandDto>> commandToTypeAdapter;
private AbstractCommandDtoTypeAdapter(
final Gson gson,
final TypeAdapter<CommandProbingDto> probingTypeAdapter,
final BiFunction<? super String, ? super JsonObject, ? extends AbstractCommandDto> commandNameToCommand,
final Function<? super AbstractCommandDto, ? extends TypeAdapter<AbstractCommandDto>> commandToTypeAdapter
) {
this.gson = gson;
this.probingTypeAdapter = probingTypeAdapter;
this.commandNameToCommand = commandNameToCommand;
this.commandToTypeAdapter = commandToTypeAdapter;
}
#Override
public void write(final JsonWriter out, final AbstractCommandDto dto)
throws IOException {
// Just pick up a delegated type adapter factory and use it
// Or just throw an UnsupportedOperationException if you're not going to serialize command arguments
final TypeAdapter<AbstractCommandDto> typeAdapter = commandToTypeAdapter.apply(dto);
typeAdapter.write(out, dto);
}
#Override
public AbstractCommandDto read(final JsonReader in) {
// Here you can two ways:
// * Either "cache" the whole JSON tree into memory (JsonElement, etc,) and simplify the command peeking
// * Or analyze the JSON token stream in a more efficient and sophisticated way
final JsonObject jsonObject = gson.fromJson(in, JsonObject.class);
final CommandProbingDto commandProbingDto = probingTypeAdapter.fromJsonTree(jsonObject);
// Or just jsonObject.get("command") and even throw abstractCommandDto, AbstractCommandProbingDto and all of it gets away
final String commandName = commandProbingDto.command;
return commandNameToCommand.apply(commandName, jsonObject);
}
}
// A synthetic class just to obtain the command field
// Gson cannot instantiate abstract classes like what AbstractCommandDto is
private static final class CommandProbingDto
extends AbstractCommandDto {
}
}
And how it's used:
public static void main(final String... args) {
// Build a command DTO-aware Gson instance
final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getAbstractCommandDtoTypeAdapterFactory())
.create();
// Build command registry
final Map<Class<?>, Consumer<?>> commandRegistry = new LinkedHashMap<>();
commandRegistry.put(HelloCommandDto.class, new HelloCommand());
// Simulate and accept a request
final AbstractCommandDto abstractCommandDto = gson.fromJson("{\"command\":\"hello\",\"name\":\"Welt\"}", AbstractCommandDto.class);
// Resolve a command
final Consumer<?> command = commandRegistry.get(abstractCommandDto.getClass());
if ( command == null ) {
throw new IllegalArgumentException("Cannot handle " + abstractCommandDto.command);
}
// Dispatch
#SuppressWarnings("unchecked")
final Consumer<AbstractCommandDto> castCommand = (Consumer<AbstractCommandDto>) command;
castCommand.accept(abstractCommandDto);
// Simulate a response
System.out.println(gson.toJson(abstractCommandDto));
}
private static final class HelloCommand
implements Consumer<HelloCommandDto> {
#Override
public void accept(final HelloCommandDto helloCommandDto) {
System.out.println("Hallo " + helloCommandDto.name);
}
}
The output:
Hallo Welt

Related

Gson - how to special processing like masking

How to configure Gson to do additional processing on the value for toJson?
public class MyClass{
#SerializedName("qwerty")
#Mask(exposeFront=2, exposeRear=2, mask="*")
private String qwerty
}
Assuming MyClass#qwerty has a value of 1234567890, how to set Gson to output {"qwerty":"12******90"}?
Gson ReflectiveTypeAdapterFactory, that is responsible for "plain" objects serialization and deserialization, is not possible to enhance to support any other annotations like #Masked. It can only use annotations like #Expose (indirectly via an exclusion strategy), #SerializedName and a few others like #Since and #Until (exclusion strategy too). Note these annotations are documented and supported by default. In general, Gson suggests using a type adapter for the declaring class, MyClass, but this also means that you must manage all fields and make sure the corresponding type adapter is updated once your class is changed. Even worse, adding a custom type adapter makes these annotations support lost.
As an another way of working around it is injecting a special string type adapter factory that can do the trick, but due to the mechanics of how it is injected, this is both limited and requires duplicating the #Masked annotation values (if you're using the annotation elsewhere in your code) and the type adapter factory configuration in #JsonAdapter.
public abstract class MaskedTypeAdapterFactory
implements TypeAdapterFactory {
private final int exposeFront;
private final int exposeRear;
private final char mask;
private MaskedTypeAdapterFactory(final int exposeFront, final int exposeRear, final char mask) {
this.exposeFront = exposeFront;
this.exposeRear = exposeRear;
this.mask = mask;
}
// must be "baked" into the class (name only represents the configuration)
public static final class _2_2_asterisk
extends MaskedTypeAdapterFactory {
private _2_2_asterisk() {
super(2, 2, '*');
}
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( typeToken.getRawType() != String.class ) {
return null;
}
#SuppressWarnings("unchecked")
final TypeAdapter<String> delegate = (TypeAdapter<String>) gson.getAdapter(typeToken);
final TypeAdapter<String> typeAdapter = new TypeAdapter<String>() {
#Override
public void write(final JsonWriter out, final String value)
throws IOException {
// mask the value
final int length = value.length();
final char[] buffer = value.toCharArray();
for ( int i = exposeFront; i < length - exposeRear; i++ ) {
buffer[i] = mask;
}
out.value(new String(buffer));
}
#Override
public String read(final JsonReader in)
throws IOException {
return delegate.read(in);
}
}
.nullSafe();
#SuppressWarnings("unchecked")
final TypeAdapter<T> adapter = (TypeAdapter<T>) typeAdapter;
return adapter;
}
}
#NoArgsConstructor
#AllArgsConstructor
final class MyClass {
#SerializedName("qwerty")
#Mask(exposeFront = 2, exposeRear = 2, mask = "*")
// unfortunately, this must duplicate the #Mask annotation values
// since type adapter (factories) do not accept supplemental information
// and Java annotations can only accept compile-time constants
#JsonAdapter(MaskedTypeAdapterFactory._2_2_asterisk.class)
#SuppressWarnings("unused")
private String qwerty;
}
Test:
public final class MaskedTypeAdapterFactoryTest {
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.create();
#Test
public void test() {
final String actual = gson.toJson(new MyClass("1234567890"));
final String expected = "{\"qwerty\":\"12******90\"}";
Assertions.assertEquals(expected, actual);
}
}
This is probably the most robust way of doing that in Gson.

Is there a function in java to check if any json attribute is null?

I have a message in JSON format that I converted to a JSONObject, and I have around 30 mandatory fields that I have to check for whether they're null or not. If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message. Is there any efficient way I can do this without going through each and every field and using isNull() ?
Also, the JSON objects are nested, so a simple anyNull() function would not work since it would only return if the object itself is null and not if the variables themselves are null.
I tried using gson to convert the message to a POJO, and created classes for 10 objects
Gson gson = new Gson();
Message message = gson.fromJson(msg, Message.class);
but since many classes are nested (and one of which is an array of objects) using simple null checkers don't work.
Actually speaking your question is not very clear because you're using a word of "message" that refers your particular class, but can also be more generic referring sent/received messages.
So something like for JSON elements in memory:
public static void failOnNullRecursively(final JsonElement jsonElement) {
if ( jsonElement.isJsonNull() ) {
throw new IllegalArgumentException("null!");
}
if ( jsonElement.isJsonPrimitive() ) {
return;
}
if ( jsonElement.isJsonArray() ) {
for ( final JsonElement element : jsonElement.getAsJsonArray() ) {
failOnNullRecursively(element);
}
return;
}
if ( jsonElement.isJsonObject() ) {
for ( final Map.Entry<String, JsonElement> e : jsonElement.getAsJsonObject().entrySet() ) {
failOnNullRecursively(e.getValue());
}
return;
}
throw new AssertionError(jsonElement);
}
or JSON documents in streams:
public final class FailOnNullJsonReader
extends JsonReader {
private FailOnNullJsonReader(final Reader reader) {
super(reader);
}
public static JsonReader create(final Reader reader) {
return new FailOnNullJsonReader(reader);
}
#Override
public void nextNull() {
throw new IllegalStateException(String.format("null at %#!", getPath()));
}
}
Both of them will throw on null. But it also seems that you want to validate Message instances:
If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message.
So this tells why the above null-checks won't fit your needs. What you're looking for is JSR-303. It won't be that efficient as you might want to want it to be (message instances are deserialized, validation takes time and resources too), but it might be efficient from the coding perspective:
final Set<ConstraintViolation<V>> violations = validator.validate(message);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
or even integrate it right into Gson so that it serves middleware:
public final class PostReadTypeAdapterFactory<V>
implements TypeAdapterFactory {
private final Predicate<? super TypeToken<?>> supports;
private final BiConsumer<? super TypeToken<V>, ? super V> onRead;
private PostReadTypeAdapterFactory(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
this.supports = supports;
this.onRead = onRead;
}
public static <V> TypeAdapterFactory create(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
return new PostReadTypeAdapterFactory<>(supports, onRead);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !supports.test(typeToken) ) {
return null;
}
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
final T readValue = delegate.read(in);
#SuppressWarnings("unchecked")
final V value = (V) readValue;
#SuppressWarnings("unchecked")
final TypeToken<V> valueTypeToken = (TypeToken<V>) typeToken;
onRead.accept(valueTypeToken, value);
return readValue;
}
};
}
}
public final class Jsr303Support {
private Jsr303Support() {
}
public static <V> TypeAdapterFactory createTypeAdapterFactory(final Validator validator) {
return PostReadTypeAdapterFactory.<V>create(
typeToken -> typeToken.getRawType().isAnnotationPresent(Validate.class),
(typeToken, value) -> {
final Set<ConstraintViolation<V>> violations = validator.validate(value);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
}
);
}
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface Validate {
}
And the test (using Lombok for brevity):
#Validate
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#NotNull
final String foo;
#NotNull
final String bar;
#NotNull
final String baz;
}
public final class Jsr303SupportTest {
private static final Validator validator;
static {
try ( final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory() ) {
validator = validatorFactory.getValidator();
}
}
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.registerTypeAdapterFactory(Jsr303Support.createTypeAdapterFactory(validator))
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final ConstraintViolationException ex = Assertions.assertThrows(ConstraintViolationException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals(1, ex.getConstraintViolations().size());
}
}
And finally, probably the most efficient (in terms of reading JSON stream), but very limited whencompared to JSR-303 (and NOT working in Gson because Gson does not propagate null-checking to downstream (de)serializers), way that could replace #NotNull with a similar "functional" annotation:
public final class NotNullTypeAdapterFactory
implements TypeAdapterFactory {
// note no external access
private NotNullTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final TypeAdapter<T> delegate = gson.getAdapter(typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, #Nullable final T value)
throws IOException {
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null");
}
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
#Nullable
final T value = delegate.read(in);
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null at " + in.getPath());
}
return value;
}
};
}
}
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String foo;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String bar;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String baz;
}
public final class NotNullTypeAdapterFactoryTest {
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals("whatever here, the above does not work anyway", ex.getMessage());
}
}
The third, JSR-303, looks like the best for you.

Gson: JSON deserialization issue

I'm trying to deserialize a JSON file using custom deserialization from Gson but I guess I'm failing to do it, nothing is being deserialized. Here's my code
Please also guide me if I'm using incorrect return type. I'm not sure if I'm doing it correctly as well.
GsonHelper.java
public <T> T ProcessData(Class<T> ClassType, String Data)
{
Gson gson = new GsonBuilder().registerTypeAdapter(ClassType , new
JsonDeserializerHelper(ClassType)).create();
return gson.fromJson(Data,ClassType);
}
JsonDeserializerHelper.java
public class JsonDeserializerHelper implements JsonDeserializer {
private Class<?> InstantiatedClass;
private static final Logger logger =
Logger.getLogger(JsonDeserializerHelper.class.getName());
public JsonDeserializerHelper(Class instantiatedClass) {
this.InstantiatedClass = instantiatedClass;
}
#Override
public Object deserialize(JsonElement json, Type type,
JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = (JsonObject) json;
// Implementation here
}
return null;
}
}
Also whenever I try to return a generic type of T, I can't .
What I try to achieve is to have a generic function that returns any class based on the instantiated class in it's private class.
The variable that is consuming the functions afterall
Class<?> AnonymousClass = Class.forName(ClassName);
ProcessClassHelper helperClass = new ProcessClassHelper();
Object ParsedData = (AccountInfo)helperClass.ProcessData(AnonymousClass,json);
ArrayList<AccountInfo> test = (ArrayList<AccountInfo>) ParsedData;
for (AccountInfo acc : test)
{
logger.info("First Name: " + acc.getFirstName());
}
Also the last logger outputs nothing.

Convert json response to null or void object

My code gets the response of an HTTP call and converts the string to a Json object.
return gson.fromJson( String.valueOf(execute( requestInfo.getHttpRequest() )), requestInfo.getResponseType() );
However, the gson.fromJson method, requires you to specify the object to which you want to convert it to.
I do not want any response class for this particular call, and would like to return void/null. However, since this is a shared method across multiple methods, I have to pass in some class. At the moment, I am passing in this,
private static final Type RETURN_TYPE = new TypeToken<NameOfMyResponseClass>() {
}.getType();
How can I replace this will null or void, so that nothing is returned?
PS : The reason I want to pass null is that, the response for this method contains a ton of members, and I do not want to create a POJO having all these members initialized so I can convert to a gson object, which is not even useful for me.
Ideally, if there was something like, that's what I am looking for.
private static final Type RETURN_TYPE = new TypeToken<Void>() {
}.getType();
It looks like you simply use:
private static <T> T fromJson(final String json, final Type type) {
if ( type == null ) {
return null;
}
return gson.fromJson(json, type);
}
If, for some justified reason, you cannot pass the null to the fromJson method, you can create a Void and void-friendly type adapter and bind it to your Gson instance (of course, you cannot return a void "value"):
final class VoidTypeAdapter
extends TypeAdapter<Void> {
private static final TypeAdapter<Void> voidTypeAdapter = new VoidTypeAdapter();
private VoidTypeAdapter() {
}
static TypeAdapter<Void> getVoidTypeAdapter() {
return voidTypeAdapter;
}
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final Void value)
throws IOException {
out.nullValue();
}
#Override
public Void read(final JsonReader in)
throws IOException {
// Skip the current JSON tokens stream value entirely
in.skipValue();
return null;
}
}
private static final Gson gson = new GsonBuilder()
.registerTypeAdapter(Void.class, getVoidTypeAdapter())
.registerTypeAdapter(void.class, getVoidTypeAdapter())
.create();
private static <T> T fromJson(final String json, final Type type) {
return gson.fromJson(json, type);
}
private static String toJson(final Object object, final Type type) {
return gson.toJson(object, type);
}
So a simple test might look like this:
private static void test(final Type type) {
System.out.println(type);
final Object value = fromJson("[\"foo\",\"bar\"]", type);
System.out.println("-\t" + value);
System.out.println("-\t" + toJson(value, type));
}
public static void main(final String... args) {
test(new TypeToken<List<String>>() {}.getType());
test(Void.class);
test(void.class);
}
Output:
java.util.List
- [foo, bar]
- ["foo","bar"]
class java.lang.Void
- null
- null
void
- null
- null
Note that type tokens are mostly used to build a type information for generic types. In more simple cases you can use .class to get Class<?>: int.class, Integer.class, void.class, Void.class, int[][][][][].class, etc.

Android using Retrofit with generic type

Is it possible to using retrofit in a manner that uses generic type?
for example something like this:
public interface RetroInterface<T> {
#GET("content/{id}")
T getById(#Path("id") int id);
}
I've read that Retrofit uses the method signature to determine the return Type at runtime, in that case is it even possible to have generic interface such as above?
Yes I think it's possible but be carefull retrofit return some Call So you can create an interface with Call<T> like method except
But Have you really need to create a template for a service ? Because in you get annotations you ask to server one specific ressource so you known the type of response
The only way to pass that info I can think of is introducing a wrapper to hold both value and its type (or type token to simplify Gson).
final class GenericBody<T> {
final T body;
final TypeToken<T> typeToken;
GenericBody(final T body, final TypeToken<T> typeToken) {
this.body = body;
this.typeToken = typeToken;
}
}
Then an example service might be declared as follows:
interface IGenericService {
#POST("/")
Call<Void> post(#Body #SuppressWarnings("rawtypes") GenericBody genericBody);
}
Here, the Call is declared to return nothing, and genericBody is intentionally made raw-typed to let it pass Retrofit validation.
Next, the Gson part.
final class GenericBodyTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory genericBodyTypeAdapterFactory = new GenericBodyTypeAdapterFactory();
private GenericBodyTypeAdapterFactory() {
}
static TypeAdapterFactory getGenericBodyTypeAdapterFactory() {
return genericBodyTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !GenericBody.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
final TypeAdapter<GenericBody<T>> genericBodyTypeAdapter = new TypeAdapter<GenericBody<T>>() {
#Override
public void write(final JsonWriter out, final GenericBody<T> value)
throws IOException {
final T body = value.body;
final TypeAdapter<T> typeAdapter = gson.getDelegateAdapter(GenericBodyTypeAdapterFactory.this, value.typeToken);
typeAdapter.write(out, body);
}
#Override
public GenericBody<T> read(final JsonReader in) {
throw new UnsupportedOperationException();
}
};
#SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) genericBodyTypeAdapter;
return typeAdapter;
}
}
What it does it is:
checks if it can handle GenericBody instances;
resolves appropriate type adapters for the by the bound type token;
writes the generic body value to the output.
No read is implemented.
Example of use (full of mocks (staticResponse(applicationJsonMediaType, "OK")) that can be easily translated to your code):
private static final TypeToken<List<String>> stringListTypeToken = new
TypeToken<List<String>>() {
};
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getGenericBodyTypeAdapterFactory())
.create();
private static final OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(staticResponse(applicationJsonMediaType, "OK"))
.build();
private static final Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://whatever")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
private static final IGenericService genericService =
retrofit.create(IGenericService.class);
public static void main(final String... args)
throws IOException {
final GenericBody<List<String>> body = new GenericBody<>(asList("foo", "bar", "baz"),
stringListTypeToken);
genericService.post(body).execute();
}
This would write ["foo","bar","baz"] to the output stream respecting properly configured Gson (de)serialization strategies.

Categories

Resources