Merge branch 'stable-2.13' into stable-2.14

* stable-2.13:
  Setup: Fix typo in method name
  Option to setup a copy of the master site during init

Change-Id: I9bd02845d4ff7bb5d5170f423fe817faf1cb8700
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
index bae32a2..5bff79f 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
@@ -43,6 +43,7 @@
   // main section
   static final String MAIN_SECTION = "main";
   static final String SHARED_DIRECTORY_KEY = "sharedDirectory";
+  static final String DEFAULT_SHARED_DIRECTORY = "shared";
 
   // peerInfo section
   static final String PEER_INFO_SECTION = "peerInfo";
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
index 44461ef..5534927 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Setup.java
@@ -14,17 +14,46 @@
 
 package com.ericsson.gerrit.plugins.highavailability;
 
-import static com.ericsson.gerrit.plugins.highavailability.Configuration.*;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.CACHE_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.CLEANUP_INTERVAL_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.CLUSTER_NAME_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.CONNECTION_TIMEOUT_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_CLEANUP_INTERVAL;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_CLUSTER_NAME;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_MAX_TRIES;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_RETRY_INTERVAL;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_SHARED_DIRECTORY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_THREAD_POOL_SIZE;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_TIMEOUT_MS;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.HTTP_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.INDEX_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGROUPS_SUBSECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAIN_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAX_TRIES_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PASSWORD_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PEER_INFO_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.RETRY_INTERVAL_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.SHARED_DIRECTORY_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.SOCKET_TIMEOUT_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.STATIC_SUBSECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.STRATEGY_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.THREAD_POOL_SIZE_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.URL_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.USER_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.WEBSESSION_SECTION;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration.PeerInfoStrategy;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.FileUtil;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.EnumSet;
 import java.util.Objects;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -34,14 +63,24 @@
 
   private final ConsoleUI ui;
   private final String pluginName;
+  private final InitFlags flags;
   private final SitePaths site;
+  private final SetupLocalHAReplica setupLocalHAReplica;
+
   private FileBasedConfig config;
 
   @Inject
-  public Setup(ConsoleUI ui, @PluginName String pluginName, SitePaths site) {
+  public Setup(
+      ConsoleUI ui,
+      @PluginName String pluginName,
+      InitFlags flags,
+      SitePaths site,
+      SetupLocalHAReplica setupLocalHAReplica) {
     this.ui = ui;
     this.pluginName = pluginName;
+    this.flags = flags;
     this.site = site;
+    this.setupLocalHAReplica = setupLocalHAReplica;
   }
 
   @Override
@@ -54,23 +93,28 @@
       Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
       config = new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
       config.load();
-      configureMainSection();
-      configurePeerInfoSection();
       configureHttp();
       configureCacheSection();
       configureIndexSection();
-      configureWebsessiosSection();
-      config.save();
+      configureWebsessionsSection();
+      if (!createHAReplicaSite(config)) {
+        configureMainSection();
+        configurePeerInfoSection();
+        config.save();
+      }
+      flags.cfg.setBoolean("database", "h2", "autoServer", true);
     }
   }
 
   private void configureMainSection() {
     ui.header("Main section");
-    String sharedDir =
-        promptAndSetString("Shared directory", MAIN_SECTION, SHARED_DIRECTORY_KEY, null);
-    if (!Strings.isNullOrEmpty(sharedDir)) {
-      Path shared = site.site_path.resolve(sharedDir);
-      FileUtil.mkdirsOrDie(shared, "cannot create " + shared);
+    String sharedDirDefault = ui.isBatch() ? DEFAULT_SHARED_DIRECTORY : null;
+    String shared =
+        promptAndSetString(
+            "Shared directory", MAIN_SECTION, SHARED_DIRECTORY_KEY, sharedDirDefault);
+    if (!Strings.isNullOrEmpty(shared)) {
+      Path resolved = site.site_path.resolve(Paths.get(shared));
+      FileUtil.mkdirsOrDie(resolved, "cannot create " + resolved);
     }
   }
 
@@ -127,7 +171,7 @@
         str(DEFAULT_THREAD_POOL_SIZE));
   }
 
