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
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.