Merge changes from topic 'signed-push-cleanup'

* changes:
  Move REFS_GPG_KEYS to PublicKeyStore
  config-gerrit.txt: Organize "receive" section
  PushCertificateChecker: Don't throw exceptions
  PushCertificateChecker: Tweak some error messages
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 3882443..307aefa 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -51,6 +51,17 @@
   visibility = ['PUBLIC'],
 )
 
+java_test(
+  name = 'api_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':api',
+    '//lib:truth',
+    '//lib/guice:guice',
+  ],
+  source_under_test = [':api'],
+)
+
 java_doc(
   name = 'extension-api-javadoc',
   title = 'Gerrit Review Extension API Documentation',
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index c99f233..28052ef 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -182,6 +182,23 @@
   }
 
   /**
+   * Returns {@code true} if this set contains the given item.
+   *
+   * @param item item to check whether or not it is contained.
+   * @return {@code true} if this set contains the given item.
+   */
+  public boolean contains(final T item) {
+    Iterator<T> iterator = iterator();
+    while (iterator.hasNext()) {
+      T candidate = iterator.next();
+      if (candidate == item) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
    * Add one new element to the set.
    *
    * @param item the item to add to the collection. Must not be null.
diff --git a/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java
new file mode 100644
index 0000000..dc71b12
--- /dev/null
+++ b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2015 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.extensions.registration;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Key;
+import com.google.inject.util.Providers;
+
+import org.junit.Test;
+
+public class DynamicSetTest {
+  // In tests for {@link DynamicSet#contains(Object)}, be sure to avoid
+  // {@code assertThat(ds).contains(...) @} and
+  // {@code assertThat(ds).DoesNotContains(...) @} as (since
+  // {@link DynamicSet@} is not a {@link Collection@}) those boil down to
+  // iterating over the {@link DynamicSet@} and checking equality instead
+  // of calling {@link DynamicSet#contains(Object)}.
+  // To test for {@link DynamicSet#contains(Object)}, use
+  // {@code assertThat(ds.contains(...)).isTrue() @} and
+  // {@code assertThat(ds.contains(...)).isFalse() @} instead.
+
+  @Test
+  public void testContainsWithEmpty() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    assertThat(ds.contains(2)).isFalse(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsTrueWithSingleElement() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+
+    assertThat(ds.contains(2)).isTrue(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsFalseWithSingleElement() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+
+    assertThat(ds.contains(3)).isFalse(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsTrueWithTwoElements() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+    ds.add(4);
+
+    assertThat(ds.contains(4)).isTrue(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsFalseWithTwoElements() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+    ds.add(4);
+
+    assertThat(ds.contains(3)).isFalse(); //See above comment about ds.contains
+  }
+
+  @Test
+  public void testContainsDynamic() throws Exception {
+    DynamicSet<Integer> ds = new DynamicSet<>();
+    ds.add(2);
+
+    Key<Integer> key = Key.get(Integer.class);
+    ReloadableRegistrationHandle<Integer> handle = ds.add(key, Providers.of(4));
+
+    ds.add(6);
+
+    // At first, 4 is contained.
+    assertThat(ds.contains(4)).isTrue(); //See above comment about ds.contains
+
+    // Then we remove 4.
+    handle.remove();
+
+    // And now 4 should no longer be contained.
+    assertThat(ds.contains(4)).isFalse(); //See above comment about ds.contains
+  }
+}
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
index 3345018..e215533 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -64,6 +64,7 @@
     '//lib:truth',
     '//lib/easymock:easymock',
     '//lib/guice:guice',
+    '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
     '//lib/jgit:junit',
   ],
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
index 3b48b65..bcc5842 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -15,8 +15,11 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.StopPluginListener;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.ServletModule;
 
 import java.io.IOException;
@@ -37,17 +40,62 @@
       protected void configureServlets() {
         DynamicSet.setOf(binder(), AllRequestFilter.class);
         filter("/*").through(FilterProxy.class);
+
+        bind(StopPluginListener.class)
+          .annotatedWith(UniqueAnnotations.create())
+          .to(FilterProxy.class);
       }
     };
   }
 
   @Singleton
-  static class FilterProxy implements Filter {
+  static class FilterProxy implements Filter, StopPluginListener {
     private final DynamicSet<AllRequestFilter> filters;
 
+    private DynamicSet<AllRequestFilter> initializedFilters;
+    private FilterConfig filterConfig;
+
     @Inject
     FilterProxy(DynamicSet<AllRequestFilter> filters) {
       this.filters = filters;
+      this.initializedFilters = new DynamicSet<>();
+      this.filterConfig = null;
+    }
+
+    /**
+     * Initializes a filter if needed
+     *
+     * @param filter The filter that should get initialized
+     * @return {@code true} iff filter is now initialized
+     * @throws ServletException if filter itself fails to init
+     */
+    private synchronized boolean initFilterIfNeeded(AllRequestFilter filter)
+        throws ServletException {
+      boolean ret = true;
+      if (filters.contains(filter)) {
+        // Regardless of whether or not the caller checked filter's
+        // containment in initializedFilters, we better re-check as we're now
+        // synchronized.
+        if (!initializedFilters.contains(filter)) {
+          filter.init(filterConfig);
+          initializedFilters.add(filter);
+        }
+      } else {
+        ret = false;
+      }
+      return ret;
+    }
+
+    private synchronized void cleanUpInitializedFilters() {
+      Iterable<AllRequestFilter> filtersToCleanUp  = initializedFilters;
+      initializedFilters = new DynamicSet<>();
+      for (AllRequestFilter filter : filtersToCleanUp) {
+        if (filters.contains(filter)) {
+          initializedFilters.add(filter);
+        } else {
+          filter.destroy();
+        }
+      }
     }
 
     @Override
@@ -58,28 +106,67 @@
         @Override
         public void doFilter(ServletRequest req, ServletResponse res)
             throws IOException, ServletException {
-          if (itr.hasNext()) {
-            itr.next().doFilter(req, res, this);
-          } else {
-            last.doFilter(req, res);
+          while (itr.hasNext()) {
+            AllRequestFilter filter = itr.next();
+            // To avoid {@code synchronized} on the the whole filtering (and
+            // thereby killing concurrency), we start the below disjunction
+            // with an unsynchronized check for containment. This
+            // unsynchronized check is always correct if no filters got
+            // initialized/cleaned concurrently behind our back.
+            // The case of concurrently initialized filters is saved by the
+            // call to initFilterIfNeeded. So that's fine too.
+            // The case of concurrently cleaned filters between the {@code if}
+            // condition and the call to {@code doFilter} is not saved by
+            // anything. If a filter is getting removed concurrently while
+            // another thread is in those two lines, doFilter might (but need
+            // not) fail.
+            //
+            // Since this failure only occurs if a filter is deleted
+            // (e.g.: a plugin reloaded) exactly when a thread is in those
+            // two lines, and it only breaks a single request, we're ok with
+            // it, given that this is really both really improbable and also
+            // the "proper" fix for it would basically kill concurrency of
+            // webrequests.
+            if (initializedFilters.contains(filter)
+                || initFilterIfNeeded(filter)) {
+              filter.doFilter(req, res, this);
+              return;
+            }
           }
+          last.doFilter(req, res);
         }
       }.doFilter(req, res);
     }
 
     @Override
     public void init(FilterConfig config) throws ServletException {
+      // Plugins that provide AllRequestFilters might get loaded later at
+      // runtime, long after this init method had been called. To allow to
+      // correctly init such plugins' AllRequestFilters, we keep the
+      // FilterConfig around, and reuse it to lazy init the AllRequestFilters.
+      filterConfig = config;
+
       for (AllRequestFilter f: filters) {
-        f.init(config);
+        initFilterIfNeeded(f);
       }
     }
 
     @Override
-    public void destroy() {
-      for (AllRequestFilter f: filters) {
-        f.destroy();
+    public synchronized void destroy() {
+      Iterable<AllRequestFilter> filtersToDestroy  = initializedFilters;
+      initializedFilters = new DynamicSet<>();
+      for (AllRequestFilter filter: filtersToDestroy) {
+        filter.destroy();
       }
     }
+
+    @Override
+    public void onStopPlugin(Plugin plugin) {
+      // In order to allow properly garbage collection, we need to scrub
+      // initializedFilters clean of filters stemming from plugins as they
+      // get unloaded.
+      cleanUpInitializedFilters();
+    }
   }
 
   @Override
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
new file mode 100644
index 0000000..10a35a8
--- /dev/null
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
@@ -0,0 +1,366 @@
+// Copyright (C) 2015 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.anyObject;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.newCapture;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.inject.Key;
+import com.google.inject.util.Providers;
+
+import org.easymock.Capture;
+import org.easymock.EasyMockSupport;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class AllRequestFilterFilterProxyTest {
+  /**
+   * Set of filters for FilterProxy
+   * <p/>
+   * This set is used to as set of filters when fetching an
+   * {@link AllRequestFilter.FilterProxy} instance through
+   * {@link #getFilterProxy()}.
+   */
+  private DynamicSet<AllRequestFilter> filters;
+
+  @Before
+  public void setUp() throws Exception {
+    // Force starting each test with an initially empty set of filters.
+    // Filters get added by the tests themselves.
+    filters = new DynamicSet<>();
+  }
+
+  // The wrapping of {@link #getFilterProxy()} and
+  // {@link #addFilter(AllRequestFilter)} into separate methods may seem
+  // overengineered at this point. However, if in the future we decide to not
+  // test the inner class directly, but rather test from the outside using
+  // Guice Injectors, it is now sufficient to change only those two methods,
+  // and we need not mess with the individual tests.
+
+  /**
+   * Obtain a FilterProxy with a known DynamicSet of filters
+   * <p/>
+   * The returned {@link AllRequestFilter.FilterProxy} can have new filters
+   * added dynamically by calling {@link #addFilter(AllRequestFilter)}.
+   */
+  private AllRequestFilter.FilterProxy getFilterProxy() {
+    return new AllRequestFilter.FilterProxy(filters);
+  }
+
+  /**
+   * Add a filter to created FilterProxy instances
+   * <p/>
+   * This method adds the given filter to all
+   * {@link AllRequestFilter.FilterProxy} instances created by
+   * {@link #getFilterProxy()}.
+   */
+  private ReloadableRegistrationHandle<AllRequestFilter> addFilter(
+      final AllRequestFilter filter) {
+    Key<AllRequestFilter> key = Key.get(AllRequestFilter.class);
+    return filters.add(key, Providers.of(filter));
+  }
+
+  @Test
+  public void testNoFilters() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req = ems.createMock(HttpServletRequest.class);
+    HttpServletResponse res = ems.createMock(HttpServletResponse.class);
+
+    FilterChain chain = ems.createMock(FilterChain.class);
+    chain.doFilter(req, res);
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testSingleFilterNoBubbling() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock("config", FilterConfig.class);
+    HttpServletRequest req = ems.createMock("req", HttpServletRequest.class);
+    HttpServletResponse res = ems.createMock("res", HttpServletResponse.class);
+
+    FilterChain chain = ems.createMock("chain", FilterChain.class);
+
+    AllRequestFilter filter = ems.createStrictMock("filter", AllRequestFilter.class);
+    filter.init(config);
+    filter.doFilter(eq(req), eq(res), anyObject(FilterChain.class));
+    filter.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filter);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testSingleFilterBubbling() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req = ems.createMock(HttpServletRequest.class);
+    HttpServletResponse res = ems.createMock(HttpServletResponse.class);
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock(FilterChain.class);
+
+    Capture<FilterChain> capturedChain = newCapture();
+
+    AllRequestFilter filter = mockControl.createMock(AllRequestFilter.class);
+    filter.init(config);
+    filter.doFilter(eq(req), eq(res), capture(capturedChain));
+    chain.doFilter(req, res);
+    filter.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filter);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    capturedChain.getValue().doFilter(req, res);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testTwoFiltersNoBubbling() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req = ems.createMock(HttpServletRequest.class);
+    HttpServletResponse res = ems.createMock(HttpServletResponse.class);
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock(FilterChain.class);
+
+    AllRequestFilter filterA = mockControl.createMock(AllRequestFilter.class);
+
+    AllRequestFilter filterB = mockControl.createMock(AllRequestFilter.class);
+    filterA.init(config);
+    filterB.init(config);
+    filterA.doFilter(eq(req), eq(res), anyObject(FilterChain.class));
+    filterA.destroy();
+    filterB.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filterA);
+    addFilter(filterB);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testTwoFiltersBubbling() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req = ems.createMock(HttpServletRequest.class);
+    HttpServletResponse res = ems.createMock(HttpServletResponse.class);
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock(FilterChain.class);
+
+    Capture<FilterChain> capturedChainA = newCapture();
+    Capture<FilterChain> capturedChainB = newCapture();
+
+    AllRequestFilter filterA = mockControl.createMock(AllRequestFilter.class);
+    AllRequestFilter filterB = mockControl.createMock(AllRequestFilter.class);
+
+    filterA.init(config);
+    filterB.init(config);
+    filterA.doFilter(eq(req), eq(res), capture(capturedChainA));
+    filterB.doFilter(eq(req), eq(res), capture(capturedChainB));
+    chain.doFilter(req, res);
+    filterA.destroy();
+    filterB.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filterA);
+    addFilter(filterB);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req, res, chain);
+    capturedChainA.getValue().doFilter(req, res);
+    capturedChainB.getValue().doFilter(req, res);
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testPostponedLoading() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req1 = ems.createMock("req1", HttpServletRequest.class);
+    HttpServletRequest req2 = ems.createMock("req2", HttpServletRequest.class);
+    HttpServletResponse res1 = ems.createMock("res1", HttpServletResponse.class);
+    HttpServletResponse res2 = ems.createMock("res2", HttpServletResponse.class);
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock("chain", FilterChain.class);
+
+    Capture<FilterChain> capturedChainA1 = newCapture();
+    Capture<FilterChain> capturedChainA2 = newCapture();
+    Capture<FilterChain> capturedChainB = newCapture();
+
+    AllRequestFilter filterA = mockControl.createMock("filterA", AllRequestFilter.class);
+    AllRequestFilter filterB = mockControl.createMock("filterB", AllRequestFilter.class);
+
+    filterA.init(config);
+    filterA.doFilter(eq(req1), eq(res1), capture(capturedChainA1));
+    chain.doFilter(req1, res1);
+
+    filterA.doFilter(eq(req2), eq(res2), capture(capturedChainA2));
+    filterB.init(config); // <-- This is crucial part. filterB got loaded
+    // after filterProxy's init finished. Nonetheless filterB gets initialized.
+    filterB.doFilter(eq(req2), eq(res2), capture(capturedChainB));
+    chain.doFilter(req2, res2);
+
+    filterA.destroy();
+    filterB.destroy();
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    addFilter(filterA);
+
+    filterProxy.init(config);
+    filterProxy.doFilter(req1, res1, chain);
+    capturedChainA1.getValue().doFilter(req1, res1);
+
+    addFilter(filterB); // <-- Adds filter after filterProxy's init got called.
+    filterProxy.doFilter(req2, res2, chain);
+    capturedChainA2.getValue().doFilter(req2, res2);
+    capturedChainB.getValue().doFilter(req2, res2);
+
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+
+  @Test
+  public void testDynamicUnloading() throws Exception {
+    EasyMockSupport ems = new EasyMockSupport();
+
+    FilterConfig config = ems.createMock(FilterConfig.class);
+    HttpServletRequest req1 = ems.createMock("req1", HttpServletRequest.class);
+    HttpServletRequest req2 = ems.createMock("req2", HttpServletRequest.class);
+    HttpServletRequest req3 = ems.createMock("req3", HttpServletRequest.class);
+    HttpServletResponse res1 = ems.createMock("res1", HttpServletResponse.class);
+    HttpServletResponse res2 = ems.createMock("res2", HttpServletResponse.class);
+    HttpServletResponse res3 = ems.createMock("res3", HttpServletResponse.class);
+
+    Plugin plugin = ems.createMock(Plugin.class);
+
+    IMocksControl mockControl = ems.createStrictControl();
+    FilterChain chain = mockControl.createMock("chain", FilterChain.class);
+
+    Capture<FilterChain> capturedChainA1 = newCapture();
+    Capture<FilterChain> capturedChainB1 = newCapture();
+    Capture<FilterChain> capturedChainB2 = newCapture();
+
+    AllRequestFilter filterA = mockControl.createMock("filterA", AllRequestFilter.class);
+    AllRequestFilter filterB = mockControl.createMock("filterB", AllRequestFilter.class);
+
+    filterA.init(config);
+    filterB.init(config);
+
+    filterA.doFilter(eq(req1), eq(res1), capture(capturedChainA1));
+    filterB.doFilter(eq(req1), eq(res1), capture(capturedChainB1));
+    chain.doFilter(req1, res1);
+
+    filterA.destroy(); // Cleaning up of filterA after it got unloaded
+
+    filterB.doFilter(eq(req2), eq(res2), capture(capturedChainB2));
+    chain.doFilter(req2, res2);
+
+    filterB.destroy(); // Cleaning up of filterA after it got unloaded
+
+    chain.doFilter(req3, res3);
+
+    ems.replayAll();
+
+    AllRequestFilter.FilterProxy filterProxy = getFilterProxy();
+    ReloadableRegistrationHandle<AllRequestFilter> handleFilterA =
+        addFilter(filterA);
+    ReloadableRegistrationHandle<AllRequestFilter> handleFilterB =
+        addFilter(filterB);
+
+    filterProxy.init(config);
+
+    // Request #1 with filterA and filterB
+    filterProxy.doFilter(req1, res1, chain);
+    capturedChainA1.getValue().doFilter(req1, res1);
+    capturedChainB1.getValue().doFilter(req1, res1);
+
+    // Unloading filterA
+    handleFilterA.remove();
+    filterProxy.onStopPlugin(plugin);
+
+    // Request #1 only with filterB
+    filterProxy.doFilter(req2, res2, chain);
+    capturedChainA1.getValue().doFilter(req2, res2);
+
+    // Unloading filterB
+    handleFilterB.remove();
+    filterProxy.onStopPlugin(plugin);
+
+    // Request #1 with no additional filters
+    filterProxy.doFilter(req3, res3, chain);
+
+    filterProxy.destroy();
+
+    ems.verifyAll();
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index 06d907a..aea438c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GroupList;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.inject.Inject;
 
@@ -102,15 +101,8 @@
   }
 
   private GroupList readGroupList() throws IOException {
-    ValidationError.Sink errors = new ValidationError.Sink() {
-      @Override
-      public void error(ValidationError error) {
-        log.error("Error parsing file " + GroupList.FILE_NAME + ": " + error.getMessage());
-      }
-    };
-    String text = readUTF8(GroupList.FILE_NAME);
-
-    return GroupList.parse(text, errors);
+    return GroupList.parse(readUTF8(GroupList.FILE_NAME),
+        GroupList.createLoggerSink(GroupList.FILE_NAME, log));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java
index b12e7ce..2ea6c53 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.git.QueryList;
-import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.VersionedMetaData;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -53,14 +52,8 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    ValidationError.Sink errors = new ValidationError.Sink() {
-      @Override
-      public void error(ValidationError error) {
-        log.error("Error parsing file " + QueryList.FILE_NAME + ": " +
-            error.getMessage());
-      }
-    };
-    queryList = QueryList.parse(readUTF8(QueryList.FILE_NAME), errors);
+    queryList = QueryList.parse(readUTF8(QueryList.FILE_NAME),
+        QueryList.createLoggerSink(QueryList.FILE_NAME, log));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
index 74d8f2d..13d2b1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import org.slf4j.Logger;
+
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
@@ -126,4 +128,9 @@
     }
     return r.toString();
   }
+
+  public static ValidationError.Sink createLoggerSink(String file, Logger log) {
+    return ValidationError.createLoggerSink("Error parsing file " + file + ": ",
+        log);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
index ad84046..e6a8ae4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import org.slf4j.Logger;
+
 /** Indicates a problem with Git based data. */
 public class ValidationError {
   private final String message;
@@ -42,4 +44,13 @@
   public interface Sink {
     void error(ValidationError error);
   }
+
+  public static Sink createLoggerSink(final String message, final Logger log) {
+    return new ValidationError.Sink() {
+          @Override
+          public void error(ValidationError error) {
+            log.error(message + error.getMessage());
+          }
+        };
+  }
 }