Merge changes I46de6f2a,I47e93d69

* changes:
  Document that changes can become stale in the index on config changes
  Handle invalid values for boolean config parameters gracefully
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
index 8b9407f..a9b5ea6 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginConfiguration.java
@@ -114,7 +114,7 @@
    */
   public boolean areCodeOwnerConfigsReadOnly(Project.NameKey project) {
     requireNonNull(project, "project");
-    return generalConfig.getReadOnly(getPluginConfig(project));
+    return generalConfig.getReadOnly(project, getPluginConfig(project));
   }
 
   /**
@@ -126,7 +126,7 @@
    */
   public boolean arePureRevertsExempted(Project.NameKey project) {
     requireNonNull(project, "project");
-    return generalConfig.getExemptPureReverts(getPluginConfig(project));
+    return generalConfig.getExemptPureReverts(project, getPluginConfig(project));
   }
 
   /**
@@ -140,7 +140,7 @@
    */
   public boolean rejectNonResolvableCodeOwners(Project.NameKey project) {
     requireNonNull(project, "project");
-    return generalConfig.getRejectNonResolvableCodeOwners(getPluginConfig(project));
+    return generalConfig.getRejectNonResolvableCodeOwners(project, getPluginConfig(project));
   }
 
   /**
@@ -154,7 +154,7 @@
    */
   public boolean rejectNonResolvableImports(Project.NameKey project) {
     requireNonNull(project, "project");
-    return generalConfig.getRejectNonResolvableImports(getPluginConfig(project));
+    return generalConfig.getRejectNonResolvableImports(project, getPluginConfig(project));
   }
 
   /**
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
index 75b3637..28c8456 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
@@ -202,11 +202,12 @@
    * <p>The read-only configuration controls whether code owner config files are read-only and all
    * modifications of code owner config files should be rejected.
    *
+   * @param project the project for which the read-only configuration should be read
    * @param pluginConfig the plugin config from which the read-only configuration should be read.
    * @return whether code owner config files are read-only
    */
