Create REST stub for Git LFS 2.0 Lock API

The change introduces `locks` endpoint with stubs for the following
operations (according to [1]):
  POST /locks - create lock
  GET /locks - list locks
  POST /locks/verify - list locks for verification
  POST /locks/lock_id/unlock - delete lock

[1] https://github.com/git-lfs/git-lfs/blob/master/docs/api/locking.md
Depends-On: I8299000c827b5a34d6de1ed5fc650f74be4164a2
Change-Id: I7e2debbc7a3b63694139f4759805863b37e1fd48
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/HttpModule.java
index eb5132f..1ab3cb7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/HttpModule.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.lfs;
 
 import static com.googlesource.gerrit.plugins.lfs.LfsApiServlet.LFS_OBJECTS_REGEX_REST;
+import static com.googlesource.gerrit.plugins.lfs.locks.LfsLocksServlet.LFS_LOCKS_REGEX_REST;
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.JavaScriptPlugin;
@@ -23,6 +24,7 @@
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.lfs.fs.LfsFsContentServlet;
 import com.googlesource.gerrit.plugins.lfs.fs.LocalLargeFileRepository;
+import com.googlesource.gerrit.plugins.lfs.locks.LfsLocksServlet;
 import com.googlesource.gerrit.plugins.lfs.s3.S3LargeFileRepository;
 import java.util.Map;
 
