diff options
Diffstat (limited to 'gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java')
-rw-r--r-- | gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java | 303 |
1 files changed, 241 insertions, 62 deletions
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java index 650cacdabb..4ee63c6513 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java @@ -14,23 +14,38 @@ package com.google.gerrit.client.rpc; +import static com.google.gwt.http.client.RequestBuilder.DELETE; +import static com.google.gwt.http.client.RequestBuilder.GET; +import static com.google.gwt.http.client.RequestBuilder.POST; +import static com.google.gwt.http.client.RequestBuilder.PUT; + +import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.RpcStatus; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.RequestBuilder; +import com.google.gwt.http.client.RequestBuilder.Method; 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.http.client.URL; +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; +import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.rpc.StatusCodeException; -import com.google.gwtjsonrpc.client.RemoteJsonException; -import com.google.gwtjsonrpc.client.ServerUnavailableException; -import com.google.gwtjsonrpc.common.AsyncCallback; -import com.google.gwtjsonrpc.common.JsonConstants; /** Makes a REST API call to the server. */ public class RestApi { + private static final int SC_UNAVAILABLE = 2; + private static final int SC_BAD_TRANSPORT = 3; + private static final int SC_BAD_RESPONSE = 4; + private static final String JSON_TYPE = "application/json"; + private static final String JSON_UTF8 = JSON_TYPE + "; charset=utf-8"; + private static final String TEXT_TYPE = "text/plain"; + /** * Expected JSON content body prefix that prevents XSSI. * <p> @@ -42,75 +57,131 @@ public class RestApi { */ private static final String JSON_MAGIC = ")]}'\n"; - private class MyRequestCallback<T extends JavaScriptObject> implements - RequestCallback { - private final boolean wasGet; + /** True if err is a StatusCodeException reporting Not Found. */ + public static boolean isNotFound(Throwable err) { + return isStatus(err, Response.SC_NOT_FOUND); + } + + /** True if err is describing a user that is currently anonymous. */ + public static boolean isNotSignedIn(Throwable err) { + if (err instanceof StatusCodeException) { + StatusCodeException sce = (StatusCodeException) err; + if (sce.getStatusCode() == Response.SC_UNAUTHORIZED) { + return true; + } + return sce.getStatusCode() == Response.SC_FORBIDDEN + && (sce.getEncodedResponse().equals("Authentication required") + || sce.getEncodedResponse().startsWith("Must be signed-in")); + } + return false; + } + + /** True if err is a StatusCodeException with a specific HTTP code. */ + public static boolean isStatus(Throwable err, int status) { + return err instanceof StatusCodeException + && ((StatusCodeException) err).getStatusCode() == status; + } + + /** Is the Gerrit Code Review server likely to return this status? */ + public static boolean isExpected(int statusCode) { + switch (statusCode) { + case SC_UNAVAILABLE: + case 400: // Bad Request + case 401: // Unauthorized + case 403: // Forbidden + case 404: // Not Found + case 405: // Method Not Allowed + case 409: // Conflict + case 412: // Precondition Failed + case 429: // Too Many Requests (RFC 6585) + return true; + + default: + // Assume any other code is not expected. These may be + // local proxy server errors outside of our control. + return false; + } + } + + private static class HttpCallback<T extends JavaScriptObject> + implements RequestCallback { private final AsyncCallback<T> cb; - public MyRequestCallback(boolean wasGet, AsyncCallback<T> cb) { - this.wasGet = wasGet; + HttpCallback(AsyncCallback<T> cb) { this.cb = cb; } @Override public void onResponseReceived(Request req, Response res) { int status = res.getStatusCode(); - if (status != 200) { + if (status == Response.SC_NO_CONTENT) { + cb.onSuccess(null); RpcStatus.INSTANCE.onRpcComplete(); - if ((400 <= status && status < 600) && isTextBody(res)) { - cb.onFailure(new RemoteJsonException(res.getText(), status, null)); - } else { - cb.onFailure(new StatusCodeException(status, res.getStatusText())); + + } else if (200 <= status && status < 300) { + if (!isJsonBody(res)) { + RpcStatus.INSTANCE.onRpcComplete(); + cb.onFailure(new StatusCodeException(SC_BAD_RESPONSE, "Expected " + + JSON_TYPE + "; received Content-Type: " + + res.getHeader("Content-Type"))); + return; } - return; - } - if (!isJsonBody(res)) { - RpcStatus.INSTANCE.onRpcComplete(); - cb.onFailure(new RemoteJsonException("Invalid JSON")); - return; - } + T data; + try { + // javac generics bug + data = RestApi.<T>cast(parseJson(res)); + } catch (JSONException e) { + RpcStatus.INSTANCE.onRpcComplete(); + cb.onFailure(new StatusCodeException(SC_BAD_RESPONSE, + "Invalid JSON: " + e.getMessage())); + return; + } - String json = res.getText(); - if (!json.startsWith(JSON_MAGIC)) { + cb.onSuccess(data); RpcStatus.INSTANCE.onRpcComplete(); - cb.onFailure(new RemoteJsonException("Invalid JSON")); - return; - } - json = json.substring(JSON_MAGIC.length()); - if (wasGet && json.startsWith("{\"_authkey\":")) { - RestApi.this.resendPost(cb, json); - return; - } + } else { + String msg; + if (isTextBody(res)) { + msg = res.getText().trim(); + } else if (isJsonBody(res)) { + JSONValue v; + try { + v = parseJson(res); + } catch (JSONException e) { + v = null; + } + if (v != null && v.isString() != null) { + msg = v.isString().stringValue(); + } else { + msg = trimJsonMagic(res.getText()).trim(); + } + } else { + msg = res.getStatusText(); + } - T data; - try { - // javac generics bug - data = Natives.<T> parseJSON(json); - } catch (RuntimeException e) { RpcStatus.INSTANCE.onRpcComplete(); - cb.onFailure(new RemoteJsonException("Invalid JSON")); - return; + cb.onFailure(new StatusCodeException(status, msg)); } - - cb.onSuccess(data); - RpcStatus.INSTANCE.onRpcComplete(); } @Override public void onError(Request req, Throwable err) { RpcStatus.INSTANCE.onRpcComplete(); if (err.getMessage().contains("XmlHttpRequest.status")) { - cb.onFailure(new ServerUnavailableException()); + cb.onFailure(new StatusCodeException( + SC_UNAVAILABLE, + RpcConstants.C.errorServerUnavailable())); } else { - cb.onFailure(err); + cb.onFailure(new StatusCodeException(SC_BAD_TRANSPORT, err.getMessage())); } } } private StringBuilder url; private boolean hasQueryParams; + private String ifNoneMatch; /** * Initialize a new API call. @@ -131,10 +202,40 @@ public class RestApi { url.append(name); } + public RestApi view(String name) { + return idRaw(name); + } + + public RestApi id(String id) { + return idRaw(URL.encodeQueryString(id)); + } + + public RestApi id(int id) { + return idRaw(Integer.toString(id)); + } + + public RestApi idRaw(String name) { + if (hasQueryParams) { + throw new IllegalStateException(); + } + if (url.charAt(url.length() - 1) != '/') { + url.append('/'); + } + url.append(name); + return this; + } + public RestApi addParameter(String name, String value) { return addParameterRaw(name, URL.encodeQueryString(value)); } + public RestApi addParameter(String name, String... value) { + for (String val : value) { + addParameter(name, val); + } + return this; + } + public RestApi addParameterTrue(String name) { return addParameterRaw(name, null); } @@ -165,40 +266,87 @@ public class RestApi { return this; } - public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) { - RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString()); - req.setHeader("Accept", JsonConstants.JSON_TYPE); - req.setCallback(new MyRequestCallback<T>(true, cb)); + public RestApi ifNoneMatch() { + return ifNoneMatch("*"); + } + + public RestApi ifNoneMatch(String etag) { + ifNoneMatch = etag; + return this; + } + + public String url() { + return url.toString(); + } + + public <T extends JavaScriptObject> void get(AsyncCallback<T> cb) { + send(GET, cb); + } + + public <T extends JavaScriptObject> void delete(AsyncCallback<T> cb) { + send(DELETE, cb); + } + + private <T extends JavaScriptObject> void send( + Method method, AsyncCallback<T> cb) { + HttpCallback<T> httpCallback = new HttpCallback<T>(cb); try { RpcStatus.INSTANCE.onRpcStart(); - req.send(); + request(method).sendRequest(null, httpCallback); } catch (RequestException e) { - RpcStatus.INSTANCE.onRpcComplete(); - cb.onFailure(e); + httpCallback.onError(null, e); } } - private <T extends JavaScriptObject> void resendPost( - final AsyncCallback<T> cb, String token) { - RequestBuilder req = new RequestBuilder(RequestBuilder.POST, url.toString()); - req.setHeader("Accept", JsonConstants.JSON_TYPE); - req.setHeader("Content-Type", JsonConstants.JSON_TYPE); - req.setRequestData(token); - req.setCallback(new MyRequestCallback<T>(false, cb)); + public <T extends JavaScriptObject> void post( + JavaScriptObject content, + AsyncCallback<T> cb) { + sendJSON(POST, content, cb); + } + + public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) { + send(PUT, cb); + } + + public <T extends JavaScriptObject> void put( + JavaScriptObject content, + AsyncCallback<T> cb) { + sendJSON(PUT, content, cb); + } + + private <T extends JavaScriptObject> void sendJSON( + Method method, JavaScriptObject content, + AsyncCallback<T> cb) { + HttpCallback<T> httpCallback = new HttpCallback<T>(cb); try { - req.send(); + RpcStatus.INSTANCE.onRpcStart(); + String body = new JSONObject(content).toString(); + RequestBuilder req = request(method); + req.setHeader("Content-Type", JSON_UTF8); + req.sendRequest(body, httpCallback); } catch (RequestException e) { - RpcStatus.INSTANCE.onRpcComplete(); - cb.onFailure(e); + httpCallback.onError(null, e); } } + private RequestBuilder request(Method method) { + RequestBuilder req = new RequestBuilder(method, url()); + if (ifNoneMatch != null) { + req.setHeader("If-None-Match", ifNoneMatch); + } + req.setHeader("Accept", JSON_TYPE); + if (Gerrit.getXGerritAuth() != null) { + req.setHeader("X-Gerrit-Auth", Gerrit.getXGerritAuth()); + } + return req; + } + private static boolean isJsonBody(Response res) { - return isContentType(res, JsonConstants.JSON_TYPE); + return isContentType(res, JSON_TYPE); } private static boolean isTextBody(Response res) { - return isContentType(res, "text/plain"); + return isContentType(res, TEXT_TYPE); } private static boolean isContentType(Response res, String want) { @@ -212,4 +360,35 @@ public class RestApi { } return want.equals(type); } + + private static JSONValue parseJson(Response res) + throws JSONException { + String json = trimJsonMagic(res.getText()); + if (json.isEmpty()) { + throw new JSONException("response was empty"); + } + return JSONParser.parseStrict(json); + } + + private static String trimJsonMagic(String json) { + if (json.startsWith(JSON_MAGIC)) { + json = json.substring(JSON_MAGIC.length()); + } + return json; + } + + @SuppressWarnings("unchecked") + private static <T extends JavaScriptObject> T cast(JSONValue val) { + if (val.isObject() != null) { + return (T) val.isObject().getJavaScriptObject(); + } else if (val.isArray() != null) { + return (T) val.isArray().getJavaScriptObject(); + } else if (val.isString() != null) { + return (T) NativeString.wrap(val.isString().stringValue()); + } else if (val.isNull() != null) { + return null; + } else { + throw new JSONException("unsupported JSON type"); + } + } } |