001    //$HeadURL: svn+ssh://mschneider@svn.wald.intevation.org/deegree/base/trunk/src/org/deegree/io/datastore/FeatureId.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003     This file is part of deegree.
004     Copyright (C) 2001-2006 by:
005     Department of Geography, University of Bonn
006     http://www.giub.uni-bonn.de/deegree/
007     lat/lon GmbH
008     http://www.lat-lon.de
009    
010     This library is free software; you can redistribute it and/or
011     modify it under the terms of the GNU Lesser General Public
012     License as published by the Free Software Foundation; either
013     version 2.1 of the License, or (at your option) any later version.
014    
015     This library is distributed in the hope that it will be useful,
016     but WITHOUT ANY WARRANTY; without even the implied warranty of
017     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
018     Lesser General Public License for more details.
019    
020     You should have received a copy of the GNU Lesser General Public
021     License along with this library; if not, write to the Free Software
022     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
023    
024     Contact:
025    
026     Andreas Poth
027     lat/lon GmbH
028     Aennchenstraße 19
029     53177 Bonn
030     Germany
031     E-Mail: poth@lat-lon.de
032    
033     Jens Fitzke
034     lat/lon GmbH
035     Aennchenstraße 19
036     53177 Bonn
037     Germany
038     E-Mail: jens.fitzke@uni-bonn.de
039     ---------------------------------------------------------------------------*/
040    package org.deegree.io.datastore;
041    
042    import java.io.File;
043    import java.io.FileInputStream;
044    import java.io.FileOutputStream;
045    import java.io.FilenameFilter;
046    import java.io.ObjectInputStream;
047    import java.io.ObjectOutputStream;
048    import java.util.ArrayList;
049    import java.util.Date;
050    import java.util.HashMap;
051    import java.util.List;
052    import java.util.Map;
053    import java.util.Set;
054    import java.util.TreeMap;
055    import java.util.TreeSet;
056    import java.util.UUID;
057    
058    import org.deegree.framework.log.ILogger;
059    import org.deegree.framework.log.LoggerFactory;
060    import org.deegree.i18n.Messages;
061    import org.deegree.ogcwebservices.wfs.operation.LockFeature;
062    
063    /**
064     * Keeps track of all persistent features that are in a locked state, i.e. a {@link LockFeature}
065     * request has been issued to lock them.
066     * <p>
067     * Locked features cannot be updated or deleted except by transactions that specify the appropriate
068     * lock identifier.
069     * <p>
070     * The <code>LockManager</code> also ensures that active locks survive a restart of the VM -
071     * therefore it keeps serialized and up-to-date versions of all active {@link Lock} instances in a
072     * temporary directory. The directory is specified by the <code>java.io.tmpdir</code> system
073     * property. On first initialization, i.e. the first call to {@link #getInstance()}, the directory
074     * is scanned for all files matching the pattern <code>deegree-lock*.tmp</code>, and these are
075     * deserialized to rebuild the <code>LockManager</code>'s status.
076     * 
077     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
078     * @author last edited by: $Author:$
079     * 
080     * @version $Revision:$, $Date:$
081     */
082    public class LockManager {
083    
084        private static final ILogger LOG = LoggerFactory.getLogger( LockManager.class );
085    
086        static String FILE_PREFIX = "deegree-lock";
087    
088        static String FILE_SUFFIX = ".tmp";
089    
090        private static LockManager instance;
091    
092        private static File workingDir;
093    
094        // maps expiry time to Lock, first key is the one that will timeout first
095        private TreeMap<Long, Lock> expiryTimeToLock = new TreeMap<Long, Lock>();
096    
097        private Map<String, Lock> lockIdToLock = new HashMap<String, Lock>();
098    
099        private Map<String, Lock> fidToLock = new HashMap<String, Lock>();
100    
101        private Map<Lock, File> lockToFile = new HashMap<Lock, File>();
102    
103        private LockManager( File workingDir ) throws DatastoreException {
104            if ( workingDir == null ) {
105                String msg = "No working directory for the lock manager specified. Using default temp directory: '"
106                             + System.getProperty( "java.io.tmpdir" ) + "'";
107                LOG.logInfo( msg );
108                workingDir = new File( System.getProperty( "java.io.tmpdir" ) );
109            }
110    
111            LockManager.workingDir = workingDir;
112            if ( !workingDir.isDirectory() ) {
113                String msg = "Specified working directory for the lock manager '" + workingDir
114                             + "' does not denote a directory.";
115                throw new DatastoreException( msg );
116            }
117            if ( !workingDir.canWrite() ) {
118                String msg = "Cannot write to the lock manager's working directory ('" + workingDir + "' ).";
119                throw new DatastoreException( msg );
120            }
121    
122            String msg = "Lock manager will use directory '" + workingDir + "' to persist it's locks.";
123            LOG.logInfo( msg );
124    
125            restoreLocks();
126            checkForExpiredLocks();
127        }
128    
129        private void restoreLocks() {
130    
131            // get all persistent locks from temporary directory
132            String[] fileNames = workingDir.list( new FilenameFilter() {
133                public boolean accept( File dir, String name ) {
134                    return name.startsWith( FILE_PREFIX ) && name.endsWith( FILE_SUFFIX );
135                }
136            } );
137    
138            if ( fileNames != null ) {
139                String msg = Messages.getMessage( "DATASTORE_LOCK_RESTORING", fileNames.length, workingDir );
140                LOG.logInfo( msg );
141                for ( String fileName : fileNames ) {
142                    File file = new File( workingDir + File.separator + fileName );
143                    try {
144                        FileInputStream fis = null;
145                        ObjectInputStream ois = null;
146                        try {
147                            fis = new FileInputStream( file );
148                            ois = new ObjectInputStream( fis );
149                            Lock lock = (Lock) ois.readObject();
150                            registerLock( lock, file );
151                        } finally {
152                            if ( ois != null ) {
153                                ois.close();
154                            }
155                        }
156                    } catch ( Exception e ) {
157                        msg = Messages.getMessage( "DATASTORE_LOCK_RESTORE_FAILED", fileName );
158                        LOG.logError( msg, e );
159                    }
160                }
161            }
162        }
163    
164        /**
165         * Initializes the <code>LockManager</code>.
166         * 
167         * @param workingDir
168         *            directory where the <code>LockManager</code> will persists its locks
169         * @throws DatastoreException
170         */
171        public static synchronized void initialize( File workingDir )
172                                throws DatastoreException {
173            if ( instance != null ) {
174                String msg = "LockManager has already been initialized.";
175                throw new DatastoreException( msg );
176            }
177            instance = new LockManager( workingDir );
178        }
179    
180        /**
181         * Returns the only instance of <code>LockManager</code>.
182         * 
183         * @return the only instance of <code>LockManager</code>
184         */
185        public static synchronized LockManager getInstance() {
186            if ( instance == null ) {
187                String msg = "LockManager has not been initialized yet.";
188                throw new RuntimeException( msg );
189            }
190            return instance;
191        }
192    
193        /**
194         * Returns whether the specified feature is locked.
195         * 
196         * @param fid
197         *            id of the feature
198         * @return true, if the specified feature is locked, false otherwise
199         */
200        public boolean isLocked( FeatureId fid ) {
201            return getLockId( fid ) != null;
202        }
203    
204        /**
205         * Returns the id of the lock that locks the specified feature (if it is locked).
206         * 
207         * @param fid
208         *            id of the feature
209         * @return the lock id, or null if it is not locked
210         */
211        public String getLockId( FeatureId fid ) {
212    
213            checkForExpiredLocks();
214    
215            String lockId = null;
216            synchronized ( this ) {
217                Lock lock = this.fidToLock.get( fid.getAsString() );
218                if ( lock != null ) {
219                    lockId = lock.getId();
220                }
221            }
222            return lockId;
223        }
224    
225        /**
226         * Acquires a lock for the given {@link LockFeature} request. The affected feature instances and
227         * their descendant features + super features have to be specified as well.
228         * <p>
229         * If the lockAction in the request is set to ALL and not all requested features could be
230         * locked, a {@link DatastoreException} will be thrown.
231         * <p>
232         * If no features have been locked at all, a lock will be issued, but the lock is not registered
233         * (as requested by the WFS spec.).
234         * 
235         * @param request
236         *            <code>LockFeature</code> request
237         * @param fidsToLock
238         *            all feature instances that are affected by the request
239         * @return the acquired lock, never null
240         * @throws DatastoreException
241         */
242        public Lock acquireLock( LockFeature request, List<FeatureId> fidsToLock )
243                                throws DatastoreException {
244    
245            checkForExpiredLocks();
246    
247            Lock lock = null;
248    
249            synchronized ( this ) {
250                String lockId = UUID.randomUUID().toString();
251    
252                Set<String> lockableFids = new TreeSet<String>();
253                List<String> notLockableFids = new ArrayList<String>( fidsToLock.size() );
254    
255                for ( FeatureId fid : fidsToLock ) {
256                    String fidAsString = fid.getAsString();
257                    if ( this.fidToLock.get( fidAsString ) != null ) {
258                        notLockableFids.add( fidAsString );
259                    } else {
260                        lockableFids.add( fidAsString );
261                    }
262                }
263    
264                if ( request.lockAllFeatures() && !notLockableFids.isEmpty() ) {
265                    StringBuffer sb = new StringBuffer();
266                    for ( int i = 0; i < notLockableFids.size(); i++ ) {
267                        sb.append( notLockableFids.get( i ) );
268                        if ( i != notLockableFids.size() - 1 ) {
269                            sb.append( ", " );
270                        }
271                    }
272                    String msg = Messages.getMessage( "DATASTORE_LOCK_SOME_HELD", sb );
273                    throw new DatastoreException( msg );
274                }
275    
276                if ( !lockableFids.isEmpty() ) {
277                    long duration = request.getExpiry();
278                    long expiryTime = System.currentTimeMillis() + duration;
279                    lock = new Lock( lockId, lockableFids, expiryTime );
280                    File file = persistLock( lock );
281                    registerLock( lock, file );
282                } else {
283                    lock = new Lock( lockId, lockableFids, System.currentTimeMillis() );
284                    String msg = Messages.getMessage( "DATASTORE_EMPTY_LOCK", lockId );
285                    LOG.logInfo( msg );
286                }
287            }
288            return lock;
289        }
290    
291        /**
292         * Releases the specified lock completely (all associated features are unlocked) and removes it
293         * (also from the temporary directory).
294         * 
295         * @param lockId
296         *            lock identifier
297         * @throws DatastoreException
298         */
299        public void releaseLock( String lockId )
300                                throws DatastoreException {
301            synchronized ( this ) {
302                Lock lock = this.lockIdToLock.get( lockId );
303                if ( lock == null ) {
304                    String msg = Messages.getMessage( "DATASTORE_UNKNOWN_LOCK", lockId );
305                    throw new DatastoreException( msg );
306                }
307                releaseLock( lock );
308            }
309        }
310    
311        /**
312         * Releases the given lock completely (all associated features are unlocked) and removes it
313         * (also from the temporary directory).
314         * 
315         * @param lock
316         *            lock to be released
317         */
318        public void releaseLock( Lock lock ) {
319            synchronized ( this ) {
320                this.lockIdToLock.remove( lock.getId() );
321                this.expiryTimeToLock.remove( lock.getExpiryTime() );
322                Set<String> lockedFids = lock.getLockedFids();
323                for ( String fid : lockedFids ) {
324                    this.fidToLock.remove( fid );
325                }
326                File file = this.lockToFile.get( lock );
327                file.delete();
328                this.lockToFile.remove( lock );
329            }
330        }
331    
332        /**
333         * Releases the specified lock partly (all specified features are unlocked).
334         * <p>
335         * If there are no more features associated with the lock, the lock is removed.
336         * 
337         * @param lockId
338         *            lock identifier
339         * @param unlockFids
340         *            features to be unlocked
341         * @throws DatastoreException
342         */
343        public void releaseLockPartly( String lockId, Set<FeatureId> unlockFids )
344                                throws DatastoreException {
345    
346            synchronized ( this ) {
347                Lock lock = this.lockIdToLock.get( lockId );
348                if ( lock == null ) {
349                    String msg = Messages.getMessage( "DATASTORE_UNKNOWN_LOCK", lockId );
350                    throw new DatastoreException( msg );
351                }
352    
353                Set<String> lockedFeatures = lock.getLockedFids();
354    
355                for ( FeatureId fid : unlockFids ) {
356                    String fidAsString = fid.getAsString();
357                    this.fidToLock.remove( fidAsString );
358                    lockedFeatures.remove( fidAsString );
359                }
360    
361                if ( lockedFeatures.isEmpty() ) {
362                    String msg = Messages.getMessage( "DATASTORE_LOCK_CLEARED", lock.getId() );
363                    LOG.logInfo( msg );
364                    this.lockIdToLock.remove( lockId );
365                    this.expiryTimeToLock.remove( lock.getExpiryTime() );
366                }
367                persistLock( lock );
368            }
369        }
370    
371        /**
372         * Checks for expired locks and releases them.
373         */
374        private void checkForExpiredLocks() {
375            synchronized ( this ) {
376                while ( !this.expiryTimeToLock.isEmpty() ) {
377                    long expiry = this.expiryTimeToLock.firstKey();
378                    if ( expiry > System.currentTimeMillis() ) {
379                        break;
380                    }
381                    Lock lock = this.expiryTimeToLock.get( expiry );
382                    String msg = Messages.getMessage( "DATASTORE_LOCK_EXPIRED", lock.getId(),
383                                                      new Date( lock.getExpiryTime() ) );
384                    LOG.logInfo( msg );
385                    releaseLock( lock );
386                }
387            }
388        }
389    
390        /**
391         * Registers the given lock in the lookup maps of the <code>LockManager</code>.
392         * <p>
393         * This includes:
394         * <ul>
395         * <li><code>fidToLock-Map</code></li>
396         * <li><code>lockIdToLock-Map</code></li>
397         * <li><code>expiryTimeToLock-Map</code></li>
398         * <li><code>lockToFile-Map</code></li>
399         * </ul>
400         * 
401         * @param lock
402         *            the lock to be registered
403         * @param file
404         *            file that stores the persistent representation of the lock
405         */
406        private void registerLock( Lock lock, File file ) {
407            for ( String fid : lock.getLockedFids() ) {
408                this.fidToLock.put( fid, lock );
409            }
410            this.expiryTimeToLock.put( lock.getExpiryTime(), lock );
411            this.lockIdToLock.put( lock.getId(), lock );
412            this.lockToFile.put( lock, file );
413            String msg = Messages.getMessage( "DATASTORE_LOCK_TIMEOUT_INFO", lock.getId(), new Date( lock.getExpiryTime() ) );
414            LOG.logInfo( msg );
415        }
416    
417        /**
418         * Persists the given {@link Lock} to a temporary directory.
419         * <p>
420         * <ul>
421         * <li>If the lock is empty (it holds no features), a potentially existing file is deleted.</li>
422         * <li>If the lock is not empty and it has not been stored yet, it is written to a temporary
423         * file.</li>
424         * <li>If the lock is not empty and it has already been stored, the existing file is
425         * overwritten with the current lock status.</li>
426         * </ul>
427         * 
428         * @param lock
429         *            the <code>Lock</code> to be persisted
430         * @return <code>File</code> that stores the <code>Lock</code>, may be null
431         * @throws DatastoreException
432         */
433        private File persistLock( Lock lock )
434                                throws DatastoreException {
435    
436            File file = this.lockToFile.get( lock );
437            if ( !lock.getLockedFids().isEmpty() ) {
438                // only store it if any features are hold by the lock
439    
440                // delete file if it already exists
441                if ( file != null ) {
442                    file.delete();
443                }
444    
445                // write lock to file
446                FileOutputStream fos = null;
447                ObjectOutputStream oos = null;
448                try {
449                    try {
450                        if ( file == null ) {
451                            file = File.createTempFile( FILE_PREFIX, FILE_SUFFIX, workingDir );
452                        }
453                        String msg = Messages.getMessage( "DATASTORE_LOCK_STORE", lock.getId(), file.getAbsolutePath() );
454                        LOG.logDebug( msg );
455                        fos = new FileOutputStream( file );
456                        oos = new ObjectOutputStream( fos );
457                        oos.writeObject( lock );
458                    } finally {
459                        if ( oos != null ) {
460                            oos.flush();
461                            oos.close();
462                        } else if ( fos != null ) {
463                            fos.close();
464                        }
465                    }
466                } catch ( Exception e ) {
467                    String msg = Messages.getMessage( "DATASTORE_LOCK_STORING_FAILED", lock.getId(),
468                                                      file.getAbsolutePath(), e.getMessage() );
469                    LOG.logError( msg, e );
470                    throw new DatastoreException( msg );
471                }
472            } else if ( file != null ) {
473                // else (and file exists) delete file
474                LOG.logDebug( "Deleting lock '" + lock.getId() + "' in file: " + file.getAbsolutePath() );
475                file.delete();
476            }
477            return file;
478        }
479    
480        @Override
481        public String toString() {
482            StringBuffer sb = new StringBuffer( "LockManager status:\n" );
483            sb.append( "- number of locked features: " + this.fidToLock.size() + "\n" );
484            sb.append( "- active locks: " + this.lockIdToLock.size() + "\n" );
485            for ( Lock lock : this.lockIdToLock.values() ) {
486                sb.append( "- " );
487                sb.append( lock );
488                sb.append( "\n" );
489            }
490            return sb.toString();
491        }
492    }