blob: 650cacdabbfe8012ba4c35d31aaeb8e9a94f397b [file] [log] [blame]
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.client.rpc;
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.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.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 {
/**
* Expected JSON content body prefix that prevents XSSI.
* <p>
* The server always includes this line as the first line of the response
* content body when the response body is formatted as JSON. It gets inserted
* by the server to prevent the resource from being imported into another
* domain's page using a &lt;script&gt; tag. This line must be removed before
* the JSON can be parsed.
*/
private static final String JSON_MAGIC = ")]}'\n";
private class MyRequestCallback<T extends JavaScriptObject> implements
RequestCallback {
private final boolean wasGet;
private final AsyncCallback<T> cb;
public MyRequestCallback(boolean wasGet, AsyncCallback<T> cb) {
this.wasGet = wasGet;
this.cb = cb;
}
@Override
public void onResponseReceived(Request req, Response res) {
int status = res.getStatusCode();
if (status != 200) {
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()));
}
return;
}
if (!isJsonBody(res)) {
RpcStatus.INSTANCE.onRpcComplete();
cb.onFailure(new RemoteJsonException("Invalid JSON"));
return;
}
String json = res.getText();
if (!json.startsWith(JSON_MAGIC)) {
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;
}
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.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());
} else {
cb.onFailure(err);
}
}
}
private StringBuilder url;
private boolean hasQueryParams;
/**
* Initialize a new API call.
* <p>
* By default the JSON format will be selected by including an HTTP Accept
* header in the request.
*
* @param name URL of the REST resource to access, e.g. {@code "/projects/"}
* to list accessible projects from the server.
*/
public RestApi(String name) {
if (name.startsWith("/")) {
name = name.substring(1);
}
url = new StringBuilder();
url.append(GWT.getHostPageBaseURL());
url.append(name);
}
public RestApi addParameter(String name, String value) {
return addParameterRaw(name, URL.encodeQueryString(value));
}
public RestApi addParameterTrue(String name) {
return addParameterRaw(name, null);
}
public RestApi addParameter(String name, boolean value) {
return addParameterRaw(name, value ? "t" : "f");
}
public RestApi addParameter(String name, int value) {
return addParameterRaw(name, String.valueOf(value));
}
public RestApi addParameter(String name, Enum<?> value) {
return addParameterRaw(name, value.name());
}
public RestApi addParameterRaw(String name, String value) {
if (hasQueryParams) {
url.append("&");
} else {
url.append("?");
hasQueryParams = true;
}
url.append(name);
if (value != null) {
url.append("=").append(value);
}
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));
try {
RpcStatus.INSTANCE.onRpcStart();
req.send();
} catch (RequestException e) {
RpcStatus.INSTANCE.onRpcComplete();
cb.onFailure(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));
try {
req.send();
} catch (RequestException e) {
RpcStatus.INSTANCE.onRpcComplete();
cb.onFailure(e);
}
}
private static boolean isJsonBody(Response res) {
return isContentType(res, JsonConstants.JSON_TYPE);
}
private static boolean isTextBody(Response res) {
return isContentType(res, "text/plain");
}
private static boolean isContentType(Response res, String want) {
String type = res.getHeader("Content-Type");
if (type == null) {
return false;
}
int semi = type.indexOf(';');
if (semi >= 0) {
type = type.substring(0, semi).trim();
}
return want.equals(type);
}
}