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 }