diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java index 76ce3844e0..955c8e2648 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java @@ -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); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java new file mode 100644 index 0000000000..e820fe060f --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java @@ -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 extends JavaScriptObject { + protected NativeList() { + } + + public final List asList() { + return new AbstractList() { + @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; }-*/; +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java new file mode 100644 index 0000000000..cde9041bc0 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java @@ -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 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 > AsyncCallback copyKeysIntoChildren( + AsyncCallback callback) { + return copyKeysIntoChildren("name", callback); + } + + /** Loop through the result map and set asProperty on the children. */ + public static > AsyncCallback copyKeysIntoChildren( + final String asProperty, AsyncCallback callback) { + return new TransformCallback(callback) { + @Override + protected M transform(M result) { + result.copyKeysIntoChildren(asProperty); + return result; + } + }; + } + + protected NativeMap() { + } + + public final Set keySet() { + return Natives.keys(this); + } + + public final native NativeList 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; + } + } + }-*/; +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java new file mode 100644 index 0000000000..3d99c9e4a8 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java @@ -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 keys(JavaScriptObject obj) { + if (obj != null) { + return new JSONObject(obj).keySet(); + } + return Collections.emptySet(); + } + + public static T parseJSON(String json) { + if (parser == null) { + parser = bestJsonParser(); + } + return parse0(parser, json); + } + + private static native + 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() { + } +} 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 new file mode 100644 index 0000000000..bd6909248e --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java @@ -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. + *

+ * 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. + *

+ * 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 void send(final AsyncCallback 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); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java new file mode 100644 index 0000000000..2cd22cb4e3 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java @@ -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 implements AsyncCallback{ + private final AsyncCallback callback; + + protected TransformCallback(AsyncCallback 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); +}