    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    }