Install all packaged plugins automatically on auto site init

When Gerrit is deployed in a servlet container and the system property
'gerrit.init' is defined then, besides the automatic site
initialization, also all packaged plugins (under WEB-INF/plugins) will
be installed.

Change-Id: Ia1e50d88ba63e96fc70717f854c87c7409f0c618
diff --git a/Documentation/config-auto-site-initialization.txt b/Documentation/config-auto-site-initialization.txt
index 2bb0d47..3856e71 100644
--- a/Documentation/config-auto-site-initialization.txt
+++ b/Documentation/config-auto-site-initialization.txt
@@ -4,8 +4,9 @@
 
 Gerrit supports automatic site initialization on server startup
 when Gerrit runs in a servlet container. Both creation of a new site
-and upgrade of an existing site are supported. Installation of
-plugins during the site creation/initialization is not yet supported.
+and upgrade of an existing site are supported.  All packaged plugins
+will be installed when Gerrit is deployed in a servlet container and the
+location of the Gerrit distribution can be determined at runtime.
 
 This feature may be useful for such setups where Gerrit administrators
 don't have direct access to the database and the file system of the
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index c24342a..b7162ed 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -72,6 +72,7 @@
     '//lib:guava',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
+    '//lib/log:api',
   ],
   compile_deps = ['//gerrit-launcher:launcher'],
   visibility = [
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
index 07e985a..8221f5f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.pgm.init.InitFlags;
 import com.google.gerrit.pgm.init.InitModule;
 import com.google.gerrit.pgm.init.InstallPlugins;
+import com.google.gerrit.pgm.init.PluginsDistribution;
 import com.google.gerrit.pgm.init.SitePathInitializer;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.pgm.util.Die;
@@ -47,7 +48,11 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.spi.Message;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -55,24 +60,31 @@
 
 /** Initialize a new Gerrit installation. */
 public class BaseInit extends SiteProgram {
+  private static final Logger log =
+      LoggerFactory.getLogger(BaseInit.class);
 
   private final boolean standalone;
   private final boolean initDb;
+  protected final PluginsDistribution pluginsDistribution;
 
-  public BaseInit() {
+  protected BaseInit(PluginsDistribution pluginsDistribution) {
     this.standalone = true;
     this.initDb = true;
+    this.pluginsDistribution = pluginsDistribution;
   }
 
-  public BaseInit(File sitePath, boolean standalone, boolean initDb) {
-    this(sitePath, null, standalone, initDb);
+  public BaseInit(File sitePath, boolean standalone, boolean initDb,
+      PluginsDistribution pluginsDistribution) {
+    this(sitePath, null, standalone, initDb, pluginsDistribution);
   }
 
   public BaseInit(File sitePath, final Provider<DataSource> dsProvider,
-      boolean standalone, boolean initDb) {
+      boolean standalone, boolean initDb,
+      PluginsDistribution pluginsDistribution) {
     super(sitePath, dsProvider);
     this.standalone = standalone;
     this.initDb = initDb;
+    this.pluginsDistribution = pluginsDistribution;
   }
 
   @Override
@@ -123,7 +135,13 @@
   }
 
   protected List<String> getInstallPlugins() {
-    return null;
+    try {
+      return pluginsDistribution.listPluginNames();
+    } catch (FileNotFoundException e) {
+      log.warn("Couldn't find distribution archive location."
+          + " No plugin will be installed");
+      return null;
+    }
   }
 
   protected boolean getAutoStart() {
@@ -161,6 +179,7 @@
             Objects.firstNonNull(getInstallPlugins(), Lists.<String> newArrayList());
         bind(new TypeLiteral<List<String>>() {}).annotatedWith(
             InstallPlugins.class).toInstance(plugins);
+        bind(PluginsDistribution.class).toInstance(pluginsDistribution);
       }
     });
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index d97c6d0..03973c0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -60,10 +60,11 @@
   Browser browser;
 
   public Init() {
+    super(new WarDistribution());
   }
 
   public Init(File sitePath) {
-    super(sitePath, true, true);
+    super(sitePath, true, true, new WarDistribution());
     batchMode = true;
     noAutoStart = true;
   }
@@ -74,7 +75,7 @@
 
     if (!skipPlugins) {
       final List<PluginData> plugins =
-          InitPlugins.listPluginsAndRemoveTempFiles(init.site);
+          InitPlugins.listPluginsAndRemoveTempFiles(init.site, pluginsDistribution);
       ConsoleUI ui = ConsoleUI.getInstance(false);
       verifyInstallPluginList(ui, plugins);
       if (listPlugins) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/WarDistribution.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/WarDistribution.java
new file mode 100644
index 0000000..2c34711
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/WarDistribution.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2014 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.google.gerrit.pgm;
+
+import static com.google.gerrit.pgm.init.InitPlugins.JAR;
+import static com.google.gerrit.pgm.init.InitPlugins.PLUGIN_DIR;
+
+import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.pgm.init.PluginsDistribution;
+import com.google.inject.Singleton;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+@Singleton
+public class WarDistribution implements PluginsDistribution {
+
+  @Override
+  public void foreach(Processor processor) throws FileNotFoundException, IOException {
+    File myWar = GerritLauncher.getDistributionArchive();
+    if (myWar.isFile()) {
+      try (ZipFile zf = new ZipFile(myWar)) {
+        Enumeration<? extends ZipEntry> e = zf.entries();
+        while (e.hasMoreElements()) {
+          ZipEntry ze = e.nextElement();
+          if (ze.isDirectory()) {
+            continue;
+          }
+
+          if (ze.getName().startsWith(PLUGIN_DIR) && ze.getName().endsWith(JAR)) {
+            String pluginJarName = new File(ze.getName()).getName();
+            String pluginName = pluginJarName.substring(0,
+                pluginJarName.length() - JAR.length());
+            final InputStream in = zf.getInputStream(ze);
+            processor.process(pluginName, in);
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public List<String> listPluginNames() throws FileNotFoundException {
+    // not yet used
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
index bbab099..4ce9a24 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -58,9 +58,7 @@
     step().to(InitSshd.class);
     step().to(InitHttpd.class);
     step().to(InitCache.class);
-    if (standalone) {
-      step().to(InitPlugins.class);
-    }
+    step().to(InitPlugins.class);
   }
 
   protected LinkedBindingBuilder<InitStep> step() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
index cc7c323..e5f6f56 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.collect.Lists;
-import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.plugins.PluginLoader;
@@ -25,18 +24,15 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Enumeration;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
 
 @Singleton
 public class InitPlugins implements InitStep {
-  private static final String PLUGIN_DIR = "WEB-INF/plugins/";
-  private static final String JAR = ".jar";
+  public static final String PLUGIN_DIR = "WEB-INF/plugins/";
+  public static final String JAR = ".jar";
 
   public static class PluginData {
     public final String name;
@@ -50,46 +46,31 @@
     }
   }
 
-  public static List<PluginData> listPlugins(SitePaths site) throws IOException {
-    return listPlugins(site, false);
+  public static List<PluginData> listPlugins(SitePaths site,
+      PluginsDistribution pluginsDistribution) throws IOException {
+    return listPlugins(site, false, pluginsDistribution);
   }
 
-  public static List<PluginData> listPluginsAndRemoveTempFiles(SitePaths site) throws IOException {
-    return listPlugins(site, true);
+  public static List<PluginData> listPluginsAndRemoveTempFiles(SitePaths site,
+      PluginsDistribution pluginsDistribution) throws IOException {
+    return listPlugins(site, true, pluginsDistribution);
   }
 
-  private static List<PluginData> listPlugins(SitePaths site, boolean deleteTempPluginFile) throws IOException {
-    final File myWar = GerritLauncher.getDistributionArchive();
+  private static List<PluginData> listPlugins(final SitePaths site,
+      final boolean deleteTempPluginFile, PluginsDistribution pluginsDistribution)
+          throws IOException {
     final List<PluginData> result = Lists.newArrayList();
-    try {
-      final ZipFile zf = new ZipFile(myWar);
-      try {
-        final Enumeration<? extends ZipEntry> e = zf.entries();
-        while (e.hasMoreElements()) {
-          final ZipEntry ze = e.nextElement();
-          if (ze.isDirectory()) {
-            continue;
-          }
-
-          if (ze.getName().startsWith(PLUGIN_DIR) && ze.getName().endsWith(JAR)) {
-            final String pluginJarName = new File(ze.getName()).getName();
-            final String pluginName = pluginJarName.substring(0,  pluginJarName.length() - JAR.length());
-            final InputStream in = zf.getInputStream(ze);
-            final File tmpPlugin = PluginLoader.storeInTemp(pluginName, in, site);
-            final String pluginVersion = getVersion(tmpPlugin);
-            if (deleteTempPluginFile) {
-              tmpPlugin.delete();
-            }
-
-            result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
-          }
+    pluginsDistribution.foreach(new PluginsDistribution.Processor() {
+      @Override
+      public void process(String pluginName, InputStream in) throws IOException {
+        File tmpPlugin = PluginLoader.storeInTemp(pluginName, in, site);
+        String pluginVersion = getVersion(tmpPlugin);
+        if (deleteTempPluginFile) {
+          tmpPlugin.delete();
         }
-      } finally {
-        zf.close();
+        result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
       }
-    } catch (IOException e) {
-      throw new IOException("Failure during plugin installation", e);
-    }
+    });
     return result;
   }
 
@@ -97,14 +78,17 @@
   private final SitePaths site;
   private final InitFlags initFlags;
   private final InitPluginStepsLoader pluginLoader;
+  private final PluginsDistribution pluginsDistribution;
 
   @Inject
   InitPlugins(final ConsoleUI ui, final SitePaths site,
-      InitFlags initFlags, InitPluginStepsLoader pluginLoader) {
+      InitFlags initFlags, InitPluginStepsLoader pluginLoader,
+      PluginsDistribution pluginsDistribution) {
     this.ui = ui;
     this.site = site;
     this.initFlags = initFlags;
     this.pluginLoader = pluginLoader;
+    this.pluginsDistribution = pluginsDistribution;
   }
 
   @Override
@@ -121,7 +105,7 @@
   }
 
   private void installPlugins() throws IOException {
-    List<PluginData> plugins = listPlugins(site);
+    List<PluginData> plugins = listPlugins(site, pluginsDistribution);
     for (PluginData plugin : plugins) {
       String pluginName = plugin.name;
       try {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PluginsDistribution.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PluginsDistribution.java
new file mode 100644
index 0000000..6b7386d
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PluginsDistribution.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2014 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.google.gerrit.pgm.init;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Represents the plugins packaged in the Gerrit distribution
+ */
+public interface PluginsDistribution {
+
+  public interface Processor {
+    /**
+     * @param pluginName the name of the plugin (without the .jar extension)
+     * @param in the content of the plugin .jar file. Implementors don't have to
+     *        close this stream.
+     * @throws IOException implementations will typically propagate any
+     *         IOException caused by dealing with the InputStream back to the
+     *         caller
+     */
+    public void process(String pluginName, InputStream in) throws IOException;
+  }
+
+  /**
+   * Iterate over plugins package in the Gerrit distribution
+   *
+   * @param processor invoke for each plugin via its process method
+   * @throws FileNotFoundException if the location of the plugins couldn't be
+   *         determined
+   * @throws IOException in case of any other IO error caused by reading the
+   *         plugin input stream
+   */
+  public void foreach(Processor processor) throws FileNotFoundException, IOException;
+
+  /**
+   * List plugins included in the Gerrit distribution
+   * @return list of plugins names included in the Gerrit distribution
+   * @throws FileNotFoundException if the location of the plugins couldn't be
+   *         determined
+   */
+  public List<String> listPluginNames() throws FileNotFoundException;
+}
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index 55363b11..cde128e 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -9,6 +9,7 @@
     '//gerrit-httpd:httpd',
     '//gerrit-lucene:lucene',
     '//gerrit-openid:openid',
+    '//gerrit-pgm:init-api',
     '//gerrit-pgm:init-base',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
index a572fc8..237881b 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.pgm.BaseInit;
+import com.google.gerrit.pgm.init.PluginsDistribution;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -31,10 +32,13 @@
 
   private final String sitePath;
   private final String initPath;
+  private final PluginsDistribution pluginsDistribution;
 
-  SiteInitializer(String sitePath, String initPath) {
+  SiteInitializer(String sitePath, String initPath,
+      PluginsDistribution pluginsDistribution) {
     this.sitePath = sitePath;
     this.initPath = initPath;
+    this.pluginsDistribution = pluginsDistribution;
   }
 
   public void init() {
@@ -43,7 +47,7 @@
         File site = new File(sitePath);
         LOG.info(String.format("Initializing site at %s",
             site.getAbsolutePath()));
-        new BaseInit(site, false, true).run();
+        new BaseInit(site, false, true, pluginsDistribution).run();
         return;
       }
 
@@ -56,7 +60,8 @@
         if (site != null) {
           LOG.info(String.format("Initializing site at %s",
               site.getAbsolutePath()));
-          new BaseInit(site, new ReviewDbDataSourceProvider(), false, false).run();
+          new BaseInit(site, new ReviewDbDataSourceProvider(), false, false,
+              pluginsDistribution).run();
         }
       } finally {
         conn.close();
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
new file mode 100644
index 0000000..665d420
--- /dev/null
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2014 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.google.gerrit.httpd;
+
+import static com.google.gerrit.pgm.init.InitPlugins.JAR;
+import static com.google.gerrit.pgm.init.InitPlugins.PLUGIN_DIR;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.pgm.init.PluginsDistribution;
+import com.google.inject.Singleton;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import javax.servlet.ServletContext;
+
+@Singleton
+class UnzippedDistribution implements PluginsDistribution {
+
+  private ServletContext servletContext;
+  private File pluginsDir;
+
+  public UnzippedDistribution(ServletContext servletContext) {
+    this.servletContext = servletContext;
+  }
+
+  @Override
+  public void foreach(Processor processor) throws FileNotFoundException, IOException {
+    File[] list = getPluginsDir().listFiles();
+    if (list != null) {
+      for (File p : list) {
+        String pluginJarName = p.getName();
+        String pluginName = pluginJarName.substring(0,
+                pluginJarName.length() - JAR.length());
+        try (InputStream in = new FileInputStream(p)) {
+          processor.process(pluginName, in);
+        }
+      }
+    }
+  }
+
+  @Override
+  public List<String> listPluginNames() throws FileNotFoundException {
+    List<String> names = Lists.newArrayList();
+    String[] list = getPluginsDir().list();
+    if (list != null) {
+      for (String pluginJarName : list) {
+        String pluginName = pluginJarName.substring(0,
+            pluginJarName.length() - JAR.length());
+        names.add(pluginName);
+      }
+    }
+    return names;
+  }
+
+  private File getPluginsDir() {
+    if (pluginsDir == null) {
+      File root = new File(servletContext.getRealPath(""));
+      pluginsDir = new File(root, PLUGIN_DIR);
+    }
+    return pluginsDir;
+  }
+}
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 32fac33..132c729 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -83,6 +83,7 @@
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
 import javax.servlet.ServletContextEvent;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
@@ -105,6 +106,8 @@
   private LifecycleManager manager;
   private GuiceFilter filter;
 
+  private ServletContext servletContext;
+
   @Override
   public void doFilter(ServletRequest req, ServletResponse res,
       FilterChain chain) throws IOException, ServletException {
@@ -119,7 +122,8 @@
       }
 
       if (System.getProperty("gerrit.init") != null) {
-        new SiteInitializer(path, System.getProperty("gerrit.init_path")).init();
+        new SiteInitializer(path, System.getProperty("gerrit.init_path"),
+            new UnzippedDistribution(servletContext)).init();
       }
 
       try {
@@ -339,7 +343,8 @@
 
   @Override
   public void init(FilterConfig cfg) throws ServletException {
-    contextInitialized(new ServletContextEvent(cfg.getServletContext()));
+    servletContext = cfg.getServletContext();
+    contextInitialized(new ServletContextEvent(servletContext));
     init();
     manager.start();
   }