-  boolean getReadOnly(Config pluginConfig) {
-    return getBooleanConfig(pluginConfig, KEY_READ_ONLY, /* defaultValue= */ false);
+  boolean getReadOnly(Project.NameKey project, Config pluginConfig) {
+    return getBooleanConfig(project, pluginConfig, KEY_READ_ONLY, /* defaultValue= */ false);
   }
 
   /**
@@ -216,11 +217,13 @@
    * <p>The exempt-pure-reverts configuration controls whether pure revert changes are exempted from
    * needing code owner approvals for submit.
    *
+   * @param project the project for which the exempt-pure-revert configuration should be read
    * @param pluginConfig the plugin config from which the read-only configuration should be read.
    * @return whether pure reverts are exempted from needing code owner approvals for submit
    */
-  boolean getExemptPureReverts(Config pluginConfig) {
-    return getBooleanConfig(pluginConfig, KEY_EXEMPT_PURE_REVERTS, /* defaultValue= */ false);
+  boolean getExemptPureReverts(Project.NameKey project, Config pluginConfig) {
+    return getBooleanConfig(
+        project, pluginConfig, KEY_EXEMPT_PURE_REVERTS, /* defaultValue= */ false);
   }
 
   /**
@@ -231,14 +234,16 @@
    * with newly added non-resolvable code owners should be rejected on commit received and on
    * submit.
    *
+   * @param project the project for which the freject-non-resolvable-code-owners configuration
+   *     should be read
    * @param pluginConfig the plugin config from which the reject-non-resolvable-code-owners
    *     configuration should be read.
    * @return whether code owner config files with newly added non-resolvable code owners should be
    *     rejected on commit received and on submit
    */
-  boolean getRejectNonResolvableCodeOwners(Config pluginConfig) {
+  boolean getRejectNonResolvableCodeOwners(Project.NameKey project, Config pluginConfig) {
     return getBooleanConfig(
-        pluginConfig, KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS, /* defaultValue= */ true);
+        project, pluginConfig, KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS, /* defaultValue= */ true);
   }
 
   /**
@@ -248,26 +253,46 @@
    * <p>The reject-non-resolvable-imports configuration controls whether code owner config files
    * with newly added non-resolvable imports should be rejected on commit received and on submit.
    *
+   * @param project the project for which the freject-non-resolvable-imports configuration should be
+   *     read
    * @param pluginConfig the plugin config from which the reject-non-resolvable-imports
    *     configuration should be read.
    * @return whether code owner config files with newly added non-resolvable imports should be
    *     rejected on commit received and on submit
    */
-  boolean getRejectNonResolvableImports(Config pluginConfig) {
+  boolean getRejectNonResolvableImports(Project.NameKey project, Config pluginConfig) {
     return getBooleanConfig(
-        pluginConfig, KEY_REJECT_NON_RESOLVABLE_IMPORTS, /* defaultValue= */ true);
+        project, pluginConfig, KEY_REJECT_NON_RESOLVABLE_IMPORTS, /* defaultValue= */ true);
   }
 
-  private boolean getBooleanConfig(Config pluginConfig, String key, boolean defaultValue) {
+  private boolean getBooleanConfig(
+      Project.NameKey project, Config pluginConfig, String key, boolean defaultValue) {
+    requireNonNull(project, "project");
     requireNonNull(pluginConfig, "pluginConfig");
     requireNonNull(key, "key");
 
-    if (pluginConfig.getString(SECTION_CODE_OWNERS, /* subsection= */ null, key) != null) {
-      return pluginConfig.getBoolean(
-          SECTION_CODE_OWNERS, /* subsection= */ null, key, defaultValue);
+    String value = pluginConfig.getString(SECTION_CODE_OWNERS, /* subsection= */ null, key);
+    if (value != null) {
+      try {
+        return pluginConfig.getBoolean(
+            SECTION_CODE_OWNERS, /* subsection= */ null, key, defaultValue);
+      } catch (IllegalArgumentException e) {
+        logger.atWarning().withCause(e).log(
+            "Ignoring invalid value %s for %s in '%s.config' of project %s."
+                + " Falling back to global config.",
+            value, key, pluginName, project.get());
+      }
     }
 
-    return pluginConfigFromGerritConfig.getBoolean(key, defaultValue);
+    try {
+      return pluginConfigFromGerritConfig.getBoolean(key, defaultValue);
+    } catch (IllegalArgumentException e) {
+      logger.atWarning().withCause(e).log(
+          "Ignoring invalid value %s for %s in gerrit.config (parameter"
+              + " plugin.%s.%s). Falling back to default value %s.",
+          pluginConfigFromGerritConfig.getString(key), key, pluginName, key, defaultValue);
+      return defaultValue;
+    }
   }
 
   /**
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
index 4e43414..b5716ee 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
@@ -107,23 +107,33 @@
   }
 
   @Test
+  public void cannotGetReadOnlyForNullProject() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> generalConfig.getReadOnly(/* project= */ null, new Config()));
+    assertThat(npe).hasMessageThat().isEqualTo("project");
+  }
+
+  @Test
   public void cannotGetReadOnlyForNullPluginConfig() throws Exception {
     NullPointerException npe =
         assertThrows(
-            NullPointerException.class, () -> generalConfig.getReadOnly(/* pluginConfig= */ null));
+            NullPointerException.class,
+            () -> generalConfig.getReadOnly(project, /* pluginConfig= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
   }
 
   @Test
   public void noReadOnlyConfiguration() throws Exception {
-    assertThat(generalConfig.getReadOnly(new Config())).isFalse();
+    assertThat(generalConfig.getReadOnly(project, new Config())).isFalse();
   }
 
   @Test
   @GerritConfig(name = "plugin.code-owners.readOnly", value = "true")
   public void readOnlyConfigurationIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
       throws Exception {
-    assertThat(generalConfig.getReadOnly(new Config())).isTrue();
+    assertThat(generalConfig.getReadOnly(project, new Config())).isTrue();
   }
 
   @Test
@@ -132,7 +142,30 @@
       throws Exception {
     Config cfg = new Config();
     cfg.setString(SECTION_CODE_OWNERS, /* subsection= */ null, KEY_READ_ONLY, "false");
-    assertThat(generalConfig.getReadOnly(cfg)).isFalse();
+    assertThat(generalConfig.getReadOnly(project, cfg)).isFalse();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.readOnly", value = "true")
+  public void invalidReadOnlyConfigurationInPluginConfigIsIgnored() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_CODE_OWNERS, /* subsection= */ null, KEY_READ_ONLY, "INVALID");
+    assertThat(generalConfig.getReadOnly(project, cfg)).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.readOnly", value = "INVALID")
+  public void invalidReadOnlyConfigurationInGerritConfigIsIgnored() throws Exception {
+    assertThat(generalConfig.getReadOnly(project, new Config())).isFalse();
+  }
+
+  @Test
+  public void cannotGetExemptPureRevertsForNullProject() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> generalConfig.getExemptPureReverts(/* project= */ null, new Config()));
+    assertThat(npe).hasMessageThat().isEqualTo("project");
   }
 
   @Test
