Support flushing of all caches via REST

Change-Id: I82807e71e7fceb7ba3bba23d51263b72998f251d
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 8a980e2..6f9ef46 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -339,6 +339,33 @@
   done
 ----
 
+[cache-operations]]
+=== Cache Operations
+--
+'POST /config/server/caches/'
+--
+
+Executes a cache operation that is specified in the request body in a
+link:#cache-operation-input[CacheOperationInput] entity.
+
+[[flush-all-caches]]
+==== Flush All Caches
+
+.Request
+----
+  POST /config/server/caches/ HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "operation": "FLUSH_ALL"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+----
+
 [[get-cache]]
 === Get Cache
 --
@@ -589,6 +616,20 @@
 |`name`               |capability name
 |=================================
 
+[[cache-operation-input]]
+=== CacheOperationInput
+The `CacheOperationInput` entity contains information about an
+operation that should be executed on caches.
+
+[options="header",width="50%",cols="1,6"]
+|=================================
+|Field Name           |Description
+|`operation`          |
+The cache operation that should be executed:
+
+`FLUSH_ALL`: Flushes all caches, except the `web_sessions` cache.
+|=================================
+
 [[entries-info]]
 === EntriesInfo
 The `EntriesInfo` entity contains information about the entries in a
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
new file mode 100644
index 0000000..0038a54
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2014 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.acceptance.rest.config;
+
+import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.server.config.ListCaches.CacheInfo;
+import com.google.gerrit.server.config.PostCaches;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class CacheOperationsIT extends AbstractDaemonTest {
+
+  @Test
+  public void flushAll() throws IOException {
+    RestResponse r = adminSession.get("/config/server/caches/project_list");
+    CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertTrue(cacheInfo.entries.mem.longValue() > 0);
+
+    r = adminSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL));
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/config/server/caches/project_list");
+    cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
+    assertNull(cacheInfo.entries.mem);
+  }
+
+  @Test
+  public void flushAll_Forbidden() throws IOException {
+    RestResponse r = userSession.post("/config/server/caches/",
+        new PostCaches.Input(FLUSH_ALL));
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
index 5e6da74..5bff20c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
@@ -18,10 +18,12 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsPost;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
@@ -32,26 +34,29 @@
 @RequiresCapability(GlobalCapability.VIEW_CACHES)
 @Singleton
 public class CachesCollection implements
-    ChildCollection<ConfigResource, CacheResource> {
+    ChildCollection<ConfigResource, CacheResource>, AcceptsPost<ConfigResource> {
 
   private final DynamicMap<RestView<CacheResource>> views;
-  private final ListCaches list;
+  private final Provider<ListCaches> list;
   private final Provider<CurrentUser> self;
   private final DynamicMap<Cache<?, ?>> cacheMap;
+  private final PostCaches postCaches;
 
   @Inject
   CachesCollection(DynamicMap<RestView<CacheResource>> views,
-      ListCaches list, Provider<CurrentUser> self,
-      DynamicMap<Cache<?, ?>> cacheMap) {
+      Provider<ListCaches> list, Provider<CurrentUser> self,
+      DynamicMap<Cache<?, ?>> cacheMap,
+      PostCaches postCaches) {
     this.views = views;
     this.list = list;
     this.self = self;
     this.cacheMap = cacheMap;
+    this.postCaches = postCaches;
   }
 
   @Override
   public RestView<ConfigResource> list() {
-    return list;
+    return list.get();
   }
 
   @Override
@@ -85,4 +90,10 @@
   public DynamicMap<RestView<CacheResource>> views() {
     return views;
   }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public PostCaches post(ConfigResource parent) throws RestApiException {
+    return postCaches;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
index 076be5d4..a7c03be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
@@ -31,7 +31,7 @@
   public static class Input {
   }
 
-  private static final String WEB_SESSIONS = "web_sessions";
+  public static final String WEB_SESSIONS = "web_sessions";
 
   private final Provider<CurrentUser> self;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
new file mode 100644
index 0000000..6a4856b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2014 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.server.config;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.config.PostCaches.Input;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@RequiresCapability(GlobalCapability.FLUSH_CACHES)
+@Singleton
+public class PostCaches implements RestModifyView<ConfigResource, Input> {
+  public static class Input {
+    public Operation operation;
+
+    public Input() {
+    }
+
+    public Input(Operation op) {
+      operation = op;
+    }
+  }
+
+  public static enum Operation {
+    FLUSH_ALL;
+  }
+
+  private final DynamicMap<Cache<?, ?>> cacheMap;
+  private final FlushCache flushCache;
+
+  @Inject
+  public PostCaches(DynamicMap<Cache<?, ?>> cacheMap,
+      FlushCache flushCache) {
+    this.cacheMap = cacheMap;
+    this.flushCache = flushCache;
+  }
+
+  @Override
+  public Object apply(ConfigResource rsrc, Input input) throws AuthException,
+      ResourceNotFoundException, BadRequestException {
+    if (input == null || input.operation == null) {
+      throw new BadRequestException("operation must be specified");
+    }
+
+    switch (input.operation) {
+      case FLUSH_ALL:
+        for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+          CacheResource cacheResource =
+              new CacheResource(e.getPluginName(), e.getExportName(),
+                  e.getProvider());
+          if (FlushCache.WEB_SESSIONS.equals(cacheResource.getName())) {
+            continue;
+          }
+          flushCache.apply(cacheResource, null);
+        }
+        return Response.ok("");
+      default:
+        throw new BadRequestException("unsupported operation: " + input.operation);
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index bd27598..bc6687a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL;
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.common.cache.Cache;
@@ -26,6 +27,7 @@
 import com.google.gerrit.server.config.FlushCache;
 import com.google.gerrit.server.config.ListCaches;
 import com.google.gerrit.server.config.ListCaches.OutputFormat;
+import com.google.gerrit.server.config.PostCaches;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -40,8 +42,6 @@
 @CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory",
   runsAt = MASTER_OR_SLAVE)
 final class FlushCaches extends CacheCommand {
-  private static final String WEB_SESSIONS = "web_sessions";
-
   @Option(name = "--cache", usage = "flush named cache", metaVar = "NAME")
   private List<String> caches = new ArrayList<>();
 
@@ -57,32 +57,44 @@
   @Inject
   private Provider<ListCaches> listCaches;
 
+  @Inject
+  private PostCaches postCaches;
+
   @Override
   protected void run() throws Failure {
-    if (list) {
-      if (all || caches.size() > 0) {
-        throw error("error: cannot use --list with --all or --cache");
+    try {
+      if (list) {
+        if (all || caches.size() > 0) {
+          throw error("error: cannot use --list with --all or --cache");
+        }
+        doList();
+        return;
       }
-      doList();
-      return;
-    }
 
-    if (all && caches.size() > 0) {
-      throw error("error: cannot combine --all and --cache");
-    } else if (!all && caches.size() == 1 && caches.contains("all")) {
-      caches.clear();
-      all = true;
-    } else if (!all && caches.isEmpty()) {
-      all = true;
-    }
-
-    List<String> names = cacheNames();
-    for (String n : caches) {
-      if (!names.contains(n)) {
-        throw error("error: cache \"" + n + "\" not recognized");
+      if (all && caches.size() > 0) {
+        throw error("error: cannot combine --all and --cache");
+      } else if (!all && caches.size() == 1 && caches.contains("all")) {
+        caches.clear();
+        all = true;
+      } else if (!all && caches.isEmpty()) {
+        all = true;
       }
+
+      if (all) {
+        postCaches.apply(new ConfigResource(),
+            new PostCaches.Input(FLUSH_ALL));
+      } else {
+        List<String> names = cacheNames();
+        for (String n : caches) {
+          if (!names.contains(n)) {
+            throw error("error: cache \"" + n + "\" not recognized");
+          }
+        }
+        doBulkFlush();
+      }
+    } catch (RestApiException e) {
+      throw die(e.getMessage());
     }
-    doBulkFlush();
   }
 
   private static UnloggedFailure error(final String msg) {
@@ -95,7 +107,7 @@
         .apply(new ConfigResource());
   }
 
-  private void doList() throws UnloggedFailure {
+  private void doList() throws RestApiException {
     for (String name : cacheNames()) {
       stderr.print(name);
       stderr.print('\n');
@@ -107,7 +119,7 @@
     try {
       for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
         String n = cacheNameOf(e.getPluginName(), e.getExportName());
-        if (flush(n)) {
+        if (caches.contains(n)) {
           try {
             flushCache.apply(
                 new CacheResource(e.getPluginName(), e.getExportName(),
@@ -122,19 +134,4 @@
       stderr.flush();
     }
   }
-
-  private boolean flush(final String cacheName) {
-    if (caches.contains(cacheName)) {
-      return true;
-
-    } else if (all) {
-      if (WEB_SESSIONS.equals(cacheName)) {
-        return false;
-      }
-      return true;
-
-    } else {
-      return false;
-    }
-  }
 }