6

I am trying to incorporate a data cache for one of my GWT widgets.
I have a datasource interface/class which retrieves some data from my backend via RequestBuilder and JSON. Because I display the widget multiple times I only want to retrieve the data once.

So I tried to come with an app cache. The naive approach is to use a HashMap in a singleton object to store the data. However I also want to make use of HTML5's localStorage/sessionStorage if supported.
HTML5 localStorage only supports String values. So I have to convert my object into JSON and store as a string. However somehow I can't come up with a nice clean way of doing this. here is what I have so far.

I define a interface with two functions: fetchStatsList() fetches the list of stats that can be displayed in the widget and fetchStatsData() fetches the actual data.

public interface DataSource {
    public void fetchStatsData(Stat stat,FetchStatsDataCallback callback);
    public void fetchStatsList(FetchStatsListCallback callback);
}

The Stat class is a simple Javascript Overlay class (JavaScriptObject) with some getters (getName(), etc) I have a normal non-cachable implementation RequestBuilderDataSource of my DataSource which looks like the following:

public class RequestBuilderDataSource implements DataSource {
    @Override
    public void fetchStatsList(final FetchStatsListCallback callback) {
         // create RequestBuilderRequest, retrieve response and parse JSON 
        callback.onFetchStatsList(stats);
    }

    @Override
    public void fetchStatsData(List<Stat> stats,final FetchStatsDataCallback callback) {
        String url = getStatUrl(stats);
        //create RequestBuilderRquest, retrieve response and parse JSON
        callback.onFetchStats(dataTable); //dataTable is of type DataTable
    }
}

I left out most of the code for the RequestBuilder as it is quite straightforward.

This works out of the box however the list of stats and also the data is retrieved everytime even tough the data is shared among each widget instance.

For supporting caching I add a Cache interface and two Cache implementations (one for HTML5 localStorage and one for HashMap):

public interface Cache {
    void put(Object key, Object value);
    Object get(Object key);
    void remove(Object key);
    void clear();
}

I add a new class RequestBuilderCacheDataSource which extends the RequestBuilderDataSource and takes a Cache instance in its constructor.

public class RequestBuilderCacheDataSource extends RequestBuilderDataSource {

    private final Cache cache;

    publlic RequestBuilderCacheDataSource(final Cache cache) {
        this.cache = cache;
    }

    @Override
    public void fetchStatsList(final FetchStatsListCallback callback) {
       Object value = cache.get("list");
       if (value != null) {
           callback.fetchStatsList((List<Stat>)value);
       }
       else {
           super.fetchStatsList(stats,new FetchStatsListCallback() {
               @Override
               public void onFetchStatsList(List<Stat>stats) {
                   cache.put("list",stats);
                   callback.onFetchStatsList(stats);
               }
           });
           super.fetchStatsList(callback);
       }
    }

    @Override
    public void fetchStatsData(List<Stat> stats,final FetchStatsDataCallback callback) {
        String url = getStatUrl(stats);
        Object value = cache.get(url);
        if (value != null) {
            callback.onFetchStatsData((DataTable)value); 
        }
        else {
            super.fetchStatsData(stats,new FetchStatsDataCallback() {
                @Override
                public void onFetchStatsData(DataTable dataTable) {
                    cache.put(url,dataTable);
                    callback.onFetchStatsData(dataTable);
                }
            });
        }
    }
}

Basically the new class will lookup the value in the Cache and if it is not found it will call the fetch function in the parent class and intercept the callback to put it into the cache and then call the actual callback.
So in order to support both HTML5 localstorage and normal JS HashMap storage I created two implementations of my Cache interface:

JS HashMap storage:

public class DefaultcacheImpl implements Cache {
    private HashMap<Object, Object> map;

    public DefaultCacheImpl() {
        this.map = new HashMap<Object, Object>();
    }

    @Override
    public void put(Object key, Object value) {
        if (key == null) {
            throw new NullPointerException("key is null");
        }
        if (value == null) {
            throw new NullPointerException("value is null");
        }
        map.put(key, value);
    }

    @Override
    public Object get(Object key) {
        // Check for null as Cache should not store null values / keys
        if (key == null) {
            throw new NullPointerException("key is null");
        }
        return map.get(key);
    }