-  private void configureWebsessiosSection() {
+  private void configureWebsessionsSection() {
     ui.header("Websession section");
     promptAndSetString(
         "Cleanup interval", WEBSESSION_SECTION, CLEANUP_INTERVAL_KEY, DEFAULT_CLEANUP_INTERVAL);
@@ -156,6 +200,30 @@
     return Integer.toString(n);
   }
 
+  private boolean createHAReplicaSite(FileBasedConfig pluginConfig) throws Exception {
+    ui.header("HA replica site setup");
+    ui.message(
+        "It is possible to create a copy of the master site and configure both sites to run\n"
+            + "in HA mode as peers. This is possible when the directory where the copy will be\n"
+            + "created is accessible from this machine\n"
+            + "\n"
+            + "NOTE: This step is optional. If you want to create the other site manually, or\n"
+            + "if the other site needs to be created in a directory not accessible from this\n"
+            + "machine then please skip this step.\n");
+    if (ui.yesno(true, "Create a HA replica site")) {
+      String replicaPath = ui.readString("ha/1", "Location of the HA replica");
+      Path replica = site.site_path.resolve(Paths.get(replicaPath));
+      if (Files.exists(replica)) {
+        ui.message("%s already exists, exiting", replica);
+        return true;
+      }
+      config.save();
+      setupLocalHAReplica.run(new SitePaths(replica), pluginConfig);
+      return true;
+    }
+    return false;
+  }
+
   @Override
   public void postRun() throws Exception {}
 }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/SetupLocalHAReplica.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/SetupLocalHAReplica.java
