Add a project admin screen to show the projects you own

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/appjar/src/main/java/com/google/gerrit/Gerrit.gwt.xml b/appjar/src/main/java/com/google/gerrit/Gerrit.gwt.xml
index 77e4058..71b0040 100644
--- a/appjar/src/main/java/com/google/gerrit/Gerrit.gwt.xml
+++ b/appjar/src/main/java/com/google/gerrit/Gerrit.gwt.xml
@@ -30,6 +30,8 @@
            class='com.google.gerrit.server.GroupAdminServiceSrv'/>
   <servlet path='/rpc/PatchDetailService'
            class='com.google.gerrit.server.PatchDetailServiceSrv'/>
+  <servlet path='/rpc/ProjectAdminService'
+           class='com.google.gerrit.server.ProjectAdminServiceSrv'/>
   <servlet path='/rpc/SuggestService'
            class='com.google.gerrit.server.SuggestServiceSrv'/>
   <servlet path='/rpc/SystemInfoService'
diff --git a/appjar/src/main/java/com/google/gerrit/client/Link.java b/appjar/src/main/java/com/google/gerrit/client/Link.java
index 857c5bd..6a21b5d 100644
--- a/appjar/src/main/java/com/google/gerrit/client/Link.java
+++ b/appjar/src/main/java/com/google/gerrit/client/Link.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.account.NewAgreementScreen;
 import com.google.gerrit.client.admin.AccountGroupScreen;
 import com.google.gerrit.client.admin.GroupListScreen;
+import com.google.gerrit.client.admin.ProjectListScreen;
 import com.google.gerrit.client.changes.AccountDashboardScreen;
 import com.google.gerrit.client.changes.ChangeScreen;
 import com.google.gerrit.client.changes.MineStarredScreen;
@@ -29,6 +30,7 @@
 import com.google.gerrit.client.reviewdb.AccountGroup;
 import com.google.gerrit.client.reviewdb.Change;
 import com.google.gerrit.client.reviewdb.Patch;
+import com.google.gerrit.client.reviewdb.Project;
 import com.google.gerrit.client.rpc.RpcUtil;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gwt.core.client.GWT;
@@ -86,6 +88,10 @@
     return "admin,group," + id.toString();
   }
 
+  public static String toProjectAdmin(final Project.Id id) {
+    return "admin,project," + id.toString();
+  }
+
   public void onHistoryChanged(final String token) {
     Screen s;
     try {
@@ -150,6 +156,10 @@
       if (ADMIN_GROUPS.equals(token)) {
         return new GroupListScreen();
       }
+
+      if (ADMIN_PROJECTS.equals(token)) {
+        return new ProjectListScreen();
+      }
     }
 
     return null;
diff --git a/appjar/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/appjar/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 3cbf826..41df89f 100644
--- a/appjar/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/appjar/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -37,4 +37,5 @@
   String columnGroupDescription();
 
   String groupListTitle();
+  String projectListTitle();
 }
diff --git a/appjar/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/appjar/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index cab3b58..625c506 100644
--- a/appjar/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/appjar/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -18,3 +18,4 @@
 columnGroupDescription = Description
 
 groupListTitle = Groups
