Add skeleton for list pending checks REST endpoint

This adds a skeleton for the REST endpoint that allows listing pending
checks. The actual implementation of the REST endpoint will be done in a
follow-up change.

This change adds:
- the boilerplate code for the new REST endpoint
- the boilerplate code for the new extension API
- a REST bindings test for the new REST endpoint
- an integration test for the new REST endpoint
- the input options of the new REST endpoint
- the definition of the JSON output
- the REST endpoint documentation

The new REST endpoint to list pending checks is exposed under:
GET /plugins/checks/checks.pending

As input a checker UUID or a checker scheme must be specified as request
parameter. If a checker scheme is specified pending checks for all
checkers with that scheme are returned.

Optionally one or multiple check states can be specified as request
parameter to define which states are considered as pending. If not
specified it's assumed that only checks in state NOT_STARTED should be
returned.

The pending checks are returned as PendingChecksInfo which has the
following fields:
- patchSet: Patch set for which checks are pending. This entity contains
  fields for the project, the change number and the patch set ID
  (further fields may be added later on need)
- pendingChecks: The pending checks on the patch set as map of checker
  UUID to PendingCheckInfo. This is a map because if a scheme is
  specified pending checks for multiple checkers may need to be
  returned.

The PendingCheckInfo has only one field with the check state. It's a
seperate entity so that we can easily add further fields in the future.
The check state must be returned because it's possible to specify
multiple check states as input and callers want to know which pending
check has which state.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I2d1688e2557d52a2d4c7057fc169dde64ed26ce4
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java b/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java
index 3f73a70..1fa9f2d 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/AbstractCheckersTest.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperations;
 import com.google.gerrit.plugins.checks.api.Checkers;
 import com.google.gerrit.plugins.checks.api.ChecksFactory;
+import com.google.gerrit.plugins.checks.api.PendingChecks;
 import org.junit.Before;
 
 // TODO(dborowitz): Improve the plugin test framework so we can avoid subclassing:
@@ -37,6 +38,7 @@
   protected CheckOperations checkOperations;
   protected Checkers checkersApi;
   protected ChecksFactory checksApiFactory;
+  protected PendingChecks pendingChecksApi;
 
   @Override
   protected ProjectResetter.Config resetProjects() {
@@ -50,6 +52,7 @@
     checkOperations = plugin.getSysInjector().getInstance(CheckOperations.class);
     checkersApi = plugin.getHttpInjector().getInstance(Checkers.class);
     checksApiFactory = plugin.getHttpInjector().getInstance(ChecksFactory.class);
+    pendingChecksApi = plugin.getHttpInjector().getInstance(PendingChecks.class);
 
     allowGlobalCapabilities(group("Administrators").getGroupUUID(), "checks-administrateCheckers");
   }
diff --git a/java/com/google/gerrit/plugins/checks/api/ApiModule.java b/java/com/google/gerrit/plugins/checks/api/ApiModule.java
index de1cb00..be4519c 100644
--- a/java/com/google/gerrit/plugins/checks/api/ApiModule.java
+++ b/java/com/google/gerrit/plugins/checks/api/ApiModule.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.plugins.checks.api.CheckResource.CHECK_KIND;
 import static com.google.gerrit.plugins.checks.api.CheckerResource.CHECKER_KIND;
+import static com.google.gerrit.plugins.checks.api.PendingCheckResource.PENDING_CHECK_KIND;
 import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
 
 import com.google.gerrit.extensions.config.FactoryModule;
@@ -33,6 +34,9 @@
     bind(CheckersCollection.class);
     bind(Checkers.class).to(CheckersImpl.class);
 
