generic JSON-RPC implementation using JSONP (JSON with Padding).  the central part of this change consists of:
* refactored JsonRpcProxy to extract all XmlHttpRequest-specific logic into a new XhrHttpRequest subclass, and made JsonRpcProxy abstract
* introduced new PaddedJsonRpcProxy subclass of XmlHttpRequest that uses JSONP instead of XHR
* added new handle_jsonp_rpc_request() method to rpc_handler.py, to handle JSONP requests on the server side

This enables the entire frontend (either AFE or TKO) to operate via JSONP instead of XHR.  I didn't make them do that now, since there's no reason, but it will be critical when we go to make components embeddable in other pages (on other domains).  Other changes here include:

* made TestDetailView use PaddedJsonRpcProxy (it previous had its own custom JSONP logic, which was all moved into PaddedJsonRpcProxy).
* made retrieve_logs.cgi and jsonp_fetcher.cgi support JSONP requests, so that log fetching requests could go through the shared JsonRpcProxy codepath.  retrieve_logs.cgi still supports the old GET params interface for backwards compatibility (at least old TKO still uses it, and possible other scripts).

Signed-off-by: Steve Howard <showard@google.com>



git-svn-id: http://test.kernel.org/svn/autotest/trunk@2943 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/afe/json_rpc/serviceHandler.py b/frontend/afe/json_rpc/serviceHandler.py
index 5a6ba29..9ea5e3a 100644
--- a/frontend/afe/json_rpc/serviceHandler.py
+++ b/frontend/afe/json_rpc/serviceHandler.py
@@ -111,7 +111,8 @@
 
         return resultdata
 
-    def translateRequest(self, data):
+    @staticmethod
+    def translateRequest(data):
         try:
             req = json_decoder.decode(data)
         except:
@@ -133,7 +134,8 @@
     def invokeServiceEndpoint(self, meth, args):
         return meth(*args)
 
-    def translateResult(self, rslt, err, err_traceback, id_):
+    @staticmethod
+    def translateResult(rslt, err, err_traceback, id_):
         if err is not None:
             err = {"name": err.__class__.__name__, "message":str(err),
                    "traceback": err_traceback}
diff --git a/frontend/afe/rpc_handler.py b/frontend/afe/rpc_handler.py
index 70ef6cd..b6bd697 100644
--- a/frontend/afe/rpc_handler.py
+++ b/frontend/afe/rpc_handler.py
@@ -5,8 +5,8 @@
 
 __author__ = 'showard@google.com (Steve Howard)'
 
+import traceback, pydoc, re, urllib
 import django.http
-import traceback, pydoc
 
 from frontend.afe.json_rpc import serviceHandler
 from frontend.afe import rpc_utils
@@ -19,8 +19,7 @@
 class RpcHandler(object):
     def __init__(self, rpc_interface_modules, document_module=None):
         self._rpc_methods = RpcMethodHolder()
-        self._dispatcher = serviceHandler.ServiceHandler(
-            self._rpc_methods)
+        self._dispatcher = serviceHandler.ServiceHandler(self._rpc_methods)
 
         # store all methods from interface modules
         for module in rpc_interface_modules:
@@ -33,18 +32,41 @@
         self.html_doc = pydoc.html.document(document_module)
 
 
-    def handle_rpc_request(self, request):
-        response = django.http.HttpResponse()
-        if len(request.POST):
-            response.write(self._dispatcher.handleRequest(
-                request.raw_post_data))
-        else:
-            response.write(self.html_doc)
-
+    def _raw_response(self, response_data, content_type=None):
+        response = django.http.HttpResponse(response_data)
         response['Content-length'] = str(len(response.content))
+        if content_type:
+            response['Content-Type'] = content_type
         return response
 
 
