Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Retrofit 2 multiple converters (Gson & SimpleXML) error

In my project (I am using Dagger 2, Retrofit 2 & OkHTTP, RxAndroid) i have 2 different API calls. From one I receive JSON, from other - XML. I studied this link and added 2 converters that i need to my Retrofit.Builder():

@Provides
@Singleton
public Gson providesGson() {
    return new GsonBuilder().create();
}

@Provides
@Singleton
public Retrofit providesRetrofit(@NonNull OkHttpClient okHttpClient, @NonNull Gson gson) {
    return new Retrofit.Builder()
            .baseUrl(ConstantsManager.BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(SimpleXmlConverterFactory.create())
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .build();
}

But then, when I receive JSON (XML gets converted correctly), I get the following:

D/OkHttp: {"date":"20.11.2016","bank":"PB","baseCurrency":980,"baseCurrencyLit":"UAH","exchangeRate":[{"baseCurrency":"UAH","currency":"AUD","saleRateNB":19.4452740,"purchaseRateNB":19.4452740},{"baseCurrency":"UAH","currency":"CAD","saleRateNB":19.4047320,"purchaseRateNB":19.4047320},{"baseCurrency":"UAH","currency":"CZK","saleRateNB":1.0322170,"purchaseRateNB":1.0322170,"saleRate":1.0800000,"purchaseRate":0.9800000},{"baseCurrency":"UAH","currency":"DKK","saleRateNB":3.7519280,"purchaseRateNB":3.7519280},{"baseCurrency":"UAH","currency":"HUF","saleRateNB":0.0902556,"purchaseRateNB":0.0902556},{"baseCurrency":"UAH","currency":"ILS","saleRateNB":6.7524710,"purchaseRateNB":6.7524710,"saleRate":7.0000000,"purchaseRate":6.3000000},{"baseCurrency":"UAH","currency":"JPY","saleRateNB":0.2384005,"purchaseRateNB":0.2384005,"saleRate":0.2500000,"purchaseRate":0.2200000},{"baseCurrency":"UAH","currency":"LVL","saleRateNB":0.2384005,"purchaseRateNB":0.2384005},{"baseCurrency":"UAH","currency":"LTL","saleRateNB":0.2384005,"purchaseRateNB":0.2384005},{"baseCurrency":"UAH","currency":"NOK","saleRateNB":3.0724120,"purchaseRateNB":3.0724120,"saleRate":3.2000000,"purchaseRate":2.9000000},{"baseCurrency":"UAH","currency":"SKK","saleRateNB":3.0724120,"purchaseRateNB":3.0724120},{"baseCurrency":"UAH","currency":"SEK","saleRateNB":2.8384710,"purchaseRateNB":2.8384710},{"baseCurrency":"UAH","currency":"CHF","saleRateNB":26.0049080,"purchaseRateNB":26.0049080,"saleRate":27.5000000,"purchaseRate":25.0000000},{"baseCurrency":"UAH","currency":"RUB","saleRateNB":0.4013400,"purchaseRateNB":0.4013400,"saleRate":0.4200000,"purchaseRate":0.4000000},{"baseCurrency":"UAH","currency":"GBP","saleRateNB":32.4460750,"purchaseRateNB":32.4460750,"saleRate":34.0000000,"purchaseRate":31.0000000},{"baseCurrency":"UAH","currency":"USD","saleRateNB":26.0534380,"purchaseRateNB":26.0534380,"saleRate":27.0000000,"purchaseRate":26.6000000},{"baseCurrency":"UAH","currency":"BYR","saleRateNB":26.0534380,"purchaseRateNB":26.0534380},{"baseCurrency":"UAH","currency":"EUR","saleRateNB":27.9214700,"purchaseRateNB":27.9214700,"saleRate":28.6000000,"purchaseRate":28.2000000},{"baseCurrency":"UAH","currency":"GEL","saleRateNB":10.5921530,"purchaseRateNB":10.5921530},{"baseCurrency":"UAH","currency":"PLZ","saleRateNB":6.2818280,"purchaseRateNB":6.2818280,"saleRate":6.6000000,"purchaseRate":5.9000000}]}
D/OkHttp: <-- END HTTP (2377-byte body)
E/DateCurrencyService: Error while loading data occurred!
                   java.lang.RuntimeException: org.xmlpull.v1.XmlPullParserException: Unexpected token (position:TEXT {"date":"20.11.2...@1:2378 in java.io.InputStreamReader@2ec8965) 
                       at retrofit2.converter.simplexml.SimpleXmlResponseBodyConverter.convert(SimpleXmlResponseBodyConverter.java:44)
                       at retrofit2.converter.simplexml.SimpleXmlResponseBodyConverter.convert(SimpleXmlResponseBodyConverter.java:23)
                       at retrofit2.ServiceMethod.toResponse(ServiceMethod.java:117)
                       at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:211)
                       at retrofit2.OkHttpCall.execute(OkHttpCall.java:174)
                       at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$RequestArbiter.request(RxJavaCallAdapterFactory.java:171)
                       at rx.internal.operators.OperatorSubscribeOn$1$1$1.request(OperatorSubscribeOn.java:80)
                       at rx.Subscriber.setProducer(Subscriber.java:209)
                       at rx.Subscriber.setProducer(Subscriber.java:205)
                       at rx.Subscriber.setProducer(Subscriber.java:205)
                       at rx.internal.operators.OperatorSubscribeOn$1$1.setProducer(OperatorSubscribeOn.java:76)
                       at rx.Subscriber.setProducer(Subscriber.java:205)
                       at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:152)
                       at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:138)
                       at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:50)
                       at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
                       at rx.Observable.unsafeSubscribe(Observable.java:8666)
                       at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94)
                       at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:220)
                       at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
                       at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:428)
                       at java.util.concurrent.FutureTask.run(FutureTask.java:237)
                       at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:272)
                       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
                       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
                       at java.lang.Thread.run(Thread.java:761)
                    Caused by: org.xmlpull.v1.XmlPullParserException: Unexpected token (position:TEXT {"date":"20.11.2...@1:2378 in java.io.InputStreamReader@2ec8965) 
                       at org.kxml2.io.KXmlParser.next(KXmlParser.java:432)
                       at org.kxml2.io.KXmlParser.next(KXmlParser.java:313)
                       at org.simpleframework.xml.stream.PullReader.read(PullReader.java:105)
                       at org.simpleframework.xml.stream.PullReader.next(PullReader.java:89)
                       at org.simpleframework.xml.stream.NodeReader.readElement(NodeReader.java:111)
                       at org.simpleframework.xml.stream.NodeReader.readRoot(NodeReader.java:85)
                       at org.simpleframework.xml.stream.NodeBuilder.read(NodeBuilder.java:84)
                       at org.simpleframework.xml.stream.NodeBuilder.read(NodeBuilder.java:71)
                       at org.simpleframework.xml.core.Persister.read(Persister.java:562)
                       at retrofit2.converter.simplexml.SimpleXmlResponseBodyConverter.convert(SimpleXmlResponseBodyConverter.java:36)
                       at retrofit2.converter.simplexml.SimpleXmlResponseBodyConverter.convert(SimpleXmlResponseBodyConverter.java:23) 
                       at retrofit2.ServiceMethod.toResponse(ServiceMethod.java:117) 
                       at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:211) 
                       at retrofit2.OkHttpCall.execute(OkHttpCall.java:174) 
                       at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$RequestArbiter.request(RxJavaCallAdapterFactory.java:171) 
                       at rx.internal.operators.OperatorSubscribeOn$1$1$1.request(OperatorSubscribeOn.java:80) 
                       at rx.Subscriber.setProducer(Subscriber.java:209) 
                       at rx.Subscriber.setProducer(Subscriber.java:205) 
                       at rx.Subscriber.setProducer(Subscriber.java:205) 
                       at rx.internal.operators.OperatorSubscribeOn$1$1.setProducer(OperatorSubscribeOn.java:76) 
                       at rx.Subscriber.setProducer(Subscriber.java:205) 
                       at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:152) 
                       at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:138) 
                       at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:50) 
                       at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30) 
                       at rx.Observable.unsafeSubscribe(Observable.java:8666) 
                       at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94) 
                       at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:220) 
                       at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) 
                       at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:428) 
                       at java.util.concurrent.FutureTask.run(FutureTask.java:237) 
                       at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:272) 
                       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133) 
                       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) 
                       at java.lang.Thread.run(Thread.java:761) 

