Clarify and cleanup Config variables and functions.

* Clarify gerrit.config and project.config parameters.
  * "alwaysShowButton" is obsolete, assumed true now.
  * Include all parameters to REST API returned JSON object;
    fix expected test output in ApiIT.java.
  * Explain all parameters in rest-api.md.
* Minimize repeated calls to create PluginConfig:
  * Change Action, Cache, Checker, OwnersDb, FindOwners to reuse
    Config instead of creating it from PluginConfigFactory.
  * Save one gerritConfig in Config and Cache projectConfig in Config.
  * Add/simplify unit test interface functions.
* Coding style improvements:
  * Use Duration.ofSeconds instead of TimeUnit.SECONDS.

Change-Id: If27b7ae0e0f59f434ec2d536e1b08af0857708e0
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 0d9f967..c97b9f8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
@@ -58,7 +58,6 @@
   private final ChangeData.Factory changeDataFactory;
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
-  private final PluginConfigFactory configFactory;
   private final Provider<CurrentUser> userProvider;
   private final ProjectCache projectCache;
   private final Config config;
@@ -86,7 +85,6 @@
     this.emails = emails;
     this.repoManager = repoManager;
     this.projectCache = projectCache;
-    this.configFactory = configFactory;
     this.config = new Config(configFactory);
   }
 
@@ -164,7 +162,7 @@
     ProjectState projectState = projectCache.get(changeData.project());
     Boolean useCache = params.nocache == null || !params.nocache;
     OwnersDb db =
-        Cache.getInstance(configFactory, repoManager)
+        Cache.getInstance(config, repoManager)
             .get(
                 useCache,
                 permissionBackend,
@@ -172,22 +170,17 @@
                 accountCache,
                 emails,
                 repoManager,
-                configFactory,
                 changeData,
                 patchset);
     Collection<String> changedFiles = changeData.currentFilePaths();
     Map<String, Set<String>> file2Owners = db.findOwners(changedFiles);
 
     Boolean addDebugMsg = (params.debug != null) ? params.debug : config.getAddDebugMsg();
-    RestResult obj =
-        new RestResult(config.getMinOwnerVoteLevel(projectState, changeData), addDebugMsg);
-    obj.change = changeData.getId().get();
+    RestResult obj = new RestResult(config, projectState, changeData, addDebugMsg);
     obj.patchset = patchset;
     obj.ownerRevision = db.revision;
     if (addDebugMsg) {
       obj.dbgmsgs.user = getUserName();
-      obj.dbgmsgs.project = changeData.change().getProject().get();
-      obj.dbgmsgs.branch = changeData.change().getDest().branch();
       obj.dbgmsgs.errors = db.errors;
       obj.dbgmsgs.path2owners = Util.makeSortedMap(db.path2Owners);
       obj.dbgmsgs.owner2paths = Util.makeSortedMap(db.owner2Paths);
@@ -220,23 +213,6 @@
               && userProvider.get() instanceof IdentifiedUser
               && status != Status.ABANDONED
               && status != Status.MERGED;
-      // If alwaysShowButton is true, skip expensive owner lookup.
-      if (needFindOwners && !config.getAlwaysShowButton()) {
-        needFindOwners = false; // Show button only if some owner is found.
-        OwnersDb db =
-            Cache.getInstance(configFactory, repoManager)
-                .get(
-                    true, // use cached OwnersDb
-                    permissionBackend,
-                    projectCache.get(resource.getProject()),
-                    accountCache,
-                    emails,
-                    repoManager,
-                    configFactory,
-                    changeData);
-        logger.atFiner().log("getDescription db key = %s", db.key);
-        needFindOwners = db.getNumOwners() > 0;
-      }
       return new Description()
           .setLabel("Find Owners")
           .setTitle("Find owners to add to Reviewers list")
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 7d2cfa5..df766e4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
@@ -14,18 +14,17 @@
 
 package com.googlesource.gerrit.plugins.findowners;
 
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.cache.CacheBuilder;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Emails;
-import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
+import java.time.Duration;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
@@ -61,8 +60,11 @@
   // dbCache key is generated by makeKey.
   private com.google.common.cache.Cache<String, OwnersDb> dbCache;
 
-  private Cache(int maxSeconds, int maxSize) {
-    init(maxSeconds, maxSize);
+  private Config config; // global config shared by all OwnersDb in dbCache
+
+  private Cache(Config config) {
+    this.config = config;
+    init(config.getMaxCacheAge(), config.getMaxCacheSize());
   }
 
   long size() {
@@ -80,7 +82,7 @@
       dbCache =
           CacheBuilder.newBuilder()
               .maximumSize(maxSize)
-              .expireAfterWrite(maxSeconds, SECONDS)
+              .expireAfterWrite(Duration.ofSeconds(maxSeconds))
               .build();
     } else {
       logger.atInfo().log("Cache disabled.");
@@ -97,7 +99,6 @@
       AccountCache accountCache,
       Emails emails,
       GitRepositoryManager repoManager,
-      PluginConfigFactory configFactory,
       ChangeData changeData)
       throws StorageException {
     return get(
@@ -107,7 +108,6 @@
         accountCache,
         emails,
         repoManager,
-        configFactory,
         changeData,
         changeData.currentPatchSet().id().get());
   }
@@ -120,7 +120,6 @@
       AccountCache accountCache,
       Emails emails,
       GitRepositoryManager repoManager,
-      PluginConfigFactory configFactory,
       ChangeData changeData,
       int patchset)
       throws StorageException {
@@ -135,7 +134,6 @@
         emails,
         dbKey,
         repoManager,
-        configFactory,
         changeData,
         branch,
         changeData.currentFilePaths());
@@ -150,7 +148,6 @@
       Emails emails,
       String key,
       GitRepositoryManager repoManager,
-      PluginConfigFactory configFactory,
       ChangeData changeData,
       String branch,
       Collection<String> files) {
@@ -163,7 +160,7 @@
           emails,
           key,
           repoManager,
-          configFactory,
+          config,
           changeData,
           branch,
           files);
