Implement Git LFS 2.0 locking

This patch introduces persistency to Git LFS 2.0 locking. Lock is
represented by the following JSON object:
  {
     "id":"lock_id",
     "path":"IMG_1226.PNG.jpg",
     "locked_at":"2017-05-05T14:40:44.683Z",
     "owner":{
        "name":"admin"
     }
  }

and is stored under plugin data directory:
$GERRIT_SITE/data/lfs/lfs_locks/{project}/ as regular file with "id"
used as filename.

Design decisions:
LFS locks are kept in 2 levels caches. First level contains project to
locks mapping and is loaded on first request to lock API for given
project. Second level contains lock id to lock data (path, owner, etc.)
mapping that is backed up by lock id named file (mentioned above).

Change-Id: I434fe7de9f5b2561167e38d6518feb91a03b747d
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
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
index c08d937..e30703e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java
@@ -18,7 +18,7 @@
 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.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
@@ -27,13 +27,10 @@
 import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.lfs.LfsAuthUserProvider;
 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.eclipse.jgit.lfs.errors.LfsUnauthorized;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class LfsGetLocksAction extends LfsLocksAction {
   interface Factory extends LfsLocksAction.Factory<LfsGetLocksAction> {}
@@ -41,14 +38,13 @@
   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(
       ProjectCache projectCache,
       LfsAuthUserProvider userProvider,
+      LfsLocksHandler handler,
       @Assisted LfsLocksContext context) {
-    super(projectCache, userProvider, context);
+    super(projectCache, userProvider, handler, context);
   }
 
   @Override
@@ -70,48 +66,19 @@
 
   @Override
   protected void doRun(ProjectState project, CurrentUser user) throws LfsException, IOException {
-    listLocks(project);
-  }
-
-  private void listLocks(ProjectState project) throws IOException {
-    log.debug("Get list of locks for {} project", project.getProject().getName());
-    //TODO method stub for getting project's locks list
-
-    // stub for searching lock by path
+    Project.NameKey name = project.getProject().getNameKey();
     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));
+      context.sendResponse(handler.listLocksByPath(name, path));
       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));
+      context.sendResponse(handler.listLocksById(name, id));
       return;
     }
 
-    // stub for returning all locks
-    context.sendResponse(new LfsGetLocksResponse(Collections.emptyList(), null));
+    context.sendResponse(handler.listLocks(name));
   }
 }
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
index 3dc3056..e6a9b0a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksResponse.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksResponse.java
@@ -14,13 +14,13 @@
 
 package com.googlesource.gerrit.plugins.lfs.locks;
 
