Merge "Refactor: Extract group list manipulation"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
new file mode 100644
index 0000000..29948c2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
@@ -0,0 +1,142 @@
+// 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.google.gerrit.server.git;
+
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class GroupList {
+  public static final String FILE_NAME = "groups";
+  private final Map<AccountGroup.UUID, GroupReference> byUUID;
+
+  private GroupList(Map<AccountGroup.UUID, GroupReference> byUUID) {
+        this.byUUID = byUUID;
+  }
+
+  public static GroupList parse(String text, ValidationError.Sink errors) throws IOException {
+    Map<AccountGroup.UUID, GroupReference> groupsByUUID = new HashMap<>();
+
+    BufferedReader br = new BufferedReader(new StringReader(text));
+    String s;
+    for (int lineNumber = 1; (s = br.readLine()) != null; lineNumber++) {
+      if (s.isEmpty() || s.startsWith("#")) {
+        continue;
+      }
+
+      int tab = s.indexOf('\t');
+      if (tab < 0) {
+        errors.error(new ValidationError(FILE_NAME, lineNumber, "missing tab delimiter"));
+        continue;
+      }
+
+      AccountGroup.UUID uuid = new AccountGroup.UUID(s.substring(0, tab).trim());
+      String name = s.substring(tab + 1).trim();
+      GroupReference ref = new GroupReference(uuid, name);
+
+      groupsByUUID.put(uuid, ref);
+    }
+
+    return new GroupList(groupsByUUID);
+  }
+
+  public GroupReference byUUID(AccountGroup.UUID uuid) {
+    return byUUID.get(uuid);
+  }
+
+  public GroupReference resolve(GroupReference group) {
+    if (group != null) {
+      GroupReference ref = byUUID.get(group.getUUID());
+      if (ref != null) {
+        return ref;
+      }
+      byUUID.put(group.getUUID(), group);
+    }
+    return group;
+  }
+
+  public Collection<GroupReference> references() {
+    return byUUID.values();
+  }
+
+  public Set<AccountGroup.UUID> uuids() {
+    return byUUID.keySet();
+  }
+
+  public void put(UUID uuid, GroupReference reference) {
+    byUUID.put(uuid, reference);
+  }
+
+  private static String pad(int len, String src) {
+    if (len <= src.length()) {
+      return src;
+    }
+
+    StringBuilder r = new StringBuilder(len);
+    r.append(src);
+    while (r.length() < len) {
+      r.append(' ');
+    }
+    return r.toString();
+  }
+
+  private static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
+    ArrayList<T> r = new ArrayList<>(m);
+    Collections.sort(r);
+    return r;
+  }
+
+  public String asText() {
+    if (byUUID.isEmpty()) {
+      return null;
+    }
+
+    final int uuidLen = 40;
+    StringBuilder buf = new StringBuilder();
+    buf.append(pad(uuidLen, "# UUID"));
+    buf.append('\t');
+    buf.append("Group Name");
+    buf.append('\n');
+
+    buf.append('#');
+    buf.append('\n');
+
+    for (GroupReference g : sort(byUUID.values())) {
+      if (g.getUUID() != null && g.getName() != null) {
+        buf.append(pad(uuidLen, g.getUUID().get()));
+        buf.append('\t');
+        buf.append(g.getName());
+        buf.append('\n');
+      }
+    }
+    return buf.toString();
+  }
+
+  public void retainUUIDs(Collection<AccountGroup.UUID> toBeRetained) {
+    byUUID.keySet().retainAll(toBeRetained);
+  }
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 8b902bd..3142798 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -58,9 +58,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.util.StringUtils;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -75,7 +73,7 @@
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
-public class ProjectConfig extends VersionedMetaData {
+public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
   public static final String COMMENTLINK = "commentlink";
   private static final String KEY_MATCH = "match";
   private static final String KEY_HTML = "html";
@@ -83,7 +81,6 @@
   private static final String KEY_ENABLED = "enabled";
 
   public static final String PROJECT_CONFIG = "project.config";
-  private static final String GROUP_LIST = "groups";
 
   private static final String PROJECT = "project";
   private static final String KEY_DESCRIPTION = "description";
@@ -154,7 +151,7 @@
   private Project.NameKey projectName;
   private Project project;
   private AccountsSection accountsSection;
-  private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
+  private GroupList groupList;
   private Map<String, AccessSection> accessSections;
   private BranchOrderSection branchOrderSection;
   private Map<String, ContributorAgreement> contributorAgreements;
@@ -324,24 +321,17 @@
   }
 
   public GroupReference resolve(GroupReference group) {
-    if (group != null) {
-      GroupReference ref = groupsByUUID.get(group.getUUID());
-      if (ref != null) {
-        return ref;
-      }
-      groupsByUUID.put(group.getUUID(), group);
-    }
-    return group;
+    return groupList.resolve(group);
   }
 
   /** @return the group reference, if the group is used by at least one rule. */
   public GroupReference getGroup(AccountGroup.UUID uuid) {
-    return groupsByUUID.get(uuid);
+    return groupList.byUUID(uuid);
   }
 
   /** @return set of all groups used by this configuration. */
   public Set<AccountGroup.UUID> getAllGroupUUIDs() {
-    return Collections.unmodifiableSet(groupsByUUID.keySet());
+    return groupList.uuids();
   }
 
   /**
@@ -375,7 +365,7 @@
    */
   public boolean updateGroupNames(GroupBackend groupBackend) {
     boolean dirty = false;
-    for (GroupReference ref : groupsByUUID.values()) {
+    for (GroupReference ref : groupList.references()) {
       GroupDescription.Basic g = groupBackend.get(ref.getUUID());
       if (g != null && !g.getName().equals(ref.getName())) {
         dirty = true;
@@ -405,7 +395,8 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    Map<String, GroupReference> groupsByName = readGroupList();
+    readGroupList();
+    Map<String, GroupReference> groupsByName = mapGroupReferences();
 
     rulesId = getObjectId("rules.pl");
     Config rc = readConfig(PROJECT_CONFIG);
@@ -531,7 +522,7 @@
             n.addEmail(ref);
           } else {
             error(new ValidationError(PROJECT_CONFIG,
-                "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+                "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
           }
         } else if (dst.startsWith("user ")) {
           error(new ValidationError(PROJECT_CONFIG, dst + " not supported"));
@@ -627,7 +618,7 @@
         ref = rule.getGroup();
         groupsByName.put(ref.getName(), ref);
         error(new ValidationError(PROJECT_CONFIG,
-            "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+            "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
       }
 
       rule.setGroup(ref);
@@ -774,31 +765,18 @@
     return new PluginConfig(pluginName, pluginConfig, this);
   }
 
-  private Map<String, GroupReference> readGroupList() throws IOException {
-    groupsByUUID = new HashMap<>();
-    Map<String, GroupReference> groupsByName = new HashMap<>();
+  private void readGroupList() throws IOException {
+    groupList = GroupList.parse(readUTF8(GroupList.FILE_NAME), this);
+  }
 
-    BufferedReader br = new BufferedReader(new StringReader(readUTF8(GROUP_LIST)));
-    String s;
-    for (int lineNumber = 1; (s = br.readLine()) != null; lineNumber++) {
-      if (s.isEmpty() || s.startsWith("#")) {
-        continue;
-      }
-
-      int tab = s.indexOf('\t');
-      if (tab < 0) {
-        error(new ValidationError(GROUP_LIST, lineNumber, "missing tab delimiter"));
-        continue;
-      }
-
-      AccountGroup.UUID uuid = new AccountGroup.UUID(s.substring(0, tab).trim());
-      String name = s.substring(tab + 1).trim();
-      GroupReference ref = new GroupReference(uuid, name);
-
-      groupsByUUID.put(uuid, ref);
-      groupsByName.put(name, ref);
+  private Map<String, GroupReference> mapGroupReferences() {
+    Collection<GroupReference> references = groupList.references();
+    Map<String, GroupReference> result = new HashMap<>(references.size());
+    for (GroupReference ref : references) {
+      result.put(ref.getName(), ref);
     }
-    return groupsByName;
+
+    return result;
   }
 
   @Override
@@ -837,7 +815,7 @@
     saveContributorAgreements(rc, keepGroups);
     saveAccessSections(rc, keepGroups);
     saveNotifySections(rc, keepGroups);
-    groupsByUUID.keySet().retainAll(keepGroups);
+    groupList.retainUUIDs(keepGroups);
     saveLabelSections(rc);
     savePluginSections(rc);
 
@@ -1110,30 +1088,7 @@
   }
 
   private void saveGroupList() throws IOException {
-    if (groupsByUUID.isEmpty()) {
-      saveFile(GROUP_LIST, null);
-      return;
-    }
-
-    final int uuidLen = 40;
-    StringBuilder buf = new StringBuilder();
-    buf.append(pad(uuidLen, "# UUID"));
-    buf.append('\t');
-    buf.append("Group Name");
-    buf.append('\n');
-
-    buf.append('#');
-    buf.append('\n');
-
-    for (GroupReference g : sort(groupsByUUID.values())) {
-      if (g.getUUID() != null && g.getName() != null) {
-        buf.append(pad(uuidLen, g.getUUID().get()));
-        buf.append('\t');
-        buf.append(g.getName());
-        buf.append('\n');
-      }
-    }
-    saveUTF8(GROUP_LIST, buf.toString());
+    saveUTF8(GroupList.FILE_NAME, groupList.asText());
   }
 
   private <E extends Enum<?>> E getEnum(Config rc, String section,
@@ -1146,26 +1101,13 @@
     }
   }
 
-  private void error(ValidationError error) {
+  public void error(ValidationError error) {
     if (validationErrors == null) {
       validationErrors = new ArrayList<>(4);
     }
     validationErrors.add(error);
   }
 
-  private static String pad(int len, String src) {
-    if (len <= src.length()) {
-      return src;
-    }
-
-    StringBuilder r = new StringBuilder(len);
-    r.append(src);
-    while (r.length() < len) {
-      r.append(' ');
-    }
-    return r.toString();
-  }
-
   private static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
     ArrayList<T> r = new ArrayList<>(m);
     Collections.sort(r);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
index e1ab41d..ad84046 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
@@ -38,4 +38,8 @@
   public String toString() {
     return "ValidationError[" + message + "]";
   }
+
+  public interface Sink {
+    void error(ValidationError error);
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java
new file mode 100644
index 0000000..e741a91c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java
@@ -0,0 +1,121 @@
+// 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.google.gerrit.server.git;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+public class GroupListTest {
+
+  private static final String TEXT =
+      "# UUID                                  \tGroup Name\n" + "#\n"
+          + "d96b998f8a66ff433af50befb975d0e2bb6e0999\tNon-Interactive Users\n"
+          + "ebe31c01aec2c9ac3b3c03e87a47450829ff4310\tAdministrators\n";
+
+  private GroupList groupList;
+
+  @Before
+  public void setup() throws IOException {
+    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
+    replay(sink);
+    groupList = GroupList.parse(TEXT, sink);
+  }
+
+  @Test
+  public void testByUUID() throws Exception {
+    AccountGroup.UUID uuid =
+        new AccountGroup.UUID("d96b998f8a66ff433af50befb975d0e2bb6e0999");
+
+    GroupReference groupReference = groupList.byUUID(uuid);
+
+    assertEquals(uuid, groupReference.getUUID());
+    assertEquals("Non-Interactive Users", groupReference.getName());
+  }
+
+  @Test
+  public void testPut() {
+    AccountGroup.UUID uuid = new AccountGroup.UUID("abc");
+    GroupReference groupReference = new GroupReference(uuid, "Hutzliputz");
+
+    groupList.put(uuid, groupReference);
+
+    assertEquals(3, groupList.references().size());
+    GroupReference found = groupList.byUUID(uuid);
+    assertEquals(groupReference, found);
+  }
+
+  @Test
+  public void testReferences() throws Exception {
+    Collection<GroupReference> result = groupList.references();
+
+    assertEquals(2, result.size());
+    AccountGroup.UUID uuid =
+        new AccountGroup.UUID("ebe31c01aec2c9ac3b3c03e87a47450829ff4310");
+    GroupReference expected = new GroupReference(uuid, "Administrators");
+
+    assertTrue(result.contains(expected));
+  }
+
+  @Test
+  public void testUUIDs() throws Exception {
+    Set<AccountGroup.UUID> result = groupList.uuids();
+
+    assertEquals(2, result.size());
+    AccountGroup.UUID expected =
+        new AccountGroup.UUID("ebe31c01aec2c9ac3b3c03e87a47450829ff4310");
+    assertTrue(result.contains(expected));
+  }
+
+  @Test
+  public void testValidationError() throws Exception {
+    ValidationError.Sink sink = createMock(ValidationError.Sink.class);
+    sink.error(anyObject(ValidationError.class));
+    expectLastCall().times(2);
+    replay(sink);
+    groupList = GroupList.parse(TEXT.replace("\t", "    "), sink);
+    verify(sink);
+  }
+
+  @Test
+  public void testRetainAll() throws Exception {
+    AccountGroup.UUID uuid =
+        new AccountGroup.UUID("d96b998f8a66ff433af50befb975d0e2bb6e0999");
+    groupList.retainUUIDs(Collections.singleton(uuid));
+
+    assertNotNull(groupList.byUUID(uuid));
+    assertNull(groupList.byUUID(new AccountGroup.UUID(
+        "ebe31c01aec2c9ac3b3c03e87a47450829ff4310")));
+  }
+
+}