summaryrefslogtreecommitdiffstats
path: root/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
diff options
context:
space:
mode:
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.java303
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");
+ }
+ }
}