-import java.util.List;
+import java.util.Collection;
 
 public class LfsGetLocksResponse {
-  public final List<LfsLock> locks;
+  public final Collection<LfsLock> locks;
   public final String nextCursor;
 
-  LfsGetLocksResponse(List<LfsLock> locks, String nextCursor) {
+  LfsGetLocksResponse(Collection<LfsLock> locks, String nextCursor) {
     this.locks = locks;
     this.nextCursor = nextCursor;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockFile.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockFile.java
new file mode 100644
index 0000000..6d7f489
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockFile.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 org.eclipse.jgit.internal.storage.file.LockFile;
+
+public class LfsLockFile {
+  public final LfsLock lfsLock;
+  public final LockFile fileLock;
+
+  LfsLockFile(LfsLock lfsLock, LockFile fileLock) {
+    this.lfsLock = lfsLock;
+    this.fileLock = fileLock;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockResponse.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockResponse.java
new file mode 100644
index 0000000..a82e71d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockResponse.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 LfsLockResponse {
+  public final LfsLock lock;
+
+  LfsLockResponse(LfsLock lock) {
+    this.lock = lock;
+  }
+}
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
index 4384c6d..8a13514 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.lfs.locks;
 
 import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
+import static org.apache.http.HttpStatus.SC_CONFLICT;
 import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
 import static org.apache.http.HttpStatus.SC_NOT_FOUND;
 import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
@@ -28,14 +29,11 @@
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.googlesource.gerrit.plugins.lfs.LfsAuthUserProvider;
+import com.googlesource.gerrit.plugins.lfs.locks.LfsLocksHandler.LfsLockExistsException;
 import java.io.IOException;
 import org.eclipse.jgit.lfs.errors.LfsException;
 import org.eclipse.jgit.lfs.errors.LfsRepositoryNotFound;
 import org.eclipse.jgit.lfs.errors.LfsUnauthorized;
-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;
 
@@ -45,18 +43,22 @@
   }
 
   private static final Logger log = LoggerFactory.getLogger(LfsLocksAction.class);
-  private static final DateTimeFormatter ISO = ISODateTimeFormat.dateTime();
   /** Git LFS client uses 'upload' operation to authorize SSH Lock requests */
   private static final String LFS_LOCKING_OPERATION = "upload";
 
   protected final ProjectCache projectCache;
   protected final LfsAuthUserProvider userProvider;
+  protected final LfsLocksHandler handler;
   protected final LfsLocksContext context;
 
   protected LfsLocksAction(
-      ProjectCache projectCache, LfsAuthUserProvider userProvider, LfsLocksContext context) {
+      ProjectCache projectCache,
+      LfsAuthUserProvider userProvider,
+      LfsLocksHandler handler,
+      LfsLocksContext context) {
     this.projectCache = projectCache;
     this.userProvider = userProvider;
+    this.handler = handler;
     this.context = context;
   }
 
@@ -72,6 +74,8 @@
       context.sendError(SC_UNAUTHORIZED, e.getMessage());
     } catch (LfsRepositoryNotFound e) {
       context.sendError(SC_NOT_FOUND, e.getMessage());
+    } catch (LfsLockExistsException e) {
+      context.sendError(SC_CONFLICT, e.error);
     } catch (LfsException e) {
       context.sendError(SC_INTERNAL_SERVER_ERROR, e.getMessage());
     }
@@ -109,8 +113,4 @@
             "operation %s unauthorized for user %s on project %s", op, userName, project));
     throw new LfsUnauthorized(op, project);
   }
