Merge changes from topic 'rc-cleanup'

* changes:
  Use refsByChange Multimap to build changeRefsById in ReceiveCommits
  Lazily load the changes byKey map in autoCloseChanges
  Simplify autoCloseCommits() handling of exact SHA-1 matches
  Cache reused values in autoCloseChanges
  Simplify parseBody call when looking at changes to close
  Refactor auto close change filtering in ReceiveCommits
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 655f07e..fcc8680 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -17,11 +17,16 @@
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.block;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.base.Joiner;
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -32,6 +37,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -48,6 +54,8 @@
 import org.apache.http.HttpStatus;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Rule;
 import org.junit.rules.TestRule;
@@ -93,6 +101,9 @@
   @Inject
   protected ProjectCache projectCache;
 
+  @Inject
+  protected GroupCache groupCache;
+
   protected Git git;
   protected GerritServer server;
   protected TestAccount admin;
@@ -266,4 +277,29 @@
     }
     projectCache.evict(cfg.getProject());
   }
+
+  protected void grant(String permission, Project.NameKey project, String ref)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(project);
+    md.setMessage(String.format("Grant %s on %s", permission, ref));
+    ProjectConfig config = ProjectConfig.read(md);
+    AccessSection s = config.getAccessSection(ref, true);
+    Permission p = s.getPermission(permission, true);
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    p.add(new PermissionRule(config.resolve(adminGroup)));
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+
+  protected void blockRead(Project.NameKey project, String ref) throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    block(cfg, Permission.READ, REGISTERED_USERS, ref);
+    saveProjectConfig(project, cfg);
+  }
+
+  protected PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+      IOException {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+    return push.to(git, ref);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index a96e721..5dbf366 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -278,10 +278,4 @@
     PushOneCommit.Result r = pushTo("refs/for/master%hashtag=tag1");
     r.assertErrorStatus("cannot add hashtags; noteDb is disabled");
   }
-
-  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
index 2e0dfbd..6f13c17 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
@@ -58,12 +58,6 @@
     r.assertErrorStatus("cannot upload drafts");
   }
 
-  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
-  }
-
   private void saveProjectConfig(ProjectConfig cfg) throws IOException {
     MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
     try {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index b2e1b3f..cdd3740 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -22,28 +22,21 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
@@ -63,9 +56,6 @@
   private ApprovalsUtil approvalsUtil;
 
   @Inject
-  private GroupCache groupCache;
-
-  @Inject
   private ChangeNotes.Factory changeNotesFactory;
 
   @Inject
@@ -217,19 +207,6 @@
     assertEquals(Change.Status.MERGED, c.getStatus());
   }
 
-  private void grant(String permission, Project.NameKey project, String ref)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    md.setMessage(String.format("Grant %s on %s", permission, ref));
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection(ref, true);
-    Permission p = s.getPermission(permission, true);
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
-    p.add(new PermissionRule(config.resolve(adminGroup)));
-    config.commit(md);
-    projectCache.evict(config.getProject());
-  }
-
   private PatchSetApproval getSubmitter(PatchSet.Id patchSetId)
       throws OrmException {
     Change c = db.changes().get(patchSetId.getParentKey());
@@ -291,12 +268,6 @@
     }
   }
 
-  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
-  }
-
   private PushOneCommit.Result push(String ref, String subject,
       String fileName, String content) throws GitAPIException, IOException {
     PushOneCommit push =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index b51a4e2..2e28655 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -16,17 +16,12 @@
 
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches;
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.block;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.gson.reflect.TypeToken;
 
@@ -123,12 +118,6 @@
     return adminSession.get(endpoint);
   }
 
-  private void blockRead(Project.NameKey project, String ref) throws Exception {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    block(cfg, Permission.READ, REGISTERED_USERS, ref);
-    saveProjectConfig(project, cfg);
-  }
-
   private static List<BranchInfo> toBranchInfoList(RestResponse r)
       throws IOException {
     List<BranchInfo> result =
@@ -136,10 +125,4 @@
             new TypeToken<List<BranchInfo>>() {}.getType());
     return result;
   }
