PaddedJsonRpcProxy.java revision ef6fe028fcc667366e8ac30fe63ba314a4b1d745
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 = 10000;
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                    GWT.log("timeout firing " + requestId, null);
52                    timedOut = true;
53                    cleanup();
54                    notify.showError("Request timed out");
55                    JsonpRequest.this.rpcCallback.onError(null);
56                }
57            };
58        }
59
60        private String getFullUrl(String rpcUrl) {
61            Map<String, String> arguments = new HashMap<String, String>();
62            arguments.put("callback", callbackName);
63            arguments.put("request", requestData);
64            return rpcUrl + "?" + Utils.encodeUrlArguments(arguments);
65        }
66
67        public void send(String rpcUrl) {
68            scriptTag = addScript(getFullUrl(rpcUrl), requestId);
69            timeoutTimer.schedule(REQUEST_TIMEOUT_MILLIS);
70            notify.setLoading(true);
71            GWT.log("request sent " + requestId + " <" + requestData + ">", null);
72        }
73
74        public void cleanup() {
75            dropScript(scriptTag);
76            dropCallback(callbackName);
77            timeoutTimer.cancel();
78            notify.setLoading(false);
79        }
80
81        /**
82         * This method is called directly from native code (the dynamically loaded <script> calls
83         * our callback method, which calls this), so we need to do proper GWT exception handling
84         * manually.
85         *
86         * See the implementation of com.google.gwt.user.client.Timer.fire(), from which this
87         * technique was borrowed.
88         */
89        public void handleResponse(JavaScriptObject responseJso) {
90            UncaughtExceptionHandler handler = GWT.getUncaughtExceptionHandler();
91            if (handler == null) {
92                handleResponseImpl(responseJso);
93                return;
94            }
95
96            try {
97                handleResponseImpl(responseJso);
98            } catch (Throwable throwable) {
99                handler.onUncaughtException(throwable);
100            }
101        }
102
103        public void handleResponseImpl(JavaScriptObject responseJso) {
104            GWT.log("response arrived " + requestId, null);
105            cleanup();
106            if (timedOut) {
107                GWT.log("already timed out " + requestId, null);
108                return;
109            }
110
111            JSONObject responseObject = new JSONObject(responseJso);
112            GWT.log("handling response " + requestId
113                    + " (" + responseJso.toString().length() + ")",
114                    null);
115            handleResponseObject(responseObject, rpcCallback);
116            GWT.log("done " + requestId, null);
117        }
118    }
119
120    public PaddedJsonRpcProxy(String rpcUrl) {
121        this.rpcUrl = rpcUrl;
122    }
123
124    private static int getRequestId() {
125        return idCounter++;
126    }
127
128    private static native void addCallback(JsonpRequest request, String callbackName) /*-{
129        window[callbackName] = function(someData) {
130            request.@autotest.common.PaddedJsonRpcProxy.JsonpRequest::handleResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(someData);
131        }
132    }-*/;
133
134    private static native void dropCallback(String callbackName) /*-{
135        delete window[callbackName];
136    }-*/;
137
138    private static Element addScript(String url, int requestId) {
139        String scriptId = SCRIPT_TAG_PREFIX + requestId;
140        Element scriptElement = addScriptToDocument(scriptId, url);
141        return scriptElement;
142    }
143
144    private static native Element addScriptToDocument(String uniqueId, String url) /*-{
145        var elem = document.createElement("script");
146        elem.setAttribute("language", "JavaScript");
147        elem.setAttribute("src", url);
148        elem.setAttribute("id", uniqueId);
149        document.getElementsByTagName("body")[0].appendChild(elem);
150        return elem;
151    }-*/;
152
153    private static native void dropScript(Element scriptElement) /*-{
154        document.getElementsByTagName("body")[0].removeChild(scriptElement);
155    }-*/;
156
157    @Override
158    protected void sendRequest(JSONObject request, JsonRpcCallback callback) {
159        JsonpRequest jsonpRequest = new JsonpRequest(request.toString(), callback);
160        jsonpRequest.send(rpcUrl);
161    }
162}
163