Merge "Clean up Validator code"
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
index 537df74..20b5d11 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
@@ -101,12 +101,8 @@
               .getFromProjectConfigWithInheritance(projectState, PLUGIN_NAME)
               .getString(OWNERS_FILE_NAME, OWNERS);
       if (name.trim().equals("")) {
-        logger.atSevere().log(
-            "Project %s has wrong %s: \"%s\" for %s"
-                + projectState.getProject()
-                + OWNERS_FILE_NAME
-                + name
-                + getChangeId(c));
+        logger.atSevere().log("Project %s has wrong %s: \"%s\" for %s",
+            projectState.getProject(), OWNERS_FILE_NAME, name, getChangeId(c));
         return OWNERS;
       }
       return name;
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 59887c9..23cc28a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
@@ -107,7 +107,8 @@
           String found = "Found";
           if (content.isEmpty()) {
             String changeId = Config.getChangeId(changeData);
-            logger.atSevere().log("Missing root %s for %s", ownersFileName, changeId);
+            logger.atSevere().log("Missing root %s for %s of %s",
+                ownersFileName, changeId, projectName);
             found = "Missing";
           }
           logs.add(found + " root " + ownersFileName);
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 25103e4..838234b 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -1,6 +1,133 @@
 Configuration
 =============
 
