Use preferred email addresses.

OWNERS file could contain any owner email address,
which should be converted to a registered account's
preferred email address and compared with
code reviewer email addresses.

Add DebugMessages.errors to report bad owner email addresses
and other server side errors.

Change-Id: I88c6aef15295ea9bdbf5301834afe688a6306289
diff --git a/BUILD b/BUILD
index a3006a8..eb304bb 100644
--- a/BUILD
+++ b/BUILD
@@ -1,6 +1,6 @@
 load("//lib/prolog:prolog.bzl", "prolog_cafe_library")
 load("//tools/bzl:junit.bzl", "junit_tests")
-load("//tools/bzl:plugin.bzl", "gerrit_plugin", "PLUGIN_DEPS")
+load("//tools/bzl:plugin.bzl", "gerrit_plugin", "PLUGIN_DEPS", "PLUGIN_TEST_DEPS")
 
 java_library(
   name = 'find-owners-lib',
@@ -39,9 +39,8 @@
   srcs = glob(['src/test/java/**/*.java']),
   # resources = glob(['src/test/resources/**/*']),
   tags = ['findowners'],
-  deps = PLUGIN_DEPS + [
+  deps = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
     ':find-owners-lib',
     ':find-owners-prolog-rules',
-    '//gerrit-acceptance-framework:lib',
   ],
 )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
index 13a1cc9..0eba65b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -54,6 +55,7 @@
   private static final Logger log = LoggerFactory.getLogger(Action.class);
 
   private AccountCache accountCache;
+  private Accounts accounts;
   private ChangeData.Factory changeDataFactory;
   private GitRepositoryManager repoManager;
   private Provider<CurrentUser> userProvider;
@@ -72,11 +74,13 @@
       SchemaFactory<ReviewDb> reviewDbProvider,
       ChangeData.Factory changeDataFactory,
       AccountCache accountCache,
+      Accounts accounts,
       GitRepositoryManager repoManager) {
     this.userProvider = userProvider;
     this.reviewDbProvider = reviewDbProvider;
     this.changeDataFactory = changeDataFactory;
     this.accountCache = accountCache;
+    this.accounts = accounts;
     this.repoManager = repoManager;
     Config.setVariables(pluginName, configFactory);
     Cache.getInstance(); // Create a single Cache.
@@ -167,7 +171,7 @@
       Repository repository, Parameters params, ChangeData changeData)
       throws OrmException, BadRequestException {
     int patchset = getValidPatchsetNum(changeData, params.patchset);
-    OwnersDb db = Cache.getInstance().get(repository, changeData, patchset);
+    OwnersDb db = Cache.getInstance().get(accountCache, accounts, repository, changeData, patchset);
     Collection<String> changedFiles = changeData.currentFilePaths();
     Map<String, Set<String>> file2Owners = db.findOwners(changedFiles);
 
@@ -180,6 +184,7 @@
       obj.dbgmsgs.user = getUserName();
       obj.dbgmsgs.project = changeData.change().getProject().get();
       obj.dbgmsgs.branch = changeData.change().getDest().get();
+      obj.dbgmsgs.errors = db.errors;
       obj.dbgmsgs.path2owners = Util.makeSortedMap(db.path2Owners);
       obj.dbgmsgs.owner2paths = Util.makeSortedMap(db.owner2Paths);
     }
@@ -211,7 +216,7 @@
       if (needFindOwners && !Config.getAlwaysShowButton()) {
         needFindOwners = false; // Show button only if some owner is found.
         try (Repository repo = repoManager.openRepository(change.getProject())) {
-          OwnersDb db = Cache.getInstance().get(repo, changeData);
+          OwnersDb db = Cache.getInstance().get(accountCache, accounts, repo, changeData);
           log.trace("getDescription db key = " + db.key);
           needFindOwners = db.getNumOwners() > 0;
         }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
index 48f86e6..10fdebf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
@@ -18,6 +18,8 @@
 
 import com.google.common.cache.CacheBuilder;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import java.util.Collection;
@@ -85,21 +87,32 @@
   }
 
   /** Returns a cached or new OwnersDb, for the current patchset. */
