1package autotest.common;
2
3import com.google.gwt.core.client.GWT;
4import com.google.gwt.core.client.JavaScriptObject;
5import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
6import com.google.gwt.dom.client.Element;
7import com.google.gwt.json.client.JSONObject;
8import com.google.gwt.user.client.Timer;
9
10import java.util.HashMap;
11import java.util.Map;
12
13/**
14 * JsonRpcProxy that uses "JSON with Padding" (JSONP) to make requests.  This allows it to get
15 * around the Same-Origin Policy that limits XmlHttpRequest-based techniques.  However, it requires
16 * close coupling with the server and it allows the server to execute arbitrary JavaScript within
17 * the page, so it should only be used with trusted servers.
18 *
19 * See http://code.google.com/docreader/#p=google-web-toolkit-doc-1-5&s=google-web-toolkit-doc-1-5&t=Article_UsingGWTForJSONMashups.
20 * Much of the code here is borrowed from or inspired by that article.
21 */
22public class PaddedJsonRpcProxy extends JsonRpcProxy {
23    private static final int REQUEST_TIMEOUT_MILLIS = 60000;
24    private static final String SCRIPT_TAG_PREFIX = "__jsonp_rpc_script";
25    private static final String CALLBACK_PREFIX = "__jsonp_rpc_callback";
26
27    private static int idCounter = 0;
28
29    private String rpcUrl;
30
31    private static class JsonpRequest {
32        private int requestId;
33        private String requestData;
34        private Element scriptTag;
35        private String callbackName;
36        private Timer timeoutTimer;
37        private JsonRpcCallback rpcCallback;
38        private boolean timedOut = false;
39
40        public JsonpRequest(String requestData, JsonRpcCallback rpcCallback) {
41            requestId = getRequestId();
42            this.requestData = requestData;
43            this.rpcCallback = rpcCallback;
44
45            callbackName = CALLBACK_PREFIX + requestId;
46            addCallback(this, callbackName);
47
48            timeoutTimer = new Timer() {
49                @Override
50                public void run() {
51                    timedOut = true;
52                    cleanup();
53                    notify.showError("Request timed out");
54                    JsonpRequest.this.rpcCallback.onError(null);
55                }
56            };
57        }
58
59        private String getFullUrl(String rpcUrl) {
60            Map<String, String> arguments = new HashMap<String, String>();
61            arguments.put("callback", callbackName);
62            arguments.put("request", requestData);
63            return rpcUrl + "?" + Utils.encodeUrlArguments(arguments);
64        }
65
66        public void send(String rpcUrl) {
67            scriptTag = addScript(getFullUrl(rpcUrl), requestId);
68            timeoutTimer.schedule(REQUEST_TIMEOUT_MILLIS);
69            notify.setLoading(true);
70        }
71
72        public void cleanup() {
73            dropScript(scriptTag);
74            dropCallback(callbackName);
75            timeoutTimer.cancel();
76            notify.setLoading(false);
77        }
78
79        /**
80         * This method is called directly from native code (the dynamically loaded <script> calls
81         * our callback method, which calls this), so we need to do proper GWT exception handling
82         * manually.
83         *
84         * See the implementation of com.google.gwt.user.client.Timer.fire(), from which this
85         * technique was borrowed.
86         */
87        @SuppressWarnings("unused")
88        public void handleResponse(JavaScriptObject responseJso) {
89            UncaughtExceptionHandler handler = GWT.getUncaughtExceptionHandler();
90            if (handler == null) {
91                handleResponseImpl(responseJso);
92                return;
93            }
94
95            try {
96                handleResponseImpl(responseJso);
97            } catch (Throwable throwable) {
98                handler.onUncaughtException(throwable);
99            }
100        }
101
102        public void handleResponseImpl(JavaScriptObject responseJso) {
103            cleanup();
104            if (timedOut) {
105                return;
106            }
107
108            JSONObject responseObject = new JSONObject(responseJso);
109            handleResponseObject(responseObject, rpcCallback);
110        }
111    }
112
113    public PaddedJsonRpcProxy(String rpcUrl) {
114        this.rpcUrl = rpcUrl;
115    }
116
117    private static int getRequestId() {
118        return idCounter++;
119    }
120
121    private static native void addCallback(JsonpRequest request, String callbackName) /*-{
122        window[callbackName] = function(someData) {
123            request.@autotest.common.PaddedJsonRpcProxy.JsonpRequest::handleResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(someData);
124        }
125    }-*/;
126
127    private static native void dropCallback(String callbackName) /*-{
128        delete window[callbackName];
129    }-*/;
130
131    private static Element addScript(String url, int requestId) {
132        String scriptId = SCRIPT_TAG_PREFIX + requestId;
133        Element scriptElement = addScriptToDocument(scriptId, url);
134        return scriptElement;
135    }
136
137    private static native Element addScriptToDocument(String uniqueId, String url) /*-{
138        var elem = document.createElement("script");
139        elem.setAttribute("language", "JavaScript");
140        elem.setAttribute("src", url);
141        elem.setAttribute("id", uniqueId);
142        document.getElementsByTagName("body")[0].appendChild(elem);
143        return elem;
144    }-*/;
145
146    private static native void dropScript(Element scriptElement) /*-{
147        document.getElementsByTagName("body")[0].removeChild(scriptElement);
148    }-*/;
149
150    @Override
151    protected void sendRequest(JSONObject request, JsonRpcCallback callback) {
152        JsonpRequest jsonpRequest = new JsonpRequest(request.toString(), callback);
153        jsonpRequest.send(rpcUrl);
154    }
155}
156