+    def get_rpc_documentation(self):
+        return self._raw_response(self.html_doc)
+
+
+    def _raw_request_data(self, request):
+        if request.method == 'POST':
+            return request.raw_post_data
+        return urllib.unquote(request.META['QUERY_STRING'])
+
+
+    def handle_rpc_request(self, request):
+        request_data = self._raw_request_data(request)
+        result = self._dispatcher.handleRequest(request_data)
+        return self._raw_response(result)
+
+
+    def handle_jsonp_rpc_request(self, request):
+        request_data = request.GET['request']
+        callback_name = request.GET['callback']
+        # callback_name must be a simple identifier
+        assert re.search(r'^\w+$', callback_name)
+
+        result = self._dispatcher.handleRequest(request_data)
+        padded_result = '%s(%s)' % (callback_name, result)
+        return self._raw_response(padded_result, content_type='text/javascript')
+
+
     @staticmethod
     def _allow_keyword_args(f):
         """\
@@ -67,6 +89,5 @@
             attribute = getattr(module, name)
             if not callable(attribute):
                 continue
-            decorated_function = (
-                RpcHandler._allow_keyword_args(attribute))
+            decorated_function = RpcHandler._allow_keyword_args(attribute)
             setattr(self._rpc_methods, name, decorated_function)
diff --git a/frontend/afe/urls.py b/frontend/afe/urls.py
index 0f46b02..09bff6c 100644
--- a/frontend/afe/urls.py
+++ b/frontend/afe/urls.py
@@ -7,7 +7,9 @@
     'jobs' : feed.JobFeed
 }
 
-pattern_list = [(r'^(?:|noauth/)rpc/', 'frontend.afe.views.handle_rpc')]
+pattern_list = [(r'^(?:|noauth/)rpc/', 'frontend.afe.views.handle_rpc'),
+                (r'^rpc_doc', 'frontend.afe.views.rpc_documentation'),
+               ]
 
 debug_pattern_list = [
     (r'^model_doc/', 'frontend.afe.views.model_documentation'),
diff --git a/frontend/afe/views.py b/frontend/afe/views.py
index 3638770..fa05517 100644
--- a/frontend/afe/views.py
+++ b/frontend/afe/views.py
@@ -16,6 +16,10 @@
     return rpc_handler_obj.handle_rpc_request(request)
 
 
+def rpc_documentation(request):
+    return rpc_handler_obj.get_rpc_documentation()
+
+
 def model_documentation(request):
     doc = '<h2>Models</h2>\n'
     for model_name in ('Label', 'Host', 'Test', 'User', 'AclGroup', 'Job',
diff --git a/frontend/client/src/autotest/afe/JobDetailView.java b/frontend/client/src/autotest/afe/JobDetailView.java
index 161131f..8a6b3d0 100644
--- a/frontend/client/src/autotest/afe/JobDetailView.java
+++ b/frontend/client/src/autotest/afe/JobDetailView.java
@@ -267,7 +267,7 @@
      * @param jobLogsId id-owner, e.g. "172-showard"
      */
     protected String getLogsURL(String jobLogsId) {
-        return Utils.getLogsURL(jobLogsId);
+        return Utils.getRetrieveLogsUrl(jobLogsId);
     }
     
     protected void pointToResults(String resultsUrl, String logsUrl, String oldResultsUrl) {
@@ -367,7 +367,7 @@
     }
 
     private String getLogsLinkHtml(String url, String text) {
-        url = Utils.getLogsURL(url);
+        url = Utils.getRetrieveLogsUrl(url);
         return "<a target=\"_blank\" href=\"" + url + "\">" + text + "</a>";
     }
 
diff --git a/frontend/client/src/autotest/common/JsonRpcCallback.java b/frontend/client/src/autotest/common/JsonRpcCallback.java
index f892205..93b47be 100644
--- a/frontend/client/src/autotest/common/JsonRpcCallback.java
+++ b/frontend/client/src/autotest/common/JsonRpcCallback.java
@@ -6,16 +6,44 @@
 import com.google.gwt.json.client.JSONString;
 import com.google.gwt.json.client.JSONValue;
 
+/**
+ * One of onSuccess() and onError() is guaranteed to be called for every RPC request.
+ */
 public abstract class JsonRpcCallback {
+    /**
+     * Called when a request completes successfully.
+     * @param result the value returned by the server.
+     */
     public abstract void onSuccess(JSONValue result);
+
+    /**
+     * Called when any request error occurs
+     * @param errorObject the error object returned by the server, containing keys "name", 
+     * "message", and "traceback".  This argument may be null in the case where no server response
+     * was received at all. 
+     */
     public void onError(JSONObject errorObject) {
-        String name = errorObject.get("name").isString().stringValue();
-        String message = errorObject.get("message").isString().stringValue();
+        if (errorObject == null) {
+            return;
+        }
+
+        String errorString =  getErrorString(errorObject);
         JSONString tracebackString = errorObject.get("traceback").isString();
         String traceback = null;
-        if (tracebackString != null)
+        if (tracebackString != null) {
             traceback = tracebackString.stringValue();
-        String errorString =  name + ": " + message;
+        }
+
         NotifyManager.getInstance().showError(errorString, traceback);
     }
+    
+    protected String getErrorString(JSONObject errorObject) {
+        if (errorObject == null) {
+            return "";
+        }
+
+        String name = Utils.jsonToString(errorObject.get("name"));
+        String message = Utils.jsonToString(errorObject.get("message"));
+        return name + ": " + message;
+    }
 }
diff --git a/frontend/client/src/autotest/common/JsonRpcProxy.java b/frontend/client/src/autotest/common/JsonRpcProxy.java
index 2209c6d..eff1d58 100644
--- a/frontend/client/src/autotest/common/JsonRpcProxy.java
+++ b/frontend/client/src/autotest/common/JsonRpcProxy.java
@@ -2,17 +2,10 @@
 
 import autotest.common.ui.NotifyManager;
 
-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.http.client.Response;
 import com.google.gwt.json.client.JSONArray;
-import com.google.gwt.json.client.JSONException;
 import com.google.gwt.json.client.JSONNull;
 import com.google.gwt.json.client.JSONNumber;
 import com.google.gwt.json.client.JSONObject;
-import com.google.gwt.json.client.JSONParser;
 import com.google.gwt.json.client.JSONString;
 import com.google.gwt.json.client.JSONValue;
 
@@ -20,42 +13,50 @@
 import java.util.Map;
 import java.util.Set;
 
-/**
- * A singleton class to facilitate RPC calls to the server.
- */
-public class JsonRpcProxy {
+public abstract class JsonRpcProxy {
     public static final String AFE_BASE_URL = "/afe/server/";
     public static final String TKO_BASE_URL = "/new_tko/server/";
     private static final String RPC_URL_SUFFIX = "rpc/";
+
     private static String defaultBaseUrl;
-    
-    private static final Map<String,JsonRpcProxy> instanceMap = new HashMap<String,JsonRpcProxy>();
-    
-    protected NotifyManager notify = NotifyManager.getInstance();
-    
-    protected RequestBuilder requestBuilder;
-    
+    private static final Map<String, JsonRpcProxy> instanceMap =
+        new HashMap<String, JsonRpcProxy>();
+    protected static NotifyManager notify = NotifyManager.getInstance();
+
     public static void setDefaultBaseUrl(String baseUrl) {
         defaultBaseUrl = baseUrl;
     }
-    
+
     public static JsonRpcProxy getProxy(String baseUrl) {
         if (!instanceMap.containsKey(baseUrl)) {
-            instanceMap.put(baseUrl, new JsonRpcProxy(baseUrl));
+            instanceMap.put(baseUrl, new XhrJsonRpcProxy(baseUrl + RPC_URL_SUFFIX));
         }
         return instanceMap.get(baseUrl);
     }
-    
+
     public static JsonRpcProxy getProxy() {
         assert defaultBaseUrl != null;
         return getProxy(defaultBaseUrl);
     }
-    
-    private JsonRpcProxy(String url) {
-        requestBuilder = new RequestBuilder(RequestBuilder.POST, url + RPC_URL_SUFFIX);
-    }
 
-    protected JSONArray processParams(JSONObject params) {
+    /**
+     * Make an RPC call.
+     * @param method name of the method to call
+     * @param params dictionary of parameters to pass
+     * @param callback callback to be notified of RPC call results
+     */
+    public void rpcCall(String method, JSONObject params, final JsonRpcCallback callback) {
+        JSONObject request = new JSONObject();
+        request.put("method", new JSONString(method));
+        request.put("params", processParams(params));
+        request.put("id", new JSONNumber(0));
+        
+        sendRequest(request, callback);
+    }
+    
+    protected abstract void sendRequest(JSONObject request, final JsonRpcCallback callback); 
+
+    private JSONArray processParams(JSONObject params) {
         JSONArray result = new JSONArray();
         JSONObject newParams = new JSONObject();
         if (params != null) {
@@ -68,78 +69,21 @@
         result.set(0, newParams);
         return result;
     }
-
-    /**
-     * Make an RPC call.
-     * @param method name of the method to call
-     * @param params dictionary of parameters to pass
-     * @param callback callback to be notified of RPC call results
-     */
-    public void rpcCall(String method, JSONObject params,
-                        final JsonRpcCallback callback) {
-        JSONObject request = new JSONObject();
-        request.put("method", new JSONString(method));
-        request.put("params", processParams(params));
-        request.put("id", new JSONNumber(0));
-
-        notify.setLoading(true);
-
-        try {
-          requestBuilder.sendRequest(request.toString(),
-                                     new RpcHandler(callback));
+    
+    protected static void handleResponseObject(JSONObject responseObject, 
+                                               JsonRpcCallback callback) {
+        JSONValue error = responseObject.get("error");
+        if (error == null) {
+            notify.showError("Bad JSON response", responseObject.toString());
+            callback.onError(null);
+            return;
         }
-        catch (RequestException e) {
-            notify.showError("Unable to connect to server");
-        }
-    }
-
-    class RpcHandler implements RequestCallback {
-        private JsonRpcCallback callback;
-
-        public RpcHandler(JsonRpcCallback callback) {
-            this.callback = callback;
+        else if (error.isObject() != null) {
+            callback.onError(error.isObject());
+            return;
         }
 
-        public void onError(Request request, Throwable exception) {
-            notify.showError("Unable to make RPC call", exception.toString());
-        }
-
-        public void onResponseReceived(Request request, Response response) {
-            notify.setLoading(false);
-
-            String responseText = response.getText();
-            int statusCode = response.getStatusCode();
-            if (statusCode != 200) {
-                notify.showError("Received error " +
-                                 Integer.toString(statusCode) + " " +
-                                 response.getStatusText(),
-                                 response.getHeadersAsString() + "\n\n" +
-                                 responseText);
-                return;
-            }
-
-            JSONValue responseValue = null;
-            try {
-                responseValue = JSONParser.parse(responseText);
-            }
-            catch (JSONException exc) {
-                notify.showError(exc.toString(), responseText);
-                return;
-            }
-
-            JSONObject responseObject = responseValue.isObject();
-            JSONValue error = responseObject.get("error");
-            if (error == null) {
-                notify.showError("Bad JSON response", responseText);
-                return;
-            }
-            else if (error.isObject() != null) {
-                callback.onError(error.isObject());
-                return;
-            }
-
-            JSONValue result = responseObject.get("result");
-            callback.onSuccess(result);
-        }
+        JSONValue result = responseObject.get("result");
+        callback.onSuccess(result);
     }
 }
diff --git a/frontend/client/src/autotest/common/PaddedJsonRpcProxy.java b/frontend/client/src/autotest/common/PaddedJsonRpcProxy.java
new file mode 100644
index 0000000..fc742eb
--- /dev/null
+++ b/frontend/client/src/autotest/common/PaddedJsonRpcProxy.java
@@ -0,0 +1,162 @@
+package autotest.common;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.user.client.Timer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JsonRpcProxy that uses "JSON with Padding" (JSONP) to make requests.  This allows it to get 
+ * around the Same-Origin Policy that limits XmlHttpRequest-based techniques.  However, it requires 
+ * close coupling with the server and it allows the server to execute arbitrary JavaScript within 
+ * the page, so it should only be used with trusted servers.
+ * 
+ * See http://code.google.com/docreader/#p=google-web-toolkit-doc-1-5&s=google-web-toolkit-doc-1-5&t=Article_UsingGWTForJSONMashups.
+ * Much of the code here is borrowed from or inspired by that article.
+ */
+public class PaddedJsonRpcProxy extends JsonRpcProxy {
+    private static final int REQUEST_TIMEOUT_MILLIS = 10000;
+    private static final String SCRIPT_TAG_PREFIX = "__jsonp_rpc_script";
+    private static final String CALLBACK_PREFIX = "__jsonp_rpc_callback";
+
+    private static int idCounter = 0;
+
+    private String rpcUrl;
+
+    private static class JsonpRequest {
+        private int requestId;
+        private String requestData;
+        private Element scriptTag;
+        private String callbackName;
+        private Timer timeoutTimer;
+        private JsonRpcCallback rpcCallback;
+        private boolean timedOut = false;
+
+        public JsonpRequest(String requestData, JsonRpcCallback rpcCallback) {
+            requestId = getRequestId();
+            this.requestData = requestData;
+            this.rpcCallback = rpcCallback;
+
+            callbackName = CALLBACK_PREFIX + requestId;
+            addCallback(this, callbackName);
+
+            timeoutTimer = new Timer() {
+                @Override
+                public void run() {
+                    GWT.log("timeout firing " + requestId, null);
+                    timedOut = true;
+                    cleanup();
+                    notify.showError("Request timed out");
+                    JsonpRequest.this.rpcCallback.onError(null);
+                }
+            };
+        }
+        
+        private String getFullUrl(String rpcUrl) {
+            Map<String, String> arguments = new HashMap<String, String>();
+            arguments.put("callback", callbackName);
+            arguments.put("request", requestData);
+            return rpcUrl + "?" + Utils.encodeUrlArguments(arguments);
+        }
+
+        public void send(String rpcUrl) {
+            scriptTag = addScript(getFullUrl(rpcUrl), requestId);
+            timeoutTimer.schedule(REQUEST_TIMEOUT_MILLIS);
+            notify.setLoading(true);
+            GWT.log("request sent " + requestId + " <" + requestData + ">", null);
+        }
+
+        public void cleanup() {
+            dropScript(scriptTag);
+            dropCallback(callbackName);
+            timeoutTimer.cancel();
+            notify.setLoading(false);
+        }
+
+        /**
+         * This method is called directly from native code (the dynamically loaded <script> calls
+         * our callback method, which calls this), so we need to do proper GWT exception handling
+         * manually.
+         * 
+         * See the implementation of com.google.gwt.user.client.Timer.fire(), from which this
+         * technique was borrowed.
+         */
+        public void handleResponse(JavaScriptObject responseJso) {
+            UncaughtExceptionHandler handler = GWT.getUncaughtExceptionHandler();
+            if (handler == null) {
+                handleResponseImpl(responseJso);
+                return;
+            }
+
+            try {
+                handleResponseImpl(responseJso);
+            } catch (Throwable throwable) {
+                handler.onUncaughtException(throwable);
+            }
+        }
+
+        public void handleResponseImpl(JavaScriptObject responseJso) {
+            GWT.log("response arrived " + requestId, null);
+            cleanup();
+            if (timedOut) {
+                GWT.log("already timed out " + requestId, null);
+                return;
+            }
+
+            JSONObject responseObject = new JSONObject(responseJso);
+            GWT.log("handling response " + requestId 
+                    + " (" + responseJso.toString().length() + ")", 
+                    null);
+            handleResponseObject(responseObject, rpcCallback);
+            GWT.log("done " + requestId, null);
+        }
+    }
+
+    public PaddedJsonRpcProxy(String rpcUrl) {
+        this.rpcUrl = rpcUrl;
+    }
+
+    private static int getRequestId() {
+        return idCounter++;
+    }
+
+    private static native void addCallback(JsonpRequest request, String callbackName) /*-{
+        window[callbackName] = function(someData) {
+            request.@autotest.common.PaddedJsonRpcProxy.JsonpRequest::handleResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(someData);
+        }
+    }-*/;
+
+    private static native void dropCallback(String callbackName) /*-{
+        delete window[callbackName];
+    }-*/;
+
+    private static Element addScript(String url, int requestId) {
+        String scriptId = SCRIPT_TAG_PREFIX + requestId;
+        Element scriptElement = addScriptToDocument(scriptId, url);
+        return scriptElement;
+    }
+
+    private static native Element addScriptToDocument(String uniqueId, String url) /*-{
+        var elem = document.createElement("script");
+        elem.setAttribute("language", "JavaScript");
+        elem.setAttribute("src", url);
+        elem.setAttribute("id", uniqueId);
+        document.getElementsByTagName("body")[0].appendChild(elem);
+        return elem;
+    }-*/;
+
+    private static native void dropScript(Element scriptElement) /*-{
+        document.getElementsByTagName("body")[0].removeChild(scriptElement);
+    }-*/;
+
+    @Override
+    protected void sendRequest(JSONObject request, JsonRpcCallback callback) {
+        JsonpRequest jsonpRequest = new JsonpRequest(request.toString(), callback);
+        jsonpRequest.send(rpcUrl);
+    }
+}
diff --git a/frontend/client/src/autotest/common/Utils.java b/frontend/client/src/autotest/common/Utils.java
index cb4eb3f..0f880da 100644
--- a/frontend/client/src/autotest/common/Utils.java
+++ b/frontend/client/src/autotest/common/Utils.java
@@ -14,6 +14,8 @@
 
 public class Utils {
     public static final String JSON_NULL = "<null>";
+    public static final String RETRIEVE_LOGS_URL = "/tko/retrieve_logs.cgi";
+
     private static final String[][] escapeMappings = {
         {"&", "&amp;"},
         {">", "&gt;"},
@@ -166,13 +168,13 @@
     /**
      * @param path should be of the form "123-showard/status.log" or just "123-showard"
      */
-    public static String getLogsURL(String path) {
-        String val = URL.encode("/results/" + path);
-        return "/tko/retrieve_logs.cgi?job=" + val;
+    public static String getLogsUrl(String path) {
+        return "/results/" + path;
     }
-    
-    public static String getJsonpLogsUrl(String path, String callbackName) {
-        return getLogsURL(path) + "&jsonp_callback=" + callbackName;
+
+    public static String getRetrieveLogsUrl(String path) {
+        String logUrl = URL.encode(getLogsUrl(path));
+        return RETRIEVE_LOGS_URL + "?job=" + logUrl;
     }
 
     public static String jsonToString(JSONValue value) {
diff --git a/frontend/client/src/autotest/common/XhrJsonRpcProxy.java b/frontend/client/src/autotest/common/XhrJsonRpcProxy.java
new file mode 100644
index 0000000..fd7deb4
--- /dev/null
+++ b/frontend/client/src/autotest/common/XhrJsonRpcProxy.java
@@ -0,0 +1,85 @@
+package autotest.common;
+
+
+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.http.client.Response;
+import com.google.gwt.json.client.JSONException;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONValue;
+
+
+/**
+ * JsonRpcProxy that uses XmlHttpRequests to make requests to the server.  This is the standard 
+ * technique for AJAX and suffers from the usual restrictions -- Same-Origin Policy and a maximum of
+ * two simultaneous outstanding requests.
+ */
+class XhrJsonRpcProxy extends JsonRpcProxy {
+    protected RequestBuilder requestBuilder;
+    
+    public XhrJsonRpcProxy(String url) {
+        requestBuilder = new RequestBuilder(RequestBuilder.POST, url);
+    }
+
+    @Override
+    protected void sendRequest(JSONObject request, final JsonRpcCallback callback) {
+        try {
+          requestBuilder.sendRequest(request.toString(), new RpcHandler(callback));
+        }
+        catch (RequestException e) {
+            notify.showError("Unable to connect to server");
+            callback.onError(null);
+            return;
+        }
+
+        notify.setLoading(true);
+    }
+
+    private class RpcHandler implements RequestCallback {
+        private JsonRpcCallback callback;
+
+        public RpcHandler(JsonRpcCallback callback) {
+            this.callback = callback;
+        }
+
+        public void onError(Request request, Throwable exception) {
+            notify.setLoading(false);
+            notify.showError("Unable to make RPC call", exception.toString());
+            callback.onError(null);
+        }
+
+        public void onResponseReceived(Request request, Response response) {
+            notify.setLoading(false);
+
+            String responseText = response.getText();
+            int statusCode = response.getStatusCode();
+            if (statusCode != 200) {
+                notify.showError("Received error " + Integer.toString(statusCode) + " " +
+                                 response.getStatusText(),
+                                 response.getHeadersAsString() + "\n\n" + responseText);
+                callback.onError(null);
+                return;
+            }
+
+            handleResponseText(responseText, callback);
+        }
+    }
+
+    private static void handleResponseText(String responseText, JsonRpcCallback callback) {
+        JSONValue responseValue = null;
+        try {
+            responseValue = JSONParser.parse(responseText);
+        }
+        catch (JSONException exc) {
+            JsonRpcProxy.notify.showError(exc.toString(), responseText);
+            callback.onError(null);
+            return;
+        }
+    
+        JSONObject responseObject = responseValue.isObject();
+        handleResponseObject(responseObject, callback);
+    }
+}
diff --git a/frontend/client/src/autotest/tko/TestDetailView.java b/frontend/client/src/autotest/tko/TestDetailView.java
index a27bdf1..8019f5b 100644
--- a/frontend/client/src/autotest/tko/TestDetailView.java
+++ b/frontend/client/src/autotest/tko/TestDetailView.java
@@ -1,17 +1,17 @@
 package autotest.tko;
 
 import autotest.common.JsonRpcCallback;
+import autotest.common.JsonRpcProxy;
+import autotest.common.PaddedJsonRpcProxy;
 import autotest.common.Utils;
 import autotest.common.ui.DetailView;
 import autotest.common.ui.NotifyManager;
 import autotest.common.ui.RealHyperlink;
 
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Element;
 import com.google.gwt.json.client.JSONNumber;
 import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
 import com.google.gwt.json.client.JSONValue;
-import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.DisclosureEvent;
 import com.google.gwt.user.client.ui.DisclosureHandler;
@@ -31,11 +31,10 @@
 
 class TestDetailView extends DetailView {
     private static final int NO_TEST_ID = -1;
-    
-    private static List<Element> scripts = new ArrayList<Element>();
-    private static List<String> callbacks = new ArrayList<String>();
-    private static int callbackCounter = 0;
-    
+
+    private static final JsonRpcProxy logLoadingProxy = 
+        new PaddedJsonRpcProxy(Utils.RETRIEVE_LOGS_URL);
+
     private int testId = NO_TEST_ID;
     private String jobTag;
     private List<LogFileViewer> logFileViewers = new ArrayList<LogFileViewer>();
@@ -44,11 +43,25 @@
     private Panel logPanel;
     
     private class LogFileViewer extends Composite implements DisclosureHandler {
-        private static final int LOG_LOAD_TIMEOUT_MS = 5000;
         private DisclosurePanel panel;
         private String logFilePath;
-        private String callbackName;
-        private Timer loadTimeout = null;
+
+        private JsonRpcCallback rpcCallback = new JsonRpcCallback() {
+            @Override
+            public void onError(JSONObject errorObject) {
+                super.onError(errorObject);
+                String errorString = getErrorString(errorObject);
+                if (errorString.equals("")) {
+                    errorString = "Failed to load log "+ logFilePath;
+                }
+                setStatusText(errorString);
+            }
+
+            @Override
+            public void onSuccess(JSONValue result) {
+                handle(result);
+            }
+        };
         
         public LogFileViewer(String logFilePath, String logFileName) {
             this.logFilePath = logFilePath;
@@ -56,41 +69,27 @@
             panel.addEventHandler(this);
             panel.addStyleName("log-file-panel");
             initWidget(panel);
-
-            callbackName = setupCallback(this);
         }
         
         public void onOpen(DisclosureEvent event) {
-            addScript(getLogUrl());
+            JSONObject params = new JSONObject();
+            params.put("path", new JSONString(getLogUrl()));
+            logLoadingProxy.rpcCall("dummy", params, rpcCallback);
+
             setStatusText("Loading...");
-            loadTimeout = new Timer() {
-                @Override
-                public void run() {
-                    setStatusText("Failed to load log file");
-                }
-            };
-            loadTimeout.schedule(LOG_LOAD_TIMEOUT_MS);
         }
 
         private String getLogUrl() {
-            return Utils.getJsonpLogsUrl(jobTag + "/" + logFilePath, callbackName);
+            return Utils.getLogsUrl(jobTag + "/" + logFilePath);
         }
         
-        public void handle(JavaScriptObject jso) {
-            JSONObject object = new JSONObject(jso);
-            if (object.containsKey("error")) {
-                setStatusText(Utils.jsonToString(object.get("error")));
+        public void handle(JSONValue value) {
+            String logContents = value.isString().stringValue();
+            if (logContents.equals("")) {
+                setStatusText("Log file is empty");
             } else {
-                assert object.containsKey("contents");
-                String logContents = Utils.jsonToString(object.get("contents"));
-                if (logContents.equals("")) {
-                    setStatusText("Log file is empty");
-                } else {
-                    setLogText(logContents);
-                }
+                setLogText(logContents);
             }
-            
-            loadTimeout.cancel();
         }
 
         private void setLogText(String text) {
@@ -141,62 +140,6 @@
         }
     }
 
-    // JSON-P related methods
-
-    private static String setupCallback(LogFileViewer viewer) {
-        String callbackName = "__gwt_callback" + callbackCounter++;
-        addCallbackToWindow(viewer, callbackName);
-        callbacks.add(callbackName);
-        return callbackName;
-    }
-    /**
-     * See http://code.google.com/docreader/#p=google-web-toolkit-doc-1-5&s=google-web-toolkit-doc-1-5&t=Article_UsingGWTForJSONMashups.
-     */
-    private native static void addCallbackToWindow(LogFileViewer viewer, String callbackName) /*-{
-        window[callbackName] = function(someData) {
-            viewer.@autotest.tko.TestDetailView.LogFileViewer::handle(Lcom/google/gwt/core/client/JavaScriptObject;)(someData);
-        }
-    }-*/;
-    
-    private native static void dropCallback(String callbackName) /*-{
-        window[callbackName] = null;
-    }-*/;
-    
-    private static void addScript(String url) {
-        String scriptId = "__gwt_script" + callbackCounter++;
-        Element scriptElement = addScriptToDocument(scriptId, url);
-        scripts.add(scriptElement);
-    }
-    
-    /**
-     * See http://code.google.com/docreader/#p=google-web-toolkit-doc-1-5&s=google-web-toolkit-doc-1-5&t=Article_UsingGWTForJSONMashups.
-     */
-    private static native Element addScriptToDocument(String uniqueId, String url) /*-{
-        var elem = document.createElement("script");
-        elem.setAttribute("language", "JavaScript");
-        elem.setAttribute("src", url);
-        elem.setAttribute("id", uniqueId);
-        document.getElementsByTagName("body")[0].appendChild(elem);
-        return elem;
-    }-*/;
-    
-    private static native void dropScript(Element scriptElement) /*-{
-        document.getElementsByTagName("body")[0].removeChild(scriptElement);
-    }-*/;
-    
-    private void cleanupCallbacksAndScripts() {
-        for (String callbackName : callbacks) {
-            dropCallback(callbackName);
-        }
-        callbacks.clear();
-
-        for (Element scriptElement : scripts) {
-            dropScript(scriptElement);
-        }
-
-        scripts.clear();
-    }
-    
     @Override
     public void initialize() {
         super.initialize();
@@ -327,7 +270,7 @@
         attributePanel.clear();
         attributePanel.add(new AttributeTable(attributes));
         
-        logLink.setHref(Utils.getLogsURL(jobTag));
+        logLink.setHref(Utils.getRetrieveLogsUrl(jobTag));
         addLogViewers(testName);
         
         displayObjectData("Test " + testName + " (job " + jobTag + ")");
@@ -335,6 +278,5 @@
     @Override
     public void resetPage() {
         super.resetPage();
-        cleanupCallbacksAndScripts();
     }
 }