+Here we show first an example use case followed by more
+details of each configuration rule and variable.
+
+## Example from AOSP
+
+The **find-owners** plugin is used in Android Open Source Project (AOSP).
+Here is [one AOSP change](https://android-review.googlesource.com/c/platform/build/+/734949)
+that modified multiple OWNERS files.
+If you sign in to the AOSP Gerrit server,
+you can see and click the `[FIND OWNERS]` button and
+it will pop up a window with *owners* of the changed files.
+
+The AOSP Gerrit server and projects are configured with the
+following steps:
+
+1. Enable the plugin in Gerrit configuration file **gerrit.config**:
+
+    ```text
+    [plugin "find-owners"]
+        enable = true
+        maxCacheAge = 30
+        maxCacheSize = 2000
+        alwaysShowButton = true
+    ```
+
+  * **maxCacheAge** is the number of seconds owners info will stay in
+    a cache before refreshed. This reduces repeated access to the same
+    OWNERS files in a repository. If it is set to 0 (default), there will
+    be no cache.
+  * **maxCacheSize** limits the number of owners info stored in the
+    cache to reduce memory footprint.
+  * **alwaysShowButton** parameter is useful for older Gerrit UI.
+    Current Gerrit UI always displays the `[FIND OWNERS]` button.
+
+2. Enable the upload validator in **project.config** of
+   the **All-Projects** project, in the **refs/meta/config** branch:
+
+    ```text
+    [plugin "find-owners"]
+        rejectErrorInOwners = true
+    ```
+
+  * The upload validator checks basic syntax of uploaded OWNERS files.
+  * It also checks if all email addresses used in OWNERS files
+    belong to active Gerrit accounts.
+  * All other Gerrit projects inherit from **All-Projects**,
+    so they have the same enabled upload validator.
+
+2. Optionally redefine **OWNERS** file name in **project.config** of
+   some projects, in the **refs/meta/config** branch:
+
+    ```text
+    [plugin "find-owners"]
+        ownersFileName = OWNERS.android
+    ```
+
+  * The AOSP **platform/external/v8** project keeps a copy of upstream
+    source from https://github.com/v8/v8.
+  * The v8 upstream source already has its **OWNERS** files
+    that do not work with AOSP Gerrit, because the Email addresses
+    in those files are not all active developers for AOSP.
+    So we need to use different *owners* files,
+    with the **OWNERS.android** file name.
+
+4. Call the submit filters in **rules.pl** of **All-Projects**,
+   in the **refs/meta/config** branch:
+
+    ```prolog
+    % Special projects, branches, user accounts can opt out owners review.
+    % To disable all find_owners rules, add opt_out_find_owners :- true.
+    opt_out_find_owners :-
+        gerrit:change_branch('refs/heads/pie-gsi').
+
+    % Special projects, branches, user accounts can opt in owners review.
+    % To default to find_owners rules, add opt_in_find_owners :- true.
+    opt_in_find_owners :- true.
+
+    % If opt_out_find_owners is true, remove all 'Owner-Review-Vote' label;
+    % else if opt_in_find_owners is true, call find_owners:submit_filter;
+    % else default to no find_owners filter.
+    check_find_owners(In, Out) :-
+        ( opt_out_find_owners -> find_owners:remove_need_label(In, Temp)
+        ; opt_in_find_owners -> find_owners:submit_filter(In, Temp)
+        ; In = Temp
+        ),
+        Temp =.. [submit | A],
+        change_find_owners_labels(A, B),
+        Out =.. [submit | B].
+
+    submit_filter(In, Out) :-
+      In =.. [submit | A],
+      check_drno_review(A, B),
+      check_api_review(B, C),
+      check_qualcomm_review(C, D),
+      Temp =.. [submit | D],
+      check_find_owners(Temp, Out).
+
+    % Remove useless label('Owner-Approved',_) after final filter.
+    % Change optional label('Owner-Review-Vote', may(_)) to
+    % label('Owner-Review-Vote', need(_)) to hide the Submit button.
+    change_find_owners_labels([], []).
+
+    change_find_owners_labels([H | T], R) :-
+      H = label('Owner-Approved', _), !,
+      change_find_owners_labels(T, R).
+
+    change_find_owners_labels([H1 | T], [H2 | R]) :-
+      H1 = label('Owner-Review-Vote', may(_)), !,
+      H2 = label('Owner-Review-Vote', need(_)),
+      change_find_owners_labels(T, R).
+
+    change_find_owners_labels([H | T], [H | R]) :-
+      change_find_owners_labels(T, R).
+
+    ```
+
+  * With the predefined Gerrit Prolog rules, any project, branch, or
+    user can be matched and added to the **opt_out_find_owners**
+    or **opt_in_find_owners** rules.
+  * If the **submit_filter** output contains
+    **label('Owner-Review-Vote', need(_))**,
+    the Gerrit change cannot be submitted.
+  * For a simpler configuration without opt-out projects,
+    just call **find_owners:submit_filter**
+    and **change_find_owners_labels**.
+
+
 ## The **`submit_rule`** and **`submit_filter`**
 
 To enforce the *owner-approval-before-submit* rule, this plugin provides
@@ -45,7 +172,7 @@
   (1) add missing *owners* to the reviewers list and/or
   ask for owner's +1 Code-Review votes, or
   (2) add `Exempt-From-Owner-Approval:` to the commit message.
-  The **`[[FIND OWNERS]]`** button is useful in this situation to find
+  The **`[FIND OWNERS]`** button is useful in this situation to find
   the missing *owners* or +1 votes of any changed files.
 
 When `label('Owner-Approved', may(_))` is added to the submit rule output,
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 9f9f625..0ef3b3e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
@@ -69,7 +69,7 @@
         ")]}' { minOwnerVoteLevel:1, addDebugMsg:false, change:"
             + info1._number
             + ", patchset:1, file2owners:{}, reviewers:[], owners:[], files:[] }";
-    Cache cache = Cache.getInstance().init(0, 10); // reset, no Cache
+    Cache cache = getCache().init(0, 10); // reset, no Cache
     assertThat(cache.size()).isEqualTo(0L);
     // GetOwners GET API
     assertThat(getOwnersResponse(info1)).isEqualTo(expected);
@@ -367,8 +367,8 @@
   @Test
   public void includeProjectOwnerTest() throws Exception {
     // Test include directive with other project name.
-    Project.NameKey pA = projectOperations.newProject().create();
-    Project.NameKey pB = projectOperations.newProject().create();
+    Project.NameKey pA = newProject("PA");
+    Project.NameKey pB = newProject("PB");
     String nameA = pA.get();
     String nameB = pB.get();
     switchProject(pA);
@@ -498,7 +498,7 @@
     Multimap<String, Account.Id> email2ids = emails.getAccountsFor(wrongEmails);
     for (String email : wrongEmails) {
       assertThat(emails.getAccountFor(email)).isEmpty();
-      assertThat(email2ids.get(email)).isEmpty();
+      assertThat(email2ids).doesNotContainKey(email);
     }
   }
 
@@ -511,17 +511,42 @@
     assertThat(content).contains("\"id\": \"All-Users\",");
     assertThat(content).contains(idProject("projectTest", "project"));
     assertThat(content).doesNotContain(idProject("projectTest", "ProjectA"));
-    projectOperations.newProject().name(name("ProjectA")).create();
+    newProject("ProjectA");
     response = adminRestSession.get("/projects/?d");
     assertThat(response.getEntityContent()).contains(idProject("projectTest", "ProjectA"));
   }
 
   @Test
