Multiple converters with Retrofit 2
Asked Answered
A

4

20

I have a HATEOAS (HAL) REST service and managed to talk to it with the code below (using halarious as a conversion engine) but when I try to merge the converters (stallone and stallone2), the app will always pick up the first converter, instead of the one that is appropriate for the response type which of course leads to an error.

How could I avoid duplicate retrofits that are only different in a small type detail?

public interface Stallone {
   @GET("/discovery")
   Call<DiscoveryResponse> discover();
   @POST()
   Call<LoginResponse> login(@Url String url, @Body LoginRequest secret);
}
   public static void main(String... args) throws IOException {
      // Initialize a converter for each supported (return) type
      final Stallone stallone = new Retrofit.Builder()
         .baseUrl(BASE)
         .addConverterFactory(HALConverterFactory.create(DiscoveryResponse.class))
         .build().create(Stallone.class);
      final Stallone stallone2 = new Retrofit.Builder()
         .baseUrl(BASE)
         .addConverterFactory(HALConverterFactory.create(LoginResponse.class))
         .build().create(Stallone.class);

      // Follow the HAL links
      Response<DiscoveryResponse> response = stallone.discover().execute();
      System.out.println(response.code() + " " + response.message());
      Assert.assertNotNull(response.body());
      String loginPath = response.body().getLogin();
      Assert.assertEquals(loginPath, "/login");

      // Follow another link
      if (loginPath.startsWith("/"))
         loginPath = loginPath.substring(1);
      Response<LoginResponse> response2 =
         stallone2.login(loginPath,
                        new LoginRequest(AUTH0TOKEN, null)).execute();
      System.out.println(response2.code() + " " + response2.message());
      Assert.assertNotNull(response2.body());

      String setupPath = response2.body().getSetup();
      Assert.assertEquals(setupPath, "/setup");

      System.out.println("All OK!");
   }
public final class HALConverterFactory extends Converter.Factory {

   private final Gson gson;

   public static HALConverterFactory create(Class<?> type) {
      return new HALConverterFactory(type);
   }

   private HALConverterFactory(Class<?> type) {
      if (!HalResource.class.isAssignableFrom(type))
         throw new NullPointerException("Type should be a subclass of HalResource");
      GsonBuilder builder = new GsonBuilder();
      builder.registerTypeAdapter(HalResource.class, new HalSerializer());
      builder.registerTypeAdapter(HalResource.class, new HalDeserializer(type));
      builder.setExclusionStrategies(new HalExclusionStrategy());
      this.gson = builder.create();
   }

   @Override
   public Converter<ResponseBody, ?> fromResponseBody(Type type, Annotation[] annotations) {
      return new HALResponseBodyConverter<>(gson);
   }

   @Override public Converter<?, RequestBody> toRequestBody(Type type, Annotation[] annotations) {
      return new GsonRequestBodyConverter<>(gson, type);
   }
}
final class HALResponseBodyConverter<T extends HalResource>
   implements Converter<ResponseBody, T> {
   private final Gson gson;

   HALResponseBodyConverter(Gson gson) {
      this.gson = gson;
   }

   @Override public T convert(ResponseBody value) throws IOException {
      BufferedSource source = value.source();
      try {
         String s = source.readString(Charset.forName("UTF-8"));
         return (T) gson.fromJson(s, HalResource.class);
      } catch (Exception e) {
         throw new RuntimeException(e);
      } finally {
         closeQuietly(source);
      }
   }

   private static void closeQuietly(Closeable closeable) {
      if (closeable == null) return;
      try {
         closeable.close();
      } catch (IOException ignored) {
      }
   }
}

Again, the problem is that when you try to shorten the above like this:

  final Stallone stallone = new Retrofit.Builder()
     .baseUrl(BASE)
.addConverterFactory(HALConverterFactory.create(DiscoveryResponse.class))
     .addConverterFactory(HALConverterFactory.create(LoginResponse.class))
     .build().create(Stallone.class);

you'll get an exception at the Response<LoginResponse> response2 = ... line:

Exception in thread "main" java.lang.ClassCastException: com.example.retrofit.DiscoveryResponse cannot be cast to com.example.retrofit.LoginResponse

Asteria answered 29/10, 2015 at 2:0 Comment(1)
What's GsonRequestBodyConverter?Dreyfus
W
21

You need to return null from the Converter.Factory if the type does not match. Keep the Class<?> around in a field to compare it against.

@Override
public Converter<ResponseBody, ?> fromResponseBody(Type type, Annotation[] annotations) {
  if (!this.type.equals(type)) {
    return null;
  }
  return new HALResponseBodyConverter<>(gson);
}

