Add support for forwarding cache eviction for custom caches

Allow to specify cache.pattern in the plugin configuration, which
will add additional patterns to the regex to test if a cache eviction
should be forwarded. This will allow caches created by other plugins
to be handled in addition to the default core caches.

For example:

  [cache]
    synchronize = true
    pattern = ^my_cache.*
    pattern = other_cache

Note that evictions for core caches are always forwarded. Specifying
cache.pattern only adds extra matches; it doesn't override forwarding
of eviction for the core caches.

Change-Id: Ia415d53a3c08d744324e88a6f7115a761f94c1f6
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
index 6c56a8c..4ae47fe 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
@@ -28,6 +28,9 @@
 import com.google.inject.Singleton;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -55,6 +58,7 @@
 
   // cache section
   static final String CACHE_SECTION = "cache";
+  static final String PATTERN_KEY = "pattern";
 
   // event section
   static final String EVENT_SECTION = "event";
@@ -242,15 +246,21 @@
 
   public static class Cache extends Forwarding {
     private final int threadPoolSize;
+    private final List<String> patterns;
 
     private Cache(Config cfg) {
       super(cfg, CACHE_SECTION);
       threadPoolSize = getInt(cfg, CACHE_SECTION, THREAD_POOL_SIZE_KEY, DEFAULT_THREAD_POOL_SIZE);
+      patterns = Arrays.asList(cfg.getStringList(CACHE_SECTION, null, PATTERN_KEY));
     }
 
     public int threadPoolSize() {
       return threadPoolSize;
     }
+
+    public List<String> patterns() {
+      return Collections.unmodifiableList(patterns);
+    }
   }
 
   public static class Event extends Forwarding {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java
index 43e3642..f8d71f3 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/CachePatternMatcher.java
@@ -14,9 +14,12 @@
 
 package com.ericsson.gerrit.plugins.highavailability.cache;
 
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.Pattern;
 
@@ -34,8 +37,11 @@
 
   private final Pattern pattern;
 
-  public CachePatternMatcher() {
-    this.pattern = Pattern.compile(Joiner.on("|").join(DEFAULT_PATTERNS));
+  @Inject
+  public CachePatternMatcher(Configuration cfg) {
+    List<String> patterns = new ArrayList<>(DEFAULT_PATTERNS);
+    patterns.addAll(cfg.cache().patterns());
+    this.pattern = Pattern.compile(Joiner.on("|").join(patterns));
   }
 
   public boolean matches(String cacheName) {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/Constants.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/Constants.java
index 6b047d3..94c3d30 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/Constants.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/cache/Constants.java
@@ -15,7 +15,7 @@
 package com.ericsson.gerrit.plugins.highavailability.cache;
 
 public final class Constants {
-
+  public static final String GERRIT = "gerrit";
   public static final String PROJECT_LIST = "project_list";
   public static final String ACCOUNTS = "accounts";
   public static final String GROUPS = "groups";
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java
index b388b0b..253ff4a 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/CacheRestApiServlet.java
@@ -19,6 +19,7 @@
 
 import com.ericsson.gerrit.plugins.highavailability.cache.Constants;
 import com.ericsson.gerrit.plugins.highavailability.forwarder.Context;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
 import com.google.common.cache.Cache;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -37,7 +38,6 @@
 class CacheRestApiServlet extends HttpServlet {
   private static final int CACHENAME_INDEX = 1;
   private static final long serialVersionUID = -1L;
-  private static final String GERRIT = "gerrit";
   private static final Logger logger = LoggerFactory.getLogger(CacheRestApiServlet.class);
 
   private final DynamicMap<Cache<?, ?>> cacheMap;
@@ -57,10 +57,17 @@
       String cacheName = params.get(CACHENAME_INDEX);
       String json = req.getReader().readLine();
       Object key = GsonParser.fromJson(cacheName, json);
-      Cache<?, ?> cache = cacheMap.get(GERRIT, cacheName);
-      Context.setForwardedEvent(true);
-      evictCache(cache, cacheName, key);
-      rsp.setStatus(SC_NO_CONTENT);
+      CacheParameters cacheKey = getCacheParameters(cacheName);
+      Cache<?, ?> cache = cacheMap.get(cacheKey.pluginName, cacheKey.cacheName);
+      if (cache == null) {
+        String msg = String.format("cache %s not found", cacheName);
+        logger.error("Failed to process eviction request: " + msg);
+        sendError(rsp, SC_BAD_REQUEST, msg);
+      } else {
+        Context.setForwardedEvent(true);
+        evictCache(cache, cacheKey.cacheName, key);
+        rsp.setStatus(SC_NO_CONTENT);
+      }
     } catch (IOException e) {
       logger.error("Failed to process eviction request: " + e.getMessage(), e);
       sendError(rsp, SC_BAD_REQUEST, e.getMessage());
@@ -69,6 +76,26 @@
     }
   }
 
+  @VisibleForTesting
+  public static class CacheParameters {
+    public final String pluginName;
+    public final String cacheName;
+
+    public CacheParameters(String pluginName, String cacheName) {
+      this.pluginName = pluginName;
+      this.cacheName = cacheName;
+    }
+  }
+
+  @VisibleForTesting
+  public static CacheParameters getCacheParameters(String cache) {
+    int dot = cache.indexOf(".");
+    if (dot > 0) {
+      return new CacheParameters(cache.substring(0, dot), cache.substring(dot + 1));
+    }
+    return new CacheParameters(Constants.GERRIT, cache);
+  }
+
   private static void sendError(HttpServletResponse rsp, int statusCode, String message) {
     try {
       rsp.sendError(statusCode, message);
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParser.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParser.java
index 23f42ab..8a177de 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParser.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/GsonParser.java
@@ -44,7 +44,11 @@
         key = gson.fromJson(Strings.nullToEmpty(json), Object.class);
         break;
       default:
-        key = gson.fromJson(Strings.nullToEmpty(json).trim(), String.class);
+        try {
+          key = gson.fromJson(Strings.nullToEmpty(json).trim(), String.class);
+        } catch (Exception e) {
+          key = gson.fromJson(Strings.nullToEmpty(json), Object.class);
+        }
     }
     return key;
   }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 8ec9432..1ab2858 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -62,6 +62,13 @@
 :   Maximum number of threads used to send cache evictions to the target instance.
     Defaults to 1.
 
+cache.pattern
+:   Pattern to match names of custom caches for which evictions should be
+    forwarded (in addition to the core caches that are always forwarded). May be
+    specified more than once to add multiple patterns.
+    Defaults to an empty list, meaning only evictions of the core caches are
+    forwarded.
+
 event.synchronize
 :   Whether to synchronize stream events.
     Defaults to true.