Expose only extension-api to extensions

Unless a plugin declares "Gerrit-ApiType: plugin" in its manifest,
assume it is an extension and only make the gerrit-extension-api
available to it through the ClassLoader.

For non-plugins, do not make any Guice bindings available from the
server. This further restricts what an extension can see and do with
the system internals.

Change-Id: Ia38336c42786afb1419d64c06b0d908ae92a64d1
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 468e767..53683e2 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1,61 +1,296 @@
 Gerrit Code Review - Plugin Development
 =======================================
 
-A plugin in gerrit is tightly coupled code that runs in the same
-JVM as gerrit. It has full access to all gerrit internals. Plugins
-are coupled to a specific major.minor gerrit version.
+A plugin in Gerrit is tightly coupled code that runs in the same
+JVM as Gerrit. It has full access to all server internals. Plugins
+are tightly coupled to a specific major.minor server version and
+may require source code changes to compile against a different
+server version.
+
+An extension in Gerrit runs inside of the same JVM as Gerrit
+in the same way as a plugin, but has limited visibility to the
+server's internals. The limited visiblity reduces the extension's
+dependencies, enabling it to be compatiable across a wider range
+of server versions.
+
+Most of this documentation refers to either type as a plugin.
 
 Requirements
 ------------
 
-To start development, download the sample maven project, which downloads the
-following dependencies:
+To start development, download the sample maven project, which
+downloads the dependencies file that matches the war file to develop
+against. Dependencies are offered in two different formats:
 
-* gerrit-sdk.jar file that matches the war file to develop against
+gerrit-extension-api.jar::
+  A stable but thin interface. Suitable for extensions that need
+  to be notified of events, but do not require tight coupling to
+  the internals of Gerrit. Extensions built against this API can
+  expect to be binary compatible across a wide range of server
+  versions.
 
+gerrit-plugin-api.jar::
+  The complete internals of the Gerrit server, permitting a
+  plugin to tightly couple itself and provide additional
+  functionality that is not possible as an extension. Plugins
+  built against this API are expected to break at the source
+  code level between every major.minor Gerrit release. A plugin
+  that compiles against 2.5 will probably need source code level
+  changes to work with 2.6, 2.7, and so on.
 
 Manifest
 --------
 
-Plugins need to include the following data in the jar manifest file:
+Plugins may provide optional description information with standard
+manifest fields:
 
-  Gerrit-Module = pkg.class
+====
+  Implementation-Title: Example plugin showing examples
+  Implementation-Version: 1.0
+  Implementation-Vendor: Example, Inc.
+  Implementation-URL: http://example.com/opensource/plugin-foo/
+====
 
-Optionally include:
+ApiType
+~~~~~~~
 
-  Gerrit-ReloadMode = 'reload' (default) or 'restart'
+Plugins using the tightly coupled `gerrit-plugin-api.jar` must
+declare this API dependency in the manifest to gain access to server
+internals. If no Gerrit-ApiType is specified the stable `extension`
+API will be assumed. This may cause ClassNotFoundExceptions when
+loading a plugin that needs the plugin API.
 
