Support audit events rendered as JSON string

Allow to configure a different rendering for audit events
by specifying the renderer in gerrit.config.

Example:

[plugins "audit-sl4j"]
   renderer = JSON

Change-Id: Iff734fc5429f15913df66779570d91dc51a73a32
diff --git a/BUILD b/BUILD
index 69730a6..2dec1e5 100644
--- a/BUILD
+++ b/BUILD
@@ -22,9 +22,22 @@
 
 junit_tests(
     name = "audit_sl4j_tests",
-    srcs = glob(["src/test/java/**/*.java"]),
+    srcs = glob(["src/test/java/**/*Test.java"]),
     visibility = ["//visibility:public"],
     deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
         ":audit-sl4j__plugin",
+        ":audit_sl4j_util",
+    ],
+)
+
+java_library(
+    name = "audit_sl4j_util",
+    testonly = 1,
+    srcs = glob(
+        ["src/test/java/**/*.java"],
+        exclude = ["src/test/java/**/*Test.java"],
+    ),
+    deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
+        ":audit-sl4j__plugin",
     ],
 )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java
new file mode 100644
index 0000000..a89be0a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.gerrit.audit.AuditEvent;
+
+public class AuditRecord {
+  public final String type;
+  public final AuditEvent event;
+
+  public AuditRecord(AuditEvent event) {
+    super();
+
+    String eventClass = event.getClass().getName();
+    this.type = eventClass.substring(eventClass.lastIndexOf('.') + 1);
+    this.event = event;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRenderTypes.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRenderTypes.java
new file mode 100644
index 0000000..9d2e2f1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRenderTypes.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.auditsl4j;
+
+public enum AuditRenderTypes {
+  CSV,
+  JSON;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRenderer.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRenderer.java
index f89046e..293c511 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRenderer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRenderer.java
@@ -15,10 +15,11 @@
 package com.googlesource.gerrit.plugins.auditsl4j;
 
 import com.google.gerrit.audit.AuditEvent;
-import com.google.inject.ImplementedBy;
+import java.util.Optional;
 
-@ImplementedBy(AuditRendererToCsv.class)
 public interface AuditRenderer {
 
   String render(AuditEvent auditEvent);
+
+  Optional<String> headers();
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java
index 4f2a6e3..041f846 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.Multimap;
 import com.google.gerrit.audit.AuditEvent;
 import java.util.Collection;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -41,6 +42,12 @@
         auditEvent.elapsed);
   }
 
+  @Override
+  public Optional<String> headers() {
+    return Optional.of(
+        "EventId | EventTS | SessionId | User | Protocol data | Action | Parameters | Result | StartTS | Elapsed");
+  }
+
   private Object getFormattedAuditList(Multimap<String, ?> params) {
     if (params == null || params.size() == 0) {
       return "[]";
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java
new file mode 100644
index 0000000..7047adc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.audit.AuditEvent;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Optional;
+
+public class AuditRendererToJson implements AuditRenderer {
+  private final ExclusionStrategy INCLUDE_ONLY_WHITELISTED =
+      new ExclusionStrategy() {
+        private final HashSet<Class<?>> WHITELIST_CLASSES =
+            new HashSet<>(
+                Arrays.asList(
+                    String.class,
+                    Object.class,
+                    CurrentUser.class,
+                    Long.class,
+                    Long.TYPE,
+                    Integer.class,
+                    Integer.TYPE,
+                    AccessPath.class,
+                    CurrentUser.PropertyKey.class,
+                    Account.Id.class,
+                    AuditRecord.class));
+        private final HashSet<String> BLACKLIST_FIELDS =
+            new HashSet<>(Arrays.asList("anonymousCowardName"));
+
+        @Override
+        public boolean shouldSkipField(FieldAttributes f) {
+          return BLACKLIST_FIELDS.contains(f.getName());
+        }
+
+        @Override
+        public boolean shouldSkipClass(Class<?> clazz) {
+          return !AuditEvent.class.isAssignableFrom(clazz)
+              && !CurrentUser.class.isAssignableFrom(clazz)
+              && !ListMultimap.class.isAssignableFrom(clazz)
+              && !AuditEvent.UUID.class.isAssignableFrom(clazz)
+              && !WHITELIST_CLASSES.contains(clazz);
+        }
+      };
+
+  private final Gson gson =
+      OutputFormat.JSON_COMPACT
+          .newGsonBuilder()
+          .setExclusionStrategies(INCLUDE_ONLY_WHITELISTED)
+          .create();
+
+  @Override
+  public String render(AuditEvent auditEvent) {
+    return gson.toJson(new AuditRecord(auditEvent));
+  }
+
+  @Override
+  public Optional<String> headers() {
+    return Optional.empty();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToStringList.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToStringList.java
new file mode 100644
index 0000000..bb24056
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToStringList.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+public class AuditWriterToStringList implements AuditWriter {
+  public final List<String> strings = new ArrayList<>();
+
+  @Override
+  public void write(String msg) {
+    strings.add(msg);
+  }
+
+  @Override
+  public String toString() {
+    return strings.toString();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java
index 7328c60..5a0b590 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java
@@ -31,12 +31,7 @@
     this.auditWriter = auditWriter;
     this.auditRenderer = auditRenderer;
 
-    writeHeaders(auditWriter);
-  }
-
-  private void writeHeaders(AuditWriter auditWriter) {
-    auditWriter.write(
-        "EventId | EventTS | SessionId | User | Protocol data | Action | Parameters | Result | StartTS | Elapsed");
+    auditRenderer.headers().ifPresent(auditWriter::write);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/Module.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/Module.java
index ba76f93..d70f4ac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/Module.java
@@ -15,12 +15,35 @@
 package com.googlesource.gerrit.plugins.auditsl4j;
 
 import com.google.gerrit.audit.AuditListener;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
 
 public class Module extends AbstractModule {
+  private final PluginConfig config;
+
+  @Inject
+  public Module(@PluginName String pluginName, PluginConfigFactory configFactory) {
+    config = configFactory.getFromGerritConfig(pluginName);
+  }
+
   @Override
   protected void configure() {
     DynamicSet.bind(binder(), AuditListener.class).to(LoggerAudit.class);
+
+    AuditRenderTypes rendererType = config.getEnum("renderer", AuditRenderTypes.CSV);
+    switch (rendererType) {
+      case CSV:
+        bind(AuditRenderer.class).to(AuditRendererToCsv.class);
+        break;
+      case JSON:
+        bind(AuditRenderer.class).to(AuditRendererToJson.class);
+        break;
+      default:
+        throw new IllegalArgumentException("Unsupported renderer '" + rendererType + "'");
+    }
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditTest.java b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToCsvTest.java
similarity index 71%
rename from src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditTest.java
rename to src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToCsvTest.java
index 5470641..df60cab 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToCsvTest.java
@@ -17,9 +17,14 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.audit.AuditListener;
 import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.ArrayList;
@@ -27,43 +32,36 @@
 import org.apache.http.client.fluent.Request;
 import org.junit.Test;
 
+@Sandboxed
 @TestPlugin(
     name = "audit-sl4j",
-    sysModule = "com.googlesource.gerrit.plugins.auditsl4j.LoggerAuditTest$TestModule")
-public class LoggerAuditTest extends LightweightPluginDaemonTest {
+    sysModule = "com.googlesource.gerrit.plugins.auditsl4j.LoggerAuditToCsvTest$TestModule")
+public class LoggerAuditToCsvTest extends LightweightPluginDaemonTest implements WaitForCondition {
 
   @Inject @CanonicalWebUrl private String webUrl;
 
-  public static class TestModule extends Module {
+  public static class TestModule extends AbstractModule {
 
     @Override
     protected void configure() {
       bind(AuditWriter.class).to(AuditWriterToStringList.class);
-      super.configure();
-    }
-  }
-
-  @Singleton
-  public static class AuditWriterToStringList implements AuditWriter {
-    public final List<String> strings = new ArrayList<>();
-
-    @Override
-    public void write(String msg) {
-      strings.add(msg);
+      bind(AuditRenderer.class).to(AuditRendererToCsv.class);
+      DynamicSet.bind(binder(), AuditListener.class).to(LoggerAudit.class);
     }
   }
 
   @Test
-  public void testHttpAudit() throws Exception {
+  public void testHttpCsvAudit() throws Exception {
     AuditWriterToStringList auditStrings = getPluginInstance(AuditWriterToStringList.class);
 
     Request.Get(webUrl + "config/server/version").execute().returnResponse();
 
-    assertThat(auditStrings.strings).hasSize(2);
+    assertThat(waitFor(() -> auditStrings.strings.size() == 2)).isTrue();
     assertThat(auditStrings.strings.get(1)).contains(Version.getVersion());
   }
 
   private <T> T getPluginInstance(Class<T> clazz) {
     return plugin.getSysInjector().getInstance(clazz);
   }
+
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditTest.java b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToJsonTest.java
similarity index 62%
copy from src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditTest.java
copy to src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToJsonTest.java
index 5470641..e8a568f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToJsonTest.java
@@ -17,50 +17,49 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.audit.AuditListener;
 import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.ArrayList;
-import java.util.List;
 import org.apache.http.client.fluent.Request;
 import org.junit.Test;
 
+@Sandboxed
 @TestPlugin(
     name = "audit-sl4j",
-    sysModule = "com.googlesource.gerrit.plugins.auditsl4j.LoggerAuditTest$TestModule")
-public class LoggerAuditTest extends LightweightPluginDaemonTest {
+    sysModule = "com.googlesource.gerrit.plugins.auditsl4j.LoggerAuditToJsonTest$TestModule")
+public class LoggerAuditToJsonTest extends LightweightPluginDaemonTest implements WaitForCondition {
 
   @Inject @CanonicalWebUrl private String webUrl;
 
-  public static class TestModule extends Module {
+  public static class TestModule extends AbstractModule {
 
     @Override
     protected void configure() {
       bind(AuditWriter.class).to(AuditWriterToStringList.class);
-      super.configure();
-    }
-  }
-
-  @Singleton
-  public static class AuditWriterToStringList implements AuditWriter {
-    public final List<String> strings = new ArrayList<>();
-
-    @Override
-    public void write(String msg) {
-      strings.add(msg);
+      bind(AuditRenderer.class).to(AuditRendererToJson.class);
+      DynamicSet.bind(binder(), AuditListener.class).to(LoggerAudit.class);
     }
   }
 
   @Test
-  public void testHttpAudit() throws Exception {
+  public void testHttpJsonAudit() throws Exception {
     AuditWriterToStringList auditStrings = getPluginInstance(AuditWriterToStringList.class);
 
     Request.Get(webUrl + "config/server/version").execute().returnResponse();
 
-    assertThat(auditStrings.strings).hasSize(2);
-    assertThat(auditStrings.strings.get(1)).contains(Version.getVersion());
+    assertThat(waitFor(() -> auditStrings.strings.size() == 1)).isTrue();
+
+    String auditJsonString = auditStrings.strings.get(0);
+    assertThat(auditJsonString).contains(Version.getVersion());
+    JsonObject auditJson = new Gson().fromJson(auditJsonString, JsonObject.class);
+    assertThat(auditJson).isNotNull();
   }
 
   private <T> T getPluginInstance(Class<T> clazz) {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/WaitForCondition.java b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/WaitForCondition.java
new file mode 100644
index 0000000..5548391
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/WaitForCondition.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.common.base.Stopwatch;
+import java.time.Duration;
+import java.util.function.Supplier;
+
+public interface WaitForCondition {
+
+  public default Duration waitTimeout() {
+    return Duration.ofSeconds(5);
+  }
+
+  public default Duration waitInterval() {
+    return Duration.ofMillis(100);
+  }
+
+  public default boolean waitFor(Supplier<Boolean> condition) {
+    Stopwatch stopwatch = Stopwatch.createStarted();
+    try {
+      Duration maxWait = waitTimeout();
+      Duration sleep = waitInterval();
+      boolean conditionSucceeded = condition.get();
+      while (!conditionSucceeded && stopwatch.elapsed().compareTo(maxWait) < 0) {
+        try {
+          Thread.sleep(sleep.toMillis());
+        } catch (InterruptedException e) {
+        }
+        conditionSucceeded = condition.get();
+      }
+      return conditionSucceeded;
+    } finally {
+      stopwatch.stop();
+    }
+  }
+}