Unify exceptions handling and error reporting with LfsApiServlet

Introduce abstract LfsLocksAction (refactor LfsLocksServlet) so that
exception handling is based on LfsException handling (enables re-use of
existing LfsException derived classes in the following changes) which is
in line with LfsApiServlet.
Send errors back in the way that it is understood by Git LFS client
(again in line with LfsApiServlet).

Change-Id: Id32cada65d441b148a6611a3477b4b2dd56cc5b9
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/Module.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/Module.java
index 089ebc4..a6769a0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/Module.java
@@ -22,6 +22,7 @@
 import com.google.inject.internal.UniqueAnnotations;
 import com.googlesource.gerrit.plugins.lfs.fs.LfsFsContentServlet;
 import com.googlesource.gerrit.plugins.lfs.fs.LocalLargeFileRepository;
+import com.googlesource.gerrit.plugins.lfs.locks.LfsLocksModule;
 import com.googlesource.gerrit.plugins.lfs.s3.S3LargeFileRepository;
 
 public class Module extends FactoryModule {
@@ -43,5 +44,6 @@
     factory(S3LargeFileRepository.Factory.class);
     factory(LocalLargeFileRepository.Factory.class);
     factory(LfsFsContentServlet.Factory.class);
+    install(new LfsLocksModule());
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java
new file mode 100644
index 0000000..4314f77
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java
@@ -0,0 +1,97 @@
+// 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.LFS_LOCKS_PATH_REGEX;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_URL_REGEX_TEMPLATE;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.lfs.errors.LfsException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LfsGetLocksAction extends LfsLocksAction {
+  interface Factory extends LfsLocksAction.Factory<LfsGetLocksAction> {}
+
+  static final Pattern LFS_LOCKS_URL_PATTERN =
+      Pattern.compile(String.format(LFS_URL_REGEX_TEMPLATE, LFS_LOCKS_PATH_REGEX));
+
+  private static final Logger log = LoggerFactory.getLogger(LfsGetLocksAction.class);
+
+  @Inject
+  LfsGetLocksAction(@Assisted LfsLocksContext context) {
+    super(context);
+  }
+
+  @Override
+  protected void doRun() throws LfsException, IOException {
+    Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(context.path);
+    if (matcher.matches()) {
+      String project = matcher.group(1);
+      listLocks(project);
+    }
+
+    throw new LfsException("no repository at " + context.path);
+  }
+
+  private void listLocks(String project) 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 = context.getParam("path");
+    if (!Strings.isNullOrEmpty(path)) {
+      context.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 = context.getParam("id");
+    if (!Strings.isNullOrEmpty(id)) {
+      context.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
+    context.sendResponse(new LfsGetLocksResponse(Collections.emptyList(), null));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java
new file mode 100644
index 0000000..1408348
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java
@@ -0,0 +1,52 @@
+// 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 org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+
+import java.io.IOException;
+import org.eclipse.jgit.lfs.errors.LfsException;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+
+abstract class LfsLocksAction {
+  interface Factory<T extends LfsLocksAction> {
+    T create(LfsLocksContext context);
+  }
+
+  private static final DateTimeFormatter ISO = ISODateTimeFormat.dateTime();
+
+  protected final LfsLocksContext context;
+
+  protected LfsLocksAction(LfsLocksContext context) {
+    this.context = context;
+  }
+
+  public void run() throws IOException {
+    try {
+      doRun();
+    } catch (LfsException e) {
+      context.sendError(SC_INTERNAL_SERVER_ERROR, e.getMessage());
+    }
+  }
+
+  protected abstract void doRun() throws LfsException, IOException;
+
+  protected String now() {
+    return ISO.print(DateTime.now().toDateTime(DateTimeZone.UTC));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java
new file mode 100644
index 0000000..309c252
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java
@@ -0,0 +1,129 @@
+// 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 java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.http.HttpStatus.SC_OK;
+
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class LfsLocksContext {
+  private static final Logger log = LoggerFactory.getLogger(LfsLocksContext.class);
+
+  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;
+
+  LfsLocksContext(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/LfsLocksModule.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksModule.java
new file mode 100644
index 0000000..5aa3507
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksModule.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;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+
+public class LfsLocksModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    factory(LfsGetLocksAction.Factory.class);
+    factory(LfsPutLocksAction.Factory.class);
+  }
+}
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
index 6195ded..9cdeb62 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksServlet.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksServlet.java
@@ -14,268 +14,45 @@
 
 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.Inject;
 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);
 
+  private final LfsGetLocksAction.Factory getters;
+  private final LfsPutLocksAction.Factory putters;
+
+  @Inject
+  LfsLocksServlet(LfsGetLocksAction.Factory getters, LfsPutLocksAction.Factory putters) {
+    this.getters = getters;
+    this.putters = putters;
+  }
+
   @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));
+    LfsLocksContext context = new LfsLocksContext(req, resp);
+    getters.create(context).run();
   }
 
   @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;
-    }
+    LfsLocksContext context = new LfsLocksContext(req, resp);
+    putters.create(context).run();
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java
new file mode 100644
index 0000000..c26a75c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java
@@ -0,0 +1,104 @@
+// 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.LFS_URL_REGEX_TEMPLATE;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_VERIFICATION_PATH;
+import static com.googlesource.gerrit.plugins.lfs.locks.LfsGetLocksAction.LFS_LOCKS_URL_PATTERN;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.lfs.errors.LfsException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LfsPutLocksAction extends LfsLocksAction {
+  interface Factory extends LfsLocksAction.Factory<LfsPutLocksAction> {}
+
+  private static final Logger log = LoggerFactory.getLogger(LfsPutLocksAction.class);
+  private static final Pattern LFS_VERIFICATION_URL_PATTERN =
+      Pattern.compile(String.format(LFS_URL_REGEX_TEMPLATE, LFS_VERIFICATION_PATH));
+
+  @Inject
+  LfsPutLocksAction(@Assisted LfsLocksContext context) {
+    super(context);
+  }
+
+  @Override
+  protected void doRun() throws LfsException, IOException {
+    Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(context.path);
+    if (matcher.matches()) {
+      String project = matcher.group(1);
+      String lockId = matcher.group(2);
+      if (Strings.isNullOrEmpty(lockId)) {
+        createLock(project, context);
+      } else {
+        deleteLock(project, lockId, context);
+      }
+      return;
+    }
+
+    matcher = LFS_VERIFICATION_URL_PATTERN.matcher(context.path);
+    if (matcher.matches()) {
+      verifyLocks(matcher.group(1), context);
+      return;
+    }
+
+    throw new LfsException(String.format("Unsupported path %s was provided", context.path));
+  }
+
+  private void verifyLocks(String project, LfsLocksContext 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 deleteLock(String project, String lockId, LfsLocksContext 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, LfsLocksContext 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);
+  }
+}