-If the plugin holds an exclusive resource that must be released before loading
-the plugin again, ReloadMode must be set to 'restart'. Otherwise 'reload' is
-sufficient.
+====
+  Gerrit-ApiType: plugin
+====
+
+Explicit Registration
+~~~~~~~~~~~~~~~~~~~~~
+
+Plugins that use explicit Guice registration must name the Guice
+modules in the manifest. Up to three modules can be named in the
+manifest. Gerrit-Module supplies bindings to the core server;
+Gerrit-SshModule supplies SSH commands to the SSH server (if
+enabled); Gerrit-HttpModule supplies servlets and filters to the HTTP
+server (if enabled). If no modules are named automatic registration
+will be performed by scanning all classes in the plugin JAR for
+`@Listen` and `@Export("")` annotations.
+
+====
+  Gerrit-Module:     tld.example.project.CoreModuleClassName
+  Gerrit-SshModule:  tld.example.project.SshModuleClassName
+  Gerrit-HttpModule: tld.example.project.HttpModuleClassName
+====
+
+Reload Method
+~~~~~~~~~~~~~
+
+If a plugin holds an exclusive resource that must be released before
+loading the plugin again (for example listening on a network port or
+acquiring a file lock) the manifest must declare Gerrit-ReloadMode
+to be `restart`. Otherwise the preferred method of `reload` will
+be used, as it enables the server to hot-patch an updated plugin
+with no down time.
+
+====
+  Gerrit-ReloadMode: restart
+====
+
+In either mode ('restart' or 'reload') any plugin or extension can
+be updated without restarting the Gerrit server. The difference is
+how Gerrit handles the upgrade:
+
+restart::
+  The old plugin is completely stopped. All registrations of SSH
+  commands and HTTP servlets are removed. All registrations of any
+  extension points are removed. All registered LifecycleListeners
+  have their `stop()` method invoked in reverse order. The new
+  plugin is started, and registrations are made from the new
+  plugin. There is a brief window where neither the old nor the
+  new plugin is connected to the server. This means SSH commands
+  and HTTP servlets will return not found errors, and the plugin
+  will not be notified of events that occurred during the restart.
+
+reload::
+  The new plugin is started. Its LifecycleListeners are permitted
+  to perform their `start()` methods. All SSH and HTTP registrations
+  are atomically swapped out from the old plugin to the new plugin,
+  ensuring the server never returns a not found error. All extension
+  point listeners are atomically swapped out from the old plugin to
+  the new plugin, ensuring no events are missed (however some events
+  may still route to the old plugin if the swap wasn't complete yet).
+  The old plugin is stopped.
+
+Classpath
+---------
+
+Each plugin is loaded into its own ClassLoader, isolating plugins
+from each other. A plugin or extension inherits the Java runtime
+and the Gerrit API chosen by `Gerrit-ApiType` (extension or plugin)
+from the hosting server.
+
+Plugins are loaded from a single JAR file. If a plugin needs
+additional libraries, it must include those dependencies within
+its own JAR. Plugins built using Maven may be able to use the
+link:http://maven.apache.org/plugins/maven-shade-plugin/[shade plugin]
+to package additional dependencies. Relocating (or renaming) classes
+should not be necessary due to the ClassLoader isolation.
 
 SSH Commands
 ------------
 
-Plugins may provide commands that can be accessed through the SSH interface.
-These commands register themselves as a part of link:cmd-index.html[SSH Commands].
+Plugins may provide commands that can be accessed through the SSH
+interface (extensions do not have this option).
 
-Each of the plugin commands needs to extend SshCommand.
+Command implementations must extend the base class SshCommand:
 
-Any plugin which implements at least one ssh command needs to also provide a
-class which extends the PluginCommandModule in order to register the ssh
-command(s) in its configure method which must be overriden.
+====
+  import com.google.gerrit.sshd.SshCommand;
 
-Registering is done by calling:
+  class PrintHello extends SshCommand {
+    protected abstract void run() {
+      stdout.print("Hello\n");
+    }
+  }
+====
 
-  command(String commandName).to(ClassName<? extends SshCommand> klass)
+If no Guice modules are declared in the manifest, SSH commands may
+use auto-registration by providing an @Export annotatation:
+
+====
+  import com.google.gerrit.extensions.annotations.Export;
+  import com.google.gerrit.sshd.SshCommand;
+
+  @Export("print")
+  class PrintHello extends SshCommand {
+    protected abstract void run() {
+      stdout.print("Hello\n");
+    }
+  }
+====
+
+If explicit registration is being used, a Guice module must be
+supplied to register the SSH command and declared in the manifest
+with the `Gerrit-SshModule` attribute:
+
+====
+  import com.google.gerrit.sshd.PluginCommandModule;
+
+  class MyCommands extends PluginCommandModule {
+    protected void configureCommands() {
+      command("print").to(PrintHello.class);
+    }
+  }
+====
+
+For a plugin installed as name `helloworld`, the command implemented
+by PrintHello class will be available to users as:
+
+----
+$ ssh -P 29418 review.example.com helloworld print
+----
+
+HTTP Servlets
+-------------
+
+Plugins or extensions may register additional HTTP servlets, and
+wrap them with HTTP filters.
+
+Servlets may use auto-registration to declare the URL they handle:
+
+====
+  import com.google.gerrit.extensions.annotations.Export;
+  import com.google.inject.Singleton;
+  import javax.servlet.http.HttpServlet;
+  import javax.servlet.http.HttpServletRequest;
+  import javax.servlet.http.HttpServletResponse;
+
+  @Export("/print")
+  @Singleton
+  class HelloServlet extends HttpServlet {
+    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+      res.setContentType("text/plain");
+      res.setCharacterEncoding("UTF-8");
+      res.getWriter().write("Hello");
+    }
+  }
+====
+
+If explicit registration is being used, a Guice ServletModule must
+be supplied to register the HTTP servlets, and the module must be
+declared in the manifest with the `Gerrit-HttpModule` attribute:
+
+====
+  import com.google.inject.servlet.ServletModule;
+
+  class MyWebUrls extends ServletModule {
+    protected void configureServlets() {
+      serve("/print").with(HelloServlet.class);
+    }
+  }
+====
+
+For a plugin installed as name `helloworld`, the servlet implemented
+by HelloServlet class will be available to users as:
+
+----
+$ curl http://review.example.com/plugins/helloworld/print
+----
 
 Documentation
 -------------
 
