2015-10-29 37 views
14

Ho un servizio REST HATEOAS (HAL) e sono riuscito a parlare con il codice qui sotto (utilizzando come motore di conversione), ma quando provo a merge the converters (stallone e stallone2), l'applicazione sarà sempre salire il primo convertitore, invece di quello appropriato per il tipo di risposta che, naturalmente, porta a un errore.più convertitori con retrofit 2

Come è possibile evitare duplicati di retrofit che sono diversi solo in un piccolo dettaglio di tipo?

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) { 
     } 
    } 
} 

Anche in questo caso, il problema è che quando si tenta di ridurre quanto sopra in questo modo:

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

si otterrà un'eccezione al Response<LoginResponse> response2 = ... riga:

Exception in thread "main" java.lang.ClassCastException: com.example.retrofit.DiscoveryResponse non può essere lanciato a com.example.retrofit.LoginResponse

+1

Che cos'è 'GsonRequestBodyConverter'? – naXa

risposta

18

È necessario tornare null dal Converter.Factory se il tipo non corrisponde. Mantenere lo Class<?> in un campo per confrontarlo.

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

Ciò consentirà l'utilizzo di più istanze poiché ciascuna si applica solo al proprio tipo.

Detto questo, però, probabilmente si può ottenere via con solo con un singolo convertitore e tirando la classe dalla Type che viene passato.

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

Potete guardare il convertitore Wire in pronti contro termine, che fa questo per un esempio completo.

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); 
    } 
} 
0

ho fatto quasi la stessa @ jake-Wharton ha detto in https://stackoverflow.com/a/33459073/2055854 ma ha aggiunto alcune modifiche:

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; 
     } 
    } 
} 

tipo viene da un'interfaccia retrofit per esempio se si dispone di:

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

Per il metodo getCustomElements() il tipo sarà GenericArrayType10 con GenericComponentType come CustomElement.class e per il secondo tipo di metodo sarà solo CustomElement.class

Non sono sicuro se sia la soluzione migliore ma per me funziona. Spero che sia d'aiuto.

0

Nel mio caso avevo bisogno di serializzare e deserializzare solo una classe in XML. Per tutto il resto avevo bisogno di Json.Così ho registrato i miei adattatori come questo:

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

perché non ho potuto estendere SimpleXmlConverterFactory (purtroppo) ho dovuto usare la mia classe e cambiare la seguente riga:

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

a

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

In questo modo solo le risposte e le richieste di tipo NeedToBeXML vengono convertite in XML e tutto il resto JSON.