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
GsonRequestBodyConverter
? – Dreyfus