-Place files into Documentation/ or static/ and package them into the plugin jar
-to access them in a browser via <canonicalWebURL>/plugins/<pluginName>/...
+If a plugin does not register a filter or servlet to handle URLs
+`/Documentation/*` or `/static/*`, the core Gerrit server will
+automatically export these resources over HTTP from the plugin JAR.
+
+Static resources under `static/` directory in the JAR will be
+available as `/plugins/helloworld/static/resource`.
+
+Documentation files under `Documentation/` directory in the JAR
+will be available as `/plugins/helloworld/Documentation/resource`.
+
+Documentation may be written in
+link:http://daringfireball.net/projects/markdown/[Markdown] style
+if the file name ends with `.md`. Gerrit will automatically convert
+Markdown to HTML if accessed with extension `.html`.
 
 Deployment
 ----------
 
-Deploy plugins into <review_site>/plugins/. The file name in that directory will
-be the plugin name on the server.
+Compiled plugins and extensions can be deployed to a
+running Gerrit server using the SSH interface by any user with
+link:access-control.html#capability_administrateServer[Administrate Server]
+capability. Binaries can be specified in three different formats:
+
+* Absolute file path on the server's host. The server will copy
+  the plugin from this location to its own site path.
++
+----
+$ ssh -P 29418 localhost gerrit plugin install -n name $(pwd)/my-plugin.jar
+----
+
+* Valid URL, including any HTTP or FTP site reachable by the
+  server. The server will download the plugin and save a copy in
+  its own site path.
++
+----
+$ ssh -P 29418 localhost gerrit plugin install -n name http://build-server/output/our-plugin.jar
+----
+
+* As piped input to the plugin install command. The server will
+  copy input until EOF, and save a copy under its own site path.
++
+----
+$ ssh -P 29418 localhost gerrit plugin install -n name - <target/name-0.1.jar
+----
+
+Plugins can also be copied directly into the server's
+directory at `$site_path/plugins/$name.jar`.  The name of
+the JAR file, minus the `.jar` extension, will be used as the
+plugin name. Unless disabled, servers periodically scan this
+directory for updated plugins. The time can be adjusted by
+link:config-gerrit.html#plugins.checkFrequency[plugins.checkFrequency].
 
 GERRIT
 ------
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 0209f3f..ff672d5 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -56,6 +56,8 @@
         <artifactId>maven-shade-plugin</artifactId>
         <configuration>
           <createSourcesJar>true</createSourcesJar>
+          <shadedArtifactAttached>true</shadedArtifactAttached>
+          <shadedClassifierName>all</shadedClassifierName>
         </configuration>
         <executions>
           <execution>
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 61bb52f..9cca559 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -31,9 +31,10 @@
 import java.net.URLClassLoader;
 import java.security.CodeSource;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.Enumeration;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
