| /* |
| * 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); |
| } |
| } |