As i understood what is going on there is that Simple XML Converter tries to convert the JSON response i get from second call (XML response from first call, as i said earlier, get converted correctly). What i need to do is to convert each response with appropriate converter (XML response with the Simple XML Converter and JSON response with the Gson Converter)

If i try to switch the order of adding converters i will get a similar error when receiving XML (because Gson converter tries to convert XML response, obviously):

D/OkHttp: <exchangerate><exchangerate ccy="EUR" ccy_name_ru="Евро                               " ccy_name_ua="Євро                               " ccy_name_en="Euro                               " base_ccy="UA" buy="27247312" unit="100.00000" date="2016.11.29" /><exchangerate ccy="RUR" ccy_name_ru="Российский рубль                   " ccy_name_ua="Росiйський рубль                   " ccy_name_en="Russian Rouble                     " base_ccy="UA" buy="39810" unit="10.00000" date="2016.11.29" /><exchangerate ccy="USD" ccy_name_ru="Доллар США                         " ccy_name_ua="Долар США                          " ccy_name_en="US Dollar                          " base_ccy="UA" buy="25724426" unit="100.00000" date="2016.11.29" /></exchangerate>
D/OkHttp: <-- END HTTP (799-byte body)
E/SyncService: Error while loading data occurred!
           com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $
               at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1559)
               at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1401)
               at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:593)
               at com.google.gson.stream.JsonReader.peek(JsonReader.java:425)
               at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:205)
               at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:37)
               at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:25)
               at retrofit2.ServiceMethod.toResponse(ServiceMethod.java:117)
               at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:211)
               at retrofit2.OkHttpCall.execute(OkHttpCall.java:174)
               at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$RequestArbiter.request(RxJavaCallAdapterFactory.java:171)
               at rx.internal.operators.OperatorSubscribeOn$1$1$1.request(OperatorSubscribeOn.java:80)
               at rx.Subscriber.setProducer(Subscriber.java:209)
               at rx.Subscriber.setProducer(Subscriber.java:205)
               at rx.Subscriber.setProducer(Subscriber.java:205)
               at rx.internal.operators.OperatorSubscribeOn$1$1.setProducer(OperatorSubscribeOn.java:76)
               at rx.Subscriber.setProducer(Subscriber.java:205)
               at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:152)
               at retrofit2.adapter.rxjava.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:138)
               at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:50)
               at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
               at rx.Observable.unsafeSubscribe(Observable.java:8666)
               at rx.internal.operators.OperatorSubscribeOn$1.call(OperatorSubscribeOn.java:94)
               at rx.internal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:220)
               at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
               at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:428)
               at java.util.concurrent.FutureTask.run(FutureTask.java:237)
               at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:272)
               at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
               at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
               at java.lang.Thread.run(Thread.java:761)

