Revision API: Implement methods to access files and diff info

Change-Id: Ib9c01304ea40392812c07fcedd5816c5bf12e202
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index f896692..14dca0b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -57,7 +57,7 @@
 public class PushOneCommit {
   public static final String SUBJECT = "test commit";
   public static final String FILE_NAME = "a.txt";
-  private static final String FILE_CONTENT = "some content";
+  public static final String FILE_CONTENT = "some content";
 
   public interface Factory {
     PushOneCommit create(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 11ed309..3f72c28 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -15,9 +15,12 @@
 package com.google.gerrit.acceptance.api.revision;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.junit.Assert.fail;
 
+import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -29,10 +32,13 @@
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Patch;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -40,7 +46,9 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 
 @NoHttpd
 public class RevisionIT extends AbstractDaemonTest {
@@ -280,6 +288,48 @@
     // TODO(dborowitz): Test for other-branches.
   }
 
+  @Test
+  public void files() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertThat(Iterables.all(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .files()
+        .keySet(), new Predicate<String>() {
+            @Override
+            public boolean apply(String file) {
+              return file.matches(FILE_NAME + '|' + Patch.COMMIT_MSG);
+            }
+         }))
+      .isTrue();
+  }
+
+  @Test
+  public void diff() throws Exception {
+    PushOneCommit.Result r = createChange();
+    DiffInfo diff = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file(FILE_NAME)
+        .diff();
+    assertThat(diff.metaA).isNull();
+    assertThat(diff.metaB.lines).isEqualTo(1);
+  }
+
+  @Test
+  public void content() throws Exception {
+    PushOneCommit.Result r = createChange();
+    BinaryResult bin = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file(FILE_NAME)
+        .content();
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    bin.writeTo(os);
+    String res = new String(os.toByteArray(), StandardCharsets.UTF_8);
+    assertThat(res).isEqualTo(FILE_CONTENT);
+  }
+
   private void assertMergeable(String id, boolean expected) throws Exception {
     MergeableInfo m = gApi.changes().id(id).current().mergeable();
     assertThat(m.mergeable).isEqualTo(expected);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
new file mode 100644
index 0000000..42c1e23
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -0,0 +1,77 @@
+// 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.api.changes;
+
+import com.google.gerrit.extensions.api.changes.FileApi;
+import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.change.FileResource;
+import com.google.gerrit.server.change.GetContent;
+import com.google.gerrit.server.change.GetDiff;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import java.io.IOException;
+
+class FileApiImpl extends FileApi.NotImplemented implements FileApi {
+  interface Factory {
+    FileApiImpl create(FileResource r);
+  }
+
+  private final GetContent getContent;
+  private final Provider<GetDiff> getDiff;
+  private final FileResource file;
+
+  @Inject
+  FileApiImpl(GetContent getContent,
+      Provider<GetDiff> getDiff,
+      @Assisted FileResource file) {
+    this.getContent = getContent;
+    this.getDiff = getDiff;
+    this.file = file;
+  }
+
+  @Override
+  public BinaryResult content() throws RestApiException {
+    try {
+      return getContent.apply(file);
+    } catch (NoSuchChangeException | IOException | OrmException e) {
+      throw new RestApiException("Cannot retrieve file content", e);
+    }
+  }
+
+  @Override
+  public DiffInfo diff() throws RestApiException {
+    try {
+      return getDiff.get().apply(file).value();
+    } catch (IOException | InvalidChangeOperationException | OrmException e) {
+      throw new RestApiException("Cannot retrieve diff", e);
+    }
+  }
+
+  @Override
+  public DiffInfo diff(String base) throws RestApiException {
+    try {
+      return getDiff.get().setBase(base).apply(file).value();
+    } catch (IOException | InvalidChangeOperationException | OrmException e) {
+      throw new RestApiException("Cannot retrieve diff", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
index dbf5f27..aa63996 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
@@ -24,5 +24,6 @@
 
     factory(ChangeApiImpl.Factory.class);
     factory(RevisionApiImpl.Factory.class);
+    factory(FileApiImpl.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index ab788c9..65b4f05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -19,9 +19,11 @@
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.FileApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -44,6 +46,7 @@
 import com.google.inject.assistedinject.Assisted;
 
 import java.io.IOException;
+import java.util.Map;
 import java.util.Set;
 
 class RevisionApiImpl extends RevisionApi.NotImplemented implements RevisionApi {
@@ -65,6 +68,7 @@
   private final Provider<Files.ListFiles> listFiles;
   private final Provider<PostReview> review;
   private final Provider<Mergeable> mergeable;
+  private final FileApiImpl.Factory fileApi;
 
   @Inject
   RevisionApiImpl(Changes changes,
@@ -80,6 +84,7 @@
       Provider<Files.ListFiles> listFiles,
       Provider<PostReview> review,
       Provider<Mergeable> mergeable,
+      FileApiImpl.Factory fileApi,
       @Assisted RevisionResource r) {
     this.changes = changes;
     this.cherryPick = cherryPick;
@@ -94,6 +99,7 @@
     this.deleteReviewed = deleteReviewed;
     this.listFiles = listFiles;
     this.mergeable = mergeable;
+    this.fileApi = fileApi;
     this.revision = r;
   }
 
@@ -211,4 +217,31 @@
       throw new RestApiException("Cannot check mergeability", e);
     }
   }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Map<String, FileInfo> files() throws RestApiException {
+    try {
+      return (Map<String, FileInfo>)listFiles.get().apply(revision).value();
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot retrieve files", e);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Map<String, FileInfo> files(String base) throws RestApiException {
+    try {
+      return (Map<String, FileInfo>) listFiles.get().setBase(base)
+          .apply(revision).value();
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot retrieve files", e);
+    }
+  }
+
+  @Override
+  public FileApi file(String path) {
+    return fileApi.create(files.get().parse(revision,
+        IdString.fromDecoded(path)));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 4a3082c..1c0b0a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -86,8 +86,7 @@
   }
 
   @Override
-  public FileResource parse(RevisionResource rev, IdString id)
-      throws ResourceNotFoundException, OrmException, AuthException {
+  public FileResource parse(RevisionResource rev, IdString id) {
     return new FileResource(rev, id.get());
   }
 
@@ -334,5 +333,10 @@
         git.close();
       }
     }
+
+    public ListFiles setBase(String base) {
+      this.base = base;
+      return this;
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 7c7bcdae..f731b5f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -251,6 +251,11 @@
     return links.isEmpty() ? null : links.toList();
   }
 
+  public GetDiff setBase(String base) {
+    this.base = base;
+    return this;
+  }
+
   private static class Content {
     final List<ContentEntry> lines;
     final SparseFileContent fileA;