Use RestApiServlet and SSH commands for plugin actions

This change is adding one endpoint with three methods:

* GET /config/server/readonly~readonly
* PUT /config/server/readonly~readonly
* DELETE /config/server/readonly~readonly

and based on this endpoint, three SSH commands:

* ssh host readonly get
* ssh host readonly put
* ssh host readonly delete

Change-Id: I1797906aebb30674c7c37ecfe03a8b944b631299
diff --git a/BUILD b/BUILD
index 73863f9..da50b99 100644
--- a/BUILD
+++ b/BUILD
@@ -15,7 +15,7 @@
         "Gerrit-SshModule: com.googlesource.gerrit.plugins.readonly.SshModule",
         "Gerrit-HttpModule: com.googlesource.gerrit.plugins.readonly.HttpModule",
     ],
-    resources = glob(["src/main/**/*"]),
+    resources = glob(["src/main/resources/**/*"]),
 )
 
 junit_tests(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/DeleteReadOnlyCommand.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/DeleteReadOnlyCommand.java
new file mode 100644
index 0000000..392df17
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/DeleteReadOnlyCommand.java
@@ -0,0 +1,35 @@
+// 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.readonly;
+
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+
+@RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+@CommandMetaData(name = "delete", description = "Disable read only mode")
+class DeleteReadOnlyCommand extends SshCommand {
+  @Inject ReadOnlyEndpoint.Delete delete;
+
+  @Override
+  protected void run() throws IOException {
+    delete.apply(null, null);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java
index d287483..19b2592 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java
@@ -40,6 +40,8 @@
     this.state = state;
     this.disableCommand = pluginName + " disable";
     allowPatterns.add(Pattern.compile(String.format(PATTERN, pluginName)));
+    // Allow all SSH commands from this plugin
+    allowPrefixes.add(pluginName);
     for (String allow : config.allowSshCommands()) {
       if (allow.startsWith("^")) {
         allowPatterns.add(Pattern.compile(allow));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/GetReadOnlyCommand.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/GetReadOnlyCommand.java
new file mode 100644
index 0000000..3a5c9db
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/GetReadOnlyCommand.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.readonly;
+
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+@RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+@CommandMetaData(name = "get", description = "Show read only mode state")
+class GetReadOnlyCommand extends SshCommand {
+  @Inject ReadOnlyEndpoint.Get get;
+
+  @Override
+  protected void run() {
+    stdout.println(get.apply(null));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/Module.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/Module.java
index 88f681a..69fa804 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/Module.java
@@ -14,7 +14,10 @@
 
 package com.googlesource.gerrit.plugins.readonly;
 
+import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.inject.AbstractModule;
 
@@ -22,5 +25,14 @@
   @Override
   protected void configure() {
     DynamicSet.bind(binder(), CommitValidationListener.class).to(ReadOnly.class);
+    install(
+        new RestApiModule() {
+          @Override
+          protected void configure() {
+            put(CONFIG_KIND, "readonly").to(ReadOnlyEndpoint.Put.class);
+            delete(CONFIG_KIND, "readonly").to(ReadOnlyEndpoint.Delete.class);
+            get(CONFIG_KIND, "readonly").to(ReadOnlyEndpoint.Get.class);
+          }
+        });
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/PutReadOnlyCommand.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/PutReadOnlyCommand.java
new file mode 100644
index 0000000..bf5ffef
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/PutReadOnlyCommand.java
@@ -0,0 +1,35 @@
+// 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.readonly;
+
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+
+@RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+@CommandMetaData(name = "put", description = "Enable read only mode")
+class PutReadOnlyCommand extends SshCommand {
+  @Inject ReadOnlyEndpoint.Put put;
+
+  @Override
+  protected void run() throws IOException {
+    put.apply(null, null);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnly.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnly.java
index 54b35e7..824ddc1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnly.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnly.java
@@ -41,12 +41,14 @@
   private final ReadOnlyState state;
   private final ReadOnlyConfig config;
   private final String endpoint;
+  private final String endpoint2;
 
   @Inject
   ReadOnly(ReadOnlyState state, ReadOnlyConfig config, @PluginName String pluginName) {
     this.state = state;
     this.config = config;
     this.endpoint = pluginName + ENDPOINT;
+    this.endpoint2 = String.format("/config/server/%s~readonly", pluginName);
   }
 
   @Override
@@ -73,9 +75,10 @@
 
   private boolean shouldBlock(HttpServletRequest request) {
     String method = request.getMethod();
-    String uri = request.getRequestURI();
-    return !uri.endsWith(endpoint)
-        && (("POST".equals(method) && !uri.endsWith(GIT_UPLOAD_PACK_PROTOCOL))
+    String servletPath = request.getServletPath();
+    return !servletPath.endsWith(endpoint)
+        && !servletPath.endsWith(endpoint2)
+        && (("POST".equals(method) && !servletPath.endsWith(GIT_UPLOAD_PACK_PROTOCOL))
             || "PUT".equals(method)
             || "DELETE".equals(method));
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyEndpoint.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyEndpoint.java
new file mode 100644
index 0000000..caabb57
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyEndpoint.java
@@ -0,0 +1,81 @@
+// 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.readonly;
+
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+
+public class ReadOnlyEndpoint {
+  static class Input {}
+
+  @RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+  @Singleton
+  public static class Get implements RestReadView<ConfigResource> {
+    private final ReadOnlyState state;
+
+    @Inject
+    Get(ReadOnlyState state) {
+      this.state = state;
+    }
+
+    @Override
+    public String apply(ConfigResource resource) {
+      return state.isReadOnly() ? "on" : "off";
+    }
+  }
+
+  @RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+  @Singleton
+  public static class Put implements RestModifyView<ConfigResource, Input> {
+    private final ReadOnlyState state;
+
+    @Inject
+    Put(ReadOnlyState state) {
+      this.state = state;
+    }
+
+    @Override
+    public Response<String> apply(ConfigResource resource, Input input) throws IOException {
+      state.setReadOnly(true);
+      return Response.ok("");
+    }
+  }
+
+  @RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+  @Singleton
+  public static class Delete implements RestModifyView<ConfigResource, Input> {
+    private final ReadOnlyState state;
+
+    @Inject
+    Delete(ReadOnlyState state) {
+      this.state = state;
+    }
+
+    @Override
+    public Response<String> apply(ConfigResource resource, Input input) throws IOException {
+      state.setReadOnly(false);
+      return Response.ok("");
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/SshModule.java
index 8067516..c89816e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/SshModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/SshModule.java
@@ -24,5 +24,14 @@
     DynamicItem.bind(binder(), SshCreateCommandInterceptor.class)
         .to(DisableCommandInterceptor.class);
     command(DisableCommand.class);
+
+    command(PutReadOnlyCommand.class);
+    alias("on", PutReadOnlyCommand.class);
+
+    command(DeleteReadOnlyCommand.class);
+    alias("off", DeleteReadOnlyCommand.class);
+
+    command(GetReadOnlyCommand.class);
+    alias("status", GetReadOnlyCommand.class);
   }
 }