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 }