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 }