001/*
002 *  Copyright 2015 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016
017package org.ametys.runtime.data;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.RandomAccessFile;
022import java.nio.channels.FileChannel;
023import java.nio.channels.FileLock;
024import java.nio.channels.OverlappingFileLockException;
025
026/**
027 * Exclusive lock on the Ametys Home directory.
028 * Based on the Jackrabbit repository lock. 
029 */
030public class AmetysHomeLock
031{
032    /**
033     * Name of the lock file within a directory.
034     */
035    private static final String __LOCK_FILE = ".lock";
036    
037    /** The ametys home directory */
038    private File _directory;
039
040    /** The lock file within the directory */
041    private File _file;
042
043    /** The random access file. */
044    private RandomAccessFile _randomAccessFile;
045
046    /**
047     * Unique identifier (canonical path name) of the locked directory.
048     * Used to ensure exclusive locking within a single JVM.
049     */
050    private String _identifier;
051
052    /**
053     * The file lock. Used to ensure exclusive locking across process
054     * boundaries.
055     */
056    private FileLock _lock;
057
058    /**
059     * Default constructor.
060     * Initialize the instance. The lock still needs to be
061     * explicitly acquired using the {@link #acquire()} method.
062     * @param ametysHome The ametys home directory
063     * @throws AmetysHomeLockException if the canonical path of the directory can not be determined
064     */
065    public AmetysHomeLock(File ametysHome) throws AmetysHomeLockException
066    {
067        try
068        {
069            _directory = ametysHome.getCanonicalFile();
070            _file = new File(_directory, __LOCK_FILE);
071            _identifier = (AmetysHomeLock.class.getName() + ":" + _directory.getPath()).intern();
072            _lock = null;
073        }
074        catch (IOException e)
075        {
076            throw new AmetysHomeLockException("Unable to construct the Ametys home lock instance at path " + ametysHome.getPath(), e);
077        }
078    }
079
080    /**
081     * Lock the Ametys home directory
082     * @throws AmetysHomeLockException if the repository lock can not be acquired
083     */
084    public void acquire() throws AmetysHomeLockException
085    {
086        if (_file.exists())
087        {
088            System.out.println("[WARN] Existing lock file " + _file + " detected. Previous lock was not released properly.");
089        }
090        try
091        {
092            _tryLock();
093        }
094        catch (AmetysHomeLockException e)
095        {
096            _closeRandomAccessFile();
097            throw e;
098        }
099    }
100
101    /**
102     * Try to lock the random access file.
103     * @throws AmetysHomeLockException If an error occurs during the lock attempt.
104     */
105    private void _tryLock() throws AmetysHomeLockException
106    {
107        try
108        {
109            _randomAccessFile = new RandomAccessFile(_file, "rw");
110            _lock = _randomAccessFile.getChannel().tryLock();
111        }
112        catch (IOException e)
113        {
114            throw new AmetysHomeLockException("Unable to create or lock file " + _file, e);
115        }
116        catch (OverlappingFileLockException e)
117        {
118            // OverlappingFileLockException with JRE 1.6
119            throw new AmetysHomeLockException("The Ametys home " + _directory + " appears to be in use since the file named " + _file.getName()
120                    + " is already locked by the current process.");
121        }
122        
123        if (_lock == null)
124        {
125            throw new AmetysHomeLockException("The Ametys home " + _directory + " appears to be in use since the file named " + _file.getName()
126                    + " is locked by another process.");
127        }
128        
129        // due to a bug in java 1.4/1.5 on *nix platforms
130        // it's possible that java.nio.channels.FileChannel.tryLock()
131        // returns a non-null FileLock object although the lock is already
132        // held by *this* jvm process
133        synchronized (_identifier)
134        {
135            if (null != System.getProperty(_identifier))
136            {
137                // note that the newly acquired (redundant) file lock
138                // is deliberately *not* released because this could
139                // potentially cause, depending on the implementation,
140                // the previously acquired lock(s) to be released
141                // as well
142                throw new AmetysHomeLockException("The Ametys home " + _directory + " appears to be" + " already locked by the current process.");
143            }
144            else
145            {
146                try
147                {
148                    System.setProperty(_identifier, _identifier);
149                }
150                catch (SecurityException e)
151                {
152                    System.out.println("[WARN] Unable to set system property: " + _identifier);
153                    e.printStackTrace();
154                }
155            }
156        }
157    }
158    
159    /**
160     * Close the random access file if it is open, and set it to null.
161     */
162    private void _closeRandomAccessFile()
163    {
164        if (_randomAccessFile != null)
165        {
166            try
167            {
168                _randomAccessFile.close();
169            }
170            catch (IOException e)
171            {
172                System.out.println("[WARN] Unable to close the random access file " + _file);
173                e.printStackTrace();
174            }
175            _randomAccessFile = null;
176        }
177    }
178    
179    /**
180     * Releases repository lock.
181     */
182    public void release()
183    {
184        if (_lock != null)
185        {
186            try
187            {
188                try (FileChannel channel = _lock.channel())
189                {
190                    _lock.release();
191                }
192            }
193            catch (IOException e)
194            {
195                // ignore
196            }
197            
198            _lock = null;
199            _closeRandomAccessFile();
200        }
201
202        if (!_file.delete())
203        {
204            System.out.println("[WARN] Unable to delete repository lock file");
205        }
206
207        // see #acquire()
208        synchronized (_identifier)
209        {
210            try
211            {
212                System.getProperties().remove(_identifier);
213            }
214            catch (SecurityException e)
215            {
216                System.out.println("[WARN] Unable to clear system property: " + _identifier);
217                e.printStackTrace();
218            }
219        }
220    }
221}