001package co.codewizards.cloudstore.core.io;
002
003import static co.codewizards.cloudstore.core.util.Util.*;
004
005import java.io.File;
006import java.io.IOException;
007import java.nio.channels.FileLock;
008import java.util.HashMap;
009import java.util.Map;
010
011/**
012 * Factory creating {@link LockFile} instances.
013 * <p>
014 * All methods of this class are thread-safe.
015 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co
016 */
017public class LockFileFactory {
018
019        private static class LockFileFactoryHolder {
020                public static final LockFileFactory instance = new LockFileFactory();
021        }
022
023        protected final Object mutex = new Object();
024
025        protected LockFileFactory() { }
026
027        public static LockFileFactory getInstance() {
028                return LockFileFactoryHolder.instance;
029        }
030
031        private Map<File, LockFileImpl> file2LockFileImpl = new HashMap<File, LockFileImpl>();
032
033        /**
034         * Acquire an exclusive lock on the specified file.
035         * <p>
036         * <b>Important:</b> You <i>must</i> invoke {@link LockFile#release()} on the returned object! Use a try-finally-block
037         * to ensure it:
038         * <pre>  LockFile lockFile = LockFileFactory.acquire(theFile, theTimeout);
039         *  try {
040         *    // do something
041         *  } finally {
042         *    lockFile.release();
043         *  }</pre>
044         * If the JVM is interrupted or shut down before {@code release()}, the file-lock is released by the
045         * operating system, but a missing {@code release()} causes the file to be locked for the entire remaining runtime
046         * of the JVM!
047         * <p>
048         * <b>Important:</b> This is <i>not</i> usable for the synchronization of multiple threads within the same Java virtual machine!
049         * Multiple {@link LockFile}s on the same {@link File} are possible within the same JVM! This locking mechanism
050         * only locks against separate processes! Since this implementation is based on {@link FileLock}, please consult
051         * its Javadoc for further information.
052         * <p>
053         * Multiple invocations of this method on the same given {@code file} return multiple different {@code LockFile} instances.
054         * The actual lock is held until the last {@code LockFile} instance was {@linkplain LockFile#release() released}.
055         * <p>
056         * This method is thread-safe.
057         * @param file the file to be locked. Must not be <code>null</code>. If this file does not exist in the file system,
058         * it is created by this method.
059         * @param timeoutMillis the timeout to wait for the lock to be acquired in milliseconds. The value 0 means to
060         * wait forever.
061         * @return the {@code LockFile}. Never <code>null</code>. This <i>must</i> be {@linkplain FileLock#release() released}
062         * (use a try-finally-block)!
063         * @throws TimeoutException if the {@code LockFile} could not be acquired within the timeout specified by {@code timeoutMillis}.
064         * @see LockFile#release()
065         */
066        public LockFile acquire(File file, long timeoutMillis) throws TimeoutException {
067                assertNotNull("file", file);
068                try {
069                        file = file.getCanonicalFile();
070                } catch (IOException e) {
071                        throw new RuntimeException(e);
072                }
073
074                LockFileImpl lockFileImpl;
075                synchronized (this) {
076                        lockFileImpl = file2LockFileImpl.get(file);
077                        if (lockFileImpl == null) {
078                                lockFileImpl = new LockFileImpl(this, file);
079                                file2LockFileImpl.put(file, lockFileImpl);
080                        }
081                        lockFileImpl.acquireRunningCounter.incrementAndGet();
082                }
083                try {
084                        // The following must NOT be synchronized! Otherwise we might wait here longer than the current timeout
085                        // (as long as the longest timeout of all acquire methods running concurrently).
086                        lockFileImpl.acquire(timeoutMillis);
087                } finally {
088                        synchronized (this) {
089                                int lockCounter = lockFileImpl.getLockCounter();
090                                int acquireRunningCounter = lockFileImpl.acquireRunningCounter.decrementAndGet();
091
092                                if (lockCounter < 1 && acquireRunningCounter < 1)
093                                        file2LockFileImpl.remove(file);
094                        }
095                }
096                return new LockFileProxy(lockFileImpl);
097        }
098
099        protected synchronized void postRelease(LockFileImpl lockFileImpl) {
100                LockFileImpl lockFileImpl2 = file2LockFileImpl.get(lockFileImpl.getFile());
101                if (lockFileImpl != lockFileImpl2)
102                        throw new IllegalArgumentException("Unknown lockFileImpl instance (not managed by this registry): " + lockFileImpl);
103
104                int lockCounter = lockFileImpl.getLockCounter();
105                int acquireRunningCounter = lockFileImpl.acquireRunningCounter.decrementAndGet();
106
107                if (lockCounter < 1 && acquireRunningCounter < 1)
108                        file2LockFileImpl.remove(lockFileImpl.getFile());
109        }
110
111}