+    bind(PendingChecksCollection.class);
+    bind(PendingChecks.class).to(PendingChecksImpl.class);
+
     install(
         new RestApiModule() {
           @Override
@@ -47,6 +51,8 @@
             postOnCollection(CHECK_KIND).to(PostCheck.class);
             get(CHECK_KIND).to(GetCheck.class);
             post(CHECK_KIND).to(UpdateCheck.class);
+
+            DynamicMap.mapOf(binder(), PENDING_CHECK_KIND);
           }
         });
 
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckablePatchSetInfo.java b/java/com/google/gerrit/plugins/checks/api/CheckablePatchSetInfo.java
new file mode 100644
index 0000000..c8747e1
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckablePatchSetInfo.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+/** REST API representation of a patch set for which checks are pending. */
+public class CheckablePatchSetInfo {
+  /** Project name. */
+  public String project;
+
+  /** Change number. */
+  public int changeNumber;
+
+  /** Patch set ID. */
+  public int patchSetId;
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckerUuidHandler.java b/java/com/google/gerrit/plugins/checks/api/CheckerUuidHandler.java
new file mode 100644
index 0000000..5e563ff
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/CheckerUuidHandler.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import static com.google.gerrit.util.cli.Localizable.localizable;
+
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class CheckerUuidHandler extends OptionHandler<CheckerUuid> {
+  @Inject
+  public CheckerUuidHandler(
+      @Assisted CmdLineParser parser,
+      @Assisted OptionDef option,
+      @Assisted Setter<CheckerUuid> setter) {
+    super(parser, option, setter);
+  }
+
+  @Override
+  public int parseArguments(Parameters params) throws CmdLineException {
+    String token = params.getParameter(0);
+
+    if (!CheckerUuid.isUuid(token)) {
+      throw new CmdLineException(owner, localizable("Invalid checker UUID: %s"), token);
+    }
+
+    CheckerUuid checkerUuid = CheckerUuid.parse(token);
+    setter.addValue(checkerUuid);
+    return 1;
+  }
+
+  @Override
+  public String getDefaultMetaVariable() {
+    return "UUID";
+  }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckersRestApiServlet.java b/java/com/google/gerrit/plugins/checks/api/CheckersRestApiServlet.java
index efda053..67618c3 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckersRestApiServlet.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckersRestApiServlet.java
@@ -14,81 +14,17 @@
 
 package com.google.gerrit.plugins.checks.api;
 
-import static com.google.common.base.Preconditions.checkState;
-
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.io.IOException;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
-import javax.servlet.http.HttpServletResponse;
 
 @Singleton
-public class CheckersRestApiServlet extends RestApiServlet {
+public class CheckersRestApiServlet extends ChecksRestApiServlet {
   private static final long serialVersionUID = 1L;
 
   @Inject
   CheckersRestApiServlet(RestApiServlet.Globals globals, Provider<CheckersCollection> checkers) {
-    super(globals, checkers);
-  }
-
-  // TODO(dborowitz): Consider making
-  // RestApiServlet#service(HttpServletRequest, HttpServletResponse) non-final of overriding the
-  // non-HTTP overload.
-  @Override
-  public void service(ServletRequest servletRequest, ServletResponse servletResponse)
-      throws ServletException, IOException {
-    // This is...unfortunate. HttpPluginServlet (and/or ContextMapper) doesn't properly set the
-    // servlet path on the wrapped request. Based on what RestApiServlet produces for non-plugin
-    // requests, it should be:
-    //   contextPath = "/plugins/checks"
-    //   servletPath = "/checkers/"
-    //   pathInfo = checkerUuid
-    // Instead it does:
-    //   contextPath = "/plugins/checks"
-    //   servletPath = ""
-    //   pathInfo = "/checkers/" + checkerUuid
-    // This results in RestApiServlet splitting the pathInfo into ["", "checkers", checkerUuid], and
-    // it passes the "" to CheckersCollection#parse, which understandably, but unfortunately, fails.
-    //
-    // This frankly seems like a bug that should be fixed, but it would quite likely break existing
-    // plugins in confusing ways. So, we work around it by introducing our own request wrapper with
-    // the correct paths.
-    HttpServletRequest req = (HttpServletRequest) servletRequest;
-
-    String pathInfo = req.getPathInfo();
-    String correctServletPath = "/checkers/";
-
-    // Ensure actual request object matches the format explained above.
-    checkState(
-        req.getContextPath().endsWith("/checks"),
-        "unexpected context path: %s",
-        req.getContextPath());
-    checkState(req.getServletPath().isEmpty(), "unexpected servlet path: %s", req.getServletPath());
-    checkState(
-        req.getPathInfo().startsWith(correctServletPath),
-        "unexpected servlet path: %s",
-        req.getServletPath());
-
-    String fixedPathInfo = pathInfo.substring(correctServletPath.length());
-    HttpServletRequestWrapper wrapped =
-        new HttpServletRequestWrapper(req) {
-          @Override
-          public String getServletPath() {
-            return correctServletPath;
-          }
-
-          @Override
-          public String getPathInfo() {
-            return fixedPathInfo;
-          }
-        };
-
-    super.service(wrapped, (HttpServletResponse) servletResponse);
+    super(globals, checkers, "/checkers/");
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/ChecksRestApiServlet.java b/java/com/google/gerrit/plugins/checks/api/ChecksRestApiServlet.java
new file mode 100644
index 0000000..d8dd93c
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/ChecksRestApiServlet.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.inject.Provider;
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+public abstract class ChecksRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  private final String correctServletPath;
+
+  ChecksRestApiServlet(
+      RestApiServlet.Globals globals,
+      Provider<? extends RestCollection<? extends RestResource, ? extends RestResource>> members,
+      String correctServletPath) {
+    super(globals, members);
+    this.correctServletPath = correctServletPath;
+  }
+
+  // TODO(dborowitz): Consider making
+  // RestApiServlet#service(HttpServletRequest, HttpServletResponse) non-final of overriding the
+  // non-HTTP overload.
+  @Override
+  public void service(ServletRequest servletRequest, ServletResponse servletResponse)
+      throws ServletException, IOException {
+    // This is...unfortunate. HttpPluginServlet (and/or ContextMapper) doesn't properly set the
+    // servlet path on the wrapped request. Based on what RestApiServlet produces for non-plugin
+    // requests, it should be:
+    //   contextPath = "/plugins/checks"
+    //   servletPath = "/<collection>/"
+    //   pathInfo = <id>
+    // Instead it does:
+    //   contextPath = "/plugins/checks"
+    //   servletPath = ""
+    //   pathInfo = "/<collection>/<id>"
+    // This results in RestApiServlet splitting the pathInfo into ["", "<collection>", "<id>"], and
+    // it passes the "" to RestCollection#parse, which understandably, but unfortunately, fails.
+    //
+    // This frankly seems like a bug that should be fixed, but it would quite likely break existing
+    // plugins in confusing ways. So, we work around it by introducing our own request wrapper with
+    // the correct paths.
+    HttpServletRequest req = (HttpServletRequest) servletRequest;
+
+    String pathInfo = req.getPathInfo();
+
+    // Ensure actual request object matches the format explained above.
+    checkState(
+        req.getContextPath().endsWith("/checks"),
+        "unexpected context path: %s",
+        req.getContextPath());
+    checkState(req.getServletPath().isEmpty(), "unexpected servlet path: %s", req.getServletPath());
+    checkState(
+        req.getPathInfo().startsWith(correctServletPath),
+        "unexpected servlet path: %s",
+        req.getServletPath());
+
+    String fixedPathInfo = pathInfo.substring(correctServletPath.length());
+    HttpServletRequestWrapper wrapped =
+        new HttpServletRequestWrapper(req) {
+          @Override
+          public String getServletPath() {
+            return correctServletPath;
+          }
+
+          @Override
+          public String getPathInfo() {
+            return fixedPathInfo;
+          }
+        };
+
+    super.service(wrapped, (HttpServletResponse) servletResponse);
+  }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/HttpModule.java b/java/com/google/gerrit/plugins/checks/api/HttpModule.java
index 21ab47f..3c34503 100644
--- a/java/com/google/gerrit/plugins/checks/api/HttpModule.java
+++ b/java/com/google/gerrit/plugins/checks/api/HttpModule.java
@@ -21,5 +21,6 @@
   @Override
   protected void configureServlets() {
     serveRegex("^/checkers/(.*)$").with(CheckersRestApiServlet.class);
+    serveRegex("^/checks.pending/(.*)$").with(PendingChecksRestApiServlet.class);
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java b/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
new file mode 100644
index 0000000..53f5502
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.checks.AdministrateCheckersPermission;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+import org.kohsuke.args4j.Option;
+
+public class ListPendingChecks implements RestReadView<TopLevelResource> {
+  private final PermissionBackend permissionBackend;
+  private final AdministrateCheckersPermission permission;
+
+  private CheckerUuid checkerUuid;
+  private String scheme;
+  private List<CheckState> states = new ArrayList<>(CheckState.values().length);
+
+  @Option(
+      name = "--checker",
+      metaVar = "UUID",
+      usage = "checker UUID formated as '<scheme>:<id>'",
+      handler = CheckerUuidHandler.class)
+  public void setChecker(CheckerUuid checkerUuid) {
+    this.checkerUuid = checkerUuid;
+  }
+
+  @Option(name = "--scheme", metaVar = "SCHEME", usage = "checker scheme")
+  public void setScheme(String scheme) {
+    this.scheme = scheme;
+  }
+
+  @Option(name = "--state", metaVar = "STATE", usage = "check state")
+  public void addState(CheckState state) {
+    this.states.add(state);
+  }
+
+  @Inject
+  public ListPendingChecks(
+      PermissionBackend permissionBackend, AdministrateCheckersPermission permission) {
+    this.permissionBackend = permissionBackend;
+    this.permission = permission;
+  }
+
+  @Override
+  public List<PendingChecksInfo> apply(TopLevelResource resource)
+      throws RestApiException, PermissionBackendException {
+    permissionBackend.currentUser().check(permission);
+
+    if (states.isEmpty()) {
+      // If no state was specified, assume NOT_STARTED by default.
+      states.add(CheckState.NOT_STARTED);
+    }
+
+    if (checkerUuid == null && scheme == null) {
+      throw new BadRequestException("checker or scheme is required");
+    }
+
+    if (checkerUuid != null && scheme != null) {
+      throw new BadRequestException("checker and scheme are mutually exclusive");
+    }
+
+    // TODO(ekempin): Implement this REST endpoint
+    throw new MethodNotAllowedException("not implemented");
+  }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingCheckInfo.java b/java/com/google/gerrit/plugins/checks/api/PendingCheckInfo.java
new file mode 100644
index 0000000..5e46bde
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/PendingCheckInfo.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+/**
+ * REST API representation of a pending check.
+ *
+ * <p>Checks are pending if they are in a non-final state and the external checker system intends to
+ * post further updates on them. Which states these are depends on the external checker system, by
+ * default we only consider checks in state {@link CheckState#NOT_STARTED} as pending.
+ */
+public class PendingCheckInfo {
+  /** State of the check. */
+  public CheckState state;
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingCheckResource.java b/java/com/google/gerrit/plugins/checks/api/PendingCheckResource.java
new file mode 100644
index 0000000..74e9573
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/PendingCheckResource.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class PendingCheckResource implements RestResource {
+  public static final TypeLiteral<RestView<PendingCheckResource>> PENDING_CHECK_KIND =
+      new TypeLiteral<RestView<PendingCheckResource>>() {};
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingChecks.java b/java/com/google/gerrit/plugins/checks/api/PendingChecks.java
new file mode 100644
index 0000000..8b0811b
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/PendingChecks.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import java.util.List;
+
+public interface PendingChecks {
+  /**
+   * Lists the pending checks for the specified checker.
+   *
+   * @param checkerUuid the UUID of the checker for which pending checks should be listed
+   * @param checkStates the states that should be considered as pending, if not specified {@link
+   *     CheckState#NOT_STARTED} is assumed.
+   * @return the pending checks
+   */
+  List<PendingChecksInfo> list(CheckerUuid checkerUuid, CheckState... checkStates)
+      throws RestApiException;
+
+  /**
+   * Lists the pending checks for the specified checker.
+   *
+   * @param checkerUuidString the UUID of the checker for which pending checks should be listed
+   * @param checkStates the states that should be considered as pending, if not specified {@link
+   *     CheckState#NOT_STARTED} is assumed.
+   * @return the pending checks
+   */
+  default List<PendingChecksInfo> list(String checkerUuidString, CheckState... checkStates)
+      throws RestApiException {
+    return list(
+        CheckerUuid.tryParse(checkerUuidString)
+            .orElseThrow(
+                () ->
+                    new BadRequestException(
+                        String.format("invalid checker UUID: %s", checkerUuidString))),
+        checkStates);
+  }
+
+  /**
+   * Lists the pending checks for all checkers of the specified checker scheme.
+   *
+   * @param scheme the checker scheme for which pending checks should be listed
+   * @param checkStates the states that should be considered as pending, if not specified {@link
+   *     CheckState#NOT_STARTED} is assumed.
+   * @return the pending checks
+   */
+  List<PendingChecksInfo> listForScheme(String scheme, CheckState... checkStates)
+      throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility when adding new methods to the
+   * interface.
+   */
+  class NotImplemented implements PendingChecks {
+    @Override
+    public List<PendingChecksInfo> list(CheckerUuid checkerUuid, CheckState... checkStates) {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<PendingChecksInfo> listForScheme(String scheme, CheckState... checkStates) {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingChecksCollection.java b/java/com/google/gerrit/plugins/checks/api/PendingChecksCollection.java
new file mode 100644
index 0000000..a8363d1
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/PendingChecksCollection.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+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.extensions.restapi.TopLevelResource;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class PendingChecksCollection
+    implements ChildCollection<TopLevelResource, PendingCheckResource> {
+  private final DynamicMap<RestView<PendingCheckResource>> views;
+  private final ListPendingChecks listPendingChecks;
+
+  @Inject
+  public PendingChecksCollection(
+      DynamicMap<RestView<PendingCheckResource>> views, ListPendingChecks listPendingChecks) {
+    this.views = views;
+    this.listPendingChecks = listPendingChecks;
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() throws RestApiException {
+    return listPendingChecks;
+  }
+
+  @Override
+  public PendingCheckResource parse(TopLevelResource parent, IdString id)
+      throws ResourceNotFoundException {
+    throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DynamicMap<RestView<PendingCheckResource>> views() {
+    return views;
+  }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingChecksImpl.java b/java/com/google/gerrit/plugins/checks/api/PendingChecksImpl.java
new file mode 100644
index 0000000..0b9c139
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/PendingChecksImpl.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.List;
+import java.util.stream.Stream;
+
+@Singleton
+public class PendingChecksImpl implements PendingChecks {
+  private final Provider<ListPendingChecks> listPendingChecksProvider;
+
+  @Inject
+  PendingChecksImpl(Provider<ListPendingChecks> listPendingChecksProvider) {
+    this.listPendingChecksProvider = listPendingChecksProvider;
+  }
+
+  @Override
+  public List<PendingChecksInfo> list(CheckerUuid checkerUuid, CheckState... checkStates)
+      throws RestApiException {
+    try {
+      ListPendingChecks listPendingChecks = listPendingChecksProvider.get();
+      listPendingChecks.setChecker(checkerUuid);
+      Stream.of(checkStates).forEach(listPendingChecks::addState);
+      return listPendingChecks.apply(TopLevelResource.INSTANCE);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list pending checks", e);
+    }
+  }
+
+  @Override
+  public List<PendingChecksInfo> listForScheme(String scheme, CheckState... checkStates)
+      throws RestApiException {
+    try {
+      ListPendingChecks listPendingChecks = listPendingChecksProvider.get();
+      listPendingChecks.setScheme(scheme);
+      Stream.of(checkStates).forEach(listPendingChecks::addState);
+      return listPendingChecks.apply(TopLevelResource.INSTANCE);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot list pending checks for scheme", e);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingChecksInfo.java b/java/com/google/gerrit/plugins/checks/api/PendingChecksInfo.java
new file mode 100644
index 0000000..01944b6
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/PendingChecksInfo.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import java.util.Map;
+
+/** REST API representation of pending checks on patch set. */
+public class PendingChecksInfo {
+  /** Patch set for which the checks are pending. */
+  public CheckablePatchSetInfo patchSet;
+
+  /** Pending checks on the patch set by checker UUID. */
+  public Map<CheckerUuid, PendingCheckInfo> pendingChecks;
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingChecksRestApiServlet.java b/java/com/google/gerrit/plugins/checks/api/PendingChecksRestApiServlet.java
new file mode 100644
index 0000000..fb7abbe
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/PendingChecksRestApiServlet.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class PendingChecksRestApiServlet extends ChecksRestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Inject
+  PendingChecksRestApiServlet(
+      RestApiServlet.Globals globals, Provider<PendingChecksCollection> pendingChecks) {
+    super(globals, pendingChecks, "/checks.pending/");
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
index 9130d5b..6df71c0 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
@@ -24,6 +24,11 @@
 import org.junit.Test;
 
 public class ChecksRestApiBindingsIT extends AbstractCheckersTest {
+  private static final ImmutableList<RestCall> ROOT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.post("/plugins/checks/checkers/"),
+          RestCall.get("/plugins/checks/checks.pending/"));
+
   private static final ImmutableList<RestCall> CHECKER_ENDPOINTS =
       ImmutableList.of(
           RestCall.get("/plugins/checks/checkers/%s"),
@@ -38,14 +43,14 @@
           RestCall.post("/changes/%s/revisions/%s/checks~checks/%s"));
 
   @Test
-  public void checkerEndpoints() throws Exception {
-    CheckerUuid checkerUuid = checkerOperations.newChecker().create();
-    RestApiCallHelper.execute(adminRestSession, CHECKER_ENDPOINTS, checkerUuid.toString());
+  public void rootEndpoints() throws Exception {
+    RestApiCallHelper.execute(adminRestSession, ROOT_ENDPOINTS);
   }
 
   @Test
-  public void postOnCheckerCollectionForCreate() throws Exception {
-    RestApiCallHelper.execute(adminRestSession, RestCall.post("/plugins/checks/checkers/"));
+  public void checkerEndpoints() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().create();
+    RestApiCallHelper.execute(adminRestSession, CHECKER_ENDPOINTS, checkerUuid.toString());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
new file mode 100644
index 0000000..7cb8d53
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2019 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.plugins.checks.acceptance.api;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+public class ListPendingChecksIT extends AbstractCheckersTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+
+  @Test
+  public void specifyingEitherCheckerUuidOrSchemeIsRequired() throws Exception {
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("checker or scheme is required");
+    pendingChecksApi.listForScheme(null);
+  }
+
+  @Test
+  public void cannotListPendingChecksForMalformedCheckerUuid() throws Exception {
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("invalid checker UUID: malformed::checker*UUID");
+    pendingChecksApi.list("malformed::checker*UUID");
+  }
+
+  @Test
+  public void cannotSpecifyCheckerUuidAndScheme() throws Exception {
+    // The extension API doesn't allow to specify checker UUID and scheme at the same time. Call the
+    // endpoint over REST to test this.
+    RestResponse response =
+        adminRestSession.get(
+            String.format(
+                "/plugins/checks/checks.pending/?checker=%s&scheme=%s", "foo:bar", "foo"));
+    response.assertBadRequest();
+    assertThat(response.getEntityContent()).isEqualTo("checker and scheme are mutually exclusive");
+  }
+
+  @Test
+  public void cannotListPendingChecksWithoutAdministrateCheckers() throws Exception {
+    requestScopeOperations.setApiUser(user.getId());
+
+    exception.expect(AuthException.class);
+    exception.expectMessage("not permitted");
+    pendingChecksApi.list("foo:bar");
+  }
+
+  @Test
+  public void listPendingChecksForCheckerNotImplemented() throws Exception {
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("not implemented");
+    pendingChecksApi.list("foo:bar");
+  }
+
+  @Test
+  public void listPendingChecksForSchemeNotImplemented() throws Exception {
+    exception.expect(MethodNotAllowedException.class);
+    exception.expectMessage("not implemented");
+    pendingChecksApi.listForScheme("foo");
+  }
+}
diff --git a/src/main/resources/Documentation/rest-api-pending-checks.md b/src/main/resources/Documentation/rest-api-pending-checks.md
new file mode 100644
index 0000000..39e2a1a
--- /dev/null
+++ b/src/main/resources/Documentation/rest-api-pending-checks.md
@@ -0,0 +1,160 @@
+# @PLUGIN@ - /checks.pending/ REST API
+
+This page describes the pending-checks-related REST endpoints that are
+added by the @PLUGIN@ plugin.
+
+Please also take note of the general information on the
+[REST API](../../../Documentation/rest-api.html).
+
+## <a id="pending-checks-endpoints"> Pending Checks Endpoints
+
+### <a id="get-checker"> List Pending Checks
+_'GET /checks.pending/'_
+
+Lists pending checks for a checker or for all checkers of a scheme.
+
+Checks are pending if they are in a non-final state and the external
+checker system intends to post further updates on them.
+
+By default this REST endpoint only returns checks that are in state
+`NOT_STARTED` but callers may specify the states that they are
+interested in (see [state](#state-param) request parameter).
+
+Request parameters:
+
+* <a id="checker-param"> `checker`: the UUID of the checker for which
+  pending checks should be listed (optional, if not specified `scheme`
+  must be set)
+* <a id="scheme-param"> `scheme`: the scheme of the checkers for which
+  pending checks should be listed (optional, if not specified `checker`
+  must be set)
+* <a id="state-param"> `state`: state that should be considered as
+  pending (optional, by default the state `NOT_STARTED` is assumed,
+  this option may be specified multiple times to request checks
+  matching any of several states)
+
+Note that only users with the [Administrate
+Checkers](access-control.md#capability_administrateCheckers) global capability
+are permitted to list pending checks.
+
+#### Request by checker
+
+```
+  GET /checks.pending/?checker=test:my-checker&state=NOT_STARTED&state=SCHEDULED HTTP/1.0
+```
+
+As response a list of [PendingChecksInfo](#pending-checks-info)
+entities is returned that describes the pending checks.
+
+#### Response by checker
+
+```
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+  )]}'
+  [
+    {
+      "patch_set": {
+        "project": "test-project",
+        "change_number": 1,
+        "patch_set_id": 1,
+      }
+      "pending_checks": {
+        "test:my-checker": {
+          "state": "NOT_STARTED",
+        }
+      }
+    },
+    {
+      "patch_set": {
+        "project": "test-project",
+        "change_number": 5,
+        "patch_set_id": 2,
+      }
+      "pending_checks": {
+        "test:my-checker": {
+          "state": "SCHEDULED",
+        }
+      }
+    }
+  ]
+```
+
+#### Request by checker scheme
+
+```
+  GET /checks.pending/?scheme=test&state=NOT_STARTED&state=SCHEDULED HTTP/1.0
+```
+
+As response a list of [PendingChecksInfo](#pending-checks-info)
+entities is returned that describes the pending checks.
+
+#### Response by checker scheme
+
+```
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+  )]}'
+  [
+    {
+      "patch_set": {
+        "project": "test-project",
+        "change_number": 1,
+        "patch_set_id": 1,
+      }
+      "pending_checks": {
+        "test:my-checker": {
+          "state": "NOT_STARTED",
+        },
+        "test:my-other-checker": {
+          "state": "SCHEDULED",
+        }
+      }
+    },
+    {
+      "patch_set": {
+        "project": "test-project",
+        "change_number": 5,
+        "patch_set_id": 2,
+      }
+      "pending_checks": {
+        "test:my-checker": {
+          "state": "NOT_STARTED",
+        },
+        "test:my-other-checker": {
+          "state": "NOT_STARTED",
+        }
+      }
+    }
+  ]
+```
+
+## <a id="json-entities"> JSON Entities
+
+### <a id="checkable-patch-set-info"> CheckablePatchSetInfo
+The `CheckablePatchSetInfo` entity describes a patch set for which
+checks are pending.
+
+| Field Name      | Description |
+| --------------- | ----------- |
+| `project`       | The project name that this pending check applies to.
+| `change_number` | The change number that this pending check applies to.
+| `patch_set_id`  | The ID of the patch set that this pending check applies to.
+
+### <a id="pending-check-info"> PendingCheckInfo
+The `PendingCheckInfo` entity describes a pending check.
+
+| Field Name | Description |
+| ---------- | ----------- |
+| `state`    | The [state](./rest-api-checks.md#check-state) of the pending check
+
+### <a id="pending-checks-info"> PendingChecksInfo
+The `PendingChecksInfo` entity describes the pending checks on patch set.
+
+| Field Name       | Description |
+| ---------------- | ----------- |
+| `patch_set`      | The patch set for checks are pending as [CheckablePatchSetInfo](#checkable-patch-set-info) entity.
+| `pending_checks` | The checks that are pending for the patch set as [checker UUID](./rest-api-checkers.md#checker-id) to [PendingCheckInfo](#pending-check-info) entity.
+