@@ -184,7 +181,7 @@
                   emails,
                   key,
                   repoManager,
-                  configFactory,
+                  config,
                   changeData,
                   branch,
                   files);
@@ -200,7 +197,7 @@
           emails,
           key,
           repoManager,
-          configFactory,
+          config,
           changeData,
           branch,
           files);
@@ -211,14 +208,13 @@
     return String.format("%d:%d:%H", change, patch, repoManager);
   }
 
-  public static Cache getInstance(
-      PluginConfigFactory configFactory, GitRepositoryManager repoManager) {
+  public static Cache getInstance(Config config, GitRepositoryManager repoManager) {
     Cache cache = cacheMap.get(repoManager);
     if (cache == null) {
-      Config config = new Config(configFactory);
-      cache = new Cache(config.getMaxCacheAge(), config.getMaxCacheSize());
+      cache = new Cache(config);
       cacheMap.put(repoManager, cache);
     }
+    cache.config = config; // Always use newest config, although dbCache could have older data.
     return cache;
   }
 }
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 190f1eb..0004bf2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
@@ -40,7 +40,6 @@
   private static final String EXEMPT_MESSAGE2 = "Exempted-From-Owner-Approval:";
 
   private final GitRepositoryManager repoManager;
-  private final PluginConfigFactory configFactory;
   private final Config config;
   private final ProjectState projectState; // could be null when used by FindOwnersIT
   private final ChangeData changeData;