@@ -196,7 +197,7 @@
       throw e;
     }
 
-    final ArrayList<URL> jars = new ArrayList<URL>();
+    final SortedMap<String, URL> jars = new TreeMap<String, URL>();
     try {
       final ZipFile zf = new ZipFile(path);
       try {
@@ -208,6 +209,7 @@
           }
 
           if (ze.getName().startsWith("WEB-INF/lib/")) {
+            String name = ze.getName().substring("WEB-INF/lib/".length());
             final File tmp = createTempFile(safeName(ze), ".jar");
             final FileOutputStream out = new FileOutputStream(tmp);
             try {
@@ -224,7 +226,7 @@
             } finally {
               out.close();
             }
-            jars.add(tmp.toURI().toURL());
+            jars.put(name, tmp.toURI().toURL());
           }
         }
       } finally {
@@ -237,13 +239,38 @@
     if (jars.isEmpty()) {
       return GerritLauncher.class.getClassLoader();
     }
-    Collections.sort(jars, new Comparator<URL>() {
-      public int compare(URL o1, URL o2) {
-        return o1.toString().compareTo(o2.toString());
-      }
-    });
 
-    return new URLClassLoader(jars.toArray(new URL[jars.size()]));
+    // The extension API needs to be its own ClassLoader, along
+    // with a few of its dependencies. Try to construct this first.
+    List<URL> extapi = new ArrayList<URL>();
+    move(jars, "gerrit-extension-api-", extapi);
+    move(jars, "guice-", extapi);
+    move(jars, "javax.inject-1.jar", extapi);
+    move(jars, "aopalliance-1.0.jar", extapi);
+    move(jars, "guice-servlet-", extapi);
+    move(jars, "servlet-api-", extapi);
+
+    ClassLoader parent = ClassLoader.getSystemClassLoader();
+    if (!extapi.isEmpty()) {
+      parent = new URLClassLoader(
+          extapi.toArray(new URL[extapi.size()]),
+          parent);
+    }
+    return new URLClassLoader(
+        jars.values().toArray(new URL[jars.size()]),
+        parent);
+  }
+
+  private static void move(SortedMap<String, URL> jars,
+      String prefix,
+      List<URL> extapi) {
+    SortedMap<String, URL> matches = jars.tailMap(prefix);
+    if (!matches.isEmpty()) {
+      String first = matches.firstKey();
+      if (first.startsWith(prefix)) {
+        extapi.add(jars.remove(first));
+      }
+    }
   }
 
   private static String safeName(final ZipEntry ze) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index 88f3fc3..6181b67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
@@ -40,6 +41,10 @@
 import javax.annotation.Nullable;
 
 public class Plugin {
+  public static enum ApiType {
+    EXTENSION, PLUGIN;
+  }
+
   static {
     // Guice logs warnings about multiple injectors being created.
     // Silence this in case HTTP plugins are used.
@@ -47,12 +52,26 @@
         .setLevel(java.util.logging.Level.OFF);
   }
 
+  static ApiType getApiType(Manifest manifest) throws InvalidPluginException {
+    Attributes main = manifest.getMainAttributes();
+    String v = main.getValue("Gerrit-ApiType");
+    if (Strings.isNullOrEmpty(v)
+        || ApiType.EXTENSION.name().equalsIgnoreCase(v)) {
+      return ApiType.EXTENSION;
+    } else if (ApiType.PLUGIN.name().equalsIgnoreCase(v)) {
+      return ApiType.PLUGIN;
+    } else {
+      throw new InvalidPluginException("Invalid Gerrit-ApiType: " + v);
+    }
+  }
+
   private final String name;
   private final File srcJar;
   private final FileSnapshot snapshot;
   private final JarFile jarFile;
   private final Manifest manifest;
   private final File dataDir;
+  private final ApiType apiType;
   private final ClassLoader classLoader;
   private Class<? extends Module> sysModule;
   private Class<? extends Module> sshModule;
@@ -70,6 +89,7 @@
       JarFile jarFile,
       Manifest manifest,
       File dataDir,
+      ApiType apiType,
       ClassLoader classLoader,
       @Nullable Class<? extends Module> sysModule,
       @Nullable Class<? extends Module> sshModule,
@@ -80,6 +100,7 @@
     this.jarFile = jarFile;
     this.manifest = manifest;
     this.dataDir = dataDir;
+    this.apiType = apiType;
     this.classLoader = classLoader;
     this.sysModule = sysModule;
     this.sshModule = sshModule;
@@ -94,11 +115,16 @@
     return name;
   }
 