    @Override
    public void remove(Object key) {
        map.remove(key);
    }

    @Override
    public void clear() {
       map.clear();
    }
}

HTML5 localStorage:

public class LocalStorageImpl implements Cache{

    public static enum TYPE {LOCAL,SESSION} 
    private TYPE type;
    private Storage cacheStorage = null;

    public LocalStorageImpl(TYPE type) throws Exception {
        this.type = type;
        if (type == TYPE.LOCAL) {
            cacheStorage = Storage.getLocalStorageIfSupported();
        }
        else {
            cacheStorage = Storage.getSessionStorageIfSupported();
        }
        if (cacheStorage == null) {
            throw new Exception("LocalStorage not supported");
        }
    }


    @Override
    public void put(Object key, Object value) {
        //Convert Object (could be any arbitrary object) into JSON
        String jsonData = null;
        if (value instanceof List) {   // in case it is a list of Stat objects
            JSONArray array = new JSONArray();
            int index = 0;
            for (Object val:(List)value) {
                array.set(index,new JSONObject((JavaScriptObject)val));
                index = index +1;
            }
            jsonData = array.toString();
        }
        else  // in case it is a DataTable
        {
            jsonData = new JSONObject((JavaScriptObject) value).toString();
        }
        cacheStorage.setItem(key.toString(), jsonData);
    }

    @Override
    public Object get(Object key) {
        if (key == null) {
            throw new NullPointerException("key is null");
        }
        String jsonDataString = cacheStorage.getItem(key.toString());
        if (jsonDataString == null) {
            return null;
        }
        Object data = null;
        Object jsonData = JsonUtils.safeEval(jsonDataString);
        if (!key.equals("list")) 
            data = DataTable.create((JavaScriptObject)data);
        else if (jsonData instanceof JsArray){
            JsArray<GenomeStat> jsonStats = (JsArray<GenomeStat>)jsonData;
            List<GenomeStat> stats = new ArrayList<GenomeStat>();
            for (int i = 0;i<jsonStats.length();i++) {
                stats.add(jsonStats.get(i));
            }
            data = (Object)stats;
        }
        return data;
    }

    @Override
    public void remove(Object key) {
        cacheStorage.removeItem(key.toString());
    }

    @Override
    public void clear() {
        cacheStorage.clear();
    }

    public TYPE getType() {
        return type;
    }
}

The post got a little bit long but hopefully clarifies what I try to reach. It boils down to two questions:

  1. Feedback on the design/architecture of this approach (for example subclassing RequestBilderDataSource for cache function, etc). Can this be improved (this is probably more related to general design than specifically GWT).
  2. With the DefaultCacheImpl it is really easy to store and retrieve any arbitrary objects. How can I achieve the same thing with localStorage where I have to convert and parse JSON? I am using a DataTable which requires to call the DataTable.create(JavaScriptObject jso) function to work. How can I solve this without to many if/else and instance of checks?
Ümit
  • 17,379
  • 7
  • 55
  • 74

1 Answers1

6

My first thoughts: make it two layers of cache, not two different caches. Start with the in-memory map, so no serialization/deserialization is needed for reading a given object out, and so that changing an object in one place changes it in all. Then rely on the local storage to keep data around for the next page load, avoiding the need for pulling data down from the server.

I'd tend to say skip session storage, since that doesn't last long, but it does have its benefits.

For storing/reading data, I'd encourage checking out AutoBeans instead of using JSOs. This way you could support any type of data (that can be stored as an autobean) and could pass in a Class param into the fetcher to specify what kind of data you will read from the server/cache, and decode the json to a bean in the same way. As an added bonus, autobeans are easier to define - no JSNI required. A method could look something like this (note that In DataSource and its impl, the signature is different).

public <T> void fetch(Class<T> type, List<Stat> stats, Callback<T, Throwable> callback);

That said, what is DataTable.create? If it is already a JSO, you can just cast to DataTable as you (probably) normally do when reading from the RequestBuilder data.

I would also encourage not returning a JSON array directly from the server, but wrapping it in an object, as a best practice to protect your users' data from being read by other sites. (Okay, on re-reading the issues, objects aren't great either). Rather than discussing it here, check out JSON security best practices?