+  public void projectInheritanceTest() throws Exception {
+    Project.NameKey pA = newProject("Project_A");
+    Project.NameKey pB = newProject("Project_B", pA);
+    Project.NameKey pC = newProject("Project_C", pB);
+    assertThat(projectOwnersFileName(pA)).isEqualTo("OWNERS");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("OWNERS");
+    assertThat(projectOwnersFileName(pC)).isEqualTo("OWNERS");
+    switchProject(pA);
+    setProjectConfig("ownersFileName", "OWNERS_A");
+    assertThat(projectOwnersFileName(pA)).isEqualTo("OWNERS_A");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("OWNERS_A");
+    assertThat(projectOwnersFileName(pC)).isEqualTo("OWNERS_A");
+    switchProject(pC);
+    setProjectConfig("ownersFileName", "OWNERS_C");
+    assertThat(projectOwnersFileName(pA)).isEqualTo("OWNERS_A");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("OWNERS_A");
+    assertThat(projectOwnersFileName(pC)).isEqualTo("OWNERS_C");
+    switchProject(pB);
+    setProjectConfig("ownersFileName", "OWNERS_B");
+    assertThat(projectOwnersFileName(pA)).isEqualTo("OWNERS_A");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("OWNERS_B");
+    assertThat(projectOwnersFileName(pC)).isEqualTo("OWNERS_C");
+    switchProject(pC);
+  }
+
+  @Test
   public void ownersFileNameTest() throws Exception {
-    Config.setVariables("find-owners", configFactory);
     // Default project is something like ....FindOwnersIT..._project
-    Project.NameKey pA = projectOperations.newProject().name(name("Project_A")).create();
-    Project.NameKey pB = projectOperations.newProject().name(name("Project_B")).create();
+    Project.NameKey pA = newProject("Project_A");
+    Project.NameKey pB = newProject("Project_B");
     // Add OWNERS and OWNERS.alpha file to Project_A.
     switchProject(pA);
     createBranch("BranchX");
@@ -538,9 +563,10 @@
     PushOneCommit.Result cY = createChangeInBranch("BranchY", "cY", "tY.c", "Hello Y!");
 
     // Default owners file name is "OWNERS".
+    assertThat(Config.OWNERS).isEqualTo("OWNERS");
     assertThat(Config.getDefaultOwnersFileName()).isEqualTo("OWNERS");
-    assertThat(Config.getOwnersFileName(projectCache.get(pA), null)).isEqualTo("OWNERS");
-    assertThat(Config.getOwnersFileName(projectCache.get(pB), null)).isEqualTo("OWNERS");
+    assertThat(projectOwnersFileName(pA)).isEqualTo("OWNERS");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("OWNERS");
 
     String ownerX = oneOwnerList("x@x");
     String ownerY = oneOwnerList("y@y");
@@ -578,8 +604,8 @@
     switchProject(pB);
     setProjectConfig("ownersFileName", "OWNERS.beta");
 
-    assertThat(Config.getOwnersFileName(projectCache.get(pA), null)).isEqualTo("OWNERS.alpha");
-    assertThat(Config.getOwnersFileName(projectCache.get(pB), null)).isEqualTo("OWNERS.beta");
+    assertThat(projectOwnersFileName(pA)).isEqualTo("OWNERS.alpha");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("OWNERS.beta");
     String ownerA = oneOwnerList("a@a");
     String ownerB = oneOwnerList("b@b");
     cAResponse = getOwnersDebugResponse(cA);
@@ -617,7 +643,7 @@
     // Change back to OWNERS in Project_A
     switchProject(pA);
     setProjectConfig("ownersFileName", "OWNERS");
-    assertThat(Config.getOwnersFileName(projectCache.get(pA), null)).isEqualTo("OWNERS");
+    assertThat(projectOwnersFileName(pA)).isEqualTo("OWNERS");
     cAResponse = getOwnersDebugResponse(cA);
     cBResponse = getOwnersDebugResponse(cB);
     assertThat(cAResponse).contains(ownerX + ", files:[ tA.c ]");
@@ -626,7 +652,7 @@
     // Change back to OWNERS.alpha in Project_B, but there is no OWNERS.alpha
     switchProject(pB);
     setProjectConfig("ownersFileName", "OWNERS.alpha");
-    assertThat(Config.getOwnersFileName(projectCache.get(pB), null)).isEqualTo("OWNERS.alpha");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("OWNERS.alpha");
     cAResponse = getOwnersDebugResponse(cA);
     cBResponse = getOwnersDebugResponse(cB);
     cYResponse = getOwnersDebugResponse(cY);
@@ -640,11 +666,11 @@
 
     // Do not accept empty string or all-white-spaces for ownersFileName.
     setProjectConfig("ownersFileName", "   ");
-    assertThat(Config.getOwnersFileName(projectCache.get(pB), null)).isEqualTo("OWNERS");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("OWNERS");
     setProjectConfig("ownersFileName", " \t  ");
-    assertThat(Config.getOwnersFileName(projectCache.get(pB), null)).isEqualTo("OWNERS");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("OWNERS");
     setProjectConfig("ownersFileName", "O");
-    assertThat(Config.getOwnersFileName(projectCache.get(pB), null)).isEqualTo("O");
+    assertThat(projectOwnersFileName(pB)).isEqualTo("O");
   }
 
   @Test
