Add VersionedManifests

VersionedManifests extends VersionedMetaData so that manifests in a git
commit can be retrieved and saved.

org.eclipse.jgit.junit is added at a version that is supported in Gerrit
2.9.1 so that a mock git repo/commit can be created for tests.

Change-Id: I15cc5bea49e32aa99169dfdffab663b9f21c52be
diff --git a/pom.xml b/pom.xml
index 60fc0dc..26de210 100644
--- a/pom.xml
+++ b/pom.xml
@@ -141,6 +141,13 @@
     </dependency>
 
     <dependency>
+      <groupId>org.eclipse.jgit</groupId>
+      <artifactId>org.eclipse.jgit.junit</artifactId>
+      <version>3.7.1.201504261725-r</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
       <groupId>org.jvnet.jaxb2_commons</groupId>
       <artifactId>jaxb2-basics-runtime</artifactId>
       <version>${jaxb2-basics-runtime.version}</version>
diff --git a/src/main/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifests.java b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifests.java
new file mode 100644
index 0000000..5cf3466
--- /dev/null
+++ b/src/main/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifests.java
@@ -0,0 +1,235 @@
+// 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.google.common.collect.HashBasedTable;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Table;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.VersionedMetaData;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Unmarshaller;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class VersionedManifests extends VersionedMetaData implements ManifestProvider {
+  private String refName;
+  private Unmarshaller manifestUnmarshaller;
+  private Marshaller manifestMarshaller;
+  private Map<String, Manifest> manifests;
+
+  public void setManifests(Map<String, Manifest> manifests) {
+    this.manifests = manifests;
+  }
+
+  public Map<String, Manifest> getManifests() {
+    return Collections.unmodifiableMap(manifests);
+  }
+
+  private VersionedManifests() throws JAXBException {
+    JAXBContext jaxbctx = JAXBContext.newInstance(Manifest.class);
+    this.manifestUnmarshaller = jaxbctx.createUnmarshaller();
+    this.manifestMarshaller = jaxbctx.createMarshaller();
+    this.manifestMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+  }
+
+  public VersionedManifests(String refName)
+      throws JAXBException {
+    this();
+    this.refName = refName;
+
+  }
+
+  public VersionedManifests(String refName,
+                            Map<String, Manifest> manifests) throws JAXBException {
+    this(refName);
+    this.manifests = manifests;
+
+  }
+
+  @Override
+  protected String getRefName() {
+    return refName;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    manifests = Maps.newHashMap();
+
+    String path;
+    Manifest manifest;
+
+    RevWalk rw = new RevWalk(reader);
+    RevCommit r = rw.parseCommit(getRevision());
+    TreeWalk treewalk = new TreeWalk(reader);
+    treewalk.addTree(r.getTree());
+    treewalk.setRecursive(false);
+    treewalk.setFilter(PathSuffixFilter.create(".xml"));
+    while (treewalk.next()) {
+      if (treewalk.isSubtree()) {
+        treewalk.enterSubtree();
+      } else {
+        path = treewalk.getPathString();
+        try {
+          //TODO: Should this be done more lazily?
+          //TODO: difficult to do when reader is not available outside of onLoad?
+          ByteArrayInputStream input = new ByteArrayInputStream(readFile(path));
+          manifest = (Manifest) manifestUnmarshaller.unmarshal(input);
+          manifests.put(path, manifest);
+        } catch (JAXBException e) {
+          e.printStackTrace();
+        }
+      }
+    }
+
+    treewalk.release();
+
+    //TODO load changed manifest
+//    DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException {
+    StringBuilder commitMsg = new StringBuilder();
+    commitMsg.append("Updated manifest\n\n");
+
+    String path;
+    Manifest manifest;
+    for (Map.Entry<String, Manifest> entry : manifests.entrySet()) {
+      path = entry.getKey();
+      manifest = entry.getValue();
+
+      try {
+        saveManifest(path, manifest);
+      } catch (JAXBException e) {
+        throw new IOException(e);
+      }
+    }
+
+    if (commit.getMessage() == null || "".equals(commit.getMessage())) {
+      commit.setMessage(commitMsg.toString());
+    }
+
+    return true;
+  }
+
+  @Override
+  public Manifest readManifest(String path) throws ManifestReadException {
+    if (manifests.containsKey(path)) {
+      return manifests.get(path);
+    }
+
+    throw new ManifestReadException(path);
+  }
+
+  /**
+   * Must be called inside onSave
+   *
+   * @param path
+   * @param manifest
+   * @throws JAXBException
+   * @throws IOException
+   */
+  private void saveManifest(String path, Manifest manifest)
+      throws JAXBException, IOException {
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    manifestMarshaller.marshal(manifest, output);
+    saveFile(path, output.toByteArray());
+  }
+
+  /**
+   * Pass in a {@link com.google.common.collect.Table} if you want to reuse
+   * the lookup cache
+   *
+   * @param gitRepoManager
+   * @param manifest
+   * @param lookup
+   */
+  static void affixManifest(GitRepositoryManager gitRepoManager,
+                            Manifest manifest, Table<String, String, String> lookup) {
+    if (lookup == null) {
+      // project, branch, hash
+      lookup = HashBasedTable.create();
+    }
+
+    String defaultRef = null;
+
+    if (manifest.getDefault() != null) {
+      defaultRef = manifest.getDefault().getRevision();
+    }
+
+    affixManifest(gitRepoManager, manifest.getProject(), defaultRef, lookup);
+  }
+
+  private static void affixManifest(GitRepositoryManager gitRepoManager,
+                                    List<com.amd.gerrit.plugins.manifestsubscription.manifest.Project> projects,
+                                    String defaultRef,
+                                    Table<String, String, String> lookup) {
+
+    String ref;
+    String hash;
+    String projectName;
+    Project.NameKey p;
+    for (com.amd.gerrit.plugins.manifestsubscription.manifest.Project project : projects) {
+      projectName = project.getName();
+      ref = project.getRevision();
+
+      ref = (ref == null) ? defaultRef : ref;
+
+      if (ref != null) {
+        hash = lookup.get(projectName, ref);
+
+        if (hash == null) {
+          p = new Project.NameKey(projectName);
+          try {
+            Repository db = gitRepoManager.openRepository(p);
+
+            hash = db.resolve(ref).getName();
+            db.close();
+          } catch (IOException e) {
+            e.printStackTrace();
+          }
+        }
+
+        if (hash != null) {
+          lookup.put(projectName, ref, hash);
+          project.setRevision(hash);
+          project.setUpstream(ref);
+        }
+      }
+
+      if (project.getProject().size() > 0) {
+        affixManifest(gitRepoManager, project.getProject(), defaultRef, lookup);
+      }
+    }
+  }
+
+}
diff --git a/src/test/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifestsTest.java b/src/test/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifestsTest.java
new file mode 100644
index 0000000..dff6e8d
--- /dev/null
+++ b/src/test/java/com/amd/gerrit/plugins/manifestsubscription/VersionedManifestsTest.java
@@ -0,0 +1,112 @@
+// 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.ManifestTest;
+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.*;
+import org.junit.rules.ExpectedException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class VersionedManifestsTest extends LocalDiskRepositoryTestCase {
+
+  private Repository db;
+  private TestRepository<Repository> util;
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @BeforeClass
+  public static void setUpBeforeClass() {
+
+  }
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    db = createBareRepository();
+    util = new TestRepository<>(db);
+  }
+
+  @Test
+  public void testVersionedManifestsReadFromGit() throws Exception {
+    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("nonxml.txt",
+            util.blob(IOUtils.toByteArray(
+                getClass().getResourceAsStream("/nonxml.txt")))),
+        util.file("testonly.xml",
+            util.blob(IOUtils.toByteArray(
+                getClass().getResourceAsStream("/testonly.xml"))))
+    ));
+
+    VersionedManifests versionedManifests =
+        new VersionedManifests("master");
+
+    versionedManifests.load(db, rev);
+
+    Manifest manifest;
+
+    manifest = versionedManifests.readManifest("aosp.xml");
+    ManifestTest.checkAOSPcontent(manifest);
+
+    manifest = versionedManifests.readManifest("aospinclude.xml");
+    assertThat(manifest.getInclude()).isNotEmpty();
+    manifest = versionedManifests.readManifest("aospincludereplace.xml");
+    assertThat(manifest.getInclude()).isNotEmpty();
+    manifest = versionedManifests.readManifest("multipleincludes.xml");
+    assertThat(manifest.getInclude()).isNotEmpty();
+
+    manifest = versionedManifests.readManifest("testonly.xml");
+    ManifestTest.checkTestOnlyContent(manifest);
+
+    manifest = versionedManifests.readManifest("subdir/aospincludereplace.xml");
+    assertThat(manifest.getInclude()).isNotEmpty();
+    assertThat(manifest.getInclude().get(0).getName()).isEqualTo("../aospinclude.xml");
+    assertThat(manifest.getInclude().get(1).getName()).isEqualTo("testonly1.xml");
+
+    thrown.expect(ManifestReadException.class);
+    manifest = versionedManifests.readManifest("nonxml.txt");
+  }
+
+  @After
+  public void tearDown() throws Exception {
+
+  }
+}
\ No newline at end of file