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