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 }