+projectListTitle = Projects
diff --git a/appjar/src/main/java/com/google/gerrit/client/admin/ProjectAdminService.java b/appjar/src/main/java/com/google/gerrit/client/admin/ProjectAdminService.java
new file mode 100644
index 0000000..3eb5cee
--- /dev/null
+++ b/appjar/src/main/java/com/google/gerrit/client/admin/ProjectAdminService.java
@@ -0,0 +1,39 @@
+// Copyright 2008 Google Inc.
+//
+// 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.admin;
+
+import com.google.gerrit.client.reviewdb.Project;
+import com.google.gerrit.client.rpc.SignInRequired;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.client.RemoteJsonService;
+import com.google.gwtjsonrpc.client.VoidResult;
+
+import java.util.List;
+
+public interface ProjectAdminService extends RemoteJsonService {
+  @SignInRequired
+  void ownedProjects(AsyncCallback<List<Project>> callback);
+
+  @SignInRequired
+  void projectDetail(Project.Id projectId, AsyncCallback<ProjectDetail> callback);
+
+  @SignInRequired
+  void changeProjectDescription(Project.Id projectId, String description,
+      AsyncCallback<VoidResult> callback);
+
+  @SignInRequired
+  void changeProjectOwner(Project.Id projectId, String newOwnerName,
+      AsyncCallback<VoidResult> callback);
+}
diff --git a/appjar/src/main/java/com/google/gerrit/client/admin/ProjectDetail.java b/appjar/src/main/java/com/google/gerrit/client/admin/ProjectDetail.java
new file mode 100644
index 0000000..cbf95dc
--- /dev/null
+++ b/appjar/src/main/java/com/google/gerrit/client/admin/ProjectDetail.java
@@ -0,0 +1,33 @@
+// Copyright 2008 Google Inc.
+//
+// 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.admin;
+
+import com.google.gerrit.client.reviewdb.AccountGroup;
+import com.google.gerrit.client.reviewdb.Project;
+import com.google.gerrit.client.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
+
+public class ProjectDetail {
+  protected Project project;
+  protected AccountGroup ownerGroup;
+
+  public ProjectDetail() {
+  }
+
+  public void load(final ReviewDb db, final Project g) throws OrmException {
+    project = g;
+    ownerGroup = db.accountGroups().get(project.getOwnerGroupId());
+  }
+}
diff --git a/appjar/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/appjar/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
new file mode 100644
index 0000000..2a21eba
--- /dev/null
+++ b/appjar/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -0,0 +1,122 @@
+// Copyright 2008 Google Inc.
+//
+// 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.admin;
+
+import com.google.gerrit.client.Link;
+import com.google.gerrit.client.reviewdb.Project;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.AccountScreen;
+import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.ui.Hyperlink;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.SourcesTableEvents;
+import com.google.gwt.user.client.ui.TableListener;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+
+import java.util.List;
+
+public class ProjectListScreen extends AccountScreen {
+  private ProjectTable projects;
+
+  public ProjectListScreen() {
+    super(Util.C.projectListTitle());
+  }
+
+  @Override
+  public Object getScreenCacheToken() {
+    return getClass();
+  }
+
+  @Override
+  public void onLoad() {
+    if (projects == null) {
+      initUI();
+    }
+
+    Util.PROJECT_SVC.ownedProjects(new GerritCallback<List<Project>>() {
+      public void onSuccess(final List<Project> result) {
+        if (isAttached()) {
+          projects.display(result);
+          projects.finishDisplay(true);
+        }
+      }
+    });
+  }
+
+  private void initUI() {
+    projects = new ProjectTable();
+    projects.setSavePointerId(Link.ADMIN_PROJECTS);
+    add(projects);
+
+    final VerticalPanel fp = new VerticalPanel();
+    fp.setStyleName("gerrit-AddSshKeyPanel");
+    final Label hdr = new Label(Util.C.headingCreateGroup());
+    hdr.setStyleName("gerrit-SmallHeading");
+    fp.add(hdr);
+  }
+
+  private class ProjectTable extends FancyFlexTable<Project> {
+    ProjectTable() {
+      table.setText(0, 1, Util.C.columnGroupName());
+      table.setText(0, 2, Util.C.columnGroupDescription());
+      table.addTableListener(new TableListener() {
+        public void onCellClicked(SourcesTableEvents sender, int row, int cell) {
+          if (cell != 1 && getRowItem(row) != null) {
+            movePointerTo(row);
+          }
+        }
+      });
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 1, S_DATA_HEADER);
+      fmt.addStyleName(0, 2, S_DATA_HEADER);
+    }
+
+    @Override
+    protected Object getRowItemKey(final Project item) {
+      return item.getId();
+    }
+
+    @Override
+    protected void onOpenItem(final Project item) {
+      History.newItem(Link.toProjectAdmin(item.getId()));
+    }
+
+    void display(final List<Project> result) {
+      while (1 < table.getRowCount())
+        table.removeRow(table.getRowCount() - 1);
+
+      for (final Project k : result) {
+        final int row = table.getRowCount();
+        table.insertRow(row);
+        populate(row, k);
+      }
+    }
+
+    void populate(final int row, final Project k) {
+      table.setWidget(row, 1, new Hyperlink(k.getName(), Link.toProjectAdmin(k
+          .getId())));
+      table.setText(row, 2, k.getDescription());
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(row, 1, S_DATA_CELL);
+      fmt.addStyleName(row, 2, S_DATA_CELL);
+
+      setRowItem(row, k);
+    }
+  }
+}
diff --git a/appjar/src/main/java/com/google/gerrit/client/admin/Util.java b/appjar/src/main/java/com/google/gerrit/client/admin/Util.java
index b03e528..5de4377 100644
--- a/appjar/src/main/java/com/google/gerrit/client/admin/Util.java
+++ b/appjar/src/main/java/com/google/gerrit/client/admin/Util.java
@@ -21,9 +21,13 @@
   public static final AdminConstants C = GWT.create(AdminConstants.class);
   public static final AdminMessages M = GWT.create(AdminMessages.class);
   public static final GroupAdminService GROUP_SVC;