@@ -140,13 +173,13 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> generalConfig.getExemptPureReverts(/* pluginConfig= */ null));
+            () -> generalConfig.getExemptPureReverts(project, /* pluginConfig= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
   }
 
   @Test
   public void noExemptPureRevertsConfiguration() throws Exception {
-    assertThat(generalConfig.getExemptPureReverts(new Config())).isFalse();
+    assertThat(generalConfig.getExemptPureReverts(project, new Config())).isFalse();
   }
 
   @Test
@@ -154,7 +187,7 @@
   public void
       exemptPureRevertsConfigurationIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
           throws Exception {
-    assertThat(generalConfig.getExemptPureReverts(new Config())).isTrue();
+    assertThat(generalConfig.getExemptPureReverts(project, new Config())).isTrue();
   }
 
   @Test
@@ -164,7 +197,31 @@
           throws Exception {
     Config cfg = new Config();
     cfg.setString(SECTION_CODE_OWNERS, null, KEY_EXEMPT_PURE_REVERTS, "false");
-    assertThat(generalConfig.getExemptPureReverts(cfg)).isFalse();
+    assertThat(generalConfig.getExemptPureReverts(project, cfg)).isFalse();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.exemptPureReverts", value = "true")
+  public void invalidExemptPureRevertsInPluginConfigIsIgnored() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(SECTION_CODE_OWNERS, /* subsection= */ null, KEY_EXEMPT_PURE_REVERTS, "INVALID");
+    assertThat(generalConfig.getExemptPureReverts(project, cfg)).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.exemptPureReverts", value = "INVALID")
+  public void invalidExemptPureRevertsConfigurationInGerritConfigIsIgnored() throws Exception {
+    assertThat(generalConfig.getExemptPureReverts(project, new Config())).isFalse();
+  }
+
+  @Test
+  public void cannotGetRejectNonResolvableCodeOwnersForNullProject() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () ->
+                generalConfig.getRejectNonResolvableCodeOwners(/* project= */ null, new Config()));
+    assertThat(npe).hasMessageThat().isEqualTo("project");
   }
 
   @Test
@@ -172,13 +229,14 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> generalConfig.getRejectNonResolvableCodeOwners(/* pluginConfig= */ null));
+            () ->
+                generalConfig.getRejectNonResolvableCodeOwners(project, /* pluginConfig= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
   }
 
   @Test
   public void noRejectNonResolvableCodeOwnersConfiguration() throws Exception {
-    assertThat(generalConfig.getRejectNonResolvableCodeOwners(new Config())).isTrue();
+    assertThat(generalConfig.getRejectNonResolvableCodeOwners(project, new Config())).isTrue();
   }
 
   @Test
@@ -186,7 +244,7 @@
   public void
       rejectNonResolvableCodeOwnersConfigurationIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
           throws Exception {
-    assertThat(generalConfig.getRejectNonResolvableCodeOwners(new Config())).isFalse();
+    assertThat(generalConfig.getRejectNonResolvableCodeOwners(project, new Config())).isFalse();
   }
 
   @Test
@@ -197,7 +255,35 @@
     Config cfg = new Config();
     cfg.setString(
         SECTION_CODE_OWNERS, /* subsection= */ null, KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS, "true");