This will allow multiple instances to be used because each only applies to its own type.

That said, however, you can probably get away with only using a single converter and pulling the class from the Type that is passed in.

@Override
public Converter<ResponseBody, ?> fromResponseBody(Type type, Annotation[] annotations) {
  if (!HALResponse.class.isAssignableFrom(type)) {
    return null;
  }
  // TODO create converter with `type` now that you know what it is...
}

You can look at the Wire converter in the repo which does this for a full example.

Winona answered 1/11, 2015 at 3:59 Comment(0)
R
0
package ch.halarious.core;

import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * Custom Hal Deserializer  
 *
 * @author jaren
 */
public class CustomHalDeserializer extends HalDeserializer {

    /**
     * Intialisiert ein HalDeserializer-Objekt
     *
     * @param targetType Typ, den wir eigentlich deserialisieren sollten
     */
    public CustomHalDeserializer(Class<?> targetType) {
        super(targetType);
    }

    class CustomArrayList extends ArrayList implements HalResource{}

    public HalResource deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context, Class<?> targetType) throws JsonParseException {
        // Es handelt sich um ein JSON-Objekt.
        JsonObject jsonObject = json.getAsJsonObject();
        JsonObject embeddedRoot = jsonObject.getAsJsonObject(HalConstants.EMBEDDED_ROOT);

        if(embeddedRoot != null){
            Set<Map.Entry<String, JsonElement>> set = embeddedRoot.entrySet();
            if(set.toArray().length == 1){
                JsonArray ja = embeddedRoot.getAsJsonArray(set.iterator().next().getKey());
                if(ja.isJsonArray()) {
                    CustomArrayList arrayResult = new CustomArrayList();
                    Iterator<JsonElement> i = ja.iterator();
                    while(i.hasNext()){
                        JsonElement je = i.next();
                        arrayResult.add(super.deserialize(je, typeOfT, context, targetType));
                    }
                    return arrayResult;
                }
            }
        }

        return super.deserialize(json, typeOfT, context, targetType);
    }
}
Raymund answered 3/2, 2016 at 2:0 Comment(0)
L
0

I did almost the same as @jake-wharton said in https://mcmap.net/q/623407/-multiple-converters-with-retrofit-2 but added some changes:

public class GenericConverterFactory<T> extends Converter.Factory {

    private final Class<T> clazz;

    public static GenericConverterFactory create(Class<T> clazz) {
        return new GenericConverterFactory(clazz);
    }

    private GenericConverterFactory(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        if (!isNeededType(type)) {
            return null;
        }

        // some converter that knows how to return your specific type T
        return new GenericConverter(clazz);
    }

    private boolean isNeededType(Type type) {
        if(type instanceof GenericArrayType) {
            // if type is array we should check if it has the same component as our factory clazz
            // if our factory clazz is not array getComponentType will return null
            return ((GenericArrayType) type).getGenericComponentType().equals(clazz.getComponentType());
        } else if(clazz.getComponentType() == null) {
            // if factory clazz is not array and type is not array too
            // type is just a Class<?> and we should check if they are equal
            return clazz.equals(type);
        } else {
            // otherwise our clazz is array and type is not
            return false;
        }
    }
}

Type is coming from retrofit interface for example if you have:

public interface SomeApi{
     @GET("customelement")
     CustomElement[] getCustomElements();
     @GET("customelement/{id}")
     CustomElement getCustomElement(@Path("id") int id);
}

For method getCustomElements() type will be GenericArrayType with GenericComponentType as CustomElement.class and for second method type will be just CustomElement.class

Not sure whether it is the best solution but for me it works. Hope it helps.

Lorineloriner answered 9/3, 2016 at 9:37 Comment(0)
I
0

In my case I needed to serialize and deserialize only one class to XML. For everything else I needed Json. So I registered my adapters like this:

retrofit = new Retrofit.Builder()
                .baseUrl(BuildConfig.BASE_URL)
                .addConverterFactory(EditUserXmlConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(createGson()))
                .client(httpClient.build())
                .build();

since I could not extend SimpleXmlConverterFactory (unfortunately) I had to use my own class and change the following line:

if (!(type instanceof Class)) return null;

to

if (type != NeedToBeXML.class) return null;

This way only responses and requests of type NeedToBeXML are converted to XML - and everything else JSON.

Increate answered 2/9, 2016 at 12:4 Comment(2)
Where can I find EditUserXmlConverterFactory?Nonmetal
Also, where to write this: if (type != NeedToBeXML.class) return null; ?Nonmetal

© 2022 - 2024 — McMap. All rights reserved.