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