Cache in GWT application / widgets with HTML5 localStorage

I am trying to enable the data cache for one of my GWT widgets.
I have a datasource interface / class that retrieves some data from my backend via RequestBuilder and JSON. Since I show the widget several times, I want to get the data only once.

So, I tried to connect to the application cache. The naive approach is to use a HashMap in one object to store data. However, I also want to use HTML5 localStorage / sessionStorage if supported.
HTML5 localStorage only supports String values. So I have to convert my object to JSON and save as a string. However, somehow I can't come up with a good clean way to do this. that's what i still have.

I define an interface with two functions: fetchStatsList() selects a list of statistics that can be displayed in widgets, and fetchStatsData() retrieves 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 ( JavaScriptObject ) class with some getters (getName (), etc.). I have a regular implementation without caching RequestBuilderDataSource my DataSource, which looks like this:

 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 most of the code for RequestBuilder, as it is quite simple.

This works out of the box, however, a list of statistics as well as data is retrieved each time, even hard data is distributed between each instance of the widget.

To support 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 am adding a new RequestBuilderCacheDataSource class that extends 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 look up the value in Cache , and if it is not found, it will call the select function in the parent class and intercept the callback to put it in the cache, and then call the actual callback. <w> Therefore, in order to support both HTML5 localstorage and regular JS HashMap memory, 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 message was a little long, but I hope it clarifies what I'm trying to get to. It comes down to two questions:

  • Feedback on the design / architecture of this approach (e.g. subclassing RequestBilderDataSource for cache function, etc.). Could this be improved (probably this has more to do with the overall design than with the GWT).
  • Using DefaultCacheImpl very easy to store and retrieve any arbitrary objects. How can I do the same with localStorage where I need to convert and parse JSON? I am using DataTable , which requires calling the DataTable.create(JavaScriptObject jso) function to work. How can I solve this without using many if / else and instances of checks?
+6
source share
1 answer

My first thoughts: make two cache layers, not two different cache. Start with a memory card, so serialization / deserialization is not required to read this object, and therefore changing the object in one place changes it in all. Then rely on local storage to store data for loading the next page, avoiding the need to pull data from the server.

I would say that I am talking about session throughput, as it does not last long, but it has its advantages.

For storing / reading data, I would recommend checking AutoBeans instead of using JSOs. This way you can support any type of data (which can be saved as auto-exchange) and can pass a class parameter to the collector to indicate what data you will read from the server / cache and decode json for bean in the same way. As an added bonus, auto-provisioning is easier to determine - no JSNI required. The method may look something like this (note that the signature is different in the DataSource and its impl).

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

So what is DataTable.create ? If it is already a JSO, you can simply pass it to a DataTable, as you (probably) would, as a rule, when reading from RequestBuilder data.

I would also recommend not returning the JSON array directly from the server, but wrapping it in an object as the best practice for protecting your users data from reading by other sites. (Well, re-reading the problems, the objects are also small). Instead of discussing it here, check out the JSON security guidelines?

So, all that is said, first define the data (not quite sure how this data is intended to work, so just compose it when 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 specifying a Class instance and some data. Each of these methods can be used as a kind of constructor to create a new instance on the client, and the factory can be passed to AutoBeanCodex to decode the data.

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

Delegate all the work of String <=> Object to AutoBeanCodex, but you probably want a simple wrapper around it to make it easier to call from the html5 cache and from the RequestBuilder results. A quick example is 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(); } } 
+6
source

Source: https://habr.com/ru/post/908433/


All Articles