What can I do to make each converter convert the appropriate response?

UPD: As was suggested by Marcin Jedynak i did the following:

1) Added custom Converter class:

public class RetrofitUniversalConverter extends Converter.Factory {

    private final Converter.Factory xml;
    private final Converter.Factory json;

    @Inject
    public RetrofitUniversalConverter(@NonNull Gson gson) {
        xml = SimpleXmlConverterFactory.create();
        json = GsonConverterFactory.create(gson);
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {

        for (Annotation annotation : annotations) {

            if (annotation.getClass() == Xml.class) {
                return xml.responseBodyConverter(type, annotations, retrofit);
            }

            if (annotation.getClass() == Json.class) {
                return json.responseBodyConverter(type, annotations, retrofit);
            }

        }

        return null;
    }
}

2) Rewrited my Retrofit module:

@Provides
@Singleton
public Gson providesGson() {
    return new GsonBuilder().create();
}

@Provides
@Singleton
public Retrofit providesRetrofit(@NonNull OkHttpClient okHttpClient,
                                 @NonNull RetrofitUniversalConverter converter) {
    return new Retrofit.Builder()
            .baseUrl(ConstantsManager.BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(converter)
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .build();
}

3) Then, I added annotations in my API interface:

public interface PrivatbankApi {

    @GET @Xml
    Observable<CurrentRates> loadCurrentRates(@NonNull @Url String url);

    @GET("exchange_rates") @Json
    Observable<DateRates> loadDateRates(@NonNull @Query("json") Boolean json, @NonNull @Query("date") String date);

}

But then i get exception:

E/AndroidRuntime: FATAL EXCEPTION: main
              Process: com.vedmedenko.exchangerates, PID: 5432
              java.lang.RuntimeException: Unable to start service com.vedmedenko.exchangerates.core.services.SyncService@68cf714 with Intent { flg=0x4 cmp=com.vedmedenko.exchangerates/.core.services.SyncService (has extras) }: java.lang.IllegalArgumentException: Unable to create converter for class com.vedmedenko.exchangerates.core.rest.models.current.CurrentRates
                  for method PrivatbankApi.loadCurrentRates
                  at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3343)
                  at android.app.ActivityThread.-wrap21(ActivityThread.java)
                  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1582)
                  at android.os.Handler.dispatchMessage(Handler.java:102)
                  at android.os.Looper.loop(Looper.java:154)
                  at android.app.ActivityThread.main(ActivityThread.java:6119)
                  at java.lang.reflect.Method.invoke(Native Method)
                  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
                  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
               Caused by: java.lang.IllegalArgumentException: Unable to create converter for class com.vedmedenko.exchangerates.core.rest.models.current.CurrentRates
                  for method PrivatbankApi.loadCurrentRates
                  at retrofit2.ServiceMethod$Builder.methodError(ServiceMethod.java:720)
                  at retrofit2.ServiceMethod$Builder.createResponseConverter(ServiceMethod.java:706)
                  at retrofit2.ServiceMethod$Builder.build(ServiceMethod.java:167)
                  at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:166)
                  at retrofit2.Retrofit$1.invoke(Retrofit.java:145)
                  at java.lang.reflect.Proxy.invoke(Proxy.java:813)
                  at $Proxy0.loadCurrentRates(Unknown Source)
                  at com.vedmedenko.exchangerates.core.DataManager.loadCurrentRates(DataManager.java:29)
                  at com.vedmedenko.exchangerates.core.services.SyncService.onStartCommand(SyncService.java:81)
                  at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3326)
                    ... 8 more
               Caused by: java.lang.IllegalArgumentException: Could not locate ResponseBody converter for class com.vedmedenko.exchangerates.core.rest.models.current.CurrentRates.
                Tried:
                 * retrofit2.BuiltInConverters
                 * com.vedmedenko.exchangerates.core.rest.converters.RetrofitUniversalConverter
                  at retrofit2.Retrofit.nextResponseBodyConverter(Retrofit.java:346)
                  at retrofit2.Retrofit.responseBodyConverter(Retrofit.java:308)
                  at retrofit2.ServiceMethod$Builder.createResponseConverter(ServiceMethod.java:704)
                    ... 16 more
