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 }