+  public static final ProjectAdminService PROJECT_SVC;
 
   static {
     GROUP_SVC = GWT.create(GroupAdminService.class);
     JsonUtil.bind(GROUP_SVC, "rpc/GroupAdminService");
+
+    PROJECT_SVC = GWT.create(ProjectAdminService.class);
+    JsonUtil.bind(PROJECT_SVC, "rpc/ProjectAdminService");
   }
 }
diff --git a/appjar/src/main/java/com/google/gerrit/client/reviewdb/ProjectAccess.java b/appjar/src/main/java/com/google/gerrit/client/reviewdb/ProjectAccess.java
index e419498..566c178 100644
--- a/appjar/src/main/java/com/google/gerrit/client/reviewdb/ProjectAccess.java
+++ b/appjar/src/main/java/com/google/gerrit/client/reviewdb/ProjectAccess.java
@@ -31,6 +31,9 @@
   @Query("ORDER BY name")
   ResultSet<Project> all() throws OrmException;
 
+  @Query("WHERE ownerGroupId = ?")
+  ResultSet<Project> ownedByGroup(AccountGroup.Id groupId) throws OrmException;
+
   @Query("WHERE name.name >= ? AND name.name <= ? ORDER BY name LIMIT ?")
   ResultSet<Project> suggestByName(String nameA, String nameB, int limit)
       throws OrmException;
