REST API to expose owners of each file

This API will expose, for a given patch set, all the owners
of a given file.

Here an example:

```
GET /changes/{change-id}/revisions/{revision-id}/owners~files-owners

{
  "AJavaFile.java":[
      { "name":"John", "id": 1 },
      { "name":"Bob", "id": 2 }
  ],
  "Aptyhonfileroot.py":[
      { "name":"John", "id": 1 },
      { "name":"Bob", "id": 2 },
      { "name":"John", "id": 3 }
  ]
}
```

The full name and id of the owner will be displayed. If not available
it will fallback to the account Id.

Change-Id: I4167989fe0fe58efa7e547c06489960af13de73c
diff --git a/owners/BUILD b/owners/BUILD
index ab6705e..d04d83d 100644
--- a/owners/BUILD
+++ b/owners/BUILD
@@ -1,6 +1,7 @@
-load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS_NEVERLINK", "PLUGIN_TEST_DEPS", "gerrit_plugin")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS", "PLUGIN_DEPS_NEVERLINK", "PLUGIN_TEST_DEPS", "gerrit_plugin")
 load("//lib/prolog:prolog.bzl", "prolog_cafe_library")
 load("//owners-common:common.bzl", "EXTERNAL_DEPS", "EXTERNAL_TEST_DEPS")
+load("//tools/bzl:junit.bzl", "junit_tests")
 
 PROLOG_PREDICATES = glob(["src/main/java/gerrit_owners/**/*.java"]) + [
     "src/main/java/com/googlesource/gerrit/owners/OwnersStoredValues.java",
@@ -49,5 +50,14 @@
     name = "owners__plugin_test_deps",
     testonly = 1,
     visibility = ["//visibility:public"],
-    exports = EXTERNAL_DEPS + EXTERNAL_TEST_DEPS + PLUGIN_TEST_DEPS,
+    exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":owners",
+    ],
+)
+
+junit_tests(
+    name = "owners_tests",
+    srcs = glob(["src/test/java/**/*.java"]),
+    tags = ["owners"],
+    deps = ["owners__plugin_test_deps"],
 )
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java
index 750c58e..e3a915b 100644
--- a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersModule.java
@@ -25,5 +25,6 @@
     DynamicSet.bind(binder(), PredicateProvider.class)
         .to(OwnerPredicateProvider.class)
         .asEagerSingleton();
