blob: ede41e6b842e595d7d95f6825ad8943b8bd9f086 [file] [log] [blame]
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.i18n.addressinput;
import static com.android.i18n.addressinput.Util.checkNotNull;
import com.android.i18n.addressinput.JsonpRequestBuilder.AsyncCallback;
import org.json.JSONObject;
import android.util.Log;
import java.util.EventListener;
import java.util.HashMap;
import java.util.HashSet;
/**
* Cache for dynamic address data.
*/
public final class CacheData {
/**
* Used to identify the source of a log message.
*/
private static final String TAG = "CacheData";
/**
* Time out value for the server to respond in millisecond.
*/
private final int TIMEOUT = 5000;
/**
* CacheData singleton.
*/
private static final CacheData instance = new CacheData();
/**
* URL to get public address data.
*/
private final String PUBLIC_ADDRESS_DATA_SERVER =
"http://i18napis.appspot.com/address";
/**
* Url to get address data. You can also reset it by calling {@link #setUrl(String)}.
*/
private String serviceUrl = PUBLIC_ADDRESS_DATA_SERVER;
/**
* Storage for all dynamically retrieved data.
*/
private final JsoMap theCache = JsoMap.createEmptyJsoMap();
/**
* All requests that have been sent.
*/
private final HashSet<String> requestedKeys = new HashSet<String>();
/**
* All invalid requested keys. For example, if we request a random string "asdfsdf9o", and the
* server responds by saying this key is invalid, it will be stored here.
*/
private final HashSet<String> badKeys = new HashSet<String>();
/**
* Temporary store for {@code CacheListener}s. When a key is requested and still waiting for
* server's response, the listeners for the same key will be temporary stored here. When the
* server responded, these listeners will be triggered and then removed.
*/
private final HashMap<LookupKey, HashSet<CacheListener>> temporaryListenerStore =
new HashMap<LookupKey, HashSet<CacheListener>>();
/**
* Private constructor - singleton class.
*/
private CacheData() {
}
/**
* Interface for all listeners to {@link CacheData} change. This is only used when multiple
* requests of the same key is dispatched and server has not responded yet.
*/
private static interface CacheListener extends EventListener {
/**
* The function that will be called when valid data is about to be put in the cache.
*
* @param key the key for newly arrived data.
*/
void onAdd(String key);
}
/**
* Class to handle JSON response.
*/
private class JsonHandler {
/**
* Key for the requested data.
*/
private final String key;
/**
* Pre-existing data for the requested key. Null is allowed.
*/
private final JSONObject existingJso;
private final DataLoadListener listener;
/**
* Constructs a JsonHandler instance.
*
* @param key The key for requested data.
* @param oldJso Pre-existing data for this key or null.
*/
private JsonHandler(String key, JSONObject oldJso,
DataLoadListener listener) {
checkNotNull(key);
this.key = key;
this.existingJso = oldJso;
this.listener = listener;
}
/**
* Saves valid responded data to the cache once data arrives, or if the key is invalid,
* saves it in the invalid cache. If there is pre-existing data for the key, it will merge
* the new data will the old one. It also triggers {@link DataLoadListener#dataLoadingEnd()}
* method before it returns (even when the key is invalid, or input jso is null).
*
* @param map The received JSON data as a map.
*/
private void handleJson(JsoMap map) {
// Can this ever happen?
if (map == null) {
Log.w(TAG, "server returns null for key:" + key);
badKeys.add(key);
notifyListenersAfterJobDone(key);
triggerDataLoadingEndIfNotNull(listener);
return;
}
JSONObject json = map;
String idKey = AddressDataKey.ID.name().toLowerCase();
if (!json.has(idKey)) {
Log.w(TAG, "invalid or empty data returned for key: " + key);
badKeys.add(key);
notifyListenersAfterJobDone(key);
triggerDataLoadingEndIfNotNull(listener);
return;
}
if (existingJso != null) {
map.mergeData((JsoMap) existingJso);
}
Log.w(TAG, "put the following key/value pair into cache. key:" + key
+ ", value:" + map.string());
theCache.putObj(key, map);
notifyListenersAfterJobDone(key);
triggerDataLoadingEndIfNotNull(listener);
}
}
/**
* Sets address data server URL. Input URL cannot be null.
*
* @param url The service URL.
*/
public void setUrl(String url) {
checkNotNull(url, "Cannot set URL of address data server to null.");
serviceUrl = url;
}
/**
* Gets address data server URL.
*/
public String getUrl() {
return serviceUrl;
}
/**
* Checks if key and its value is cached (Note that only valid ones are cached).
*/
public boolean containsKey(String key) {
return theCache.containsKey(key);
}
private void triggerDataLoadingEndIfNotNull(DataLoadListener listener) {
if (listener != null) {
listener.dataLoadingEnd();
}
}
/**
* Fetches data from server, or returns if the data is already cached. If the fetched data is
* valid, it will be added to the cache. This method also triggers {@link
* DataLoadListener#dataLoadingEnd()} method before it returns.
*
* @param existingJso Pre-existing data for this key or null if none.
* @param listener An optional listener to call when done.
*/
public // TODO: Remove this "public" when it's no longer used for testing.
void fetchDynamicData(final LookupKey key,
JSONObject existingJso,
final DataLoadListener listener) {
checkNotNull(key, "null key not allowed.");
if (listener != null) {
listener.dataLoadingBegin();
}
// Key is valid and cached.
if (theCache.containsKey(key.toString())) {
Log.w(TAG, "returning data for key " + key + " from the cache");
triggerDataLoadingEndIfNotNull(listener);
return;
}
// Key is invalid and cached.
if (badKeys.contains(key.toString())) {
triggerDataLoadingEndIfNotNull(listener);
return;
}
// Already requested the key, and is still waiting for server's response.
if (!requestedKeys.add(key.toString())) {
Log.w(TAG, "data for key " + key + " requested but not cached yet");
addListenerToTempStore(key, new CacheListener() {
public void onAdd(String myKey) {
triggerDataLoadingEndIfNotNull(listener);
}
});
return;
}
// Key is not cached yet, now sending the request to the server.
JsonpRequestBuilder jsonp = new JsonpRequestBuilder();
jsonp.setTimeout(TIMEOUT);
final JsonHandler handler = new JsonHandler(key.toString(),
existingJso, listener);
jsonp.requestObject(serviceUrl + "/" + key.toString(),
new AsyncCallback<JsoMap>() {
public void onFailure(Throwable caught) {
Log.w(TAG, "Request for key " + key + " failed");
requestedKeys.remove(key.toString());
notifyListenersAfterJobDone(key.toString());
triggerDataLoadingEndIfNotNull(listener);
}
public void onSuccess(JsoMap result) {
handler.handleJson(result);
}
});
}
/**
* Gets the instance of CacheData.
*/
public static CacheData getInstance() {
return instance;
}
/**
* Retrieves string data identified by key.
*
* @param key Non-null key. E.g., "data/US/CA".
* @return String value for specified key.
*/
public String get(String key) {
checkNotNull(key, "null key not allowed");
return theCache.get(key);
}
/**
* Retrieves JsoMap data identified by key.
*
* @param key Non-null key. E.g., "data/US/CA".
* @return String value for specified key.
*/
public JsoMap getObj(String key) {
checkNotNull(key, "null key not allowed");
return theCache.getObj(key);
}
private void notifyListenersAfterJobDone(String key) {
LookupKey lookupKey = new LookupKey.Builder(key).build();
HashSet<CacheListener> listeners = temporaryListenerStore.get(lookupKey);
if (listeners != null) {
for (CacheListener listener : listeners) {
listener.onAdd(key.toString());
}
listeners.clear();
}
}
private void addListenerToTempStore(LookupKey key, CacheListener listener) {
checkNotNull(key);
checkNotNull(listener);
HashSet<CacheListener> listeners = temporaryListenerStore.get(key);
if (listeners == null) {
listeners = new HashSet<CacheListener>();
temporaryListenerStore.put(key, listeners);
}
listeners.add(listener);
}
}