-    assertThat(generalConfig.getRejectNonResolvableCodeOwners(cfg)).isTrue();
+    assertThat(generalConfig.getRejectNonResolvableCodeOwners(project, cfg)).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.rejectNonResolvableCodeOwners", value = "false")
+  public void invalidRejectNonResolvableCodeOwnersInPluginConfigIsIgnored() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(
+        SECTION_CODE_OWNERS,
+        /* subsection= */ null,
+        KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
+        "INVALID");
+    assertThat(generalConfig.getRejectNonResolvableCodeOwners(project, cfg)).isFalse();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.rejectNonResolvableCodeOwners", value = "INVALID")
+  public void invalidRejectNonResolvableCodeOwnersConfigurationInGerritConfigIsIgnored()
+      throws Exception {
+    assertThat(generalConfig.getRejectNonResolvableCodeOwners(project, new Config())).isTrue();
+  }
+
+  @Test
+  public void cannotGetRejectNonResolvableImportsForNullProject() throws Exception {
+    NullPointerException npe =
+        assertThrows(
+            NullPointerException.class,
+            () -> generalConfig.getRejectNonResolvableImports(/* project= */ null, new Config()));
+    assertThat(npe).hasMessageThat().isEqualTo("project");
   }
 
   @Test
@@ -205,13 +291,13 @@
     NullPointerException npe =
         assertThrows(
             NullPointerException.class,
-            () -> generalConfig.getRejectNonResolvableImports(/* pluginConfig= */ null));
+            () -> generalConfig.getRejectNonResolvableImports(project, /* pluginConfig= */ null));
     assertThat(npe).hasMessageThat().isEqualTo("pluginConfig");
   }
 
   @Test
   public void noRejectNonResolvableImportsConfiguration() throws Exception {
-    assertThat(generalConfig.getRejectNonResolvableImports(new Config())).isTrue();
+    assertThat(generalConfig.getRejectNonResolvableImports(project, new Config())).isTrue();
   }
 
   @Test
@@ -219,7 +305,7 @@
   public void
       rejectNonResolvableImportsConfigurationIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel()
           throws Exception {
-    assertThat(generalConfig.getRejectNonResolvableImports(new Config())).isFalse();
+    assertThat(generalConfig.getRejectNonResolvableImports(project, new Config())).isFalse();
   }
 
   @Test
@@ -230,7 +316,23 @@
     Config cfg = new Config();
     cfg.setString(
         SECTION_CODE_OWNERS, /* scubsection= */ null, KEY_REJECT_NON_RESOLVABLE_IMPORTS, "true");
-    assertThat(generalConfig.getRejectNonResolvableImports(cfg)).isTrue();
+    assertThat(generalConfig.getRejectNonResolvableImports(project, cfg)).isTrue();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.rejectNonResolvableImports", value = "false")
+  public void invalidRejectNonResolvableImportsInPluginConfigIsIgnored() throws Exception {
+    Config cfg = new Config();
+    cfg.setString(
+        SECTION_CODE_OWNERS, /* subsection= */ null, KEY_REJECT_NON_RESOLVABLE_IMPORTS, "INVALID");
+    assertThat(generalConfig.getRejectNonResolvableImports(project, cfg)).isFalse();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.rejectNonResolvableImports", value = "INVALID")
+  public void invalidRejectNonResolvableImportsConfigurationInGerritConfigIsIgnored()
+      throws Exception {
+    assertThat(generalConfig.getRejectNonResolvableImports(project, new Config())).isTrue();
   }
 
   @Test
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md
index 4aa0a26..a94cf63 100644
--- a/resources/Documentation/config.md
+++ b/resources/Documentation/config.md
@@ -23,6 +23,23 @@
 *not* possible to just add a value to the inherited list, but if this is wanted
 the complete list with the additional value has to be set on project level.
 
+## <a id="staleIndexOnConfigChanges">
+**NOTE:** Some configuration changes can lead to changes becoming stale in the
+change index. E.g. if an additional branch is newly exempted in `gerrit.config`
+or in the `code-owners.config` of a parent project, the submittability of
+changes for that branch in child projects may change (since they no longer
+require code owner approvals), but it's not feasible to reindex all affected
+changes when this config change is done (as config changes can potentially
+affect all open changes on the host and reindexing that many changes would be
+too expensive). In this case the affected changes will be become stale in the
+change index (e.g. the change index contains outdated submit records) and as a
+result of this you may not observe the effects of the config change on all
+changes immediately, but only when they have been reindexed (which happens on
+any modification of the changes). If needed, you may force the reindexing of a
+change by calling the [Index
+Changes](../../../Documentation/rest-api-changes.html#index-change) REST
+endpoint or by touching the change (e.g. by adding a comment).
+
 # <a id="globalConfiguration">Global configuration in gerrit.config</a>
 
 <a id="pluginCodeOwnersDisabled">plugin.@PLUGIN@.disabled</a>