Define a native REST API client
This small shim of a client makes it easier to issue a REST API call to the server and convert the results into a "Java" object available within the UI code. To support fast conversion, requests ask to use JSON by setting the Accept header and use JavaScript overlay types to avoid any copying. A NativeList utility is provided to handle a list result that contains objects as the array members. There is no conversion cost to using this list collection type. A NativeMap utility is provided to handle a "map" of String to an object, such as might appear in a JSON object. The map class can optionally copy the map keys into the values as a property of the value, allowing the client to convert: { "a": {"id": 1}, "b": {"id": 2}, } into the following: { "a": {"id": 1, "name": "a"}, "b": {"id": 2, "name": "b"}, } This is really useful if the caller just wants a list-like structure of the children to enumerate, and doesn't wants to obtain the name from the child object without worrying about keeping the map pair. The client is currently only suitable for reading from the server, to make state changes a XSRF token system must be added, and data should be sent as part of an HTTP POST or HTTP PUT method. Change-Id: Id07b2b62fb2239bc8bde00e109188410beb68882
This commit is contained in:
@@ -56,6 +56,10 @@ public class RpcStatus implements RpcStartHandler, RpcCompleteHandler {
|
||||
|
||||
@Override
|
||||
public void onRpcStart(final RpcStartEvent event) {
|
||||
onRpcStart();
|
||||
}
|
||||
|
||||
public void onRpcStart() {
|
||||
if (++activeCalls == 1) {
|
||||
if (hideDepth == 0) {
|
||||
loading.setVisible(true);
|
||||
@@ -65,6 +69,10 @@ public class RpcStatus implements RpcStartHandler, RpcCompleteHandler {
|
||||
|
||||
@Override
|
||||
public void onRpcComplete(final RpcCompleteEvent event) {
|
||||
onRpcComplete();
|
||||
}
|
||||
|
||||
public void onRpcComplete() {
|
||||
if (--activeCalls == 0) {
|
||||
loading.setVisible(false);
|
||||
}
|
||||
|
@@ -0,0 +1,55 @@
|
||||
// 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.gwt.core.client.JavaScriptObject;
|
||||
|
||||
import java.util.AbstractList;
|
||||
import java.util.List;
|
||||
|
||||
/** A read-only list of native JavaScript objects stored in a JSON array. */
|
||||
public class NativeList<T extends JavaScriptObject> extends JavaScriptObject {
|
||||
protected NativeList() {
|
||||
}
|
||||
|
||||
public final List<T> asList() {
|
||||
return new AbstractList<T>() {
|
||||
@Override
|
||||
public T set(int index, T element) {
|
||||
T old = NativeList.this.get(index);
|
||||
NativeList.this.set0(index, element);
|
||||
return old;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(int index) {
|
||||
return NativeList.this.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return NativeList.this.size();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public final boolean isEmpty() {
|
||||
return size() == 0;
|
||||
}
|
||||
|
||||
public final native int size() /*-{ return this.length; }-*/;
|
||||
public final native T get(int i) /*-{ return this[i]; }-*/;
|
||||
private final native void set0(int i, T v) /*-{ this[i] = v; }-*/;
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
// 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.gwt.core.client.JavaScriptObject;
|
||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/** A map of native JSON objects, keyed by a string. */
|
||||
public class NativeMap<T extends JavaScriptObject> extends JavaScriptObject {
|
||||
/**
|
||||
* Loop through the result map's entries and copy the key strings into the
|
||||
* "name" property of the corresponding child object. This only runs on the
|
||||
* top level map of the result, and requires the children to be JSON objects
|
||||
* and not a JSON primitive (e.g. boolean or string).
|
||||
*/
|
||||
public static <T extends JavaScriptObject,
|
||||
M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
|
||||
AsyncCallback<M> callback) {
|
||||
return copyKeysIntoChildren("name", callback);
|
||||
}
|
||||
|
||||
/** Loop through the result map and set asProperty on the children. */
|
||||
public static <T extends JavaScriptObject,
|
||||
M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
|
||||
final String asProperty, AsyncCallback<M> callback) {
|
||||
return new TransformCallback<M, M>(callback) {
|
||||
@Override
|
||||
protected M transform(M result) {
|
||||
result.copyKeysIntoChildren(asProperty);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected NativeMap() {
|
||||
}
|
||||
|
||||
public final Set<String> keySet() {
|
||||
return Natives.keys(this);
|
||||
}
|
||||
|
||||
public final native NativeList<T> values()
|
||||
/*-{
|
||||
var s = this;
|
||||
var v = [];
|
||||
var i = 0;
|
||||
for (var k in s) {
|
||||
if (s.hasOwnProperty(k)) {
|
||||
v[i++] = s[k];
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}-*/;
|
||||
|
||||
public final int size() {
|
||||
return keySet().size();
|
||||
}
|
||||
|
||||
public final boolean isEmpty() {
|
||||
return size() == 0;
|
||||
}
|
||||
|
||||
public final boolean containsKey(String n) {
|
||||
return get(n) != null;
|
||||
}
|
||||
|
||||
public final native T get(String n) /*-{ return this[n]; }-*/;
|
||||
|
||||
public final native void copyKeysIntoChildren(String p)
|
||||
/*-{
|
||||
var s = this;
|
||||
for (var k in s) {
|
||||
if (s.hasOwnProperty(k)) {
|
||||
var c = s[k];
|
||||
c[p] = k;
|
||||
}
|
||||
}
|
||||
}-*/;
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
// 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.gwt.core.client.JavaScriptObject;
|
||||
import com.google.gwt.json.client.JSONObject;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
public class Natives {
|
||||
/**
|
||||
* Get the names of defined properties on the object. The returned set
|
||||
* iterates in the native iteration order, which may match the source order.
|
||||
*/
|
||||
public static Set<String> keys(JavaScriptObject obj) {
|
||||
if (obj != null) {
|
||||
return new JSONObject(obj).keySet();
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
public static <T extends JavaScriptObject> T parseJSON(String json) {
|
||||
if (parser == null) {
|
||||
parser = bestJsonParser();
|
||||
}
|
||||
return parse0(parser, json);
|
||||
}
|
||||
|
||||
private static native <T extends JavaScriptObject>
|
||||
T parse0(JavaScriptObject p, String s)
|
||||
/*-{ return p(s); }-*/;
|
||||
|
||||
private static JavaScriptObject parser;
|
||||
private static native JavaScriptObject bestJsonParser()
|
||||
/*-{
|
||||
if ($wnd.JSON && typeof $wnd.JSON.parse === 'function')
|
||||
return $wnd.JSON.parse;
|
||||
return function(s) { return eval('(' + s + ')'); };
|
||||
}-*/;
|
||||
|
||||
private Natives() {
|
||||
}
|
||||
}
|
@@ -0,0 +1,174 @@
|
||||
// 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 <script> tag. This line must be removed before
|
||||
* the JSON can be parsed.
|
||||
*/
|
||||
private static final String JSON_MAGIC = ")]}'\n";
|
||||
|
||||
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 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 RequestCallback() {
|
||||
@Override
|
||||
public void onResponseReceived(Request req, Response res) {
|
||||
RpcStatus.INSTANCE.onRpcComplete();
|
||||
int status = res.getStatusCode();
|
||||
if (status != 200) {
|
||||
if ((400 <= status && status < 500) && isTextBody(res)) {
|
||||
cb.onFailure(new RemoteJsonException(res.getText(), status, null));
|
||||
} else {
|
||||
cb.onFailure(new StatusCodeException(status, res.getStatusText()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isJsonBody(res)) {
|
||||
cb.onFailure(new RemoteJsonException("Invalid JSON"));
|
||||
return;
|
||||
}
|
||||
|
||||
String json = res.getText();
|
||||
if (!json.startsWith(JSON_MAGIC)) {
|
||||
cb.onFailure(new RemoteJsonException("Invalid JSON"));
|
||||
return;
|
||||
}
|
||||
|
||||
T data;
|
||||
try {
|
||||
data = Natives.parseJSON(json.substring(JSON_MAGIC.length()));
|
||||
} catch (RuntimeException e) {
|
||||
cb.onFailure(new RemoteJsonException("Invalid JSON"));
|
||||
return;
|
||||
}
|
||||
|
||||
cb.onSuccess(data);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
RpcStatus.INSTANCE.onRpcStart();
|
||||
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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
// 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.gwtjsonrpc.common.AsyncCallback;
|
||||
|
||||
/** Transforms a value and passes it on to another callback. */
|
||||
public abstract class TransformCallback<I, O> implements AsyncCallback<I>{
|
||||
private final AsyncCallback<O> callback;
|
||||
|
||||
protected TransformCallback(AsyncCallback<O> callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(I result) {
|
||||
callback.onSuccess(transform(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
callback.onFailure(caught);
|
||||
}
|
||||
|
||||
protected abstract O transform(I result);
|
||||
}
|
Reference in New Issue
Block a user