@@ -54,6 +56,7 @@
   @Override
   protected void configureServlets() {
     serveRegex(LFS_OBJECTS_REGEX_REST).with(LfsApiServlet.class);
+    serveRegex(LFS_LOCKS_REGEX_REST).with(LfsLocksServlet.class);
     populateRepository(defaultBackend);
     for (LfsBackend backend : backends.values()) {
       populateRepository(backend);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsCreateLockInput.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsCreateLockInput.java
new file mode 100644
index 0000000..c2fd783
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsCreateLockInput.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2017 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.lfs.locks;
+
+public class LfsCreateLockInput {
+  public final String path;
+
+  LfsCreateLockInput(String path) {
+    this.path = path;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsDeleteLockInput.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsDeleteLockInput.java
new file mode 100644
index 0000000..1dc5432
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsDeleteLockInput.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2017 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.lfs.locks;
+
+public class LfsDeleteLockInput {
+  public final Boolean force;
+
+  LfsDeleteLockInput(Boolean force) {
+    this.force = force;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksResponse.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksResponse.java
new file mode 100644
index 0000000..3dc3056
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksResponse.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2017 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.lfs.locks;
+
+import java.util.List;
+
+public class LfsGetLocksResponse {
+  public final List<LfsLock> locks;
+  public final String nextCursor;
+
+  LfsGetLocksResponse(List<LfsLock> locks, String nextCursor) {
+    this.locks = locks;
+    this.nextCursor = nextCursor;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLock.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLock.java
new file mode 100644
index 0000000..8abf989
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLock.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2017 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.lfs.locks;
+
+public class LfsLock {
+  public final String id;
+  public final String path;
+  public final String lockedAt;
+  public final LfsLockOwner owner;
+
+  LfsLock(String id, String path, String lockedAt, LfsLockOwner owner) {
+    this.id = id;
+    this.path = path;
+    this.lockedAt = lockedAt;
+    this.owner = owner;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockOwner.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockOwner.java
new file mode 100644
index 0000000..7bec022
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockOwner.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2017 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.lfs.locks;
+
+public class LfsLockOwner {
+  public final String name;
+
+  LfsLockOwner(String name) {
+    this.name = name;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksServlet.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksServlet.java
new file mode 100644
index 0000000..6195ded
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksServlet.java
@@ -0,0 +1,281 @@
+// Copyright (C) 2017 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.lfs.locks;
+
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.CONTENTTYPE_VND_GIT_LFS_JSON;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_LOCKS_PATH_REGEX;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_URL_REGEX_TEMPLATE;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_VERIFICATION_PATH;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.http.HttpStatus.SC_OK;
+
+import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Singleton;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class LfsLocksServlet extends HttpServlet {
+  private static final Logger log = LoggerFactory.getLogger(LfsLocksServlet.class);
+  private static final long serialVersionUID = 1L;
+  private static final Pattern LFS_LOCKS_URL_PATTERN =
+      Pattern.compile(String.format(LFS_URL_REGEX_TEMPLATE, LFS_LOCKS_PATH_REGEX));
+  private static final Pattern LFS_VERIFICATION_URL_PATTERN =
+      Pattern.compile(String.format(LFS_URL_REGEX_TEMPLATE, LFS_VERIFICATION_PATH));
+  private static final DateTimeFormatter ISO = ISODateTimeFormat.dateTime();
+
+  public static final String LFS_LOCKS_REGEX_REST =
+      String.format(LFS_URL_REGEX_TEMPLATE, LFS_LOCKS_PATH_REGEX + "|" + LFS_VERIFICATION_PATH);
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    Action action = new Action(req, resp);
+    Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(action.path);
+    if (matcher.matches()) {
+      String project = matcher.group(1);
+      listLocks(project, action);
+      return;
+    }
+
+    action.sendError(
+        SC_INTERNAL_SERVER_ERROR, String.format("Unsupported path %s was provided", action.path));
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    Action action = new Action(req, resp);
+    Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(action.path);
+    if (matcher.matches()) {
+      String project = matcher.group(1);
+      String lockId = matcher.group(2);
+      if (Strings.isNullOrEmpty(lockId)) {
+        createLock(project, action);
+      } else {
+        deleteLock(project, lockId, action);
+      }
+      return;
+    }
+
+    matcher = LFS_VERIFICATION_URL_PATTERN.matcher(action.path);
+    if (matcher.matches()) {
+      verifyLocks(matcher.group(1), action);
+      return;
+    }
+
+    action.sendError(
+        SC_INTERNAL_SERVER_ERROR, String.format("Unsupported path %s was provided", action.path));
+  }
+
+  private void verifyLocks(String project, Action action) throws IOException {
+    log.debug("Verify list of locks for {} project", project);
+    //TODO method stub for verifying locks
+    action.sendResponse(
+        new LfsVerifyLocksResponse(Collections.emptyList(), Collections.emptyList(), null));
+  }
+
+  private void listLocks(String project, Action action) throws IOException {
+    log.debug("Get list of locks for {} project", project);
+    //TODO method stub for getting project's locks list
+
+    // stub for searching lock by path
+    String path = action.getParam("path");
+    if (!Strings.isNullOrEmpty(path)) {
+      action.sendResponse(
+          new LfsGetLocksResponse(
+              ImmutableList.<LfsLock>builder()
+                  .add(
+                      new LfsLock(
+                          "random_id",
+                          path,
+                          now(),
+                          new LfsLockOwner("Lock Owner <lock_owner@example.com>")))
+                  .build(),
+              null));
+      return;
+    }
+
+    // stub for searching lock by id
+    String id = action.getParam("id");
+    if (!Strings.isNullOrEmpty(id)) {
+      action.sendResponse(
+          new LfsGetLocksResponse(
+              ImmutableList.<LfsLock>builder()
+                  .add(
+                      new LfsLock(
+                          id,
+                          "path/to/file",
+                          now(),
+                          new LfsLockOwner("Lock Owner <lock_owner@example.com>")))
+                  .build(),
+              null));
+      return;
+    }
+
+    // stub for returning all locks
+    action.sendResponse(new LfsGetLocksResponse(Collections.emptyList(), null));
+  }
+
+  private void deleteLock(String project, String lockId, Action action) throws IOException {
+    LfsDeleteLockInput input = action.input(LfsDeleteLockInput.class);
+    log.debug(
+        "Delete (-f {}) lock for {} in project {}",
+        Boolean.TRUE.equals(input.force),
+        lockId,
+        project);
+    //TODO: this is just the method stub for lock deletion
+    LfsLock lock =
+        new LfsLock(
+            "random_id",
+            "some/path/to/file",
+            now(),
+            new LfsLockOwner("Lock Owner <lock_owner@example.com>"));
+    action.sendResponse(lock);
+  }
+
+  private void createLock(String project, Action action) throws IOException {
+    LfsCreateLockInput input = action.input(LfsCreateLockInput.class);
+    log.debug("Create lock for {} in project {}", input.path, project);
+    //TODO: this is just the method stub lock creation
+    LfsLock lock =
+        new LfsLock(
+            "random_id",
+            input.path,
+            now(),
+            new LfsLockOwner("Lock Owner <lock_owner@example.com>"));
+    action.sendResponse(lock);
+  }
+
+  private String now() {
+    return ISO.print(DateTime.now().toDateTime(DateTimeZone.UTC));
+  }
+
+  private class Action {
+    public final String path;
+
+    private final HttpServletRequest req;
+    private final HttpServletResponse res;
+    private final Supplier<Writer> writer;
+    private final Supplier<Reader> reader;
+    private final Gson gson;
+
+    private Action(final HttpServletRequest req, final HttpServletResponse res) {
+      this.path = req.getPathInfo().startsWith("/") ? req.getPathInfo() : "/" + req.getPathInfo();
+      this.req = req;
+      this.res = res;
+      this.writer =
+          Suppliers.memoize(
+              new Supplier<Writer>() {
+                @Override
+                public Writer get() {
+                  try {
+                    return new BufferedWriter(new OutputStreamWriter(res.getOutputStream(), UTF_8));
+                  } catch (IOException e) {
+                    throw new RuntimeException(e);
+                  }
+                }
+              });
+      this.reader =
+          Suppliers.memoize(
+              new Supplier<Reader>() {
+                @Override
+                public Reader get() {
+                  try {
+                    return new BufferedReader(new InputStreamReader(req.getInputStream(), UTF_8));
+                  } catch (IOException e) {
+                    throw new RuntimeException(e);
+                  }
+                }
+              });
+      this.gson = createGson();
+      setLfsResponseType();
+    }
+
+    String getParam(String name) {
+      return req.getParameter(name);
+    }
+
+    <T> T input(Class<T> clazz) {
+      return gson.fromJson(getReader(), clazz);
+    }
+
+    <T> void sendResponse(T content) throws IOException {
+      res.setStatus(SC_OK);
+      gson.toJson(content, getWriter());
+      getWriter().flush();
+    }
+
+    void sendError(int status, String message) throws IOException {
+      log.error(message);
+      res.setStatus(status);
+      gson.toJson(new Error(message), getWriter());
+      getWriter().flush();
+    }
+
+    Writer getWriter() {
+      return writer.get();
+    }
+
+    Reader getReader() {
+      return reader.get();
+    }
+
+    void setLfsResponseType() {
+      res.setContentType(CONTENTTYPE_VND_GIT_LFS_JSON);
+    }
+
+    private Gson createGson() {
+      return new GsonBuilder()
+          .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+          .disableHtmlEscaping()
+          .create();
+    }
+  }
+
+  /** copied from org.eclipse.jgit.lfs.server.LfsProtocolServlet.Error */
+  static class Error {
+    String message;
+
+    Error(String m) {
+      this.message = m;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksInput.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksInput.java
new file mode 100644
index 0000000..2819d73
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksInput.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2017 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.lfs.locks;
+
+public class LfsVerifyLocksInput {
+  public final String cursor;
+  public final Integer limit;
+
+  LfsVerifyLocksInput(String cursor, Integer limit) {
+    this.cursor = cursor;
+    this.limit = limit;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksResponse.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksResponse.java
new file mode 100644
index 0000000..d55ed0f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksResponse.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2017 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.lfs.locks;
+
+import java.util.List;
+
+public class LfsVerifyLocksResponse {
+  public final List<LfsLock> ours;
+  public final List<LfsLock> theirs;
+  public final String nextCursor;
+
+  LfsVerifyLocksResponse(List<LfsLock> ours, List<LfsLock> theirs, String nextCursor) {
+    this.ours = ours;
+    this.theirs = theirs;
+    this.nextCursor = nextCursor;
+  }
+}