+    install(new OwnersRestApiModule());
   }
 }
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/OwnersRestApiModule.java b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersRestApiModule.java
new file mode 100644
index 0000000..ccd3fcb
--- /dev/null
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/OwnersRestApiModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2022 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.googlesource.gerrit.owners;
+
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.change.RevisionResource;
+import com.googlesource.gerrit.owners.restapi.GetFilesOwners;
+
+public class OwnersRestApiModule extends RestApiModule {
+  @Override
+  protected void configure() {
+    get(RevisionResource.REVISION_KIND, "files-owners").to(GetFilesOwners.class);
+  }
+}
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/entities/Owner.java b/owners/src/main/java/com/googlesource/gerrit/owners/entities/Owner.java
new file mode 100644
index 0000000..e580f45
--- /dev/null
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/entities/Owner.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2022 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.googlesource.gerrit.owners.entities;
+
+import com.google.common.base.Objects;
+import com.google.gerrit.entities.Account;
+
+/** Class representing a file Owner * */
+public class Owner {
+  private final String name;
+  private final int id;
+
+  public Owner(String name, int id) {
+    this.name = name;
+    this.id = id;
+  }
+
+  /**
+   * Get the {@link Owner} name.
+   *
+   * @return the Owner name.
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Get the {@link Owner} account id.
+   *
+   * @return an {@code int} representation of the Owner {@link Account.Id}.
+   */
+  public int getId() {
+    return id;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    Owner owner = (Owner) o;
+    return Objects.equal(id, owner.id) && Objects.equal(name, owner.name);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(name, id);
+  }
+}
diff --git a/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java b/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java
new file mode 100644
index 0000000..a59e876
--- /dev/null
+++ b/owners/src/main/java/com/googlesource/gerrit/owners/restapi/GetFilesOwners.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2022 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.googlesource.gerrit.owners.restapi;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.owners.common.Accounts;
+import com.googlesource.gerrit.owners.common.PathOwners;
+import com.googlesource.gerrit.owners.common.PluginSettings;
+import com.googlesource.gerrit.owners.entities.Owner;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.Repository;
+
+@Singleton
+public class GetFilesOwners implements RestReadView<RevisionResource> {
+
+  private final PatchListCache patchListCache;
+  private final Accounts accounts;
+  private final AccountCache accountCache;
+  private final GitRepositoryManager repositoryManager;
+  private final PluginSettings pluginSettings;
+
+  @Inject
+  GetFilesOwners(
+      PatchListCache patchListCache,
+      Accounts accounts,
+      AccountCache accountCache,
+      GitRepositoryManager repositoryManager,
+      PluginSettings pluginSettings) {
+    this.patchListCache = patchListCache;
+    this.accounts = accounts;
+    this.accountCache = accountCache;
+    this.repositoryManager = repositoryManager;
+    this.pluginSettings = pluginSettings;
+  }
+
+  @Override
+  public Response<?> apply(RevisionResource revision)
+      throws AuthException, BadRequestException, ResourceConflictException, Exception {
+    PatchSet ps = revision.getPatchSet();
+    Change change = revision.getChange();
+
+    try (Repository repository = repositoryManager.openRepository(change.getProject())) {
+      PatchList patchList = patchListCache.get(change, ps);
+
+      String branch = change.getDest().branch();
+      PathOwners owners =
+          new PathOwners(
+              accounts,
+              repository,
+              pluginSettings.isBranchDisabled(branch) ? Optional.empty() : Optional.of(branch),
+              patchList);
+
+      Map<String, Set<Owner>> fileToOwners =
+          Maps.transformValues(
+              owners.getFileOwners(),
+              ids ->
+                  ids.stream()
+                      .map(this::getOwnerFromAccountId)
+                      .flatMap(Optional::stream)
+                      .collect(Collectors.toSet()));
+
+      return Response.ok(fileToOwners);
+    }
+  }
+
+  private Optional<Owner> getOwnerFromAccountId(Account.Id accountId) {
+    return accountCache
+        .get(accountId)
+        .map(as -> new Owner(as.account().fullName(), as.account().id().get()));
+  }
+}
diff --git a/owners/src/main/resources/Documentation/rest-api.md b/owners/src/main/resources/Documentation/rest-api.md
new file mode 100644
index 0000000..2341607
--- /dev/null
+++ b/owners/src/main/resources/Documentation/rest-api.md
@@ -0,0 +1,13 @@
+# Rest API
+
+The @PLUGIN@ exposes a Rest API endpoint to list the owners associated to each file:
+
+```bash
+GET /changes/{change-id}/revisions/{revision-id}/owners~getFilesOwners
+
+{
+  "AJavaFile.java":[{ "name":"John", "id": 1 }, { "name":"Bob", "id": 2 }],
+  "Aptyhonfileroot.py":[{ "name":"John", "id": 1 }, { "name":"Bob", "id": 2 }, { "name":"John", "id": 3 }]
+}
+
+```
\ No newline at end of file
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java
new file mode 100644
index 0000000..a46f7cf
--- /dev/null
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/restapi/GetFilesOwnersIT.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2022 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.googlesource.gerrit.owners.restapi;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.server.change.RevisionResource;
+import com.googlesource.gerrit.owners.entities.Owner;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.compress.utils.Sets;
+import org.junit.Test;
+
+@TestPlugin(name = "owners", httpModule = "com.googlesource.gerrit.owners.OwnersRestApiModule")
+public class GetFilesOwnersIT extends LightweightPluginDaemonTest {
+
+  private GetFilesOwners ownersApi;
+
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    super.setUpTestPlugin();
+
+    ownersApi = plugin.getSysInjector().getInstance(GetFilesOwners.class);
+  }
+
+  @Test
+  @UseLocalDisk
+  public void shouldReturnACorrectResponse() throws Exception {
+    // Add OWNERS file to root:
+    //
+    // inherited: true
+    // owners:
+    // - Administrator
+    merge(createChange(testRepo, "master", "Add OWNER file", "OWNERS", getOwnerFileContent(), ""));
+
+    PushOneCommit.Result result = createChange();
+    RevisionResource revisionResource = parseCurrentRevisionResource(result.getChangeId());
+
+    Response<?> resp = ownersApi.apply(revisionResource);
+
+    assertThat(resp.statusCode()).isEqualTo(HttpServletResponse.SC_OK);
+
+    Map<String, Set<Owner>> responseValue = (Map<String, Set<Owner>>) resp.value();
+    HashMap<String, Set<Owner>> expectedResponseValue =
+        new HashMap<String, Set<Owner>>() {
+          {
+            put("a.txt", Sets.newHashSet(new Owner(admin.fullName(), admin.id().get())));
+          }
+        };
+
+    assertThat(responseValue).containsExactlyEntriesIn(expectedResponseValue);
+  }
+
+  private String getOwnerFileContent() {
+    return "owners:\n" + "- " + admin.email() + "\n";
+  }
+}