+  @Nullable
   public String getVersion() {
     Attributes main = manifest.getMainAttributes();
     return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
   }
 
+  public ApiType getApiType() {
+    return apiType;
+  }
+
   boolean canReload() {
     Attributes main = manifest.getMainAttributes();
     String v = main.getValue("Gerrit-ReloadMode");
@@ -139,29 +165,33 @@
     }
 
     if (env.hasSshModule()) {
+      List<Module> modules = Lists.newLinkedList();
+      if (apiType == ApiType.PLUGIN) {
+        modules.add(env.getSshModule());
+      }
       if (sshModule != null) {
-        sshInjector = sysInjector.createChildInjector(
-            env.getSshModule(),
-            sysInjector.getInstance(sshModule));
+        modules.add(sysInjector.getInstance(sshModule));
+        sshInjector = sysInjector.createChildInjector(modules);
         manager.add(sshInjector);
       } else if (auto != null && auto.sshModule != null) {
-        sshInjector = sysInjector.createChildInjector(
-            env.getSshModule(),
-            auto.sshModule);
+        modules.add(auto.sshModule);
+        sshInjector = sysInjector.createChildInjector(modules);
         manager.add(sshInjector);
       }
     }
 
     if (env.hasHttpModule()) {
+      List<Module> modules = Lists.newLinkedList();
+      if (apiType == ApiType.PLUGIN) {
+        modules.add(env.getHttpModule());
+      }
       if (httpModule != null) {
-        httpInjector = sysInjector.createChildInjector(
-            env.getHttpModule(),
-            sysInjector.getInstance(httpModule));
+        modules.add(sysInjector.getInstance(httpModule));
+        httpInjector = sysInjector.createChildInjector(modules);
         manager.add(httpInjector);
       } else if (auto != null && auto.httpModule != null) {
-        httpInjector = sysInjector.createChildInjector(
-            env.getHttpModule(),
-            auto.httpModule);
+        modules.add(auto.httpModule);
+        httpInjector = sysInjector.createChildInjector(modules);
         manager.add(httpInjector);
       }
     }
@@ -169,9 +199,19 @@
     manager.start();
   }
 
