Add ability to drop events by classname

Introduce the plugin.events.filter key which can be used to drop events
by fully qualified classname before they are stored or sent to clients
using the following syntax:

 DROP classname fully.qualified.java.classname

Also add tests to confirm that the filtering works.

Change-Id: Ic10f8c9b98f77d48c51d4ba082b96dff67782ec0
diff --git a/src/main/java/com/googlesource/gerrit/plugins/events/CoreListener.java b/src/main/java/com/googlesource/gerrit/plugins/events/CoreListener.java
index e23c9e2..3336309 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/events/CoreListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/events/CoreListener.java
@@ -17,7 +17,10 @@
 import com.google.common.base.Supplier;
 import com.google.gerrit.entities.EntitiesAdapterFactory;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.GerritServerConfigProvider;
+import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.EventListener;
 import com.google.gerrit.server.events.ProjectNameKeyAdapter;
@@ -27,6 +30,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -34,23 +39,41 @@
 public class CoreListener implements EventListener {
   private static Logger log = LoggerFactory.getLogger(CoreListener.class);
 
+  protected static final String KEY_FILTER = "filter";
+  protected static final String FILTER_TYPE_DROP = "DROP";
+  protected static final String FILTER_ELEMENT_CLASSNAME = "classname";
+
   protected static final Gson gson =
       new GsonBuilder()
           .registerTypeAdapter(Supplier.class, new SupplierSerializer())
           .registerTypeAdapter(Project.NameKey.class, new ProjectNameKeyAdapter())
           .registerTypeAdapterFactory(EntitiesAdapterFactory.create())
           .create();
+
+  protected final String pluginName;
+  protected final GerritServerConfigProvider gerritServerConfigProvider;
   protected final DynamicSet<StreamEventListener> listeners;
   protected final EventStore store;
+  protected Set<String> dropEventNames = new HashSet<>();
 
   @Inject
-  protected CoreListener(EventStore store, DynamicSet<StreamEventListener> listeners) {
+  protected CoreListener(
+      @PluginName String pluginName,
+      GerritServerConfigProvider gerritServerConfigProvider,
+      EventStore store,
+      DynamicSet<StreamEventListener> listeners) {
+    this.pluginName = pluginName;
+    this.gerritServerConfigProvider = gerritServerConfigProvider;
     this.store = store;
     this.listeners = listeners;
+    readAndParseCfg();
   }
 
   @Override
   public void onEvent(Event event) {
+    if (dropEventNames.contains(event.getClass().getName())) {
+      return;
+    }
     try {
       store.add(gson.toJson(event));
     } catch (IOException e) {
@@ -60,4 +83,19 @@
       l.onStreamEventUpdate();
     }
   }
+
+  protected void readAndParseCfg() {
+    PluginConfig cfg =
+        PluginConfig.createFromGerritConfig(pluginName, gerritServerConfigProvider.loadConfig());
+    for (String filter : cfg.getStringList(KEY_FILTER)) {
+      String pieces[] = filter.split(" ");
+      if (pieces.length == 3
+          && FILTER_TYPE_DROP.equals(pieces[0])
+          && FILTER_ELEMENT_CLASSNAME.equals(pieces[1])) {
+        dropEventNames.add(pieces[2]);
+      } else {
+        log.error("Ignoring invalid filter: " + filter);
+      }
+    }
+  }
 }
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index f0639f5..f4d8079 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -11,3 +11,34 @@
 are stored under "<site_dir>/data/plugin/events".  Events do not use
 significant disk space, however it might still make sense to regularly
 trim them with a cron job.
+
+<a id="filtering"/>
+@PLUGIN@ Filtering
+------------------
+
+The @PLUGIN@ plugin is able to drop events which site admins do not
+want stored or sent out to users. Event filtering can be configured
+in the `gerrit.config`, using the following "git-config" style
+parameter:
+
+*`plugin.@PLUGIN@.filter`
+
+: rule to filter events with. Supported rules look like:
+
+ DROP classname fully.qualified.java.ClassName
+
+If the `plugin.@PLUGIN@.filter` key is specified more than once it
+will cause events matching any of the rules to be dropped.
+
+The example config below drops all known replication plugin events:
+
+```
+[plugin "events"]
+  filter = DROP classname com.googlesource.gerrit.plugins.replication.events.ProjectDeletionReplicationDoneEvent
+  filter = DROP classname com.googlesource.gerrit.plugins.replication.events.ProjectDeletionReplicationFailedEvent
+  filter = DROP classname com.googlesource.gerrit.plugins.replication.events.ProjectDeletionReplicationScheduledEvent
+  filter = DROP classname com.googlesource.gerrit.plugins.replication.events.ProjectDeletionReplicationSucceededEvent
+  filter = DROP classname com.googlesource.gerrit.plugins.replication.events.RefReplicatedEvent
+  filter = DROP classname com.googlesource.gerrit.plugins.replication.events.RefReplicationDoneEvent
+  filter = DROP classname com.googlesource.gerrit.plugins.replication.events.ReplicationScheduledEvent
+```
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
index 1bf956b..de23263 100755
--- a/test/docker/docker-compose.yaml
+++ b/test/docker/docker-compose.yaml
@@ -16,6 +16,7 @@
     volumes:
       - "../../:/events:ro"
       - "gerrit-site-etc:/server-ssh-key:ro"
+      - "gerrit-site-etc:/gerrit.config"
     depends_on:
       - gerrit-01
     environment:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
index 179348f..e9c40bf 100755
--- a/test/docker/gerrit/Dockerfile
+++ b/test/docker/gerrit/Dockerfile
@@ -4,11 +4,9 @@
 COPY artifacts/plugins/ $GERRIT_SITE/plugins/
 
 USER root
-
-RUN git config -f "$GERRIT_SITE/etc/gerrit.config" auth.type \
-    DEVELOPMENT_BECOME_ANY_ACCOUNT
-
 COPY artifacts/bin/ /tmp/
 RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
+COPY start.sh /
 
 USER gerrit
+ENTRYPOINT /start.sh
diff --git a/test/docker/gerrit/start.sh b/test/docker/gerrit/start.sh
new file mode 100755
index 0000000..4e00b41
--- /dev/null
+++ b/test/docker/gerrit/start.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+echo "Initializing Gerrit site ..."
+java -jar "$GERRIT_SITE/bin/gerrit.war" init -d "$GERRIT_SITE" \
+    --batch --dev
+
+echo "Running Gerrit ..."
+exec "$GERRIT_SITE"/bin/gerrit.sh run
diff --git a/test/test_events_plugin.sh b/test/test_events_plugin.sh
index da8cb33..124ca4a 100755
--- a/test/test_events_plugin.sh
+++ b/test/test_events_plugin.sh
@@ -7,6 +7,15 @@
 # plugin_name
 is_plugin_installed() { gssh gerrit plugin ls | awk '{print $1}' | grep -q "^$1$"; }
 
+set_filter_rules() { # [rule]...
+    git config -f "$GERRIT_CFG" --unset-all plugin.events.filter
+    local rule
+    for rule in "$@" ; do
+        git config -f "$GERRIT_CFG" plugin.events.filter "$rule"
+    done
+    gssh gerrit plugin reload events
+}
+
 cleanup() {
     wait_event
     (kill_diff_captures ; sleep 1 ; kill_diff_captures -9 ) &
@@ -140,14 +149,18 @@
    q wait $CAPTURE_PID_SSH
 }
 
-result_type() { # test type [expected_count]
+
+result_event() { # test type [expected_count]
     local test=$1 type=$2 expected_count=$3
     [ -n "$expected_count" ] || expected_count=1
     wait_event
     local actual_count=$(grep -c "\"type\":\"$type\"" "$EVENTS")
-    result_out "$test $type" "$expected_count $type event(s)" "$actual_count $type event(s)"
+    result_out "$test" "$expected_count $type event(s)" "$actual_count $type event(s)"
 }
 
+# test type [expected_count]
+result_type() { result_event "$1 $2" "$2" "$3" ; }
+
 # ------------------------- Usage ---------------------------
 
 usage() { # [error_message]
@@ -223,6 +236,7 @@
 EVENTS_PLUGIN=$TEST_DIR/events-plugin
 EVENT_FIFO=$TEST_DIR/event-fifo
 EVENTS=$TEST_DIR/events
+GERRIT_CFG="/gerrit.config/gerrit.config"
 
 trap cleanup EXIT
 
@@ -234,6 +248,7 @@
 
 # ------------------------- Individual Event Tests ---------------------------
 GROUP=visible-events
+set_filter_rules # No rules
 setup_diff_captures
 
 type=patchset-created
@@ -291,4 +306,24 @@
 
 kill_diff_captures
 
+# ------------------------- Filtering -------------------------
+
+GROUP=restored-filtered
+set_filter_rules 'DROP classname com.google.gerrit.server.events.ChangeRestoredEvent'
+
+ch1=$(create_change "$REF_BRANCH" "$FILE_A") || exit
+type=change-abandoned
+
+capture_events 3
+review "$ch1,1" --abandon
+result_type "$GROUP" "$type"
+
+type=change-restored
+capture_events 4
+review "$ch1,1" --restore
+# Instead of timing out waiting for the filtered change-restored event,
+# create follow-on events and capture them to trigger completion.
+review "$ch1,1" --message "'\"trigger filtered completion\"'"
+result_type "$GROUP" "$type" 0
+
 exit $RESULT