So, all of that said, first define the data (not really sure how this data is intended to work, so just making up as I go)

public interface DataTable {
    String getTableName();
    void setTableName(String tableName);
}
public interface Stat {// not really clear on what this is supposed to offer
    String getKey();
    void setKey(String key);
    String getValue();
    String setValue(String value);
}
public interface TableCollection {
    List<DataTable> getTables();
    void setTables(List<DataTable> tables);
    int getRemaining();//useful for not sending all if you have too much?
}

For autobeans, we define a factory that can create any of our data when given a Class instance and some data. Each of these methods can be used as a sort of constructor to create a new instance on the client, and the factory can be passed to AutoBeanCodex to decode data.

interface DataABF extends AutoBeanFactory {
    AutoBean<DataTable> dataTable();
    AutoBean<Stat> stat();
    AutoBean<TableCollection> tableCollection();
}

Delegate all work of String<=>Object to AutoBeanCodex, but you probably want some simple wrapper around it to make it easy to call from both the html5 cache and from the RequestBuilder results. Quick example here:

public class AutoBeanSerializer {
    private final AutoBeanFactory factory;
    public AutoBeanSerializer(AutoBeanFactory factory) {
        this.factory = factory;
    }

    public String <T> encodeData(T data) {
        //first, get the autobean mapped to the data
        //probably throw something if we can't find it
        AutoBean<T> autoBean = AutoBeanUtils.getAutoBean(data);

        //then, encode it
        //no factory or type needed here since the AutoBean has those details
        return AutoBeanCodex.encode(autoBean);
    }
    public <T> T decodeData(Class<T> dataType, String json) {
        AutoBean<T> bean = AutoBeanCodex.decode(factory, dataType, json);

        //unwrap the bean, and return the actual data
        return bean.as();
    }
}
Community
  • 1
  • 1
Colin Alworth
  • 17,801
  • 2
  • 26
  • 39
  • Thanks for the feedback. ``Stat`` is just a simple bean that describes each dataTable that I can display. It has fields like (name, label, isStackable, etc). DataTable is the gwt-google-api wrapper for the DataTable of the [google charting tools](http://code.google.com/apis/chart/interactive/docs/reference.html#DataTable). The wrapper is basically a JSO. I am not sure if I can use Autobean to retrieve an instance of it because it has to call the DataTable cosntructor via JSNI ``return new $wnd.google.visualization.DataTable(jso);`` Can you elaborate why you would recommend a two layer cache? – Ümit Feb 14 '12 at 15:30
  • Two layers so you dont have to hit the storage and deserialize for every single fetch after the page is up. This may not apply to your specific case, which seems to treat the data as readonly, but for read/write data, you dont want to call a setter, persist the data, and require all consumers to re-fetch the data again - instead, they already have the most up to date instance. If you only call it once per page load, the in memory cache doesnt buy anything, if you call it multiple times, with two, you can sometimes skip deserialization. – Colin Alworth Feb 14 '12 at 16:35
  • And yes, if using an external JS library, AutoBeans may not be for you. By using that external library, you are somewhat at the mercy of the JS types it will use - JSOs dont really work with instanceof, because JS doesn't have a concept of types, so `jsonData instanceof JsArray` isn't likely to give a meaningful result, either always true, or always false. – Colin Alworth Feb 14 '12 at 16:37
  • Thanks for elaborating on the two layer cache. I didn't think of it but it makes sense. Tough in my case as you said, it's not crucially important as I only use it for readonly access. You also right with ``instanceof`` issues when dealing with JSO. I had some issues with ``instanceof``. However the above code seems to work. I will test it a little bit more. – Ümit Feb 14 '12 at 17:09
  • @ColinAlworth public String encodeData(T data) gives the following error: " T cannot be resolved to a type - The type String is not generic; it cannot be parameterized with arguments " and AutoBean autoBean = AutoBeanUtils.getAutoBean(data); gives this error " cannot resolve a type". How do I fix this? – Michael Apr 09 '14 at 23:54
  • @confile My code is a quick sketch of how to do it, but there are typos - that should be `public String encodeData` instead. The idea was to describe the concept and the though process, not to be a readymade tool. – Colin Alworth Apr 10 '14 at 03:20