/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.lucene.store;

import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;

/**
 * Implements {@link LockFactory} using {@link Files#createFile}.
 *
 * <p>The main downside with using this API for locking is that the Lucene write lock may not be
 * released when the JVM exits abnormally.
 *
 * <p>When this happens, an {@link LockObtainFailedException} is hit when trying to create a writer,
 * in which case you may need to explicitly clear the lock file first by manually removing the file.
 * But, first be certain that no writer is in fact writing to the index otherwise you can easily
 * corrupt your index.
 *
 * <p>Special care needs to be taken if you change the locking implementation: First be certain that
 * no writer is in fact writing to the index otherwise you can easily corrupt your index. Be sure to
 * do the LockFactory change all Lucene instances and clean up all leftover lock files before
 * starting the new configuration for the first time. Different implementations can not work
 * together!
 *
 * <p>If you suspect that this or any other LockFactory is not working properly in your environment,
 * you can easily test it by using {@link VerifyingLockFactory}, {@link LockVerifyServer} and {@link
 * LockStressTest}.
 *
 * <p>This is a singleton, you have to use {@link #INSTANCE}.
 *
 * @see LockFactory
 */
public final class SimpleFSLockFactory extends FSLockFactory {

  /** Singleton instance */
  public static final SimpleFSLockFactory INSTANCE = new SimpleFSLockFactory();

  private SimpleFSLockFactory() {}

  @Override
  protected Lock obtainFSLock(FSDirectory dir, String lockName) throws IOException {
    Path lockDir = dir.getDirectory();

    // Ensure that lockDir exists and is a directory.
    // note: this will fail if lockDir is a symlink
    Files.createDirectories(lockDir);

    Path lockFile = lockDir.resolve(lockName);

    // create the file: this will fail if it already exists
    try {
      Files.createFile(lockFile);
    } catch (FileAlreadyExistsException | AccessDeniedException e) {
      // convert optional specific exception to our optional specific exception
      throw new LockObtainFailedException("Lock held elsewhere: " + lockFile, e);
    }

    // used as a best-effort check, to see if the underlying file has changed
    final FileTime creationTime =
        Files.readAttributes(lockFile, BasicFileAttributes.class).creationTime();

    return new SimpleFSLock(lockFile, creationTime);
  }

  static final class SimpleFSLock extends Lock {
    private final Path path;
    private final FileTime creationTime;
    private volatile boolean closed;

    SimpleFSLock(Path path, FileTime creationTime) throws IOException {
      this.path = path;
      this.creationTime = creationTime;
    }

    @Override
    public void ensureValid() throws IOException {
      if (closed) {
        throw new AlreadyClosedException("Lock instance already released: " + this);
      }
      // try to validate the backing file name, that it still exists,
      // and has the same creation time as when we obtained the lock.
      // if it differs, someone deleted our lock file (and we are ineffective)
      FileTime ctime = Files.readAttributes(path, BasicFileAttributes.class).creationTime();
      if (!creationTime.equals(ctime)) {
        throw new AlreadyClosedException(
            "Underlying file changed by an external force at " + ctime + ", (lock=" + this + ")");
      }
    }

    @Override
    public synchronized void close() throws IOException {
      if (closed) {
        return;
      }
      try {
        // NOTE: unlike NativeFSLockFactory, we can potentially delete someone else's
        // lock if things have gone wrong. we do best-effort check (ensureValid) to
        // avoid doing this.
        try {
          ensureValid();
        } catch (Throwable exc) {
          // notify the user they may need to intervene.
          throw new LockReleaseFailedException(
              "Lock file cannot be safely removed. Manual intervention is recommended.", exc);
        }
        // we did a best effort check, now try to remove the file. if something goes wrong,
        // we need to make it clear to the user that the directory may still remain locked.
        try {
          Files.delete(path);
        } catch (Throwable exc) {
          throw new LockReleaseFailedException(
              "Unable to remove lock file. Manual intervention is recommended", exc);
        }
      } finally {
        closed = true;
      }
    }

    @Override
    public String toString() {
      return "SimpleFSLock(path=" + path + ",creationTime=" + creationTime + ")";
    }
  }
}
