stephanenicolas / robospice

Repo of the Open Source Android library : RoboSpice. RoboSpice is a modular android library that makes writing asynchronous long running tasks easy. It is specialized in network requests, supports caching and offers REST requests out-of-the box using extension modules.
Apache License 2.0
2.95k stars 545 forks source link

Caching skipped if List sub-type is involved #453

Open nicolabeghin opened 8 years ago

nicolabeghin commented 8 years ago

Hi I think I narrowed down a nasty bug. I hope I'm wrong, but I made some pretty extensive testing in order to get down to this.

I'm using Jackson2SpringAndroidSpiceService which in turns use Jackson2ObjectPersisterFactory:

    public CacheManager createCacheManager(Application application) throws CacheCreationException {
        CacheManager cacheManager = new CacheManager();
        cacheManager.addPersister(new Jackson2ObjectPersisterFactory(application));
        return cacheManager;
    }

Having a pretty simple situation like this

package com.models.collections;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.nbeghin.smartchandiser.models.ReportTemplate;
import com.models.wrappers.ReportTemplateWrapper;

import java.util.ArrayList;
import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
public class ReportTemplateWrapperList {
    private List<ReportTemplateWrapper> reportTemplates = new ArrayList<ReportTemplateWrapper>();
    private ReportTemplateWrapper reportTemplate;

    public ReportTemplateWrapperList() {
    }

    public ReportTemplateWrapperList(ReportTemplate reportTemplate) {
        this.reportTemplate = new ReportTemplateWrapper(reportTemplate);
    }

    public ReportTemplateWrapperList(List<ReportTemplate> reportTemplates) {
        for(ReportTemplate reportTemplate: reportTemplates) {
            this.reportTemplates.add(new ReportTemplateWrapper(reportTemplate));
        }
    }

    public List<ReportTemplateWrapper> getReportTemplates() {
        return reportTemplates;
    }

    public void setReportTemplates(List<ReportTemplateWrapper> reportTemplates) {
        this.reportTemplates = reportTemplates;
    }

    public ReportTemplateWrapper getReportTemplate() {
        return reportTemplate;
    }

    public void setReportTemplate(ReportTemplateWrapper reportTemplate) {
        this.reportTemplate = reportTemplate;
    }
}
package com.models.wrappers;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.models.ReportFieldInstance;
import com.models.ReportTemplate;

import java.util.ArrayList;
import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
public class ReportTemplateWrapper {
    @JsonProperty(value = "ReportTemplate")
    private ReportTemplate reportTemplate;
    @JsonProperty(value = "ReportFieldInstance")
    private List<ReportFieldInstance> reportFieldInstances = new ArrayList<ReportFieldInstance>();

    public ReportTemplateWrapper() {
    }

    public ReportTemplateWrapper(ReportTemplate reportTemplate) {
        this.reportTemplate = reportTemplate;
    }

    public ReportTemplate getReportTemplate() {
        if (this.reportFieldInstances.isEmpty() == false && reportTemplate.getReportFieldInstances().isEmpty()) {
            reportTemplate.setReportFieldInstances(this.getReportFieldInstances());
        }
        return reportTemplate;
    }

    public void setReportTemplate(ReportTemplate reportTemplate) {
        this.reportTemplate = reportTemplate;
    }

    public List<ReportFieldInstance> getReportFieldInstances() {
        return reportFieldInstances;
    }

    public void setReportFieldInstances(List<ReportFieldInstance> reportFieldInstances) {
        this.reportFieldInstances = reportFieldInstances;
    }

}

when i use

ReportTemplatesRequest request = new ReportTemplatesRequest();
this.getSpiceManager().execute(request, request.createCacheKey(), DurationInMillis.ONE_DAY, new ReportTemplatesRequestRequestListener(this));

the DurationInMillis.ONE_DAY caching is not respected (I checked Apache's access logs and requests are logged each time).

While if I change the definition to

package com.models.wrappers;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.models.ReportFieldInstance;
import com.models.ReportTemplate;

import java.util.ArrayList;
import java.util.List;

@JsonIgnoreProperties(ignoreUnknown = true)
public class ReportTemplateWrapper {
    @JsonProperty(value = "ReportTemplate")
    private ReportTemplate reportTemplate;
    @JsonProperty(value = "ReportFieldInstance222")
    private List<ReportFieldInstance> reportFieldInstances = new ArrayList<ReportFieldInstance>();

to make sure the reportFieldInstances List is not populed by Jackson unmarshaller, caching-time is respected (please note the "ReportFieldInstance" changed in "ReportFieldInstance222");

So this is my assumption: if a sub-type of the returned object is a populated List, Robospice automatically skips caching. I hope I'm wrong but changing that single property made all the difference, so something weird's happening in the background.

Library versions:

    compile 'com.octo.android.robospice:robospice:1.4.14'
    compile 'com.octo.android.robospice:robospice-spring-android:1.4.14'
    compile 'com.fasterxml.jackson.core:jackson-core:2.6.2'
    compile 'com.fasterxml.jackson.core:jackson-annotations:2.6.2'
    compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.13'
    compile 'com.octo.android.robospice:robospice-ui-spicelist:1.4.14'
    compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
    compile 'com.fasterxml.jackson.core:jackson-databind:2.6.2'
    compile 'org.jbundle.util.osgi.wrapped:org.jbundle.util.osgi.wrapped.org.apache.http.client:4.1.2'
mykolaj commented 8 years ago

It looks like your object is not getting put into cache, and because of that RoboSpice keeps executing a request every time because there's nothing in the cache. Must be some issue with Jackson serializing this 'ReportTemplateWrapper' object and fails to do so. RoboSpice writes detailed logs. Please explore a log in AndroidStudio to track down an error. And/or post a log here if needed. Log must begin from exact moment after this.getSpiceManager().execute(request, request.createCacheKey(), DurationInMillis.ONE_DAY, new ReportTemplatesRequestRequestListener(this)) is called.

nicolabeghin commented 8 years ago

hi @mykolaj you're absolutely right. I forgot to update the issue as it took me a day to figure out the source of the problem. I had to dig deeper to find out why the same exact SpiceRequest failed on Samsung devices while it didn't on other devices, and narrowed it down to a class like the one below

@JsonIgnoreProperties(ignoreUnknown = true)
@com.orm.dsl.Table(name = "report_field_instances")
public class ReportFieldInstance extends SugarRecord {

    private String title;
    @JsonProperty(value = "ReportFieldType")
    private ReportFieldType reportFieldType;
    @JsonProperty(value = "report_template_id")
    private int reportTemplateId;
    @JsonProperty(value = "report_field_type_id")
    private int reportFieldTypeId;
    private boolean required;
    @com.orm.dsl.Column(name = "orderby")
    private int order;
    @Ignore
    @JsonIgnore
    private View view;
}

Robospice invokes the Jackson method ObjectMapper.canSerialize that prevents the parsing from being carried out at all if any non-serializable field is found in the given object. In my case, failing silently on Samsung devices (and only on Samsung devices, ie on my Moto X it was carried out successfully).

In case it can help anyone: just adding the @JsonIgnore annotation fixed this (nightmare-ish) bug.

    @JsonIgnore
    private View view;