Merge branch 'stable-2.16' into stable-3.0

* stable-2.16:
  Fix compile and test issues on stable-2.16
  Document Geo-located Gerrit master selection solutions
  Fix cache eviction for projects cache

Change-Id: I7c72e8d132d03fbe24deb0b1911b9e040fb5e0da
diff --git a/DESIGN.md b/DESIGN.md
index 0995a14..270b7af 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -510,6 +510,60 @@
 set of refs in Read Only state across all the cluster if the RW node is failing after having
 sent the request to the Ref-DB but before persisting this request into its `git` layer.
 
+#### Geo located Gerrit master election
+
+Once you go multi-site multi-master you can improve the latency of your calls by
+serving traffic from the closest server to your user.
+
+Whether you are running your infrastructure in the cloud or on premise you have different solutions you can look at.
+
+##### AWS
+
+Route53 AWS DNS service offers the opportunity of doing [Geo Proximity](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-geoproximity)
+routing using [Traffic Flow](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/traffic-flow.html).
+
+Traffic flow is a tool which allows the definition of traffic policies and rules via a UI. Traffic rules are of different types, among which *Geoproximity* rules.
+
+When creating geoproximity rules for your resources you can specify one of the following values for each rule:
+
+* If you're using AWS resources, the AWS Region that you created the resource in
+* If you're using non-AWS resources, the latitude and longitude of the resource.
+
+This allows you to have an hybrid cloud-on premise infrastructure.
+
+You can define quite complex failover rules to ensure high availability of your system ([here](https://pasteboard.co/ILFSd5Y.png) an example).
+
+Overall the service provided is pretty much a smart reverse-proxy, if you want more
+complex routing strategies you will still need a proper Load Balancer.
+
+##### GCE
+
+GCE [doesn't offer](https://cloud.google.com/docs/compare/aws/networking#dns) a Geographical based routing, but it implicitly has geo-located DNS entries
+when distributing your application among different zones.
+
+The Load Balancer will balance the traffic to the [nearest available instance](https://cloud.google.com/load-balancing/docs/backend-service#backend_services_and_regions)
+, but this is not configurable and the app server has to be in GC.
+
+Hybrid architectures are supported but would make things more complicated,
+hence this solution is probably worthy only when the Gerrit instances are running in GC.
+
+##### On premise
+
+If you are going for an on premise solution and using HAProxy as Load Balancer,
+it is easy to define static ACL based on IP ranges and use them to route your traffic.
+
+This [blogpost](https://icicimov.github.io/blog/devops/Haproxy-GeoIP/) explains how to achieve it.
+
+On top of that, you want to define a DNS entry per zone and use the ACLs you just defined to
+issue redirection of the calls to most appropiate zone.
+
+You will have to add to your frontend definition your redirection strategy, i.e.:
+
+```
+http-request redirect code 307 prefix https://review-eu.gerrithub.io if acl_EU
+http-request redirect code 307 prefix https://review-am.gerrithub.io if acl_NA
+```
+
 # Next steps in the roadmap
 
 ## Step-1: Fill the gaps in multi-site Stage #7 implementation:
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/GsonParser.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/GsonParser.java
index 7930207..ddc184a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/GsonParser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/GsonParser.java
@@ -14,7 +14,7 @@
 
 package com.googlesource.gerrit.plugins.multisite.forwarder;
 
-import com.google.common.base.Strings;
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gson.Gson;
@@ -25,29 +25,34 @@
 
   private GsonParser() {}
 
-  public static Object fromJson(String cacheName, String json) {
+  @SuppressWarnings("cast")
+  public static Object fromJson(String cacheName, Object json) {
     Gson gson = new GsonBuilder().create();
     Object key;
     // Need to add a case for 'adv_bases'
     switch (cacheName) {
       case Constants.ACCOUNTS:
-        key = gson.fromJson(Strings.nullToEmpty(json).trim(), Account.Id.class);
+        key = gson.fromJson(nullToEmpty(json).toString().trim(), Account.Id.class);
         break;
       case Constants.GROUPS:
-        key = gson.fromJson(Strings.nullToEmpty(json).trim(), AccountGroup.Id.class);
+        key = gson.fromJson(nullToEmpty(json).toString().trim(), AccountGroup.Id.class);
         break;
       case Constants.GROUPS_BYINCLUDE:
       case Constants.GROUPS_MEMBERS:
-        key = gson.fromJson(Strings.nullToEmpty(json).trim(), AccountGroup.UUID.class);
+        key = gson.fromJson(nullToEmpty(json).toString().trim(), AccountGroup.UUID.class);
         break;
       case Constants.PROJECT_LIST:
-        key = gson.fromJson(Strings.nullToEmpty(json), Object.class);
+        key = gson.fromJson(nullToEmpty(json).toString(), Object.class);
         break;
       default:
-        try {
-          key = gson.fromJson(Strings.nullToEmpty(json).trim(), String.class);
-        } catch (Exception e) {
-          key = gson.fromJson(Strings.nullToEmpty(json), Object.class);
+        if (json instanceof String) {
+          key = (String) json;
+        } else {
+          try {
+            key = gson.fromJson(nullToEmpty(json).toString().trim(), String.class);
+          } catch (Exception e) {
+            key = gson.fromJson(nullToEmpty(json).toString(), Object.class);
+          }
         }
     }
     return key;
@@ -70,8 +75,16 @@
         break;
       case Constants.PROJECT_LIST:
       default:
-        json = gson.toJson(key);
+        if (key instanceof String) {
+          json = (String) key;
+        } else {
+          json = gson.toJson(key);
+        }
     }
     return json;
   }
+
+  private static Object nullToEmpty(Object value) {
+    return MoreObjects.firstNonNull(value, "");
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/CacheEvictionEventRouter.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/CacheEvictionEventRouter.java
index 4c17a95..a6cd8c4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/CacheEvictionEventRouter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/forwarder/router/CacheEvictionEventRouter.java
@@ -31,8 +31,7 @@
 
   @Override
   public void route(CacheEvictionEvent cacheEvictionEvent) throws CacheNotFoundException {
-    Object parsedKey =
-        GsonParser.fromJson(cacheEvictionEvent.cacheName, cacheEvictionEvent.key.toString());
+    Object parsedKey = GsonParser.fromJson(cacheEvictionEvent.cacheName, cacheEvictionEvent.key);
     cacheEvictionHanlder.evict(CacheEntry.from(cacheEvictionEvent.cacheName, parsedKey));
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/CacheEvictionEventRouterTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/CacheEvictionEventRouterTest.java
index 8c094cd..9292262 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/event/CacheEvictionEventRouterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/event/CacheEvictionEventRouterTest.java
@@ -44,4 +44,13 @@
 
     verify(cacheEvictionHandler).evict(CacheEntry.from(event.cacheName, event.key));
   }
+
+  @Test
+  public void routerShouldSendEventsToTheAppropriateHandler_ProjectCacheEvictionWithSlash()
+      throws Exception {
+    final CacheEvictionEvent event = new CacheEvictionEvent("cache", "some/project");
+    router.route(event);
+
+    verify(cacheEvictionHandler).evict(CacheEntry.from(event.cacheName, event.key));
+  }
 }