-  OwnersDb get(Repository repo, ChangeData changeData) throws OrmException {
-    return get(repo, changeData, changeData.currentPatchSet().getId().get());
+  OwnersDb get(AccountCache accountCache, Accounts accounts, Repository repo, ChangeData changeData)
+      throws OrmException {
+    return get(
+        accountCache, accounts, repo, changeData, changeData.currentPatchSet().getId().get());
   }
 
   /** Returns a cached or new OwnersDb, for the specified patchset. */
-  OwnersDb get(Repository repository, ChangeData changeData, int patchset) throws OrmException {
+  OwnersDb get(
+      AccountCache accountCache,
+      Accounts accounts,
+      Repository repository,
+      ChangeData changeData,
+      int patchset)
+      throws OrmException {
     Project.NameKey project = changeData.change().getProject();
     String branch = changeData.change().getDest().get();
     String dbKey = Cache.makeKey(changeData.getId().get(), patchset, branch);
     // TODO: get changed files of the given patchset?
-    return get(dbKey, repository, project, branch, changeData.currentFilePaths());
+    return get(
+        accountCache, accounts, dbKey, repository, project, branch, changeData.currentFilePaths());
   }
 
   /** Returns a cached or new OwnersDb, for the specified branch and changed files. */
   OwnersDb get(
+      AccountCache accountCache,
+      Accounts accounts,
       String key,
       Repository repository,
       Project.NameKey project,
@@ -107,7 +120,7 @@
       Collection<String> files) {
     if (dbCache == null) { // Do not cache OwnersDb
       log.trace("Create new OwnersDb, key=" + key);
-      return new OwnersDb(key, repository, project, branch, files);
+      return new OwnersDb(accountCache, accounts, key, repository, project, branch, files);
     }
     try {
       log.trace("Get from cash " + dbCache + ", key=" + key + ", cache size=" + dbCache.size());
@@ -117,12 +130,12 @@
             @Override
             public OwnersDb call() {
               log.trace("Create new OwnersDb, key=" + key);
-              return new OwnersDb(key, repository, project, branch, files);
+              return new OwnersDb(accountCache, accounts, key, repository, project, branch, files);
             }
           });
     } catch (ExecutionException e) {
       log.error("Cache.get has exception: " + e);
-      return new OwnersDb(key, repository, project, branch, files);
+      return new OwnersDb(accountCache, accounts, key, repository, project, branch, files);
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
index 57e725c..ff75081 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.googlecode.prolog_cafe.lang.Prolog;
@@ -98,9 +99,10 @@
   public static int findApproval(Prolog engine, int minVoteLevel) {
     try {
       AccountCache accountCache = StoredValues.ACCOUNT_CACHE.get(engine);
+      Accounts accounts = StoredValues.ACCOUNTS.get(engine);
       ChangeData changeData = StoredValues.CHANGE_DATA.get(engine);
       Repository repository = StoredValues.REPOSITORY.get(engine);
-      return new Checker(repository, changeData, minVoteLevel).findApproval(accountCache);
+      return new Checker(repository, changeData, minVoteLevel).findApproval(accountCache, accounts);
     } catch (OrmException e) {
       log.error("Exception", e);
       return 0; // owner approval may or may not be required.
@@ -123,13 +125,13 @@
     return (status == Status.ABANDONED || status == Status.MERGED);
   }
 
-  int findApproval(AccountCache accountCache) throws OrmException {
+  int findApproval(AccountCache accountCache, Accounts accounts) throws OrmException {
     if (isExemptFromOwnerApproval(changeData)) {
       return 0;
     }
     // One update to a Gerrit change can call submit_rule or submit_filter
     // many times. So this function should use cached values.
-    OwnersDb db = Cache.getInstance().get(repository, changeData);
+    OwnersDb db = Cache.getInstance().get(accountCache, accounts, repository, changeData);
     if (db.getNumOwners() <= 0) {
       return 0;
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java
index d4e45ef..e918be7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -56,6 +57,7 @@
       SchemaFactory<ReviewDb> reviewDbProvider,
       ChangeData.Factory dataFactory,
       AccountCache accountCache,
+      Accounts accounts,
       GitRepositoryManager repoManager) {
     this.action =
         new Action(
@@ -65,6 +67,7 @@
             reviewDbProvider,
             dataFactory,
             accountCache,
+            accounts,
             repoManager);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
index b2ce1f9..4f0f6f6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
@@ -16,7 +16,11 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.Accounts;
 import java.io.IOException;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
@@ -26,6 +30,7 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectReader;
@@ -40,6 +45,8 @@
 class OwnersDb {
   private static final Logger log = LoggerFactory.getLogger(OwnersDb.class);
 
+  private AccountCache accountCache;
+  private Accounts accounts;
   private int numOwners = -1; // # of owners of all given files.
 
   String key = ""; // key to find this OwnersDb in a cache.
@@ -49,16 +56,23 @@
   Map<String, Set<String>> path2Owners = new HashMap<>(); // dir or file glob to owner emails
   Set<String> readDirs = new HashSet<>(); // directories in which we have checked OWNERS
   Set<String> stopLooking = new HashSet<>(); // directories where OWNERS has "set noparent"
+  Map<String, String> preferredEmails = new HashMap<>(); // owner email to preferred email
+  List<String> errors = new ArrayList<>(); // error messages
 
   OwnersDb() {}
 
   OwnersDb(
+      AccountCache accountCache,
+      Accounts accounts,
       String key,
       Repository repository,
       Project.NameKey project,
       String branch,
       Collection<String> files) {
+    this.accountCache = accountCache;
+    this.accounts = accounts;
     this.key = key;
+    preferredEmails.put("*", "*");
     String ownersFileName = Config.getOwnersFileName(project);
     for (String fileName : files) {
       // Find OWNERS in fileName's directory and parent directories.
@@ -110,14 +124,50 @@
     }
   }
 
+  void addPreferredEmails(Set<String> ownerEmails) {
+    List<String> owners = new ArrayList<String>(ownerEmails);
+    owners.removeIf(o -> preferredEmails.get(o) != null);
+    if (owners.size() > 0) {
+      String[] emails = new String[owners.size()];
+      owners.toArray(emails);
+      Multimap<String, Account.Id> email2ids = null;
+      try {
+        email2ids = accounts.byEmails(emails);
+      } catch (Exception e) {
+        log.error("accounts.byEmails failed with exception: ", e);
+      }
+      for (String owner : emails) {
+        String email = owner;
+        try {
+          if (email2ids == null) {
+            errors.add(owner);
+          } else {
+            Collection<Account.Id> ids = email2ids.get(owner);
+            if (ids == null || ids.size() != 1) {
+              errors.add(owner);
+            } else {
+              email = accountCache.get(ids.iterator().next()).getAccount().getPreferredEmail();
+            }
+          }
+        } catch (Exception e) {
+          log.error("Fail to find preferred email of " + owner, e);
+          errors.add(owner);
+        }
+        preferredEmails.put(owner, email);
+      }
+    }
+  }
+
   void addFile(String dirPath, String filePath, String[] lines) {
     Parser.Result result = Parser.parseFile(dirPath, filePath, lines);
     if (result.stopLooking) {
       stopLooking.add(dirPath);
     }
+    addPreferredEmails(result.owner2paths.keySet());
     for (String owner : result.owner2paths.keySet()) {
+      String email = preferredEmails.get(owner);
       for (String path : result.owner2paths.get(owner)) {
-        addOwnerPathPair(owner, path);
+        addOwnerPathPair(email, path);
       }
     }
     if (Config.getReportSyntaxError()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/RestResult.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/RestResult.java
index f91ecbe..296f408 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/RestResult.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/RestResult.java
@@ -54,6 +54,7 @@
     String user;
     String project;
     String branch;
+    List<String> errors;
     SortedMap<String, List<String>> path2owners;
     SortedMap<String, List<String>> owner2paths;
   };
diff --git a/src/main/resources/Documentation/rest-api.md b/src/main/resources/Documentation/rest-api.md
index 7676059..10b1565 100644
--- a/src/main/resources/Documentation/rest-api.md
+++ b/src/main/resources/Documentation/rest-api.md
@@ -54,6 +54,8 @@
 
     * **branch**: the change's destination branch name.
 
+    * **errors**: error messages from OWNERS files.
+
     * **path2owners**:
       a map from directory path or file glob to an array of owner emails.
       Note that `*` is a special owner email address.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
index 5e73d66..571c0c5 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
@@ -18,10 +18,12 @@
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.collect.Multimap;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -34,6 +36,7 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.Inject;
+import java.util.Collection;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
@@ -45,8 +48,8 @@
 @TestPlugin(name = "find-owners", sysModule = "com.googlesource.gerrit.plugins.findowners.Module")
 public class FindOwnersIT extends LightweightPluginDaemonTest {
 
-  @Inject private PluginConfigFactory configFactory;
   @Inject private Accounts accounts;
+  @Inject private PluginConfigFactory configFactory;
 
   @Test
   public void getOwnersTest() throws Exception {
@@ -172,13 +175,46 @@
 
   @Test
   public void accountTest() throws Exception {
-    String[] myTestEmails = {"abc@g.com", "abc+xyz@g.com", "xyz@g.com"};
-    for (String email : myTestEmails) {
-      Account.Id id = accountCreator.create("User" + email, email, "FullName" + email).getId();
-      // Action.getReviewers uses accountCache to get email address.
-      assertThat(accountCache.get(id).getAccount().getPreferredEmail()).isEqualTo(email);
-      // Checker.getVotes uses AccountCache to get email address.
-      assertThat(accounts.get(db, id).getPreferredEmail()).isEqualTo(email);
+    String[] users = {"user1", "user2", "user3"};
+    String[] emails1 = {"abc@g.com", "abc+xyz@g.com", "xyz-team+review@g.com"};
+    String[] emails2 = {"abc@goog.com", "abc+xyz2@g.com", "xyz-team@goog.com"};
+    // Create accounts with given user name, first and second email addresses.
+    for (int i = 0; i < users.length; i++) {
+      Account.Id id = accountCreator.create(users[i], emails1[i], "FullName " + users[i]).getId();
+      EmailInput input = new EmailInput();
+      input.email = emails2[i];
+      input.noConfirmation = true;
+      gApi.accounts().id(users[i]).addEmail(input);
+    }
+    // Find accounts with given first and second email addresses.
+    // OwnersDb uses either accounts.byEmail or byEmails to get preferred email addresses.
+    Multimap<String, Account.Id> map1 = accounts.byEmails(emails1);
+    Multimap<String, Account.Id> map2 = accounts.byEmails(emails2);
+    for (int i = 0; i < users.length; i++) {
+      Collection<Account.Id> ids1 = accounts.byEmail(emails1[i]);
+      Collection<Account.Id> ids2 = accounts.byEmail(emails2[i]);
+      Collection<Account.Id> ids3 = map1.get(emails1[i]);
+      Collection<Account.Id> ids4 = map2.get(emails2[i]);
+      assertThat(ids1.size()).isEqualTo(1);
+      assertThat(ids2.size()).isEqualTo(1);
+      assertThat(ids3.size()).isEqualTo(1);
+      assertThat(ids4.size()).isEqualTo(1);
+      Account.Id id1 = ids1.iterator().next();
+      Account.Id id2 = ids2.iterator().next();
+      Account.Id id3 = ids3.iterator().next();
+      Account.Id id4 = ids4.iterator().next();
+      assertThat(id1).isEqualTo(id2); // Both emails should find the same account.
+      assertThat(id1).isEqualTo(id3);
+      assertThat(id1).isEqualTo(id4);
+      // Action.getReviewers and Checker.getVotes use accountCache to get email address.
+      assertThat(accountCache.get(id1).getAccount().getPreferredEmail()).isEqualTo(emails1[i]);
+    }
+    // Wrong or non-existing email address.
+    String[] wrongEmails = {"nobody", "@g.com", "nobody@g.com", "*"};
+    Multimap<String, Account.Id> email2ids = accounts.byEmails(wrongEmails);
+    for (String email : wrongEmails) {
+      assertThat(accounts.byEmail(email).size()).isEqualTo(0);
+      assertThat(email2ids.get(email).size()).isEqualTo(0);
     }
   }
 
@@ -275,7 +311,15 @@
     ChangeResource cr = parseChangeResource(changeInfo.changeId);
     Action.Parameters param = new Action.Parameters();
     Action action =
-        new Action("find-owners", null, null, null, changeDataFactory, accountCache, repoManager);
+        new Action(
+            "find-owners",
+            null,
+            null,
+            null,
+            changeDataFactory,
+            accountCache,
+            accounts,
+            repoManager);
     Response<RestResult> response = action.apply(db, cr, param);
     RestResult result = response.value();
     verifyRestResult(result, 1, 1, changeInfo._number, false);