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.
Related
Here is the json schema:
As you can see, rated can be both boolean and object.
I am using Retrofit 2 and Gson converter. How should I create my model for this schema?
Here's how I solved this issue:
Create a custom type adapter in your model and parse rated manually;
public class AccountState {
//#SerializedName("rated") //NOPE, parse it manually
private Integer mRated; //also don't name it rated
public Integer getRated() {
return mRated;
}
public void setRated(Integer rated) {
this.mRated = rated;
}
public static class AccountStateDeserializer implements JsonDeserializer<AccountState> {
#Override
public AccountState deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
AccountState accountState = new Gson().fromJson(json, AccountState.class);
JsonObject jsonObject = json.getAsJsonObject();
if (jsonObject.has("rated")) {
JsonElement elem = jsonObject.get("rated");
if (elem != null && !elem.isJsonNull()) {
if(elem.isJsonPrimitive()){
accountState.setRated(null);
}else{
accountState.setRated(elem.getAsJsonObject().get("value").getAsInt());
}
}
}
return accountState ;
}
}
}
Here you create your gson with this custom adapter:
final static Gson gson = new GsonBuilder()
.registerTypeAdapter(AccountState.class, new AccountState.AccountStateDeserializer())
.create();
Add it to retrofit like that:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.ENDPOINT)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build();
TADADADADADADADDAD!
You can make it work without having to implement a custom converter.
All you have to do is put a general "Object" type for the variable and then you just check which data type it is by doing this:
if(object.getClass == YourClass.class){
Whatever we = ((YourClass) object).getWhatever();
} else if(object.getClass == YourOtherClass.class){
String name = ((YourOtherClass) object).getName();
}
You can add as many data types to this variable as you like.
You can also use the java types "String.class", "Boolean.class" or whatever you like.
Gson has a nice feature allowing to inject a custom type adapter or a type adapter factory to a certain field therefore letting Gson to manage the host object and the latter's fields (de)serialization. So, you can be sure that AccountState could be still deserialized with ReflectiveTypeAdapterFactory and ReflectiveTypeAdapterFactory.Adapter so all deserialization strategies defined in GsonBuilder could be applied.
final class AccountState {
// This is what can make life easier. Note its advantages:
// * PackedBooleanTypeAdapterFactory can be reused multiple times
// * AccountState life-cycle can be managed by Gson itself,
// so it can manage *very* complex deserialization automatically.
#JsonAdapter(PackedBooleanTypeAdapterFactory.class)
final Boolean rated = null;
}
Next, how PackageBooleanTypeAdapterFactory is implemented:
final class PackedBooleanTypeAdapterFactory
implements TypeAdapterFactory {
// Gson can instantiate this itself, no need to expose
private PackedBooleanTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Check if it's the type we can handle ourself
if ( typeToken.getRawType() == Boolean.class ) {
final TypeAdapter<Boolean> typeAdapter = new PackedIntegerTypeAdapter(gson);
// Some Java "unchecked" boilerplate here...
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
return castTypeAdapter;
}
// If it's something else, let Gson pick a downstream type adapter on its own
return null;
}
private static final class PackedIntegerTypeAdapter
extends TypeAdapter<Boolean> {
private final Gson gson;
private PackedIntegerTypeAdapter(final Gson gson) {
this.gson = gson;
}
#Override
public void write(final JsonWriter out, final Boolean value) {
throw new UnsupportedOperationException();
}
#Override
public Boolean read(final JsonReader in)
throws MalformedJsonException {
// Pick next token as a JsonElement
final JsonElement jsonElement = gson.fromJson(in, JsonElement.class);
// Note that Gson uses JsonNull singleton to denote a null
if ( jsonElement.isJsonNull() ) {
return null;
}
if ( jsonElement.isJsonPrimitive() ) {
return jsonElement
.getAsJsonPrimitive()
.getAsBoolean();
}
if ( jsonElement.isJsonObject() ) {
return jsonElement
.getAsJsonObject()
.getAsJsonPrimitive("value")
.getAsBoolean();
}
// Not something we can handle
throw new MalformedJsonException("Cannot parse: " + jsonElement);
}
}
}
Demo:
public static void main(final String... args) {
parseAndDump("{\"rated\":null}");
parseAndDump("{\"rated\":true}");
parseAndDump("{\"rated\":{\"value\":true}}");
}
private static void parseAndDump(final String json) {
final AccountState accountState = gson.fromJson(json, AccountState.class);
System.out.println(accountState.rated);
}
Output:
null
true
true
Note that JsonSerializer and JsonDeserializer both have some performance and memory cost due to its tree model design (you can traverse JSON trees easily as long as they are in memory). Sometimes, for simple cases, a streaming type adapter may be preferable. Pros: consumes less memory and works faster. Cons: hard to implement.
final class AccountState {
#JsonAdapter(PackedBooleanTypeAdapter.class)
final Boolean rated = null;
}
Note that the rated field accepts a type adapter directly because it does not need Gson instances to build JSON trees (JsonElements).
final class PackedBooleanTypeAdapter
extends TypeAdapter<Boolean> {
// Gson still can instantiate this type adapter itself
private PackedBooleanTypeAdapter() {
}
#Override
public void write(final JsonWriter out, final Boolean value) {
throw new UnsupportedOperationException();
}
#Override
public Boolean read(final JsonReader in)
throws IOException {
// Peeking the next JSON token and dispatching parsing according to the given token
final JsonToken token = in.peek();
switch ( token ) {
case NULL:
return parseAsNull(in);
case BOOLEAN:
return parseAsBoolean(in);
case BEGIN_OBJECT:
return parseAsObject(in);
// The below might be omitted, since some code styles prefer all switch/enum constants explicitly
case BEGIN_ARRAY:
case END_ARRAY:
case END_OBJECT:
case NAME:
case STRING:
case NUMBER:
case END_DOCUMENT:
throw new MalformedJsonException("Cannot parse: " + token);
// Not a known token, and must never happen -- something new in a newer Gson version?
default:
throw new AssertionError(token);
}
}
private Boolean parseAsNull(final JsonReader in)
throws IOException {
// null token still has to be consumed from the reader
in.nextNull();
return null;
}
private Boolean parseAsBoolean(final JsonReader in)
throws IOException {
// Consume a boolean value from the reader
return in.nextBoolean();
}
private Boolean parseAsObject(final JsonReader in)
throws IOException {
// Consume the begin object token `{`
in.beginObject();
// Get the next property name
final String property = in.nextName();
// Not a value? Then probably it's not what we're expecting for
if ( !property.equals("value") ) {
throw new MalformedJsonException("Unexpected property: " + property);
}
// Assuming the property "value" value must be a boolean
final boolean value = in.nextBoolean();
// Consume the object end token `}`
in.endObject();
return value;
}
}
This one should work faster. The output remains the same. Note that Gson does not require a GsonBuilder for both cases. As far as I remember how Retrofit 2 works, GsonConverterFactory is still required (not sure, Gson is not the default serializer in Retrofit 2?).
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.
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.
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.
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