Add `ConfigResource` interface

This is a first step for providing replication configuration from an
external source. The `ConfigResource` bundles together JGit `Config`
instance with its version.

The future implementations can read the configuration from a Gerrit
project or external systems like ZooKeeper.

The idea here is to separate the configuration resource from
configuration options. This way it is easier to manage the configruation
externally and also provide layers of configuration like "git config"
command does.

On top of the `ConfigResource` we sill use the `ReplicationConfig` and
`ReplicationFileBasedConfig` to access plugin configuraiton options. The
auto-reloading mechanism is also reused.

Change-Id: I77d205dce223e508bf4a06c5ff9da91444a87032
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ConfigResource.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ConfigResource.java
new file mode 100644
index 0000000..7b41fe6
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ConfigResource.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication;
+
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Separates configuration resource from the configuration schema.
+ *
+ * <p>The {@code ConfigResource} splits configuration source from configuration options. The
+ * resource is just an {@code Config} object with its version. The configuration options can be
+ * loaded from different sources eg. a single file, multiple files or ZooKeeper.
+ *
+ * <p>Here we don't assume any configuration options being present in the {@link Config} object,
+ * this is the responsibility of {@link ReplicationConfig} interface. Think about {@code
+ * ConfigResource} as a "plain text file" and {@code ReplicationConfig} as a XML file.
+ */
+public interface ConfigResource {
+
+  /**
+   * Configuration for the plugin and all replication end points.
+   *
+   * @return current configuration
+   */
+  Config getConfig();
+
+  /**
+   * Current logical version string of the current configuration loaded in memory, depending on the
+   * actual implementation of the configuration on the persistent storage.
+   *
+   * @return current logical version number.
+   */
+  String getVersion();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/FanoutReplicationConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/FanoutReplicationConfig.java
index b915d0d..80d8b9d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/FanoutReplicationConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/FanoutReplicationConfig.java
@@ -17,6 +17,7 @@
 import static com.google.common.io.Files.getNameWithoutExtension;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hasher;
@@ -43,12 +44,20 @@
   private final Config config;
   private final Path remoteConfigsDirPath;
 
+  @VisibleForTesting
+  public FanoutReplicationConfig(SitePaths sitePaths, @PluginData Path pluginDataDir)
+      throws IOException, ConfigInvalidException {
+    this(
+        new ReplicationFileBasedConfig(new FileConfigResource(sitePaths), sitePaths, pluginDataDir),
+        sitePaths);
+  }
+
   @Inject
-  public FanoutReplicationConfig(SitePaths site, @PluginData Path pluginDataDir)
+  FanoutReplicationConfig(ReplicationFileBasedConfig replicationConfig, SitePaths site)
       throws IOException, ConfigInvalidException {
 
     remoteConfigsDirPath = site.etc_dir.resolve("replication");
-    replicationConfig = new ReplicationFileBasedConfig(site, pluginDataDir);
+    this.replicationConfig = replicationConfig;
     config = replicationConfig.getConfig();
     removeRemotes(config);
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/FileConfigResource.java b/src/main/java/com/googlesource/gerrit/plugins/replication/FileConfigResource.java
new file mode 100644
index 0000000..aad4666
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/FileConfigResource.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication;
+
+import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+public class FileConfigResource implements ConfigResource {
+  public static final String CONFIG_NAME = "replication.config";
+  private final FileBasedConfig config;
+
+  @Inject
+  FileConfigResource(SitePaths site) {
+    Path cfgPath = site.etc_dir.resolve(CONFIG_NAME);
+    this.config = new FileBasedConfig(cfgPath.toFile(), FS.DETECTED);
+    try {
+      config.load();
+    } catch (ConfigInvalidException e) {
+      repLog.atSevere().withCause(e).log("Config file %s is invalid: %s", cfgPath, e.getMessage());
+    } catch (IOException e) {
+      repLog.atSevere().withCause(e).log("Cannot read %s: %s", cfgPath, e.getMessage());
+    }
+  }
+
+  @Override
+  public Config getConfig() {
+    return config;
+  }
+
+  @Override
+  public String getVersion() {
+    return Long.toString(config.getFile().lastModified());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java
index a2edd0b..2cc3982 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java
@@ -14,6 +14,8 @@
 
 package com.googlesource.gerrit.plugins.replication;
 
+import static com.googlesource.gerrit.plugins.replication.FileConfigResource.CONFIG_NAME;
+
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.AbstractModule;
@@ -37,7 +39,7 @@
   @Inject
   ReplicationConfigModule(SitePaths site) {
     this.site = site;
-    this.cfgPath = site.etc_dir.resolve("replication.config");
+    this.cfgPath = site.etc_dir.resolve(CONFIG_NAME);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
index eed7556..9f45702 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfig.java
@@ -13,46 +13,39 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.replication;
 
-import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog;
-
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.PluginData;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
-import java.io.IOException;
 import java.nio.file.Path;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
 
 public class ReplicationFileBasedConfig implements ReplicationConfig {
   private static final int DEFAULT_SSH_CONNECTION_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
 
   private final SitePaths site;
-  private Path cfgPath;
   private boolean replicateAllOnPluginStart;
   private boolean defaultForceUpdate;
   private int maxRefsToLog;
   private final int maxRefsToShow;
   private int sshCommandTimeout;
   private int sshConnectionTimeout = DEFAULT_SSH_CONNECTION_TIMEOUT_MS;
-  private final FileBasedConfig config;
+  private final ConfigResource configResource;
   private final Path pluginDataDir;
 
+  @VisibleForTesting
+  public ReplicationFileBasedConfig(SitePaths paths, @PluginData Path pluginDataDir) {
+    this(new FileConfigResource(paths), paths, pluginDataDir);
+  }
+
   @Inject
-  public ReplicationFileBasedConfig(SitePaths site, @PluginData Path pluginDataDir) {
+  public ReplicationFileBasedConfig(
+      FileConfigResource configResource, SitePaths site, @PluginData Path pluginDataDir) {
     this.site = site;
-    this.cfgPath = site.etc_dir.resolve("replication.config");
-    this.config = new FileBasedConfig(cfgPath.toFile(), FS.DETECTED);
-    try {
-      config.load();
-    } catch (ConfigInvalidException e) {
-      repLog.atSevere().withCause(e).log("Config file %s is invalid: %s", cfgPath, e.getMessage());
-    } catch (IOException e) {
-      repLog.atSevere().withCause(e).log("Cannot read %s: %s", cfgPath, e.getMessage());
-    }
+    this.configResource = configResource;
+    Config config = configResource.getConfig();
     this.replicateAllOnPluginStart = config.getBoolean("gerrit", "replicateOnStartup", false);
     this.defaultForceUpdate = config.getBoolean("gerrit", "defaultForceUpdate", false);
     this.maxRefsToLog = config.getInt("gerrit", "maxRefsToLog", 0);
@@ -93,7 +86,7 @@
 
   @Override
   public int getDistributionInterval() {
-    return config.getInt("replication", "distributionInterval", 0);
+    return getConfig().getInt("replication", "distributionInterval", 0);
   }
 
   @Override
@@ -108,7 +101,7 @@
 
   @Override
   public Path getEventsDirectory() {
-    String eventsDirectory = config.getString("replication", null, "eventsDirectory");
+    String eventsDirectory = getConfig().getString("replication", null, "eventsDirectory");
     if (!Strings.isNullOrEmpty(eventsDirectory)) {
       return site.resolve(eventsDirectory);
     }
@@ -117,12 +110,12 @@
 
   @Override
   public Config getConfig() {
-    return config;
+    return configResource.getConfig();
   }
 
   @Override
   public String getVersion() {
-    return Long.toString(config.getFile().lastModified());
+    return configResource.getVersion();
   }
 
   @Override