@@ -53,7 +52,6 @@
       ChangeData changeData,
       int v) {
     this.repoManager = repoManager;
-    this.configFactory = configFactory;
     this.projectState = projectState;
     this.changeData = changeData;
     this.config = new Config(configFactory);
@@ -154,7 +152,7 @@
     // 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(configFactory, repoManager)
+        Cache.getInstance(config, repoManager)
             .get(
                 true,
                 null, /* allow submit checker to read all OWNERS files */
@@ -162,7 +160,6 @@
                 accountCache,
                 emails,
                 repoManager,
-                configFactory,
                 changeData);
     if (db.getNumOwners() <= 0) {
       return 0;
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 9d5ec98..6a35190 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
@@ -16,23 +16,30 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
+import java.util.HashMap;
+import java.util.Map;
 
 /** find-owners configuration parameters */
 class Config {
-  // Name of config parameters.
-  static final String ADD_DEBUG_MSG = "addDebugMsg";
-  static final String ALWAYS_SHOW_BUTTON = "alwaysShowButton"; // always show "Find Owners" button
+  // Name of config parameters that should be defined in gerrit.config:
+  static final String ADD_DEBUG_MSG = "addDebugMsg"; // include "dbgmsgs" in returned JSON object
   static final String MAX_CACHE_AGE = "maxCacheAge"; // seconds to stay in cache
   static final String MAX_CACHE_SIZE = "maxCacheSize"; // number of OwnersDb in cache
   static final String MIN_OWNER_VOTE_LEVEL = "minOwnerVoteLevel"; // default +1
-  static final String OWNERS = "OWNERS"; // Default file name
+  static final String REPORT_SYNTAX_ERROR = "reportSyntaxError"; // only for tests
+  // "alwaysShowButton" is obsolete, new UI design always shows the [Find Owners] button
+
+  // Name of config parameters that can be defined in project.config or gerrit.config:
   static final String OWNERS_FILE_NAME = "ownersFileName"; // config key for file name
-  static final String REJECT_ERROR_IN_OWNERS = "rejectErrorInOwners"; // config key for validator
-  static final String REPORT_SYNTAX_ERROR = "reportSyntaxError";
+  static final String REJECT_ERROR_IN_OWNERS = "rejectErrorInOwners"; // enable upload validator
+
+  static final String OWNERS = "OWNERS"; // default OWNERS file name
 
   // Name of plugin and namespace.
   static final String PLUGIN_NAME = "find-owners";
@@ -40,31 +47,45 @@
 
   private final PluginConfigFactory configFactory;
 
+  // Each call to API entry point creates one new Config and parses gerrit.config.
+  private final PluginConfig gerritConfig;
+
+  // Each Config has a cache of project.config, with projectName:changeId key.
+  private final Map<String, PluginConfig> projectConfigMap;
+
   // Global/plugin config parameters.
   private boolean addDebugMsg = false;
   private int minOwnerVoteLevel = 1;
   private int maxCacheAge = 0;
   private int maxCacheSize = 1000;
   private boolean reportSyntaxError = false;
-  private boolean alwaysShowButton = false;
-  private String ownersFileName = OWNERS;
 
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   Config(PluginConfigFactory configFactory) {
+    // Called by Action() and Checker.findApproval, through Prolog submit filter.
+    this(configFactory, null);
+  }
+
+  @VisibleForTesting
+  Config(PluginConfig rawConfig) {
+    this(null, rawConfig);
+  }
+
+  Config(PluginConfigFactory configFactory, PluginConfig config) {
     this.configFactory = configFactory;
-    if (configFactory == null) { // When called from integration tests.
+    this.projectConfigMap = new HashMap<>();
+    if (configFactory == null && config == null) { // When called from integration tests.
+      this.gerritConfig = config;
       return;
     }
-    PluginConfig gc = configFactory.getFromGerritConfig(PLUGIN_NAME);
-    // Get config variables from the plugin section of gerrit.config
-    addDebugMsg = gc.getBoolean(ADD_DEBUG_MSG, false);
-    reportSyntaxError = gc.getBoolean(REPORT_SYNTAX_ERROR, false);
-    alwaysShowButton = gc.getBoolean(ALWAYS_SHOW_BUTTON, false);
-    minOwnerVoteLevel = gc.getInt(MIN_OWNER_VOTE_LEVEL, 1);
-    maxCacheAge = gc.getInt(MAX_CACHE_AGE, 0);
-    maxCacheSize = gc.getInt(MAX_CACHE_SIZE, 1000);
-    ownersFileName = gc.getString(OWNERS_FILE_NAME, OWNERS);
+    this.gerritConfig = config == null ? configFactory.getFromGerritConfig(PLUGIN_NAME) : config;
+    // Get config variables from the plugin section of gerrit.config.
+    addDebugMsg = gerritConfig.getBoolean(ADD_DEBUG_MSG, false);
+    minOwnerVoteLevel = gerritConfig.getInt(MIN_OWNER_VOTE_LEVEL, 1);
+    maxCacheAge = gerritConfig.getInt(MAX_CACHE_AGE, 0);
+    maxCacheSize = gerritConfig.getInt(MAX_CACHE_SIZE, 1000);
+    reportSyntaxError = gerritConfig.getBoolean(REPORT_SYNTAX_ERROR, false);
   }
 
   boolean getAddDebugMsg() {
@@ -79,33 +100,91 @@
     return maxCacheSize;
   }
 
+  boolean getGlobalBooleanValue(String key) {
+    return gerritConfig != null && gerritConfig.getBoolean(key, false);
+  }
+
+  boolean getRejectErrorInOwners() {
+    return getGlobalBooleanValue(REJECT_ERROR_IN_OWNERS);
+  }
+
+  boolean getRejectErrorInOwners(Project project) {
+    return getBooleanValue(project, REJECT_ERROR_IN_OWNERS);
+  }
+
+  boolean getRejectErrorInOwners(ProjectState projectState, ChangeData changeData) {
+    return getBooleanValue(projectState, changeData, REJECT_ERROR_IN_OWNERS);
+  }
+
+  boolean getBooleanValue(Project project, String key) {
+    return getBooleanValue(project, key, getGlobalBooleanValue(key));
+  }
+
+  boolean getBooleanValue(Project project, String key, boolean defaultValue) {
+    try {
+      return getProjectConfig(project).getBoolean(key, defaultValue);
+    } catch (NoSuchProjectException e) {
+      logger.atSevere().withCause(e).log(
+          "Exception in getBooleanValue for %s:%s", project.getName(), key);
+      return defaultValue;
+    }
+  }
+
+  boolean getBooleanValue(ProjectState projectState, ChangeData changeData, String key) {
+    return getBooleanValue(projectState, changeData, key, getGlobalBooleanValue(key));
+  }
+
+  boolean getBooleanValue(
+      ProjectState projectState, ChangeData changeData, String key, boolean defaultValue) {
+    return getProjectConfig(projectState, changeData).getBoolean(key, defaultValue);
+  }
+
   boolean getReportSyntaxError() {
     return reportSyntaxError;
   }
 
-  boolean getAlwaysShowButton() {
-    return alwaysShowButton;
-  }
-
   static String getChangeId(ChangeData data) {
-    return data == null ? "(unknown change)" : ("change c/" + data.getId().get());
+    return data == null ? "(unknown change)" : ("c/" + data.getId().get());
   }
 
   String getDefaultOwnersFileName() {
-    return ownersFileName; // could be defined in gerrit.config
+    return gerritConfig == null ? OWNERS : gerritConfig.getString(OWNERS_FILE_NAME, OWNERS);
   }
 
-  private PluginConfig getPluginConfig(ProjectState state) {
-    return configFactory.getFromProjectConfigWithInheritance(state, PLUGIN_NAME);
+  // This is per ProjectState and ChangeData.
+  private PluginConfig getProjectConfig(ProjectState state, ChangeData data) {
+    // A new Config object is created for every call to Action or Checker.
+    // So it is okay to reuse a PluginConfig per (ProjectState:ChangeData).
+    // ProjectState parameter must not be null.
+    // The data parameter could be used in the future to generate change
+    // dependent PluginConfig.
+    String key = state.getName() + ":" + getChangeId(data);
+    return projectConfigMap.computeIfAbsent(
+        key, (String k) -> configFactory.getFromProjectConfigWithInheritance(state, PLUGIN_NAME));
+  }
+
+  // Used by OwnersValidator and tests, not cached.
+  PluginConfig getProjectConfig(Project project) throws NoSuchProjectException {
+    return configFactory.getFromProjectConfigWithInheritance(project.getNameKey(), PLUGIN_NAME);
+  }
+
+  String getOwnersFileName() {
+    return getOwnersFileName(null, null);
+  }
+
+  String getOwnersFileName(ProjectState projectState) {
+    return getOwnersFileName(projectState, null);
   }
 
   String getOwnersFileName(ProjectState projectState, ChangeData c) {
     String defaultName = getDefaultOwnersFileName();
     if (projectState == null) {
-      logger.atSevere().log("Null projectState for change %s", getChangeId(c));
+      if (c != null) {
+        logger.atSevere().log("Null projectState for change %s", getChangeId(c));
+      }
       return defaultName;
     }
-    String name = getPluginConfig(projectState).getString(OWNERS_FILE_NAME, defaultName);
+    String name = getProjectConfig(projectState, c).getString(OWNERS_FILE_NAME, defaultName);
     if (name.trim().isEmpty()) {
       logger.atSevere().log(
           "Project %s has wrong %s: \"%s\" for %s",
@@ -115,6 +194,22 @@
     return name;
   }
 
+  String getOwnersFileName(Project project) {
+    String defaultName = getDefaultOwnersFileName();
+    try {
+      String name = getProjectConfig(project).getString(OWNERS_FILE_NAME, defaultName);
+      if (name.trim().isEmpty()) {
+        logger.atSevere().log("Project %s has empty %s", project, OWNERS_FILE_NAME);
+        return defaultName;
+      }
+      return name;
+    } catch (NoSuchProjectException e) {
+      logger.atSevere().withCause(e).log(
+          "Exception in getOwnersFileName for %s", project.getName());
+      return defaultName;
+    }
+  }
+
   @VisibleForTesting
   void setReportSyntaxError(boolean value) {
     reportSyntaxError = value;
@@ -125,6 +220,6 @@
       logger.atSevere().log("Null projectState for change %s", getChangeId(c));
       return minOwnerVoteLevel;
     }
-    return getPluginConfig(projectState).getInt(MIN_OWNER_VOTE_LEVEL, minOwnerVoteLevel);
+    return getProjectConfig(projectState, c).getInt(MIN_OWNER_VOTE_LEVEL, minOwnerVoteLevel);
   }
 }
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 db3bc5b..34e9c1e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Emails;
-import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -82,7 +81,7 @@
       Emails emails,
       String key,
       GitRepositoryManager repoManager,
-      PluginConfigFactory configFactory,
+      Config config,
       ChangeData changeData,
       String branch,
       Collection<String> files) {
@@ -91,7 +90,7 @@
     this.repoManager = repoManager;
     this.emails = emails;
     this.key = key;
-    this.config = new Config(configFactory);
+    this.config = config;
     try {
       InetAddress inetAddress = InetAddress.getLocalHost();
       logs.add("HostName:" + inetAddress.getHostName());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
index 7669d55..aa69aa9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
@@ -15,14 +15,12 @@
 package com.googlesource.gerrit.plugins.findowners;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
-import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS;
-import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS_FILE_NAME;
 import static com.googlesource.gerrit.plugins.findowners.Config.REJECT_ERROR_IN_OWNERS;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Multimap;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.Exports;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Account;
@@ -36,7 +34,6 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
-import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import java.io.BufferedReader;
@@ -64,6 +61,8 @@
 
 /** Check syntax of changed OWNERS files. */
 public class OwnersValidator implements CommitValidationListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private interface TreeWalkVisitor {
     void onVisit(TreeWalk tw);
   }
@@ -87,67 +86,67 @@
     };
   }
 
-  private final String pluginName;
-  private final PluginConfigFactory cfgFactory;
+  private final Config config;
   private final GitRepositoryManager repoManager;
   private final Emails emails;
 
   @Inject
-  OwnersValidator(
-      @PluginName String pluginName,
-      PluginConfigFactory cfgFactory,
-      GitRepositoryManager repoManager,
-      Emails emails) {
-    this.pluginName = pluginName;
-    this.cfgFactory = cfgFactory;
+  OwnersValidator(PluginConfigFactory cfgFactory, GitRepositoryManager repoManager, Emails emails) {
+    this(new Config(cfgFactory), repoManager, emails);
+  }
+
+  @VisibleForTesting
+  OwnersValidator(PluginConfig config, GitRepositoryManager repoManager, Emails emails) {
+    this(new Config(config), repoManager, emails);
+  }
+
+  private OwnersValidator(Config config, GitRepositoryManager repoManager, Emails emails) {
+    this.config = config;
     this.repoManager = repoManager;
     this.emails = emails;
   }
 
-  public static String getOwnersFileName(PluginConfig cfg) {
-    return getOwnersFileName(cfg, OWNERS);
-  }
-
-  public static String getOwnersFileName(PluginConfig cfg, String defaultName) {
-    return cfg.getString(OWNERS_FILE_NAME, defaultName);
-  }
-
-  public String getOwnersFileName(Project.NameKey project) {
-    String name = getOwnersFileName(cfgFactory.getFromGerritConfig(pluginName, true));
-    try {
-      return getOwnersFileName(
-          cfgFactory.getFromProjectConfigWithInheritance(project, pluginName), name);
-    } catch (NoSuchProjectException e) {
-      return name;
-    }
+  @VisibleForTesting
+  String getOwnersFileName() {
+    return config.getOwnersFileName();
   }
 
   @VisibleForTesting
-  static boolean isActive(PluginConfig cfg) {
-    return cfg.getBoolean(REJECT_ERROR_IN_OWNERS, false);
+  public String getOwnersFileName(Project project) {
+    return config.getOwnersFileName(project);
+  }
+
+  @VisibleForTesting
+  boolean isActive(Project project) {
+    return config.getRejectErrorInOwners(project);
+  }
+
+  @VisibleForTesting
+  boolean isActive() {
+    return config.getRejectErrorInOwners();
   }
 
   @Override
   public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent event)
       throws CommitValidationException {
+    if (!isActive(event.project)) {
+      return new ArrayList<>();
+    }
     Checker checker = new Checker(event, false);
     try {
-      Project.NameKey project = event.project.getNameKey();
-      PluginConfig cfg = cfgFactory.getFromProjectConfigWithInheritance(project, pluginName);
-      if (isActive(cfg)) {
-        checker.check(getOwnersFileName(project));
+      checker.check(getOwnersFileName(event.project));
+      if (checker.hasError()) {
+        checker.addError(
+            "See OWNERS file syntax document at "
+                + "https://gerrit.googlesource.com/plugins/find-owners/+/"
+                + "master/src/main/resources/Documentation/syntax.md");
+        throw new CommitValidationException("found invalid owners file", checker.messages);
       }
-    } catch (NoSuchProjectException | IOException e) {
-      throw new CommitValidationException("failed to check owners files", e);
+      return checker.messages;
+    } catch (IOException e) {
+      logger.atSevere().withCause(e).log("Failed in onCommitReceived");
+      return new ArrayList<>();
     }
-    if (checker.hasError()) {
-      checker.addError(
-          "See OWNERS file syntax document at "
-              + "https://gerrit.googlesource.com/plugins/find-owners/+/"
-              + "master/src/main/resources/Documentation/syntax.md");
-      throw new CommitValidationException("found invalid owners file", checker.messages);
-    }
-    return checker.messages;
   }
 
   class Checker {
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 378f2de..dc108c7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/RestResult.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/RestResult.java
@@ -14,6 +14,8 @@
 
 package com.googlesource.gerrit.plugins.findowners;
 
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gson.annotations.SerializedName;
 import java.util.ArrayList;
 import java.util.List;
@@ -22,11 +24,23 @@
 
 /** REST API return data Java class. */
 public class RestResult {
+  @SerializedName("addDebugMsg")
+  boolean addDebugMsg;
+
+  @SerializedName("maxCacheAge")
+  int maxCacheAge;
+
+  @SerializedName("maxCacheSize")
+  int maxCacheSize;
+
   @SerializedName("minOwnerVoteLevel")
   int minOwnerVoteLevel;
 
-  @SerializedName("addDebugMsg")
-  boolean addDebugMsg;
+  @SerializedName("ownersFileName")
+  String ownersFileName;
+
+  @SerializedName("rejectErrorInOwners")
+  boolean rejectErrorInOwners;
 
   int change;
   int patchset;
@@ -40,13 +54,20 @@
   List<OwnerInfo> owners = new ArrayList<>();
   List<String> files = new ArrayList<>();
 
-  RestResult(int voteLevel, boolean addDebugMsg) {
-    minOwnerVoteLevel = voteLevel;
+  RestResult(Config config, ProjectState projectState, ChangeData changeData, boolean addDebugMsg) {
     this.addDebugMsg = addDebugMsg;
+    maxCacheAge = config.getMaxCacheAge();
+    maxCacheSize = config.getMaxCacheSize();
+    minOwnerVoteLevel = config.getMinOwnerVoteLevel(projectState, changeData);
+    ownersFileName = config.getOwnersFileName(projectState, changeData);
+    rejectErrorInOwners = config.getRejectErrorInOwners(projectState, changeData);
+    change = changeData.getId().get();
     if (addDebugMsg) {
       dbgmsgs = new DebugMessages();
       dbgmsgs.path2owners = new TreeMap<>();
       dbgmsgs.owner2paths = new TreeMap<>();
+      dbgmsgs.project = changeData.change().getProject().get();
+      dbgmsgs.branch = changeData.change().getDest().branch();
     }
   }
 
diff --git a/src/main/resources/Documentation/rest-api.md b/src/main/resources/Documentation/rest-api.md
index 435b12b..41829e5 100644
--- a/src/main/resources/Documentation/rest-api.md
+++ b/src/main/resources/Documentation/rest-api.md
@@ -16,7 +16,7 @@
 * **patchset**: is the patchset number of the change to look for changed files.
   By default the current (latest) patchset of given change is used.
 
-* **debug**: can be set to true or false to override the configuration variable
+* **debug**: can be set to true/1 or false/0 to override the configuration variable
   **addDebugMsg**.
 
 * **nocache**: can be set to true to collect owerns info without using the cached OwnersDb.
@@ -31,13 +31,28 @@
 
 This API returns a JSON object with the following attributes:
 
-* **minOwnerVoteLevel**: is 1 by default, but could be set to 2.
+* **addDebugMsg**: is false by default. It can be set to true in a
+   development/test gerrit.config file, or with the `debug=1`
+   REST URL parameter. When it is true, extra **dbgmsgs** attributes
+   are included in this JSON object.
+
+* **maxCacheAge**: has default value 0; can be defined in gerrit.config.
+   It is the number of seconds OWNERS info that will stay in a cache.
+
+* **maxCacheSize**: has default value 100; can be defined in gerrit.config.
+   It is the number of most recently accessed CLs OWNERS info that will stay in a cache.
+
+* **minOwnerVoteLevel**: is 1 by default; can be set to 2 in gerrit.config.
    It is the minimal Code-Review vote value all changed files must get
    from at least one owner to make a change submittable.
 
-* **addDebugMsg**: is false by default. In a development/test configuration,
-   this attribute could be true, to tell a client to display extra debug
-   information.
+* **ownersFileName**: is "OWNERS" by default; can be redefined in project.config
+   or gerrit.config. It is the name of OWNERS file.
+
+* **rejectErrorInOwners**: is false by default; can be redefined in
+   project.config or gerrit.config.
+   When enabled, the CL uploader will check and reject OWNERS file with
+   wrong syntax or unknown email address.
 
 * **change**: is the change number.
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/ApiIT.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/ApiIT.java
index 514d146..ecd1401 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/ApiIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/ApiIT.java
@@ -46,8 +46,9 @@
     ChangeInfo info2 = newChangeInfo("test2 GetOwners");
     assertThat(info2._number).isEqualTo(info1._number + 1);
     String expected =
-        ")]}' { minOwnerVoteLevel:1, addDebugMsg:false, change:"
-            + info1._number
+        ")]}' { addDebugMsg:false, maxCacheAge:0, maxCacheSize:1000,"
+            + " minOwnerVoteLevel:1, ownersFileName:OWNERS, rejectErrorInOwners:false,"
+            + (" change:" + info1._number)
             + ", patchset:1, file2owners:{}, reviewers:[], owners:[], files:[] }";
     Cache cache = getCache().init(0, 10); // reset, no Cache
     assertThat(cache.size()).isEqualTo(0L);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwners.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwners.java
index f0dcd43..d414755 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwners.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwners.java
@@ -176,7 +176,6 @@
             accountCache,
             emails,
             repoManager,
-            pluginConfig,
             r.getChange(),
             1);
     Checker c = new Checker(repoManager, pluginConfig, null, r.getChange(), 1);
@@ -241,10 +240,13 @@
   }
 
   protected String projectOwnersFileName(Project.NameKey name) {
+    // This function is called repeatedly in ConfigIT without recreating config.
+    // So, here we recreate config, to get the latest owners file name.
+    setConfig();
     return config.getOwnersFileName(projectCache.get(name), null);
   }
 
   protected Cache getCache() {
-    return Cache.getInstance(pluginConfig, repoManager);
+    return Cache.getInstance(config, repoManager);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorIT.java
index 0030953..0eb7df1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorIT.java
@@ -126,18 +126,22 @@
   private static final PluginConfig ENABLED_CONFIG = createEnabledConfig(); // use OWNERS
   private static final PluginConfig DISABLED_CONFIG = createDisabledConfig();
 
+  private OwnersValidator newOwnersValidator(PluginConfig cfg) {
+    return new OwnersValidator(cfg, repoManager, new MockedEmails());
+  }
+
   @Test
   public void chekIsActiveAndFileName() throws Exception {
     // This check should be enabled in project.config, default is not active.
-    assertThat(OwnersValidator.isActive(EMPTY_CONFIG)).isFalse();
-    assertThat(OwnersValidator.isActive(ENABLED_CONFIG)).isTrue();
-    assertThat(OwnersValidator.isActive(ANDROID_CONFIG)).isTrue();
-    assertThat(OwnersValidator.isActive(DISABLED_CONFIG)).isFalse();
+    assertThat(newOwnersValidator(EMPTY_CONFIG).isActive()).isFalse();
+    assertThat(newOwnersValidator(ENABLED_CONFIG).isActive()).isTrue();
+    assertThat(newOwnersValidator(ANDROID_CONFIG).isActive()).isTrue();
+    assertThat(newOwnersValidator(DISABLED_CONFIG).isActive()).isFalse();
     // Default file name is "OWNERS".
-    assertThat(OwnersValidator.getOwnersFileName(EMPTY_CONFIG)).isEqualTo(OWNERS);
-    assertThat(OwnersValidator.getOwnersFileName(ENABLED_CONFIG)).isEqualTo(OWNERS);
-    assertThat(OwnersValidator.getOwnersFileName(DISABLED_CONFIG)).isEqualTo(OWNERS);
-    assertThat(OwnersValidator.getOwnersFileName(ANDROID_CONFIG)).isEqualTo(OWNERS_ANDROID);
+    assertThat(newOwnersValidator(EMPTY_CONFIG).getOwnersFileName()).isEqualTo(OWNERS);
+    assertThat(newOwnersValidator(ENABLED_CONFIG).getOwnersFileName()).isEqualTo(OWNERS);
+    assertThat(newOwnersValidator(DISABLED_CONFIG).getOwnersFileName()).isEqualTo(OWNERS);
+    assertThat(newOwnersValidator(ANDROID_CONFIG).getOwnersFileName()).isEqualTo(OWNERS_ANDROID);
   }
 
   private static final ImmutableMap<String, String> FILES_WITHOUT_OWNERS =
@@ -392,10 +396,9 @@
 
   private List<String> validate(CommitReceivedEvent event, boolean verbose, PluginConfig cfg)
       throws Exception {
-    OwnersValidator validator =
-        new OwnersValidator("find-owners", pluginConfig, repoManager, new MockedEmails());
+    OwnersValidator validator = newOwnersValidator(cfg);
     OwnersValidator.Checker checker = validator.new Checker(event, verbose);
-    checker.check(OwnersValidator.getOwnersFileName(cfg));
+    checker.check(validator.getOwnersFileName());
     return transformMessages(checker.messages);
   }