diff --git a/appjar/src/main/java/com/google/gerrit/server/ProjectAdminServiceImpl.java b/appjar/src/main/java/com/google/gerrit/server/ProjectAdminServiceImpl.java
new file mode 100644
index 0000000..03bfbf1
--- /dev/null
+++ b/appjar/src/main/java/com/google/gerrit/server/ProjectAdminServiceImpl.java
@@ -0,0 +1,151 @@
+// Copyright 2008 Google Inc.
+//
+// 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.server;
+
+import com.google.gerrit.client.admin.ProjectAdminService;
+import com.google.gerrit.client.admin.ProjectDetail;
+import com.google.gerrit.client.reviewdb.AccountGroup;
+import com.google.gerrit.client.reviewdb.AccountGroupMember;
+import com.google.gerrit.client.reviewdb.Project;
+import com.google.gerrit.client.reviewdb.ReviewDb;
+import com.google.gerrit.client.rpc.BaseServiceImplementation;
+import com.google.gerrit.client.rpc.NoSuchEntityException;
+import com.google.gerrit.client.rpc.RpcUtil;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtorm.client.OrmException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class ProjectAdminServiceImpl extends BaseServiceImplementation
+    implements ProjectAdminService {
+  private final AccountGroup.Id adminId;
+
+  public ProjectAdminServiceImpl(final GerritServer server) {
+    super(server.getDatabase());
+    adminId = server.getAdminGroupId();
+  }
+
+  public void ownedProjects(final AsyncCallback<List<Project>> callback) {
+    run(callback, new Action<List<Project>>() {
+      public List<Project> run(ReviewDb db) throws OrmException {
+        final List<Project> result;
+        if (amAdmin(db)) {
+          result = db.projects().all().toList();
+        } else {
+          result = myOwnedProjects(db);
+          Collections.sort(result, new Comparator<Project>() {
+            public int compare(final Project a, final Project b) {
+              return a.getName().compareTo(b.getName());
+            }
+          });
+        }
+        return result;
+      }
+    });
+  }
+
+  public void projectDetail(final Project.Id projectId,
+      final AsyncCallback<ProjectDetail> callback) {
+    run(callback, new Action<ProjectDetail>() {
+      public ProjectDetail run(ReviewDb db) throws OrmException, Failure {
+        assertAmProjectOwner(db, projectId);
+        final Project proj = db.projects().get(projectId);
+        if (proj == null) {
+          throw new Failure(new NoSuchEntityException());
+        }
+
+        final ProjectDetail d = new ProjectDetail();
+        d.load(db, proj);
+        return d;
+      }
+    });
+  }
+
+  public void changeProjectDescription(final Project.Id projectId,
+      final String description, final AsyncCallback<VoidResult> callback) {
+    run(callback, new Action<VoidResult>() {
+      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
+        assertAmProjectOwner(db, projectId);
+        final Project proj = db.projects().get(projectId);
+        if (proj == null) {
+          throw new Failure(new NoSuchEntityException());
+        }
+        proj.setDescription(description);
+        db.projects().update(Collections.singleton(proj));
+        return VoidResult.INSTANCE;
+      }
+    });
+  }
+
+  public void changeProjectOwner(final Project.Id projectId,
+      final String newOwnerName, final AsyncCallback<VoidResult> callback) {
+    run(callback, new Action<VoidResult>() {
+      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
+        assertAmProjectOwner(db, projectId);
+        final Project project = db.projects().get(projectId);
+        if (project == null) {
+          throw new Failure(new NoSuchEntityException());
+        }
+
+        final AccountGroup owner =
+            db.accountGroups().get(new AccountGroup.NameKey(newOwnerName));
+        if (owner == null) {
+          throw new Failure(new NoSuchEntityException());
+        }
+
+        project.setOwnerGroupId(owner.getId());
+        db.projects().update(Collections.singleton(project));
+        return VoidResult.INSTANCE;
+      }
+    });
+  }
+
+  private static boolean amInGroup(final ReviewDb db,
+      final AccountGroup.Id groupId) throws OrmException {
+    return db.accountGroupMembers().get(
+        new AccountGroupMember.Key(RpcUtil.getAccountId(), groupId)) != null;
+  }
+
+  private boolean amAdmin(final ReviewDb db) throws OrmException {
+    return adminId != null && amInGroup(db, adminId);
+  }
+
+  private void assertAmProjectOwner(final ReviewDb db,
+      final Project.Id projectId) throws OrmException, Failure {
+    final Project project = db.projects().get(projectId);
+    if (project == null) {
+      throw new Failure(new NoSuchEntityException());
+    }
+    if (!amInGroup(db, project.getOwnerGroupId()) && !amAdmin(db)) {
+      throw new Failure(new NoSuchEntityException());
+    }
+  }
+
+  private static List<Project> myOwnedProjects(final ReviewDb db)
+      throws OrmException {
+    final List<Project> own = new ArrayList<Project>();
+    for (final AccountGroupMember m : db.accountGroupMembers().byAccount(
+        RpcUtil.getAccountId()).toList()) {
+      for (final Project g : db.projects().ownedByGroup(m.getAccountGroupId())) {
+        own.add(g);
+      }
+    }
+    return own;
+  }
+}
diff --git a/appjar/src/main/java/com/google/gerrit/server/ProjectAdminServiceSrv.java b/appjar/src/main/java/com/google/gerrit/server/ProjectAdminServiceSrv.java
new file mode 100644
index 0000000..dc00465
--- /dev/null
+++ b/appjar/src/main/java/com/google/gerrit/server/ProjectAdminServiceSrv.java
@@ -0,0 +1,24 @@
+// Copyright 2008 Google Inc.
+//
+// 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.server;
+
+
+/** Publishes {@link ProjectAdminServiceImpl} over JSON. */
+public class ProjectAdminServiceSrv extends GerritJsonServlet {
+  @Override
+  protected Object createServiceHandle() throws Exception {
+    return new ProjectAdminServiceImpl(GerritServer.getInstance());
+  }
+}
diff --git a/appwar/src/main/webapp/WEB-INF/web.xml b/appwar/src/main/webapp/WEB-INF/web.xml
index 85b4828..e9eea29 100644
--- a/appwar/src/main/webapp/WEB-INF/web.xml
+++ b/appwar/src/main/webapp/WEB-INF/web.xml
@@ -122,6 +122,16 @@
   <servlet-mapping>
     <servlet-name>PatchDetailService</servlet-name>
     <url-pattern>/rpc/PatchDetailService</url-pattern>
+
+  <servlet>
+    <servlet-name>ProjectAdminService</servlet-name>
+    <servlet-class>com.google.gerrit.server.ProjectAdminServiceSrv</servlet-class>
+    <load-on-startup>1</load-on-startup>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>ProjectAdminService</servlet-name>
+    <url-pattern>/rpc/ProjectAdminService</url-pattern>
+  </servlet-mapping>
   </servlet-mapping>
 
   <servlet>