-
-  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
index c58b6a7..fa5f360 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java
@@ -97,9 +97,13 @@
   }
 
   void sendError(int status, String message) throws IOException {
-    log.error(message);
+    sendError(status, new Error(message));
+  }
+
+  void sendError(int status, Error error) throws IOException {
+    log.error(error.message);
     res.setStatus(status);
-    gson.toJson(new Error(message), getWriter());
+    gson.toJson(error, getWriter());
     getWriter().flush();
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksHandler.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksHandler.java
new file mode 100644
index 0000000..f6ea9f1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksHandler.java
@@ -0,0 +1,168 @@
+// 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.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lfs.errors.LfsException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class LfsLocksHandler {
+  static class LfsLockExistsException extends LfsException {
+    private static final long serialVersionUID = 1L;
+
+    public final LfsLocksContext.Error error;
+
+    public LfsLockExistsException(LfsLock lock) {
+      super("Lock is already created");
+      this.error = new LockError(getMessage(), lock);
+    }
+  }
+
+  static class LockError extends LfsLocksContext.Error {
+    public final LfsLock lock;
+
+    LockError(String m, LfsLock lock) {
+      super(m);
+      this.lock = lock;
+    }
+  }
+
+  private static final Logger log = LoggerFactory.getLogger(LfsLocksHandler.class);
+  private static final String CACHE_NAME = "lfs_project_locks";
+
+  static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(CACHE_NAME, Project.NameKey.class, LfsProjectLocks.class).loader(Loader.class);
+      }
+    };
+  }
+
+  private final PathToLockId toLockId;
+  private final LoadingCache<Project.NameKey, LfsProjectLocks> projectLocks;
+
+  @Inject
+  LfsLocksHandler(
+      PathToLockId toLockId,
+      @Named(CACHE_NAME) LoadingCache<Project.NameKey, LfsProjectLocks> projectLocks) {
+    this.toLockId = toLockId;
+    this.projectLocks = projectLocks;
+  }
+
+  LfsLockResponse createLock(Project.NameKey project, CurrentUser user, LfsCreateLockInput input)
+      throws LfsException {
+    log.debug("Create lock for {} in project {}", input.path, project);
+    LfsProjectLocks locks = projectLocks.getUnchecked(project);
+    LfsLock lock = locks.createLock(user, input);
+    return new LfsLockResponse(lock);
+  }
+
+  LfsLockResponse deleteLock(
+      Project.NameKey project, CurrentUser user, String lockId, LfsDeleteLockInput input)
+      throws LfsException {
+    log.debug(
+        "Delete (-f {}) lock for {} in project {}",
+        Boolean.TRUE.equals(input.force),
+        lockId,
+        project);
+    LfsProjectLocks locks = projectLocks.getUnchecked(project);
+    Optional<LfsLock> hasLock = locks.getLock(lockId);
+    if (!hasLock.isPresent()) {
+      throw new LfsException(
+          String.format("there is no lock id %s in project %s", lockId, project));
+    }
+
+    LfsLock lock = hasLock.get();
+    if (lock.owner.name.equals(user.getUserName())) {
+      locks.deleteLock(lock);
+      return new LfsLockResponse(lock);
+    } else if (input.force) {
+      locks.deleteLock(lock);
+      return new LfsLockResponse(lock);
+    }
+
+    throw new LfsException(
+        String.format("Lock %s is owned by different user %s", lockId, lock.owner.name));
+  }
+
+  LfsVerifyLocksResponse verifyLocks(Project.NameKey project, final CurrentUser user) {
+    log.debug("Verify list of locks for {} project and user {}", project, user);
+    LfsProjectLocks locks = projectLocks.getUnchecked(project);
+    Function<LfsLock, Boolean> isOurs =
+        new Function<LfsLock, Boolean>() {
+          @Override
+          public Boolean apply(LfsLock input) {
+            return input.owner.name.equals(user.getUserName());
+          }
+        };
+    Map<Boolean, List<LfsLock>> groupByOurs =
+        locks.getLocks().stream().collect(Collectors.groupingBy(isOurs));
+    return new LfsVerifyLocksResponse(groupByOurs.get(true), groupByOurs.get(false), null);
+  }
+
+  LfsGetLocksResponse listLocksByPath(Project.NameKey project, String path) {
+    log.debug("Get lock for {} path in {} project", path, project);
+    String lockId = toLockId.apply(path);
+    return listLocksById(project, lockId);
+  }
+
+  LfsGetLocksResponse listLocksById(Project.NameKey project, String id) {
+    log.debug("Get lock for {} id in {} project", id, project);
+    LfsProjectLocks locks = projectLocks.getUnchecked(project);
+    Optional<LfsLock> lock = locks.getLock(id);
+    List<LfsLock> locksById =
+        (lock.isPresent() ? ImmutableList.of(lock.get()) : Collections.emptyList());
+    return new LfsGetLocksResponse(locksById, null);
+  }
+
+  LfsGetLocksResponse listLocks(Project.NameKey project) {
+    log.debug("Get locks for {} project", project);
+    return new LfsGetLocksResponse(projectLocks.getUnchecked(project).getLocks(), null);
+  }
+
+  static class Loader extends CacheLoader<Project.NameKey, LfsProjectLocks> {
+    private final LfsProjectLocks.Factory factory;
+
+    @Inject
+    Loader(LfsProjectLocks.Factory factory) {
+      this.factory = factory;
+    }
+
+    @Override
+    public LfsProjectLocks load(Project.NameKey project) throws Exception {
+      LfsProjectLocks locks = factory.create(project);
+      locks.load();
+      return locks;
+    }
+  }
+}
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
index 5aa3507..457a3b9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksModule.java
@@ -21,5 +21,7 @@
   protected void configure() {
     factory(LfsGetLocksAction.Factory.class);
     factory(LfsPutLocksAction.Factory.class);
+    factory(LfsProjectLocks.Factory.class);
+    install(LfsLocksHandler.module());
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsProjectLocks.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsProjectLocks.java
new file mode 100644
index 0000000..934ee00
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsProjectLocks.java
@@ -0,0 +1,196 @@
+// 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.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.lfs.locks.LfsLocksHandler.LfsLockExistsException;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.Optional;
+import org.eclipse.jgit.internal.storage.file.LockFile;
+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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class LfsProjectLocks {
+  interface Factory {
+    LfsProjectLocks create(Project.NameKey project);
+  }
+
+  private static final Logger log = LoggerFactory.getLogger(LfsProjectLocks.class);
+  private static final DateTimeFormatter ISO = ISODateTimeFormat.dateTime();
+  private static final Gson gson =
+      new GsonBuilder()
+          .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+          .disableHtmlEscaping()
+          .create();
+
+  private final PathToLockId toLockId;
+  private final String project;
+  private final Path locksPath;
+  private final Cache<String, LfsLock> locks;
+
+  @Inject
+  LfsProjectLocks(
+      PathToLockId toLockId, @PluginData Path defaultDataDir, @Assisted Project.NameKey project) {
+    this.toLockId = toLockId;
+    this.project = project.get();
+    this.locksPath = Paths.get(defaultDataDir.toString(), "lfs_locks", this.project);
+    this.locks = CacheBuilder.newBuilder().build();
+  }
+
+  void load() {
+    if (!Files.exists(locksPath)) {
+      return;
+    }
+    try {
+      Files.list(locksPath)
+          .filter(Files::isRegularFile)
+          .forEach(
+              path -> {
+                if (!Files.isReadable(path)) {
+                  log.warn("Lock file [{}] in project {} is not readable", path, project);
+                  return;
+                }
+
+                try (Reader in = Files.newBufferedReader(path)) {
+                  LfsLock lock = gson.fromJson(in, LfsLock.class);
+                  locks.put(lock.id, lock);
+                } catch (IOException e) {
+                  log.warn("Reading lock [{}] failed", path, e);
+                }
+              });
+    } catch (IOException e) {
+      log.warn("Reading locks in project {} failed", project, e);
+    }
+  }
+
+  Optional<LfsLock> getLock(String lockId) {
+    return Optional.ofNullable(locks.getIfPresent(lockId));
+  }
+
+  LfsLock createLock(CurrentUser user, LfsCreateLockInput input) throws LfsException {
+    log.debug("Create lock for {} in project {}", input.path, project);
+    String lockId = toLockId.apply(input.path);
+    LfsLock lock = locks.getIfPresent(lockId);
+    if (lock != null) {
+      throw new LfsLockExistsException(lock);
+    }
+
+    lock = new LfsLock(lockId, input.path, now(), new LfsLockOwner(user.getUserName()));
+    LockFile fileLock = new LockFile(locksPath.resolve(lockId).toFile());
+    try {
+      if (!fileLock.lock()) {
+        log.warn("Cannot lock path [{}] in project {}", input.path, project);
+        throw new LfsLockExistsException(lock);
+      }
+    } catch (IOException e) {
+      String error =
+          String.format(
+              "Locking path [%s] in project %s failed with error %s",
+              input.path, project, e.getMessage());
+      log.warn(error);
+      throw new LfsException(error);
+    }
+
+    try {
+      try (OutputStreamWriter out = new OutputStreamWriter(fileLock.getOutputStream())) {
+        gson.toJson(lock, out);
+      } catch (IOException e) {
+        String error =
+            String.format(
+                "Locking path [%s] in project %s failed during write with error %s",
+                input.path, project, e.getMessage());
+        log.warn(error);
+        throw new LfsException(error);
+      }
+      if (!fileLock.commit()) {
+        String error =
+            String.format("Committing lock to path [%s] in project %s failed", input.path, project);
+        log.warn(error);
+        throw new LfsException(error);
+      }
+      // put lock object to cache while file lock is being hold so that
+      // there is no chance that other process performs lock operation
+      // in the meantime (either cache returns with existing object or
+      // LockFile.lock fails on locking attempt)
+      locks.put(lockId, lock);
+    } finally {
+      fileLock.unlock();
+    }
+
+    return lock;
+  }
+
+  void deleteLock(LfsLock lock) throws LfsException {
+    LockFile fileLock = new LockFile(locksPath.resolve(lock.id).toFile());
+    try {
+      if (!fileLock.lock()) {
+        String error =
+            String.format(
+                "Deleting lock on path [%s] in project %s is not possible", lock.path, project);
+        log.warn(error);
+        throw new LfsException(error);
+      }
+    } catch (IOException e) {
+      String error =
+          String.format(
+              "Getting lock on path [%s] in project %s failed with error %s",
+              lock.path, project, e.getMessage());
+      log.warn(error);
+      throw new LfsException(error);
+    }
+
+    try {
+      Files.deleteIfExists(locksPath.resolve(lock.id));
+      locks.invalidate(lock.id);
+    } catch (IOException e) {
+      String error =
+          String.format(
+              "Deleting lock on path [%s] in project %s failed with error %s",
+              lock.path, project, e.getMessage());
+      log.warn(error);
+      throw new LfsException(error);
+    } finally {
+      fileLock.unlock();
+    }
+  }
+
+  Collection<LfsLock> getLocks() {
+    return locks.asMap().values();
+  }
+
+  private String now() {
+    return ISO.print(DateTime.now().toDateTime(DateTimeZone.UTC));
+  }
+}
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
index e2e28f8..ffbcc8d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java
@@ -28,18 +28,14 @@
 import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.lfs.LfsAuthUserProvider;
 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.eclipse.jgit.lfs.errors.LfsUnauthorized;
-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));
 
