/*
 * 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.jackrabbit.core.util;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;

import javax.jcr.RepositoryException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Exclusive lock on a repository home directory. This class encapsulates
 * collective experience on how to acquire an exclusive lock on a given
 * directory. The lock is expected to be exclusive both across process
 * boundaries and within a single JVM. The lock mechanism must also work
 * consistently on a variety of operating systems and JVM implementations.
 *
 * @see https://issues.apache.org/jira/browse/JCR-213
 * @see https://issues.apache.org/jira/browse/JCR-233
 * @see https://issues.apache.org/jira/browse/JCR-254
 * @see https://issues.apache.org/jira/browse/JCR-912
 * @see https://issues.apache.org/jira/browse/JCR-933
 */
public class RepositoryLock implements RepositoryLockMechanism {

    /**
     * Name of the lock file within a directory.
     */
    private static final String LOCK = ".lock";

    /**
     * Logger instance.
     */
    private static final Logger LOG =
        LoggerFactory.getLogger(RepositoryLock.class);

    /**
     * The locked directory.
     */
    private File directory;

    /**
     * The lock file within the given directory.
     */
    private File file;

    /**
     * The random access file.
     */
    private RandomAccessFile randomAccessFile;

    /**
     * Unique identifier (canonical path name) of the locked directory.
     * Used to ensure exclusive locking within a single JVM.
     *
     * @see https://issues.apache.org/jira/browse/JCR-933
     */
    private String identifier;

    /**
     * The file lock. Used to ensure exclusive locking across process boundaries.
     *
     * @see https://issues.apache.org/jira/browse/JCR-233
     */
    private FileLock lock;

    public RepositoryLock() {
        // used by the factory
    }
    
    /**
     * Create a new RepositoryLock object and initialize it.
     * @deprecated 
     * This constructor is deprecated; use the default constructor
     * and {@link #init(String)} instead.
     * 
     * @param path directory path
     * @throws RepositoryException if the canonical path of the directory
     *                             can not be determined
     */
    public RepositoryLock(String path) throws RepositoryException {
        init(path);
    }
    
    /**
     * Initialize the instance for the given directory path. The lock still needs to be 
     * explicitly acquired using the {@link #acquire()} method.
     *
     * @param path directory path
     * @throws RepositoryException if the canonical path of the directory
     *                             can not be determined
     */
    public void init(String path) throws RepositoryException {
        try {
            directory = new File(path).getCanonicalFile();
            file = new File(directory, LOCK);
            identifier =
                (RepositoryLock.class.getName() + ":" + directory.getPath())
                .intern();
            lock = null;
        } catch (IOException e) {
            throw new RepositoryException(
                    "Unable to determine canonical path of " + path, e);
        }
    }

    /**
     * Lock the repository home.
     *
     * @throws RepositoryException if the repository lock can not be acquired
     */
    public void acquire() throws RepositoryException {
        if (file.exists()) {
            LOG.warn("Existing lock file " + file + " detected."
                    + " Repository was not shut down properly.");
        }
        try {
            tryLock();
        } catch (RepositoryException e) {
            closeRandomAccessFile();
            throw e;
        }
    }

    /**
     * Try to lock the random access file.
     *
     * @throws RepositoryException
     */
    private void tryLock() throws RepositoryException {
        try {
            randomAccessFile = new RandomAccessFile(file, "rw");
            lock = randomAccessFile.getChannel().tryLock();
        } catch (IOException e) {
            throw new RepositoryException(
                    "Unable to create or lock file " + file, e);
        } catch (OverlappingFileLockException e) {
            // JCR-912: OverlappingFileLockException with JRE 1.6
            throw new RepositoryException(
                    "The repository home " + directory + " appears to be in use"
                    + " since the file named " + file.getName()
                    + " is already locked by the current process.");
        }

        if (lock == null) {
            throw new RepositoryException(
                    "The repository home " + directory + " appears to be in use"
                    + " since the file named " + file.getName()
                    + " is locked by another process.");
        }

        // JCR-933: due to a bug in java 1.4/1.5 on *nix platforms
        // it's possible that java.nio.channels.FileChannel.tryLock()
        // returns a non-null FileLock object although the lock is already
        // held by *this* jvm process
        synchronized (identifier) {
            if (null != System.getProperty(identifier)) {
                // note that the newly acquired (redundant) file lock
                // is deliberately *not* released because this could
                // potentially cause, depending on the implementation,
                // the previously acquired lock(s) to be released
                // as well
                throw new RepositoryException(
                        "The repository home " + directory + " appears to be"
                        + " already locked by the current process.");
            } else {
                try {
                    System.setProperty(identifier, identifier);
                } catch (SecurityException e) {
                    LOG.warn("Unable to set system property: " + identifier, e);
                }
            }
        }
    }

    /**
     * Close the random access file if it is open, and set it to null.
     */
    private void closeRandomAccessFile() {
        if (randomAccessFile != null) {
            try {
                randomAccessFile.close();
            } catch (IOException e) {
                LOG.warn("Unable to close the random access file " + file, e);
            }
            randomAccessFile = null;
        }
    }

    /**
     * Releases repository lock.
     */
    public void release() {
        if (lock != null) {
            try {
                FileChannel channel = lock.channel();
                lock.release();
                channel.close();
            } catch (IOException e) {
                // ignore
            }
            lock = null;
            closeRandomAccessFile();
        }

        if (!file.delete()) {
            LOG.warn("Unable to delete repository lock file");
        }

        // JCR-933: see #acquire()
        synchronized (identifier) {
            try {
                System.getProperties().remove(identifier);
            } catch (SecurityException e) {
                LOG.error("Unable to clear system property: " + identifier, e);
            }
        }
    }

}