new file mode 100644
index 0000000..53d33ed
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/SetupLocalHAReplica.java
@@ -0,0 +1,189 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.highavailability;
+
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.CLUSTER_NAME_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_CLUSTER_NAME;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.DEFAULT_SHARED_DIRECTORY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.JGROUPS_SUBSECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.MAIN_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.PEER_INFO_SECTION;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.SHARED_DIRECTORY_KEY;
+import static com.ericsson.gerrit.plugins.highavailability.Configuration.STRATEGY_KEY;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+class SetupLocalHAReplica {
+  private final SitePaths master;
+  private final FileBasedConfig masterConfig;
+
+  private Path sharedDir;
+  private SitePaths replica;
+
+  @Inject
+  SetupLocalHAReplica(SitePaths master, InitFlags flags) {
+    this.master = master;
+    this.masterConfig = flags.cfg;
+    this.sharedDir = master.site_path.resolve(DEFAULT_SHARED_DIRECTORY);
+  }
+
+  void run(SitePaths replica, FileBasedConfig pluginConfig) throws Exception {
+    this.replica = replica;
+
+    FileUtil.mkdirsOrDie(replica.site_path, "cannot create " + replica.site_path);
+
+    configureMainSection(pluginConfig);
+    configurePeerInfo(pluginConfig);
+
+    for (Path dir : listDirsForCopy()) {
+      copyFiles(dir);
+    }
+
+    mkdir(replica.logs_dir);
+    mkdir(replica.tmp_dir);
+    symlink(Paths.get(masterConfig.getString("gerrit", null, "basePath")));
+    symlink(sharedDir);
+
+    FileBasedConfig replicaConfig =
+        new FileBasedConfig(replica.gerrit_config.toFile(), FS.DETECTED);
+    replicaConfig.load();
+
+    if ("h2".equals(masterConfig.getString("database", null, "type"))) {
+      masterConfig.setBoolean("database", "h2", "autoServer", true);
+      replicaConfig.setBoolean("database", "h2", "autoServer", true);
+      symlinkH2ReviewDbDir();
+    }
+  }
+
+  private List<Path> listDirsForCopy() throws IOException {
+    ImmutableList.Builder<Path> toSkipBuilder = ImmutableList.builder();
+    toSkipBuilder.add(
+        master.resolve(masterConfig.getString("gerrit", null, "basePath")),
+        master.db_dir,
+        master.logs_dir,
+        replica.site_path,
+        master.site_path.resolve(sharedDir),
+        master.tmp_dir);
+    if ("h2".equals(masterConfig.getString("database", null, "type"))) {
+      toSkipBuilder.add(
+          master.resolve(masterConfig.getString("database", null, "database")).getParent());
+    }
+    final ImmutableList<Path> toSkip = toSkipBuilder.build();
+
+    final ArrayList<Path> dirsForCopy = new ArrayList<>();
+    Files.walkFileTree(
+        master.site_path,
+        EnumSet.of(FileVisitOption.FOLLOW_LINKS),
+        Integer.MAX_VALUE,
+        new SimpleFileVisitor<Path>() {
+          @Override
+          public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+              throws IOException {
+            if (Files.isSameFile(dir, master.site_path)) {
+              return FileVisitResult.CONTINUE;
+            }
+
+            Path p = master.site_path.relativize(dir);
+            if (shouldSkip(p)) {
+              return FileVisitResult.SKIP_SUBTREE;
+            }
+            dirsForCopy.add(p);
+            return FileVisitResult.CONTINUE;
+          }
+
+          private boolean shouldSkip(Path p) throws IOException {
+            Path resolved = master.site_path.resolve(p);
+            for (Path skip : toSkip) {
+              if (Files.exists(skip) && Files.isSameFile(resolved, skip)) {
+                return true;
+              }
+            }
+            return false;
+          }
+        });
+
+    return dirsForCopy;
+  }
+
+  private void copyFiles(Path dir) throws IOException {
+    final Path source = master.site_path.resolve(dir);
+    final Path target = replica.site_path.resolve(dir);
+    Files.createDirectories(target);
+    Files.walkFileTree(
+        source,
+        EnumSet.noneOf(FileVisitOption.class),
+        1,
+        new SimpleFileVisitor<Path>() {
+          @Override
+          public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+              throws IOException {
+            Path f = source.relativize(file);
+            if (Files.isRegularFile(file)) {
+              Files.copy(file, target.resolve(f));
+            }
+            return FileVisitResult.CONTINUE;
+          }
+        });
+  }
+
+  private static void mkdir(Path dir) throws IOException {
+    Files.createDirectories(dir);
+  }
+
+  private void symlink(Path path) throws IOException {
+    if (!path.isAbsolute()) {
+      Files.createSymbolicLink(
+          replica.site_path.resolve(path),
+          master.site_path.resolve(path).toAbsolutePath().normalize());
+    }
+  }
+
+  private void symlinkH2ReviewDbDir() throws IOException {
+    symlink(Paths.get(masterConfig.getString("database", null, "database")).getParent());
+  }
+
+  private void configureMainSection(FileBasedConfig pluginConfig) throws IOException {
+    pluginConfig.setString(
+        MAIN_SECTION,
+        null,
+        SHARED_DIRECTORY_KEY,
+        master.site_path.relativize(sharedDir).toString());
+    pluginConfig.save();
+  }
+
+  private void configurePeerInfo(FileBasedConfig pluginConfig) throws IOException {
+    pluginConfig.setString(PEER_INFO_SECTION, null, STRATEGY_KEY, "jgroups");
+    pluginConfig.setString(
+        PEER_INFO_SECTION, JGROUPS_SUBSECTION, CLUSTER_NAME_KEY, DEFAULT_CLUSTER_NAME);
+    pluginConfig.save();
+  }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 80998d5..e537545 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -34,7 +34,8 @@
     When given as a relative path, then it is resolved against the $SITE_PATH
     or Gerrit server. For example, if $SITE_PATH is "/gerrit/root" and
     sharedDirectory is given as "shared/dir" then the real path of the shared
-    directory is "/gerrit/root/shared/dir".
+    directory is "/gerrit/root/shared/dir". When not specified, the default
+    is "shared".
 
 peerInfo.strategy
 :   Strategy to find other peers. Supported strategies are `static` or `jgroups`.