Add CanonicalManifest

CanonicalManifest take a set of Manifests and their path and return
manifests with <include> and <remove-project> resolved.

Change-Id: I744c61be0fe8b7e082a8bb63554e63d23442bbbd
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/CanonicalManifest.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/CanonicalManifest.java
new file mode 100644
index 0000000..06df789
--- /dev/null
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/CanonicalManifest.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2015 Advanced Micro Devices, Inc.  All rights reserved.
+//
+// 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.amd.gerrit.plugins.manifestsubscription;
+
+import com.amd.gerrit.plugins.manifestsubscription.manifest.Include;
+import com.amd.gerrit.plugins.manifestsubscription.manifest.Manifest;
+import com.amd.gerrit.plugins.manifestsubscription.manifest.Project;
+import com.amd.gerrit.plugins.manifestsubscription.manifest.RemoveProject;
+import com.google.common.collect.Sets;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+public class CanonicalManifest {
+  private Map<String, Manifest> manifests;
+
+  public CanonicalManifest(VersionedManifests manifests) {
+    this.manifests = manifests.getManifests();
+  }
+
+  public CanonicalManifest(Map<String, Manifest> manifests) {
+    this.manifests = manifests;
+  }
+
+  Manifest getCanonicalManifest(String path) throws ManifestReadException {
+    if (manifests.containsKey(path)) {
+      Manifest manifest = (Manifest) manifests.get(path).clone();
+      Path manifestPath = Paths.get(path);
+
+
+      Manifest includedManifest;
+      Path includedPath;
+      Iterator<Include> i = manifest.getInclude().listIterator();
+      String include;
+      String parent;
+      while (i.hasNext()) {
+        parent = manifestPath.getParent() != null ? manifestPath.getParent().toString() + "/" : "";
+        include = i.next().getName();
+        includedPath = Paths.get(parent+include);
+        i.remove();
+
+        includedManifest = getCanonicalManifest(includedPath.normalize().toString());
+
+        try {
+          mergeManifestInto(includedManifest, manifest);
+        } catch (Exception e) {
+          throw new ManifestReadException(path);
+        }
+
+      }
+      // Clear remove project after all include manifest is processed
+      manifest.getRemoveProject().clear();
+
+      return manifest;
+    }
+
+    throw new ManifestReadException(path);
+  }
+
+  private Manifest mergeManifestInto(Manifest inner, Manifest outer)
+      throws Exception {
+    if (outer.getDefault() != null && inner.getDefault() != null) {
+      throw new Exception();
+    }
+    if (outer.getNotice() != null && inner.getNotice() != null) {
+      throw new Exception();
+    }
+    if (outer.getManifestServer() != null && inner.getManifestServer() != null) {
+      throw new Exception();
+    }
+    if (outer.getRepoHooks() != null && inner.getRepoHooks() != null) {
+      throw new Exception();
+    }
+
+    //TODO add more check
+    resolveRemoveProject(inner, outer);
+
+    if (outer.getDefault() == null) {
+      outer.setDefault(inner.getDefault());
+    }
+    if (outer.getNotice() == null) {
+      outer.setNotice(inner.getNotice());
+    }
+    if (outer.getManifestServer() == null) {
+      outer.setManifestServer(inner.getManifestServer());
+    }
+    if (outer.getRepoHooks() == null) {
+      outer.setRepoHooks(inner.getRepoHooks());
+    }
+
+
+    //TODO name remote name duplication check
+    outer.getRemote().addAll(inner.getRemote());
+
+    outer.getProject().addAll(inner.getProject());
+    outer.getExtendProject().addAll(inner.getExtendProject());
+
+    return outer;
+  }
+
+  private void resolveRemoveProject(Manifest manifest, Set<String> toBeRemoved) {
+    Project p;
+    Iterator<Project> i = manifest.getProject().listIterator();
+    while (i.hasNext()) {
+      p = i.next();
+
+      if (toBeRemoved.contains(p.getName())) {
+        i.remove();
+      }
+    }
+  }
+
+  private void resolveRemoveProject(Manifest manifest, Manifest toRemove) {
+    Set<String> removeProjects = Sets.newHashSet();
+
+    RemoveProject rp;
+    Iterator<RemoveProject> i = toRemove.getRemoveProject().listIterator();
+    while (i.hasNext()) {
+      rp = i.next();
+      removeProjects.add(rp.getName());
+    }
+
+    if (removeProjects.size() > 0) {
+      resolveRemoveProject(manifest, removeProjects);
+    }
+  }
+}
diff --git a/src/test/java/com/amd/gerrit/plugins/manifestsubscription/CanonicalManifestTest.java b/src/test/java/com/amd/gerrit/plugins/manifestsubscription/CanonicalManifestTest.java
new file mode 100644
index 0000000..24f7e25
--- /dev/null
+++ b/src/test/java/com/amd/gerrit/plugins/manifestsubscription/CanonicalManifestTest.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2015 Advanced Micro Devices, Inc.  All rights reserved.
+//
+// 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.amd.gerrit.plugins.manifestsubscription;
+
+import com.amd.gerrit.plugins.manifestsubscription.manifest.Manifest;
+import com.amd.gerrit.plugins.manifestsubscription.manifest.Project;
+import org.apache.commons.compress.utils.IOUtils;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static com.amd.gerrit.plugins.manifestsubscription.manifest.ManifestTest.checkAOSPcontent;
+import static com.amd.gerrit.plugins.manifestsubscription.manifest.ManifestTest.checkTestOnlyContent;
+import static com.google.common.truth.Truth.assertThat;
+
+public class CanonicalManifestTest extends LocalDiskRepositoryTestCase{
+  private Repository db;
+  private TestRepository<Repository> util;
+  private VersionedManifests versionedManifests;
+  private CanonicalManifest canonicalManifest;
+  private Manifest manifest;
+  private ManifestProvider provider;
+
+
+  @BeforeClass
+  public static void setUpBeforeClass() {
+
+  }
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    db = createBareRepository();
+    util = new TestRepository<>(db);
+
+    RevCommit rev = util.commit(util.tree(
+        util.file("aosp.xml",
+            util.blob(IOUtils.toByteArray(
+                getClass().getResourceAsStream("/aosp.xml")))),
+        util.file("aospinclude.xml",
+            util.blob(IOUtils.toByteArray(
+                getClass().getResourceAsStream("/aospinclude.xml")))),
+        util.file("aospincludereplace.xml",
+            util.blob(IOUtils.toByteArray(
+                getClass().getResourceAsStream("/aospincludereplace.xml")))),
+        util.file("multipleincludes.xml",
+            util.blob(IOUtils.toByteArray(
+                getClass().getResourceAsStream("/multipleincludes.xml")))),
+        util.file("subdir/aospincludereplace.xml",
+            util.blob(IOUtils.toByteArray(
+                getClass().getResourceAsStream("/subdir/aospincludereplace.xml")))),
+        util.file("subdir/testonly1.xml",
+            util.blob(IOUtils.toByteArray(
+                getClass().getResourceAsStream("/subdir/testonly1.xml")))),
+        util.file("testonly.xml",
+            util.blob(IOUtils.toByteArray(
+                getClass().getResourceAsStream("/testonly.xml"))))
+    ));
+
+    versionedManifests = new VersionedManifests(
+        "master");
+
+    versionedManifests.load(db, rev);
+    canonicalManifest = new CanonicalManifest(versionedManifests);
+  }
+
+  @Test
+  public void testCanonicalManifestsAOSPInclude() throws Exception {
+    manifest = canonicalManifest.getCanonicalManifest("aospinclude.xml");
+    checkAOSPcontent(manifest);
+    checkTestOnlyContent(manifest);
+  }
+
+  @Test
+  public void testCanonicalManifestsAOSPIncludeReplace() throws Exception {
+    manifest = canonicalManifest.getCanonicalManifest("aospincludereplace.xml");
+    checkAOSPcontent(manifest);
+
+    for (Project p : manifest.getProject()) {
+      switch (p.getPath()) {
+        case "git/test/project1":
+          assertThat(p.getGroups()).isNull();
+          assertThat(p.getRevision()).isEqualTo("replaced");
+          break;
+        case "git/test/project2":
+          assertThat(p.getGroups()).isNull();
+          assertThat(p.getRevision()).isEqualTo("replaced");
+          break;
+        case "git/test/project3":
+          assertThat(p.getGroups()).isNull();
+          assertThat(p.getRevision()).isEqualTo("replaced");
+          break;
+      }
+    }
+  }
+
+  @Test
+  public void testCanonicalManifestsMultipleIncludes() throws Exception {
+    manifest = canonicalManifest.getCanonicalManifest("multipleincludes.xml");
+    checkAOSPcontent(manifest);
+    checkTestOnlyContent(manifest);
+  }
+
+  @Test
+  public void testCanonicalManifestsTestOnly() throws Exception {
+    manifest = canonicalManifest.getCanonicalManifest("testonly.xml");
+    assertThat(manifest.getInclude()).isEmpty();
+    checkTestOnlyContent(manifest);
+  }
+
+  @Test
+  public void testCanonicalManifestsSubdir() throws Exception {
+    manifest = canonicalManifest.getCanonicalManifest("subdir/aospincludereplace.xml");
+    checkAOSPcontent(manifest);
+    checkTestOnlyContent(manifest);
+
+    for (Project p : manifest.getProject()) {
+      switch (p.getPath()) {
+        case "git/test/project4":
+          assertThat(p.getGroups()).isNull();
+          assertThat(p.getRevision()).isEqualTo("replaced");
+          break;
+        case "git/test/project5":
+          assertThat(p.getGroups()).isNull();
+          assertThat(p.getRevision()).isEqualTo("replaced");
+          break;
+        case "git/test/project6":
+          assertThat(p.getGroups()).isNull();
+          assertThat(p.getRevision()).isEqualTo("replaced");
+          break;
+      }
+    }
+  }
+}