Add backend functionality needed to import groups

With this commit, imported groups are actually persisted in the target
system.

Import of groups in a recursive way is not supported, i.e. owner and
member groups need to be already existent on the target system,
otherwise the import fails. A possible self-ownership relation of a
group is not considered to be a recursive import and is supported.

Change-Id: I3a291785f6201cf03a61a84940bcea370f83032c
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/GroupCommand.java b/src/main/java/com/googlesource/gerrit/plugins/importer/GroupCommand.java
index 54f65bb..a9b2d58 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/GroupCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/GroupCommand.java
@@ -19,17 +19,14 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -60,18 +57,19 @@
   private ImportGroup.Factory importGroupFactory;
 
   @Override
-  protected void run() throws OrmException, IOException, UnloggedFailure,
-      ValidationException, GitAPIException, NoSuchChangeException,
+  protected void run() throws UnloggedFailure, OrmException, IOException,
       NoSuchAccountException {
     ImportGroup.Input input = new ImportGroup.Input();
     input.from = url;
     input.user = user;
     input.pass = getPassword();
 
-    Response<String> response = importGroupFactory.create(new AccountGroup.NameKey(group)).apply(
-        new ConfigResource(), input);
-    stdout.println(response);
-
+    try {
+      importGroupFactory.create(new AccountGroup.NameKey(group)).apply(
+          new ConfigResource(), input);
+    } catch (RestApiException e){
+      throw die(e.getMessage());
+    }
   }
 
   private String getPassword() throws IOException, UnloggedFailure {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportGroup.java b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportGroup.java
index 8b79900..97afa04 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/importer/ImportGroup.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/importer/ImportGroup.java
@@ -14,17 +14,40 @@
 
 package com.googlesource.gerrit.plugins.importer;
 
+import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.PreconditionFailedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import com.googlesource.gerrit.plugins.importer.ImportGroup.Input;
 
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 @RequiresCapability(ImportCapability.ID)
 class ImportGroup implements RestModifyView<ConfigResource, Input> {
   public static class Input {
@@ -39,16 +62,130 @@
 
   private final GroupCache groupCache;
   private final AccountGroup.NameKey group;
+  private final ReviewDb db;
+  private final AccountUtil accountUtil;
+  private final AccountCache accountCache;
+  private final GroupIncludeCache groupIncludeCache;
+  private final Config cfg;
+  private RemoteApi api;
 
   @Inject
-  ImportGroup(GroupCache groupCache, @Assisted AccountGroup.NameKey group) {
+  ImportGroup(AccountUtil accountUtil, GroupCache groupCache,
+      AccountCache accountCache, GroupIncludeCache groupIncludeCache,
+      ReviewDb db, @Assisted AccountGroup.NameKey group,
+      @GerritServerConfig Config cfg) {
+    this.db = db;
+    this.accountUtil = accountUtil;
     this.groupCache = groupCache;
+    this.accountCache = accountCache;
+    this.groupIncludeCache = groupIncludeCache;
+    this.cfg = cfg;
     this.group = group;
   }
 
   @Override
-  public Response<String> apply(ConfigResource rsrc, Input input) {
-    return Response.<String> ok("TODO");
+  public Response<String> apply(ConfigResource rsrc, Input input)
+      throws ResourceConflictException, PreconditionFailedException,
+      BadRequestException, NoSuchAccountException, OrmException, IOException {
+    GroupInfo groupInfo;
+    this.api = new RemoteApi(input.from, input.user, input.pass);
+    groupInfo = api.getGroup(group.get());
+    validate(groupInfo);
+    createGroup(groupInfo);
+
+    return Response.<String> ok("OK");
+  }
+
+  private void validate(GroupInfo groupInfo) throws ResourceConflictException,
+      PreconditionFailedException, BadRequestException, IOException,
+      OrmException {
+    if (groupCache.get(new AccountGroup.NameKey(groupInfo.name)) != null) {
+      throw new ResourceConflictException(String.format(
+          "Group with name %s already exists", groupInfo.name));
+    }
+    if (groupCache.get(new AccountGroup.UUID(groupInfo.id)) != null) {
+      throw new ResourceConflictException(String.format(
+          "Group with UUID %s already exists", groupInfo.id));
+    }
+    if (!groupInfo.id.equals(groupInfo.ownerId))
+      if (groupCache.get(new AccountGroup.UUID(groupInfo.ownerId)) == null) {
+        throw new PreconditionFailedException(String.format(
+            "Owner group with UUID %s does not exist", groupInfo.ownerId));
+      }
+    for (AccountInfo member : groupInfo.members) {
+      try {
+        accountUtil.resolveUser(api, member);
+      } catch (NoSuchAccountException e) {
+        throw new PreconditionFailedException(e.getMessage());
+      }
+    }
+    for (GroupInfo include : groupInfo.includes) {
+      if (groupCache.get(new AccountGroup.UUID(include.id)) == null) {
+        throw new PreconditionFailedException(String.format(
+            "Included group with UUID %s does not exist", include.id));
+      }
+    }
+  }
+
+  private AccountGroup createGroup(GroupInfo info) throws OrmException,
+      ResourceConflictException, NoSuchAccountException, BadRequestException,
+      IOException {
+    AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId());
+    AccountGroup.UUID uuid = new AccountGroup.UUID(info.id);
+    AccountGroup group =
+        new AccountGroup(new AccountGroup.NameKey(info.name), groupId, uuid);
+    group.setVisibleToAll(cfg.getBoolean("groups", "newGroupsVisibleToAll",
+        false));
+    group.setDescription(info.description);
+    AccountGroupName gn = new AccountGroupName(group);
+    // first insert the group name to validate that the group name hasn't
+    // already been used to create another group
+    try {
+      db.accountGroupNames().insert(Collections.singleton(gn));
+    } catch (OrmDuplicateKeyException e) {
+      throw new ResourceConflictException(info.name);
+    }
+    db.accountGroups().insert(Collections.singleton(group));
+
+    addMembers(groupId, info.members);
+    addGroups(groupId, info.includes);
+
+    groupCache.evict(group);
+
+    return group;
+  }
+
+  private void addMembers(AccountGroup.Id groupId, List<AccountInfo> members)
+      throws OrmException, NoSuchAccountException, BadRequestException,
+      IOException {
+    List<AccountGroupMember> memberships = new ArrayList<>();
+    for (AccountInfo member : members) {
+      Account.Id userId = accountUtil.resolveUser(api, member);
+      AccountGroupMember membership =
+          new AccountGroupMember(new AccountGroupMember.Key(userId, groupId));
+      memberships.add(membership);
+    }
+    db.accountGroupMembers().insert(memberships);
+
+    for (AccountInfo member : members) {
+      accountCache.evict(accountUtil.resolveUser(api, member));
+    }
+  }
+
+  private void addGroups(AccountGroup.Id groupId, List<GroupInfo> includedGroups)
+      throws OrmException {
+    List<AccountGroupById> includeList = new ArrayList<>();
+    for (GroupInfo includedGroup : includedGroups) {
+      AccountGroup.UUID memberUUID = new AccountGroup.UUID(includedGroup.id);
+      AccountGroupById groupInclude =
+          new AccountGroupById(new AccountGroupById.Key(groupId, memberUUID));
+      includeList.add(groupInclude);
+    }
+    db.accountGroupById().insert(includeList);
+
+    for (GroupInfo member : includedGroups) {
+      groupIncludeCache.evictParentGroupsOf(new AccountGroup.UUID(member.id));
+    }
   }
 
 }