like image 456
oleg.v Avatar asked Nov 26 '16 23:11

oleg.v


2 Answers

Check this presentation by Jake Wharton, where he solves exactly the problem you describe (and presents many other useful tricks).

In short he suggests creating annotations representing your expected data format (for example Json and Xml in your case) and annotate your API calls accordingly. Then you define your custom ConverterFactory where you delegate to either GsonConverterFactory or SimpleXmlConverterFactory depending on the annotation encountered.

However, you need add two things to the Jake's solution.

First do not forget to annotate your annotations with @Retention(RetentionPolicy.RUNTIME) otherwise they won't be kept during the runtime:

@Retention(RetentionPolicy.RUNTIME)
public @interface Json {}

Second, the annotations you receive in your responseBodyConverter method are not actually your annotations. They are proxies created by the system to your annotations. Therefore you need to replace the conditions:

annotation.getClass() == Json.class
annotation.getClass() == Xml.class

with:

annotation.annotationType() == Json.class
annotation.annotationType() == Xml.class
like image 181
Marcin Jedynak Avatar answered Nov 02 '22 08:11

Marcin Jedynak


Thanks to @Marcin and the reference.

Below is the code from the mentioned presentation by Jake Warton adjusted according to Marcin's comments (and added Json factory as default if no annotation was found):

public class AnnotatedConverterFactory extends Converter.Factory {

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Json {}

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Xml {}


    final Map<Class<?>, Converter.Factory> mFactoryMap;

    public AnnotatedConverterFactory(Map<Class<?>, Converter.Factory> factoryMap) {
        mFactoryMap = factoryMap;
    }


    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        for(Annotation annotation : annotations){
            Converter.Factory factory = mFactoryMap.get(annotation.annotationType());
            if(factory != null){
                return factory.responseBodyConverter(type, annotations, retrofit);
            }
        }
        //try to default to json in case no annotation on current method was found
        Converter.Factory jsonFactory = mFactoryMap.get(Json.class);
        if(jsonFactory != null){
            return jsonFactory.responseBodyConverter(type, annotations, retrofit);
        }
        return null;
    }

    static class Builder {
        Map<Class<?>, Converter.Factory> mFactoryMap = new LinkedHashMap<>();

        Builder add(Class<? extends Annotation> factoryType, Converter.Factory factory){
            if(factoryType == null) throw new NullPointerException("factoryType is null");
            if(factory == null) throw new NullPointerException("factory is null");
            mFactoryMap.put(factoryType, factory);
            return this;
        }
        public AnnotatedConverterFactory build(){
            return new AnnotatedConverterFactory(mFactoryMap);
        }

    }
}

Usage:

    interface RetrofitService{
        @POST
        @AnnotatedConverterFactory.Xml
        Call<Envelope> someApiMethodWithXmlRespoonse(@Url String url, @Body Envelope envelope);

        @POST
        @AnnotatedConverterFactory.Json
        Call<Envelope> someApiMethodWithJsonRespoonse(@Url String url, @Body Model model);
    }

Instantiate retrofit:

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(mOkHttpClient)
                .addConverterFactory(new AnnotatedConverterFactory.Builder()
                        .add(AnnotatedConverterFactory.Xml.class, SimpleXmlConverterFactory.createNonStrict())
                        .add(AnnotatedConverterFactory.Json.class, GsonConverterFactory.create(mGson))
                        .build()
                )


        mRetrofitService = retrofit.create(RetrofitService.class);
like image 42
vir us Avatar answered Nov 02 '22 09:11

vir us