@@ -663,7 +689,7 @@
 
   @Test
   public void actionApplyTest() throws Exception {
-    Cache cache = Cache.getInstance().init(0, 10);
+    Cache cache = getCache().init(0, 10);
     assertThat(cache.size()).isEqualTo(0);
     // TODO: create ChangeInput in a new project.
     ChangeInfo changeInfo = newChangeInfo("test Action.apply");
@@ -671,8 +697,8 @@
     Action.Parameters param = new Action.Parameters();
     Action action =
         new Action(
-            "find-owners",
-            null,
+            Config.PLUGIN_NAME,
+            configFactory,
             null,
             changeDataFactory,
             accountCache,
@@ -800,7 +826,7 @@
 
   private int checkApproval(PushOneCommit.Result r) throws Exception {
     Project.NameKey project = r.getChange().project();
-    Cache cache = Cache.getInstance().init(0, 0);
+    Cache cache = getCache().init(0, 0);
     OwnersDb db = cache.get(true, projectCache.get(project), accountCache, emails,
                             repoManager, r.getChange(), 1);
     Checker c = new Checker(repoManager, r.getChange(), 1);
@@ -853,4 +879,20 @@
     PushOneCommit push = pushFactory.create(admin.getIdent(), testRepo, subject, fileName, content);
     return push.to("refs/for/" + branch);
   }
+
+  private Project.NameKey newProject(String name) {
+    return newProject(name, project);
+  }
+
+  private Project.NameKey newProject(String myName, Project.NameKey parent) {
+    return projectOperations.newProject().parent(parent).name(name(myName)).create();
+  }
+
+  private String projectOwnersFileName(Project.NameKey name) {
+    return Config.getOwnersFileName(projectCache.get(name), null);
+  }
+
+  private Cache getCache() {
+    return Cache.getInstance();
+  }
 }