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 }