-  private Injector newRootInjector(PluginGuiceEnvironment env) {
+  private Injector newRootInjector(final PluginGuiceEnvironment env) {
     List<Module> modules = Lists.newArrayListWithCapacity(4);
     modules.add(env.getSysModule());
+    if (apiType == ApiType.PLUGIN) {
+      modules.add(env.getSysModule());
+    } else {
+      modules.add(new AbstractModule() {
+        @Override
+        protected void configure() {
+          bind(ServerInformation.class).toInstance(env.getServerInformation());
+        }
+      });
+    }
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 4842edc..67d715f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -340,7 +341,7 @@
   }
 
   private Plugin loadPlugin(String name, File srcJar, FileSnapshot snapshot)
-      throws IOException, ClassNotFoundException {
+      throws IOException, ClassNotFoundException, InvalidPluginException {
     File tmp;
     FileInputStream in = new FileInputStream(srcJar);
     try {
@@ -353,13 +354,20 @@
     boolean keep = false;
     try {
       Manifest manifest = jarFile.getManifest();
+      Plugin.ApiType type = Plugin.getApiType(manifest);
       Attributes main = manifest.getMainAttributes();
       String sysName = main.getValue("Gerrit-Module");
       String sshName = main.getValue("Gerrit-SshModule");
       String httpName = main.getValue("Gerrit-HttpModule");
 
+      if (!Strings.isNullOrEmpty(sshName) && type != Plugin.ApiType.PLUGIN) {
+        throw new InvalidPluginException(String.format(
+            "Using Gerrit-SshModule requires Gerrit-ApiType: %s",
+            Plugin.ApiType.PLUGIN));
+      }
+
       URL[] urls = {tmp.toURI().toURL()};
-      ClassLoader parentLoader = PluginLoader.class.getClassLoader();
+      ClassLoader parentLoader = parentFor(type);
       ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
       cleanupHandles.put(
           new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue),
@@ -372,7 +380,7 @@
       return new Plugin(name,
           srcJar, snapshot,
           jarFile, manifest,
-          new File(dataDir, name), pluginLoader,
+          new File(dataDir, name), type, pluginLoader,
           sysModule, sshModule, httpModule);
     } finally {
       if (!keep) {
@@ -381,6 +389,18 @@
     }
   }
 
+  private static ClassLoader parentFor(Plugin.ApiType type)
+      throws InvalidPluginException {
+    switch (type) {
+      case EXTENSION:
+        return PluginName.class.getClassLoader();
+      case PLUGIN:
+        return PluginLoader.class.getClassLoader();
+      default:
+        throw new InvalidPluginException("Unsupported ApiType " + type);
+    }
+  }
+
   private static String tempNameFor(String name) {
     SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
     return "plugin_" + name + "_" + fmt.format(new Date()) + "_";
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
similarity index 97%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
rename to gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
index 28d267c..4dbb8d7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.commands;
+package com.google.gerrit.sshd;
 
 import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.annotations.PluginName;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
index b843893..03485f7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -69,6 +69,6 @@
   @Override
   public Module create() throws InvalidPluginException {
     Preconditions.checkState(command != null, "pluginName must be provided");
-    return this;
+    return !commands.isEmpty() ? this : null;
   }
 }
diff --git a/tools/deploy_api.sh b/tools/deploy_api.sh
index eda841f..caa286b 100755
--- a/tools/deploy_api.sh
+++ b/tools/deploy_api.sh
@@ -15,25 +15,44 @@
 esac
 URL=s3://gerrit-api@commondatastorage.googleapis.com/$type
 
-echo "Deploying API $VER to $URL"
-for module in gerrit-extension-api gerrit-plugin-api
-do
-  mvn deploy:deploy-file \
-    -DgroupId=com.google.gerrit \
-    -DartifactId=$module \
-    -Dversion=$VER \
-    -Dpackaging=jar \
-    -Dfile=$module/target/$module-$VER.jar \
-    -DrepositoryId=gerrit-api-repository \
-    -Durl=$URL
 
-  mvn deploy:deploy-file \
-    -DgroupId=com.google.gerrit \
-    -DartifactId=$module \
-    -Dversion=$VER \
-    -Dpackaging=java-source \
-    -Dfile=$module/target/$module-$VER-sources.jar \
-    -Djava-source=false \
-    -DrepositoryId=gerrit-api-repository \
-    -Durl=$URL
-done
+echo "Deploying $type gerrit-extension-api $VER"
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-extension-api \
+  -Dversion=$VER \
+  -Dpackaging=jar \
+  -Dfile=$module/target/gerrit-extension-api-$VER-all.jar \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
+
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-extension-api \
+  -Dversion=$VER \
+  -Dpackaging=java-source \
+  -Dfile=$module/target/gerrit-extension-api-$VER-all-sources.jar \
+  -Djava-source=false \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
+
+
+echo "Deploying $type gerrit-plugin-api $VER"
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-plugin-api \
+  -Dversion=$VER \
+  -Dpackaging=jar \
+  -Dfile=$module/target/gerrit-plugin-api-$VER.jar \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
+
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-plugin-api \
+  -Dversion=$VER \
+  -Dpackaging=java-source \
+  -Dfile=$module/target/gerrit-plugin-api-$VER-sources.jar \
+  -Djava-source=false \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL