/** * 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 static org.waveprotocol.wave.client.gadget.GadgetLog.log; import static org.waveprotocol.wave.client.gadget.GadgetLog.logError; import com.google.gwt.core.client.JavaScriptObject; import org.waveprotocol.wave.client.debug.logger.LogLevel; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.StringMap; /** * Implements callback logic for the Gadget RPC relay. Calls the GWT RPC * listener methods from the context of JavaScript gadget library. * * The general logic is similar to one in Google * javascript.apps.gadgets.container.Controller. * */ public class Controller { /** * Overlay type for JavaScript array of mixed element types. * TODO: Move this type into GWT's own library. */ public static final class JsArrayMixed extends JavaScriptObject { protected JsArrayMixed() { } public static native JsArrayMixed create() /*-{ return []; }-*/; public native void put(int i, JavaScriptObject value) /*-{ this[i] = value; }-*/; public native void put(int i, double value) /*-{ this[i] = value; }-*/; public native void put(int i, boolean value) /*-{ this[i] = value; }-*/; public native void put(int i, String value) /*-{ this[i] = value; }-*/; public native JavaScriptObject getObject(int i) /*-{ return this[i] == null ? null : Object(this[i]); }-*/; public native double getNumber(int i) /*-{ return Number(this[i]); }-*/; public native boolean getBoolean(int i) /*-{ return Boolean(this[i]); }-*/; public native String getString(int i) /*-{ return String(this[i]); }-*/; public native int length() /*-{ return this.length; }-*/; } /** * Service callback interface. */ private static interface ServiceCallback { /** * Relays service call to a Gadget listener implementation. * * @param listener Gadget RPC listener object to receive the call. * @param arguments RPC call arguments to pass to listener object. */ void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException; } /** * Exposes a way to call JS callback functions. */ private static final class JavaScriptFunction extends JavaScriptObject { /** * Bans external construction of this class. */ protected JavaScriptFunction() {} /** * Calls the object as JS function with given arguments. * * @param args function arguments. */ public native void call(JsArrayMixed args) /*-{ this(args); }-*/; } /** * Invalid Argument Exception used in service callback. */ private static class InvalidArgumentException extends Exception { public InvalidArgumentException(String message) { super(message); } } /** Enumeration class for supported Gadget services. */ private enum Service { /** * Informs the gadget container that the gadget is wave-aware and requests * the container to send wave-specific initializaton. This RPC call is going * to be initiated by the wave library in the gadget. The wave gadget * library will eventually be loaded as a feature requested in the Gadget * xml specification. The library will know "a priori" whether the container * is wave-capable by checking the "wave" parameter passed in the URL. */ WAVE_ENABLE("wave_enable", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.waveEnable(getArgumentAsString(0, arguments)); } }), /** * Symmetrical container-to-gadget and gadget-to-container RPC. In this * case, the RPC is received by the container from the gadgets to inform * about the gadget-initiated state change. The state parameter is in the * form of delta that contains only the key-value pairs for the values that * should be updated. */ WAVE_GADGET_STATE("wave_gadget_state", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.waveGadgetStateUpdate(getArgument(0, arguments)); } }), /** * Similar to WAVE_GADGET_STATE, but for private per-user state. */ WAVE_PRIVATE_GADGET_STATE("wave_private_gadget_state", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.wavePrivateGadgetStateUpdate(getArgument(0, arguments)); } }), /** Sets gadget container title service. */ SET_TITLE("set_title", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.setTitle(getArgumentAsString(0, arguments)); } }), /** Sets user preference. */ SET_PREF("set_pref", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { if ((arguments == null) || (arguments.length() < 1)) { throw new InvalidArgumentException("Invalid number of arguments."); } // Note: arguments[0] is a deprecated token parameter, no longer used. String[] keyValue = new String[arguments.length() - 1]; for (int i = 1; i < arguments.length(); ++i) { keyValue[i - 1] = getArgumentAsString(i, arguments); } listener.setPrefs(keyValue); } }), /** * Resizes gadget container (vertical dimension only). * * NOTE(user): This is a standard gadget RPC that only affects the height. * The width is updated by a separate non-standard RPC. */ RESIZE_IFRAME("resize_iframe", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.setIframeHeight(getArgumentAsString(0, arguments)); } }), /** Sets gadget container width. */ SET_IFRAME_WIDTH("setIframeWidth", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.setIframeWidth(getArgumentAsString(0, arguments)); } }), /** * Instructs the browser to navigate to a different view. * * TODO(user): This is normally used to change gadget views. Consider * reimplementing or eliminating this RPC. */ REQUEST_NAVIGATE_TO("requestNavigateTo", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.requestNavigateTo(getArgumentAsString(0, arguments)); } }), /** Instructs the browser to navigate to a given fragment. */ NAVIGATE_TO_FRAGMENT("navigateToFragment", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.requestNavigateTo(getArgumentAsString(0, arguments)); } }), /** Updates the persistent state of Podium gadget. */ UPDATE_PODIUM_STATE("updateState", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.updatePodiumState(getArgumentAsString(0, arguments)); } }), /** Logs a message from the gadget. */ LOG_MESSAGE("wave_log", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.logMessage(getArgumentAsString(0, arguments)); } }), /** Sets a gadget snippet displayed in the wave digest. */ SET_SNIPPET("set_snippet", new ServiceCallback() { public void callService(GadgetRpcListener listener, JsArrayMixed arguments) throws InvalidArgumentException { listener.setSnippet(getArgumentAsString(0, arguments)); } }); /** The name for the service at JavaScript/wire level. */ private final String name; /** Callback object to relay the calls. */ private final ServiceCallback callback; /** * Constructs enum object. * * @param name the RPC name used at JavaScript/wire level. * @param callback callback object to relay the call to RPC listener. */ private Service(String name, ServiceCallback callback) { this.name = name; this.callback = callback; } /** * Relays service call to a Gadget listener implementation. * * @param listener Gadget RPC listener object to receive the call. * @param serviceCallback JS function to receive the call results or null if * undefined. * @param arguments RPC call arguments to pass to listener object. */ public void callService(GadgetRpcListener listener, JavaScriptFunction serviceCallback, JsArrayMixed arguments) { try { callback.callService(listener, arguments); } catch (Exception e) { // Catch as a non-specific exception to capture unexpected parameter issues. if (TO_LOG) { logError("Unable to call service " + e.getMessage()); } } } /** * Returns the name of the service used in the RPC relay. * * @return the name of the service. */ public String getName() { return name; } /** * Helper method to fetch an argument by its index from a given argument * list. * * @param index index of the argument to get. * @param arguments the argument list. * @return the requested argument as a JavaScriptObject. * @throws InvalidArgumentException if the list does not have requested * argument. */ private static JavaScriptObject getArgument(int index, JsArrayMixed arguments) throws InvalidArgumentException { if ((arguments != null) && (arguments.length() > index)) { return arguments.getObject(index); } else { throw new InvalidArgumentException("Missing argument at " + index); } } /** * Helper method to fetch an argument by its index from a given argument * list. The argument is returned as a string. * * @param index index of the argument to get. * @param arguments the argument list. * @return the requested argument as a string. * @throws InvalidArgumentException if the list does not have requested * argument. */ private static String getArgumentAsString(int index, JsArrayMixed arguments) throws InvalidArgumentException { if ((arguments != null) && (arguments.length() > index)) { return arguments.getString(index); } else { throw new InvalidArgumentException("Missing argument at " + index); } } } private static final boolean TO_LOG = LogLevel.showDebug(); // TODO(user): Use CollectionUtils.createStringMap() instead, less bug prone // and easier to write unit tests for this class /** Callback map. */ private final StringMap<GadgetRpcListener> callbackMap = CollectionUtils.createStringMap(); /** Service map. */ private final StringMap<Service> serviceMap = CollectionUtils.createStringMap(); /** JavaScript gadgets RPC library object. */ @SuppressWarnings("unused") // Used in the JavaScript methods. private JavaScriptObject hub; /** Default Constructor used to create the singleton instance. */ private Controller() { this(getStandardGadgetRpcLibrary()); } /** * Generic constructor that can be used for testing. * * @param library the gadget RPC library object. */ // @VisibleForTesting protected Controller(JavaScriptObject library) { setGadgetRpcLibrary(library); for (Service service : Service.values()) { registerService(service.getName()); serviceMap.put(service.getName(), service); } } /** Singleton instance of this class. */ // @VisibleForTesting protected static Controller instance; /** * Returns the singleton instance of Controller. * * @return Controller singleton instance. */ public static Controller getInstance() { if (instance == null) { instance = new Controller(); } return instance; } /** * Generic callback relay that is called from the JS callback function. * Receives the RPC arguments to be processed in gwt. * * @param service the name of the RPC service called from the gadget. * @param gadgetId the name of the gadget that initiated the RPC call. * @param serviceCallback JS callback function to return service result or * null if not needed. * @param arguments RPC call arguments. */ @SuppressWarnings("unused") // Used in registerService native method. private void callback(String service, String gadgetId, JavaScriptFunction serviceCallback, JsArrayMixed arguments) { StringBuilder builder = null; if (TO_LOG) { builder = new StringBuilder(); builder.append(service + " from " + gadgetId); if (arguments != null) { for (int index = 0; index < arguments.length(); ++index) { builder.append(" arg" + (index + 1) + ":'" + arguments.getString(index) + "'"); } } log(builder.toString()); } if (callbackMap.containsKey(gadgetId) && serviceMap.containsKey(service)) { serviceMap.get(service).callService(callbackMap.get(gadgetId), serviceCallback, arguments); } } /** * Returns the standard gadget RPC library object. * * @return the standard gadget RPC library object. */ private static native JavaScriptObject getStandardGadgetRpcLibrary() /*-{ return $wnd.gadgets.rpc; }-*/; /** * Sets the gadget RPC library to be used by the controller. * * @param library the gadget RPC library to set. */ // @VisibleForTesting protected void setGadgetRpcLibrary(JavaScriptObject library) { hub = library; } /** * Creates a JS callback function for the given RPC service and registers it * with the RPC hub. * * @param service the name of the service to register. */ private native void registerService(String service) /*-{ var hub = this.@org.waveprotocol.wave.client.gadget.renderer.Controller::hub; if (hub) { hub.register(service, function() { // This function runs in the JS context that contains values for // service name, gadget ID, callback, and args in s, f, c, and a. var service = this['s']; var gadgetId = this['f']; var callback = this['c'] || null; var args = this['a']; @org.waveprotocol.wave.client.gadget.renderer.Controller::getInstance()(). @org.waveprotocol.wave.client.gadget.renderer.Controller::callback(Ljava/lang/String;Ljava/lang/String;Lorg/waveprotocol/wave/client/gadget/renderer/Controller$JavaScriptFunction;Lorg/waveprotocol/wave/client/gadget/renderer/Controller$JsArrayMixed;) (service, gadgetId, callback, args); }); } }-*/; /** * Calls remote service for the given frame id. * * @param targetId ID of the frame to send the call to. * @param serviceName the name of the service to call. * @param arguments the service call arguments. */ public native void call(String targetId, String serviceName, JsArrayMixed arguments) /*-{ var hub = this.@org.waveprotocol.wave.client.gadget.renderer.Controller::hub; hub.call(targetId, serviceName, null, arguments); }-*/; /** * Sets relay URL to be used to send RPCs to the gadget. * * @param targetId ID of the frame to send RPCs to. * @param url the URL that contains the relay code for the frame. */ public native void setRelayUrl(String targetId, String url) /*-{ var hub = this.@org.waveprotocol.wave.client.gadget.renderer.Controller::hub; hub.setRelayUrl(targetId, url, false); }-*/; /** * Sets RPC token to be used to verify RPCs to the gadget. * * @param targetId ID of the frame to send RPCs to. * @param rpcToken The rpcToken specified in the iframe URL of this gadget. */ public native void setRpcToken(String targetId, String rpcToken) /*-{ var hub = this.@org.waveprotocol.wave.client.gadget.renderer.Controller::hub; hub.setAuthToken(targetId, rpcToken); }-*/; /** * Registers a Gadget RPC listener object to receive RPC calls addressed to * the given gadget ID. * * @param gadgetId gadget ID. * @param listener Gadget RPC listener object to receive RPC calls. */ public void registerGadgetListener(String gadgetId, GadgetRpcListener listener) { callbackMap.put(gadgetId, listener); } /** * Override some of the configuration options. * * TODO(user): Investigate whether or not we really need this, as it * should come from our syndicator options. */ public static native void initializeContainerConfiguration() /*-{ if ($wnd.gadgets && $wnd.gadgets.config) { $wnd.gadgets.config.init( {"rpc" : {"useLegacyProtocol" : false, "parentRelayUrl" : "/gadgets/files/container/rpc_relay.html"} }); } }-*/; }