Add screen to list service users

Change-Id: I268d75f1abd7f00d1b28df86319f7ad630d2fab3
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserMenu.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserMenu.java
index 22a6077..74ac5ba 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserMenu.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserMenu.java
@@ -19,6 +19,8 @@
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -29,17 +31,28 @@
   private final String pluginName;
   private final Provider<CurrentUser> userProvider;
   private final List<MenuEntry> menuEntries;
+  private final Provider<ListServiceUsers> listServiceUsers;
 
   @Inject
   public ServiceUserMenu(@PluginName String pluginName,
-      Provider<CurrentUser> userProvider) {
+      Provider<CurrentUser> userProvider,
+      Provider<ListServiceUsers> listServiceUsers) {
     this.pluginName = pluginName;
     this.userProvider = userProvider;
+    this.listServiceUsers = listServiceUsers;
     menuEntries = Lists.newArrayList();
+
+    List<MenuItem> peopleItems = Lists.newArrayListWithExpectedSize(2);
     if (canCreateServiceUser()) {
-      menuEntries.add(new MenuEntry("People", Collections
-          .singletonList(new MenuItem("Create Service User", "#/x/" + pluginName + "/create", ""))));
+      peopleItems.add(new MenuItem("Create Service User", "#/x/" + pluginName + "/create", ""));
     }
+    if (canCreateServiceUser() || hasServiceUser()) {
+      peopleItems.add(new MenuItem("List Service Users", "#/x/" + pluginName + "/list", ""));
+    }
+    if (!peopleItems.isEmpty()) {
+      menuEntries.add(new MenuEntry("People", peopleItems));
+    }
+
     if (userProvider.get().getCapabilities().canAdministrateServer()) {
       menuEntries.add(new MenuEntry("Plugins", Collections
           .singletonList(new MenuItem("Service User Admin", "#/x/" + pluginName + "/admin", ""))));
@@ -56,6 +69,14 @@
     }
   }
 
+  private boolean hasServiceUser() {
+    try {
+      return !listServiceUsers.get().apply(new ConfigResource()).isEmpty();
+    } catch (OrmException e) {
+      return false;
+    }
+  }
+
   @Override
   public List<MenuEntry> getEntries() {
     return menuEntries;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/NativeMap.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/NativeMap.java
new file mode 100644
index 0000000..27bb222
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/NativeMap.java
@@ -0,0 +1,100 @@
+// 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.googlesource.gerrit.plugins.serviceuser.client;
+
+import com.google.gerrit.plugin.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.Set;
+
+/** A map of native JSON objects, keyed by a string. */
+public class NativeMap<T extends JavaScriptObject> extends JavaScriptObject {
+  public static <T extends JavaScriptObject> NativeMap<T> create() {
+    return createObject().cast();
+  }
+
+  /**
+   * 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 JsArray<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 put(String n, T v) /*-{ this[n] = v; }-*/;
+
+  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/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserInfo.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserInfo.java
new file mode 100644
index 0000000..68c0a7b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserInfo.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2014 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.googlesource.gerrit.plugins.serviceuser.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ServiceUserInfo extends JavaScriptObject {
+  public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String email() /*-{ return this.email; }-*/;
+  public final native String created_by() /*-{ return this.created_by; }-*/;
+  public final native String created_at() /*-{ return this.created_at; }-*/;
+
+  protected ServiceUserInfo() {
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserListScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserListScreen.java
new file mode 100644
index 0000000..945859e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserListScreen.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2014 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.googlesource.gerrit.plugins.serviceuser.client;
+
+import com.google.gerrit.plugin.client.Plugin;
+import com.google.gerrit.plugin.client.rpc.RestApi;
+import com.google.gerrit.plugin.client.screen.Screen;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.VerticalPanel;
+
+public class ServiceUserListScreen extends VerticalPanel {
+  static class Factory implements Screen.EntryPoint {
+    @Override
+    public void onLoad(Screen screen) {
+      screen.setPageTitle("Service Users");
+      screen.show(new ServiceUserListScreen());
+    }
+  }
+
+  ServiceUserListScreen() {
+    setStyleName("serviceuser-panel");
+
+    new RestApi("config").id("server").view(Plugin.get().getPluginName(), "serviceusers")
+        .get(NativeMap.copyKeysIntoChildren("username",
+            new AsyncCallback<NativeMap<ServiceUserInfo>>() {
+              @Override
+              public void onSuccess(NativeMap<ServiceUserInfo> info) {
+                display(info);
+              }
+
+              @Override
+              public void onFailure(Throwable caught) {
+                // never invoked
+              }
+            }));
+  }
+
+  private void display(NativeMap<ServiceUserInfo> info) {
+    int columns = 5;
+    FlexTable t = new FlexTable();
+    t.setStyleName("serviceuser-serviceUserTable");
+    FlexCellFormatter fmt = t.getFlexCellFormatter();
+    for (int c = 0; c < columns; c++) {
+      fmt.addStyleName(0, c, "dataHeader");
+      fmt.addStyleName(0, c, "topMostCell");
+    }
+    fmt.addStyleName(0, 0, "leftMostCell");
+
+    t.setText(0, 0, "Username");
+    t.setText(0, 1, "Full Name");
+    t.setText(0, 2, "Email");
+    t.setText(0, 3, "Created By");
+    t.setText(0, 4, "Created At");
+
+    int row = 1;
+    for (String username : info.keySet()) {
+      ServiceUserInfo a = info.get(username);
+
+      for (int c = 0; c < columns; c++) {
+        fmt.addStyleName(row, c, "dataCell");
+        fmt.addStyleName(row, 0, "leftMostCell");
+      }
+
+      t.setText(row, 0, username);
+      t.setText(row, 1, a.name());
+      t.setText(row, 2, a.email());
+      t.setText(row, 3, a.created_by());
+      t.setText(row, 4, a.created_at());
+      row++;
+    }
+
+    add(t);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserPlugin.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserPlugin.java
index 42c5a8f..4210adc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserPlugin.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserPlugin.java
@@ -25,5 +25,6 @@
   public void onPluginLoad() {
     Plugin.get().screen("create", new CreateServiceUserScreen.Factory());
     Plugin.get().screen("admin", new ServiceUserAdminScreen.Factory());
+    Plugin.get().screen("list", new ServiceUserListScreen.Factory());
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/TransformCallback.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/TransformCallback.java
new file mode 100644
index 0000000..b7d601c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/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.googlesource.gerrit.plugins.serviceuser.client;
+
+import com.google.gwt.user.client.rpc.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);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
index 6cf9292..a094130 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
@@ -6,3 +6,34 @@
   margin-left: 10px !important;
 }
 
+.serviceuser-serviceUserTable {
+  border-collapse: separate;
+  border-spacing: 0;
+}
+
+.serviceuser-serviceUserTable .leftMostCell {
+  border-left: 1px solid #EEE;
+}
+
+.serviceuser-serviceUserTable .topMostCell {
+  border-top: 1px solid #EEE;
+}
+
+.serviceuser-serviceUserTable .dataHeader {
+  border: 1px solid #FFF;
+  padding: 2px 6px 1px;
+  background-color: #EEE;
+  font-style: italic;
+  white-space: nowrap;
+  color: textColor;
+}
+
+.serviceuser-serviceUserTable .dataCell {
+  padding-left: 5px;
+  padding-right: 5px;
+  border-right: 1px solid #EEE;
+  border-bottom: 1px solid #EEE;
+  vertical-align: middle;
+  height: 20px;
+}
+