-
-  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
new file mode 100644
index 0000000..c47ca30
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
@@ -0,0 +1,48 @@
+// 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 javax.servlet.http.HttpServletRequest;
+
+/** Utilities for manipulating HTTP request objects. */
+public class RequestUtil {
+  /**
+   * @return the same value as {@link HttpServletRequest#getPathInfo()}, but
+   *     without decoding URL-encoded characters.
+   */
+  public static String getEncodedPathInfo(HttpServletRequest req) {
+    // Based on com.google.guice.ServletDefinition$1#getPathInfo() from:
+    // https://github.com/google/guice/blob/41c126f99d6309886a0ded2ac729033d755e1593/extensions/servlet/src/com/google/inject/servlet/ServletDefinition.java
+    String servletPath = req.getServletPath();
+    int servletPathLength = servletPath.length();
+    String requestUri = req.getRequestURI();
+    String pathInfo = requestUri.substring(req.getContextPath().length())
+        .replaceAll("[/]{2,}", "/");
+    if (pathInfo.startsWith(servletPath)) {
+      pathInfo = pathInfo.substring(servletPathLength);
+      // Corner case: when servlet path & request path match exactly (without
+      // trailing '/'), then pathinfo is null.
+      if (pathInfo.isEmpty() && servletPathLength > 0) {
+        pathInfo = null;
+      }
+    } else {
+      pathInfo = null;
+    }
+    return pathInfo;
+  }
+
+  private RequestUtil() {
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 90ce894..286f61b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -26,6 +26,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.httpd.RequestUtil;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.httpd.resources.ResourceKey;
 import com.google.gerrit.httpd.resources.SmallResource;
@@ -208,7 +209,7 @@
       throws IOException, ServletException {
     List<String> parts = Lists.newArrayList(
       Splitter.on('/').limit(3).omitEmptyStrings()
-        .split(Strings.nullToEmpty(req.getPathInfo())));
+        .split(Strings.nullToEmpty(RequestUtil.getEncodedPathInfo(req))));
 
     if (isApiCall(req, parts)) {
       managerApi.service(req, res);
@@ -255,7 +256,7 @@
       return;
     }
 
-    String pathInfo = req.getPathInfo();
+    String pathInfo = RequestUtil.getEncodedPathInfo(req);
     if (pathInfo.length() < 1) {
       Resource.NOT_FOUND.send(req, res);
       return;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 0d62392..979990a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -74,6 +74,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.httpd.RequestUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
@@ -871,7 +872,7 @@
   }
 
   private static List<IdString> splitPath(HttpServletRequest req) {
-    String path = req.getPathInfo();
+    String path = RequestUtil.getEncodedPathInfo(req);
     if (Strings.isNullOrEmpty(path)) {
       return Collections.emptyList();
     }
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/RequestUtilTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/RequestUtilTest.java
new file mode 100644
index 0000000..71a43bf
--- /dev/null
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/RequestUtilTest.java
@@ -0,0 +1,91 @@
+// 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 org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class RequestUtilTest {
+  private List<Object> mocks;
+
+  @Before
+  public void setUp() {
+    mocks = Collections.synchronizedList(new ArrayList<>());
+  }
+
+  @After
+  public void tearDown() {
+    for (Object mock : mocks) {
+      verify(mock);
+    }
+  }
+
+  @Test
+  public void emptyContextPath() {
+    assertEquals("/foo/bar", RequestUtil.getEncodedPathInfo(
+        mockRequest("/s/foo/bar", "", "/s")));
+    assertEquals("/foo%2Fbar", RequestUtil.getEncodedPathInfo(
+        mockRequest("/s/foo%2Fbar", "", "/s")));
+  }
+
+  @Test
+  public void emptyServletPath() {
+    assertEquals("/foo/bar", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/foo/bar", "/c", "")));
+    assertEquals("/foo%2Fbar", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/foo%2Fbar", "/c", "")));
+  }
+
+  @Test
+  public void trailingSlashes() {
+    assertEquals("/foo/bar/", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo/bar/", "/c", "/s")));
+    assertEquals("/foo/bar/", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo/bar///", "/c", "/s")));
+    assertEquals("/foo%2Fbar/", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo%2Fbar/", "/c", "/s")));
+    assertEquals("/foo%2Fbar/", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo%2Fbar///", "/c", "/s")));
+  }
+
+  @Test
+  public void servletPathMatchesRequestPath() {
+    assertEquals(null, RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s", "/c", "/s")));
+  }
+
+  private HttpServletRequest mockRequest(String uri, String contextPath, String servletPath) {
+    HttpServletRequest req = createMock(HttpServletRequest.class);
+    expect(req.getRequestURI()).andStubReturn(uri);
+    expect(req.getContextPath()).andStubReturn(contextPath);
+    expect(req.getServletPath()).andStubReturn(servletPath);
+    replay(req);
+    mocks.add(req);
+    return req;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
index 759700e..0eaddb3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -65,32 +65,28 @@
     this.env = env;
     this.scanner = scanner;
     this.classLoader = classLoader;
-    this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
-    this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : null;
+    this.sshGen = env.hasSshModule()
+        ? env.newSshModuleGenerator()
+        : new ModuleGenerator.NOP();
+    this.httpGen = env.hasHttpModule()
+        ? env.newHttpModuleGenerator()
+        : new HttpModuleGenerator.NOP();
   }
 
   AutoRegisterModules discover() throws InvalidPluginException {
     sysSingletons = Sets.newHashSet();
     sysListen = LinkedListMultimap.create();
 
-    if (sshGen != null) {
-      sshGen.setPluginName(pluginName);
-    }
-    if (httpGen != null) {
-      httpGen.setPluginName(pluginName);
-    }
+    sshGen.setPluginName(pluginName);
+    httpGen.setPluginName(pluginName);
 
     scan();
 
     if (!sysSingletons.isEmpty() || !sysListen.isEmpty()) {
       sysModule = makeSystemModule();
     }
-    if (sshGen != null) {
-      sshModule = sshGen.create();
-    }
-    if (httpGen != null) {
-      httpModule = httpGen.create();
-    }
+    sshModule = sshGen.create();
+    httpModule = httpGen.create();
     return this;
   }
 
@@ -158,14 +154,10 @@
     }
 
     if (is("org.apache.sshd.server.Command", clazz)) {
-      if (sshGen != null) {
-        sshGen.export(export, clazz);
-      }
+      sshGen.export(export, clazz);
     } else if (is("javax.servlet.http.HttpServlet", clazz)) {
-      if (httpGen != null) {
-        httpGen.export(export, clazz);
-        listen(clazz, clazz);
-      }
+      httpGen.export(export, clazz);
+      listen(clazz, clazz);
     } else {
       int cnt = sysListen.size();
       listen(clazz, clazz);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
index 44b2434..dd0ce67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
@@ -14,6 +14,15 @@
 
 package com.google.gerrit.server.plugins;
 
+
 public interface HttpModuleGenerator extends ModuleGenerator {
   void export(String javascript);
+
+  static class NOP extends ModuleGenerator.NOP
+      implements HttpModuleGenerator {
+    @Override
+    public void export(String javascript) {
+      // do nothing
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
index 2011e9d..ae8bb0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -26,4 +26,27 @@
   void listen(TypeLiteral<?> tl, Class<?> clazz);
 
   Module create() throws InvalidPluginException;
+
+  static class NOP implements ModuleGenerator {
+
+    @Override
+    public void setPluginName(String name) {
+      // do nothing
+    }
+
+    @Override
+    public void listen(TypeLiteral<?> tl, Class<?> clazz) {
+      // do nothing
+    }
+
+    @Override
+    public void export(Export export, Class<?> type) {
+      // do nothing
+    }
+
+    @Override
+    public Module create() throws InvalidPluginException {
+      return null;
+    }
+  }
 }