@@ -49,8 +45,9 @@
   LfsPutLocksAction(
       ProjectCache projectCache,
       LfsAuthUserProvider userProvider,
+      LfsLocksHandler handler,
       @Assisted LfsLocksContext context) {
-    super(projectCache, userProvider, context);
+    super(projectCache, userProvider, handler, context);
   }
 
   @Override
@@ -104,14 +101,7 @@
     @Override
     public void run(ProjectState project, CurrentUser user) throws LfsException, IOException {
       LfsCreateLockInput input = context.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>"));
+      LfsLockResponse lock = handler.createLock(project.getProject().getNameKey(), user, input);
       context.sendResponse(lock);
     }
   }
@@ -131,18 +121,8 @@
     @Override
     public void run(ProjectState project, CurrentUser user) throws LfsException, IOException {
       LfsDeleteLockInput input = context.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(
-              lockId,
-              "some/path/to/file",
-              now(),
-              new LfsLockOwner("Lock Owner <lock_owner@example.com>"));
+      LfsLockResponse lock =
+          handler.deleteLock(project.getProject().getNameKey(), user, lockId, input);
       context.sendResponse(lock);
     }
   }
@@ -155,10 +135,7 @@
 
     @Override
     public void run(ProjectState project, CurrentUser user) throws LfsException, IOException {
-      log.debug("Verify list of locks for {} project", project);
-      //TODO method stub for verifying locks
-      context.sendResponse(
-          new LfsVerifyLocksResponse(Collections.emptyList(), Collections.emptyList(), null));
+      context.sendResponse(handler.verifyLocks(project.getProject().getNameKey(), user));
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/PathToLockId.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/PathToLockId.java
new file mode 100644
index 0000000..d90ebb8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/PathToLockId.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 com.google.common.base.Function;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.Hashing;
+import com.google.common.io.BaseEncoding;
+import java.nio.charset.StandardCharsets;
+
+public class PathToLockId implements Function<String, String> {
+  @Override
+  public String apply(String path) {
+    HashCode hash = Hashing.sha256().hashString(path, StandardCharsets.UTF_8);
+    return BaseEncoding.base16().lowerCase().encode(hash.asBytes());
+  }
+}