/**
* Copyright 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 org.waveprotocol.wave.client.gadget.renderer;
import com.google.gwt.core.client.Duration;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import org.waveprotocol.wave.model.id.WaveletName;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Gadget data store implementation.
*
* Please see the initial metadata call spec at:
* https://cwiki.apache.org/SHINDIG/shindigs-metadata-call.html
*
*<p>
* <li>TODO(user): Add unit tests.</li>
* <li>TODO(vadimg): Consider batching gadget requests to improve performance.
* </li>
* <li>TODO(vadimg): Work out how to gracefully renew expired security tokens.
* </li>
*
*/
public class GadgetDataStoreImpl implements GadgetDataStore {
/**
* Lifetime of a cache element in milliseconds. The cache elements can be
* evicted after this time period. WFE also gets long-life security tokens (1
* hour) to support this cache expiration time.
*/
private static final double CACHE_EXPIRATION_TIME_MS = 3600000;
/** Gadget Metadata path. */
public static final String GADGET_METADATA_PATH = "/gadgets/metadata";
/**
* Cache element class that contains both cached gadget metadata and
* expiration time.
*/
private static class CacheElement {
private final GadgetMetadata metadata;
private final String securityToken;
private final double expirationTime;
CacheElement(GadgetMetadata metadata, String securityToken) {
this.metadata = metadata;
this.securityToken = securityToken;
expirationTime = Duration.currentTimeMillis() + CACHE_EXPIRATION_TIME_MS;
}
GadgetMetadata getMetadata() {
return metadata;
}
String getSecurityToken() {
return securityToken;
}
boolean expired() {
return Duration.currentTimeMillis() > expirationTime;
}
}
/** Metadata cache. Maps the gadget instance key to cache element objects. */
private final Map<String, CacheElement> metadataMap = new HashMap<String, CacheElement>();
private static GadgetDataStoreImpl singleton = null;
/** Private singleton constructor. */
private GadgetDataStoreImpl() {
}
/**
* Retrieves the class singleton.
*
* @return singleton instance of the class.
*/
static GadgetDataStore getInstance() {
if (singleton == null) {
singleton = new GadgetDataStoreImpl();
}
return singleton;
}
private void cleanupExpiredCache() {
Iterator<CacheElement> i = metadataMap.values().iterator();
while (i.hasNext()) {
if (i.next().expired()) {
i.remove();
}
}
}
private boolean fetchDataByKey(String key, DataCallback receiveDataCommand) {
if (metadataMap.containsKey(key)) {
CacheElement cache = metadataMap.get(key);
receiveDataCommand.onDataReady(cache.getMetadata(), cache.getSecurityToken());
return true;
}
return false;
}
@Override
public void getGadgetData(final String gadgetSpecUrl, WaveletName waveletName, int instanceId,
final DataCallback receiveDataCommand) {
cleanupExpiredCache();
final String secureGadgetDataKey =
waveletName.waveId + " " + waveletName.waveletId + " " + instanceId + " " + gadgetSpecUrl;
if (fetchDataByKey(secureGadgetDataKey, receiveDataCommand)) {
return;
}
final String nonSecureGadgetDataKey = gadgetSpecUrl;
if (fetchDataByKey(nonSecureGadgetDataKey, receiveDataCommand)) {
return;
}
JSONObject request = new JSONObject();
JSONObject requestContext = new JSONObject();
JSONArray gadgets = new JSONArray();
JSONObject gadget = new JSONObject();
try {
gadget.put("url", new JSONString(gadgetSpecUrl));
gadgets.set(0, gadget);
requestContext.put("container", new JSONString("wave"));
request.put("context", requestContext);
request.put("gadgets", gadgets);
RequestBuilder builder = new RequestBuilder(RequestBuilder.POST, GADGET_METADATA_PATH);
builder.sendRequest(request.toString(), new RequestCallback() {
public void onError(Request request, Throwable exception) {
receiveDataCommand.onError("Error retrieving metadata from the server.", exception);
}
public void onResponseReceived(Request request, Response response) {
JSONObject gadgetMetadata = null;
try {
gadgetMetadata =
JSONParser.parseLenient(response.getText()).isObject().get("gadgets").isArray().get(
0).isObject();
} catch (NullPointerException exception) {
receiveDataCommand.onError("Error in gadget metadata JSON.", exception);
}
if (gadgetMetadata != null) {
GadgetMetadata metadata = new GadgetMetadata(gadgetMetadata);
// TODO: Security token is unused therefore the gadget is stored
// under the non secure key.
String securityToken = null;
metadataMap.put(nonSecureGadgetDataKey, new CacheElement(metadata, securityToken));
receiveDataCommand.onDataReady(metadata, securityToken);
} else {
receiveDataCommand.onError("Error in gadget metadata JSON.", null);
}
}
});
} catch (RequestException e) {
receiveDataCommand.onError("Unable to process gadget request.", e);
}
}
}