001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.data;
017:
018: import java.io.IOException;
019: import java.util.HashMap;
020: import java.util.HashSet;
021: import java.util.Iterator;
022: import java.util.Map;
023: import java.util.Set;
024: import java.util.logging.Logger;
025:
026: import org.geotools.data.Transaction.State;
027: import org.geotools.feature.Feature;
028: import org.geotools.feature.FeatureType;
029:
030: /**
031: * Provides In-Process FeatureLocking support for DataStore implementations.
032: *
033: * <p>
034: * If at all possible DataStore implementations should provide a real Feature
035: * Locking support that is persisted to disk or database and resepected by
036: * other processes.
037: * </p>
038: *
039: * <p>
040: * This class provides a stop gap solution that implementations may use for
041: * GeoServer compatability.
042: * </p>
043: *
044: * @author Jody Garnett, Refractions Research
045: * @author Chris Holmes, TOPP
046: *
047: * @task REVISIT: I'm not sure that the map within a map is a good idea, it
048: * makes things perhaps too complicated. A nasty bug came about with
049: * releasing, as allLocks put locks into a new collection, and the
050: * iterator just removed them from that set instead of from the storage.
051: * This is now fixed, but the loop to do it is really damn complex.
052: * I'm not sure of the solution, but there should be something that is
053: * less confusing.
054: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/data/InProcessLockingManager.java $
055: */
056: public class InProcessLockingManager implements LockingManager {
057: /** The logger for the postgis module. */
058: private static final Logger LOGGER = org.geotools.util.logging.Logging
059: .getLogger("org.geotools.data.data");
060:
061: /** lockTable access by typeName stores Transactions or MemoryLocks */
062: protected Map lockTables = new HashMap();
063:
064: /**
065: * Aquire lock on featureID.
066: *
067: * <p>
068: * This method will fail if Lock is already held by another.
069: * </p>
070: *
071: * @param typeName TypeName storing feature
072: * @param featureID FeatureID to lock
073: * @param transaction Transaction to lock against
074: * @param featureLock FeatureLock describing lock request
075: *
076: * @throws FeatureLockException Indicates a problem with the lock request
077: */
078: public synchronized void lockFeatureID(String typeName,
079: String featureID, Transaction transaction,
080: FeatureLock featureLock) throws FeatureLockException {
081: Lock lock = getLock(typeName, featureID);
082:
083: // This is a loop so we can wait on Transaction Locks
084: //
085: while (lock != null) {
086: // we have a conflict
087: if (lock instanceof TransactionLock) {
088: TransactionLock tlock = (TransactionLock) lock;
089:
090: if (transaction == tlock.transaction) {
091: // lock already held by this transacstion
092: // we could just consider returning here
093: //
094: throw new FeatureLockException(
095: "Transaction Lock is already held by this Transaction",
096: featureID);
097: } else {
098: // we should wait till it is available and then grab
099: // the lock
100: try {
101: synchronized (tlock) {
102: tlock.wait();
103: }
104:
105: lock = getLock(typeName, featureID);
106: } catch (InterruptedException interupted) {
107: throw new FeatureLockException(
108: "Interupted while waiting for Transaction Lock",
109: featureID, interupted);
110: }
111: }
112: } else if (lock instanceof MemoryLock) {
113: MemoryLock mlock = (MemoryLock) lock;
114: throw new FeatureLockException(
115: "Feature Lock is held by Authorization "
116: + mlock.authID, featureID);
117: } else {
118: throw new FeatureLockException("Lock is already held "
119: + lock, featureID);
120: }
121: }
122:
123: // Lock is Available
124: //
125: lock = createLock(transaction, featureLock);
126: locks(typeName).put(featureID, lock);
127: }
128:
129: /**
130: * Lock for typeName & featureID if it exists.
131: *
132: * <p>
133: * This method will not return expired locks.
134: * </p>
135: *
136: * @param typeName
137: * @param featureID
138: *
139: * @return Lock if exists, or null
140: */
141: protected Lock getLock(String typeName, String featureID) {
142: Map locks = locks(typeName);
143: //LOGGER.info("checking for lock " + typeName + ", " + featureID
144: // + " in locks " + locks);
145:
146: synchronized (locks) {
147: if (locks.containsKey(featureID)) {
148: Lock lock = (Lock) locks.get(featureID);
149:
150: if (lock.isExpired()) {
151: locks.remove(featureID);
152: //LOGGER.info("returning null");
153:
154: return null;
155: } else {
156: //LOGGER.info("returing " + lock);
157:
158: return lock;
159: }
160: } else {
161: //LOGGER.info("locks did not contain key, returning null");
162:
163: // not found
164: return null;
165: }
166: }
167: }
168:
169: /**
170: * Creates the right sort of In-Process Lock.
171: *
172: * @param transaction
173: * @param featureLock
174: *
175: * @return In-Process Lock
176: *
177: * @throws FeatureLockException When a Transaction lock is requested
178: * against Transaction.AUTO_COMMIT
179: */
180: protected synchronized Lock createLock(Transaction transaction,
181: FeatureLock featureLock) throws FeatureLockException {
182: if (featureLock == FeatureLock.TRANSACTION) {
183: // we need a Transacstion Lock
184: if (transaction == Transaction.AUTO_COMMIT) {
185: throw new FeatureLockException(
186: "We cannot issue a Transaction lock against AUTO_COMMIT");
187: }
188:
189: TransactionLock lock = (TransactionLock) transaction
190: .getState(this );
191:
192: if (lock == null) {
193: lock = new TransactionLock();
194: transaction.putState(this , lock);
195:
196: return lock;
197: } else {
198: return lock;
199: }
200: } else {
201: return new MemoryLock(featureLock);
202: }
203: }
204:
205: /**
206: * Access to a Map of locks for typeName
207: *
208: * @param typeName typeName
209: *
210: * @return Map of Transaction or MemoryLock by featureID
211: */
212: protected Map locks(String typeName) {
213: synchronized (lockTables) {
214: if (lockTables.containsKey(typeName)) {
215: return (Map) lockTables.get(typeName);
216: } else {
217: Map locks = new HashMap();
218: lockTables.put(typeName, locks);
219:
220: return locks;
221: }
222: }
223: }
224:
225: /**
226: * Set of all locks.
227: *
228: * @return Set of all locks
229: */
230: protected Set allLocks() {
231: synchronized (lockTables) {
232: Set set = new HashSet();
233: Map fidLocks;
234:
235: for (Iterator i = lockTables.values().iterator(); i
236: .hasNext();) {
237: fidLocks = (Map) i.next();
238: set.addAll(fidLocks.values());
239: }
240:
241: return set;
242: }
243: }
244:
245: /**
246: * Checks mutability of featureID for this transaction.
247: *
248: * <p>
249: * Two behaviors are defined by FeatureLocking:
250: * </p>
251: *
252: * <ul>
253: * <li>
254: * TransactionLock (Blocking): lock held by a Transaction<br>
255: * Authorization is granted to the Transaction holding the Lock. Conflict
256: * will result in a block until the Transaction holding the lock
257: * completes. (This behavior is equivalent to a Database row-lock, or a
258: * java synchronized statement)
259: * </li>
260: * <li>
261: * FeatureLock (Error): lock held by a FeatureLock<br>
262: * Authorization is based on the set of Authorization IDs held by the
263: * provided Transaction. Conflict will result in an error. (This behavior
264: * is equivalent to the WFS locking specification)
265: * </li>
266: * </ul>
267: *
268: * <p>
269: * Right now we are just going to error out with an exception
270: * </p>
271: *
272: * @param typeName Feature type to check against
273: * @param featureID FeatureID to check
274: * @param transaction Provides Authorization
275: *
276: * @throws FeatureLockException If transaction does not have sufficient
277: * authroization
278: */
279: public void assertAccess(String typeName, String featureID,
280: Transaction transaction) throws FeatureLockException {
281: Lock lock = getLock(typeName, featureID);
282:
283: //LOGGER.info("asserting access on lock for " + typeName + ", fid: "
284: // + featureID + ", transaction: " + transaction + ", lock " + lock);
285:
286: if ((lock != null) && !lock.isAuthorized(transaction)) {
287: throw new FeatureLockException(
288: "Transaction does not have authorization for "
289: + typeName + ":" + featureID);
290: }
291: }
292:
293: /**
294: * Provides a wrapper on the provided writer that checks locks.
295: *
296: * @param writer FeatureWriter requiring access control
297: * @param transaction Transaction being used
298: *
299: * @return FeatureWriter with lock checking
300: */
301: public FeatureWriter checkedWriter(final FeatureWriter writer,
302: final Transaction transaction) {
303: FeatureType featureType = writer.getFeatureType();
304: final String typeName = featureType.getTypeName();
305:
306: return new FeatureWriter() {
307: Feature live = null;
308:
309: public FeatureType getFeatureType() {
310: return writer.getFeatureType();
311: }
312:
313: public Feature next() throws IOException {
314: live = writer.next();
315:
316: return live;
317: }
318:
319: public void remove() throws IOException {
320: if (live != null) {
321: assertAccess(typeName, live.getID(), transaction);
322: }
323:
324: writer.remove();
325: live = null;
326: }
327:
328: public void write() throws IOException {
329: if (live != null) {
330: assertAccess(typeName, live.getID(), transaction);
331: }
332:
333: writer.write();
334: live = null;
335: }
336:
337: public boolean hasNext() throws IOException {
338: live = null;
339:
340: return writer.hasNext();
341: }
342:
343: public void close() throws IOException {
344: live = null;
345: if (writer != null)
346: writer.close();
347: }
348: };
349: }
350:
351: /**
352: * Release indicated featureID, must have correct authroization.
353: *
354: * @param typeName
355: * @param featureID
356: * @param transaction
357: * @param featureLock
358: *
359: * @throws IOException If lock could not be released
360: */
361: public synchronized void unLockFeatureID(String typeName,
362: String featureID, Transaction transaction,
363: FeatureLock featureLock) throws IOException {
364: assertAccess(typeName, featureID, transaction);
365: locks(typeName).remove(featureID);
366: }
367:
368: /**
369: * Refresh locks held by the authorization <code>authID</code>.
370: *
371: * <p>
372: * (remember that the lock may have expired)
373: * </p>
374: *
375: * @param authID Authorization identifing Lock to refresh
376: * @param transaction Transaction with authorization for lockID
377: *
378: * @return <code>true</code> if lock was found and refreshed
379: *
380: * @throws IOException If transaction not authorized to refresh authID
381: * @throws IllegalArgumentException If authID or transaction not provided
382: */
383: public synchronized boolean refresh(String authID,
384: Transaction transaction) throws IOException {
385: if (authID == null) {
386: throw new IllegalArgumentException("lockID required");
387: }
388:
389: if ((transaction == null)
390: || (transaction == Transaction.AUTO_COMMIT)) {
391: throw new IllegalArgumentException(
392: "Tansaction required (with authorization for "
393: + authID + ")");
394: }
395:
396: Lock lock;
397: boolean refresh = false;
398:
399: for (Iterator i = allLocks().iterator(); i.hasNext();) {
400: lock = (Lock) i.next();
401:
402: if (lock.isExpired()) {
403: i.remove();
404: } else if (lock.isMatch(authID)) {
405: if (lock.isAuthorized(transaction)) {
406: lock.refresh();
407: refresh = true;
408: } else {
409: throw new IOException("Not authorized to refresh "
410: + lock);
411: }
412: }
413: }
414:
415: return refresh;
416: }
417:
418: /**
419: * Release locks held by the authorization <code>authID</code>.
420: *
421: * <p>
422: * (remember that the lock may have expired)
423: * </p>
424: *
425: * @param authID Authorization identifing Lock to release
426: * @param transaction Transaction with authorization for lockID
427: *
428: * @return <code>true</code> if lock was found and released
429: *
430: * @throws IOException If transaction not authorized to release authID
431: * @throws IllegalArgumentException If authID or transaction not provided
432: */
433: public boolean release(String authID, Transaction transaction)
434: throws IOException {
435: //LOGGER.info("release called on lock: " + authID + ", trans: "
436: // + transaction);
437:
438: if (authID == null) {
439: throw new IllegalArgumentException("lockID required");
440: }
441:
442: if ((transaction == null)
443: || (transaction == Transaction.AUTO_COMMIT)) {
444: throw new IllegalArgumentException(
445: "Tansaction required (with authorization for "
446: + authID + ")");
447: }
448:
449: Lock lock;
450: boolean release = false;
451:
452: //This could be done more efficiently, and perhaps cleaner,
453: //but these maps within a map are just nasty. The previous way of
454: //calling iterator.remove() didn't actually remove anything, as it
455: //was only iterating through the values of a map, which I believe
456: //java just copies, so it's immutable. Or perhaps we just moved
457: //through too many iterator layers...
458: for (Iterator i = lockTables.values().iterator(); i.hasNext();) {
459: Map fidMap = (Map) i.next();
460: Set unLockedFids = new HashSet();
461:
462: for (Iterator j = fidMap.keySet().iterator(); j.hasNext();) {
463: String fid = (String) j.next();
464: lock = (Lock) fidMap.get(fid);
465: //LOGGER.info("checking lock " + lock + ", is match "
466: // + lock.isMatch(authID));
467:
468: if (lock.isExpired()) {
469: unLockedFids.add(fid);
470:
471: //fidMap.remove(fid); concurrent modification error.
472: } else if (lock.isMatch(authID)) {
473: //LOGGER.info("matches, is authorized: "
474: // + lock.isAuthorized(transaction));
475:
476: if (lock.isAuthorized(transaction)) {
477: unLockedFids.add(fid);
478:
479: //fidMap.remove(fid);
480: release = true;
481: } else {
482: throw new IOException(
483: "Not authorized to release " + lock);
484: }
485: }
486: }
487:
488: for (Iterator k = unLockedFids.iterator(); k.hasNext();) {
489: fidMap.remove(k.next());
490: }
491: }
492:
493: return release;
494: }
495:
496: /**
497: * Implment lockExists.
498: *
499: * <p>
500: * Remeber lock may have expired.
501: * </p>
502: *
503: * @param authID
504: *
505: * @return true if lock exists for authID
506: *
507: * @see org.geotools.data.LockingManager#lockExists(java.lang.String)
508: */
509: public boolean exists(String authID) {
510: //LOGGER.info("checking existence of lock: " + authID + " in "
511: // + allLocks());
512:
513: if (authID == null) {
514: return false;
515: }
516:
517: Lock lock;
518:
519: for (Iterator i = allLocks().iterator(); i.hasNext();) {
520: lock = (Lock) i.next();
521:
522: if (lock.isExpired()) {
523: i.remove();
524: } else if (lock.isMatch(authID)) {
525: return true;
526: }
527: }
528:
529: return false;
530: }
531:
532: /**
533: * Used by test cases
534: *
535: * @param typeName
536: * @param featureID
537: *
538: * @return Return if feature is currently locked
539: */
540: public boolean isLocked(String typeName, String featureID) {
541: return getLock(typeName, featureID) != null;
542: }
543:
544: /**
545: * Represents In-Process locks for Transactions or FeatureLocks.
546: *
547: * @author Jody Garnett, Refractions Research
548: */
549: interface Lock {
550: /**
551: * Check if lock has expired, it will be removed if so
552: *
553: * @return <code>true</code> if Lock has gone stale
554: */
555: boolean isExpired();
556:
557: /**
558: * Check if authID matches this lock
559: *
560: * @return <code>true</code> if authID matches
561: */
562: boolean isMatch(String authID);
563:
564: /**
565: * Check if transaction is authorized for this lock
566: *
567: * @return <code>true</code> if transaction is authorized
568: */
569: boolean isAuthorized(Transaction transaction);
570:
571: /**
572: * Refresh lock
573: */
574: void refresh();
575:
576: /**
577: * Release lock
578: */
579: void release();
580: }
581:
582: /**
583: * Class representing TransactionDuration locks.
584: *
585: * <p>
586: * Implements Transasction.State so it can remomve itself when commit() or
587: * rollback() is called.
588: * </p>
589: *
590: * <p>
591: * Threads may wait on this object, it will notify when it releases the
592: * lock due to a commit or rollback opperation
593: * </p>
594: *
595: * @author Jody Garnett, Refractions Research
596: */
597: class TransactionLock implements Lock, State {
598: /** This will be non-null while lock is fresh */
599: Transaction transaction;
600:
601: /**
602: * A new TranasctionLock for use.
603: *
604: * <p>
605: * The lock will be stale until added to Tranasction.putState( key,
606: * Lock )
607: * </p>
608: */
609: TransactionLock() {
610: }
611:
612: /**
613: * Transaction locks do not match authIDs
614: *
615: * @param authID Authorization ID being checked
616: *
617: * @return <code>false</code>
618: */
619: public boolean isMatch(String authID) {
620: return false;
621: }
622:
623: /**
624: * <code>true</code> if Lock has gone stale
625: *
626: * @return <code>true</code> if lock is stale
627: */
628: public boolean isExpired() {
629: return transaction != null;
630: }
631:
632: /**
633: * Release lock - notify those who are waiting
634: */
635: public void release() {
636: transaction = null;
637: notifyAll();
638: }
639:
640: /**
641: * TransactionLocks do not need to be refreshed
642: */
643: public void refresh() {
644: // do not need to implement
645: }
646:
647: /**
648: * <code>true </code> if tranasction is the same one that provided this
649: * lock
650: *
651: * @param transaction Transaction to check authorization against
652: *
653: * @return true if transaction is authorized
654: */
655: public boolean isAuthorized(Transaction transaction) {
656: return this .transaction == transaction;
657: }
658:
659: /**
660: * Call back from Transaction.State
661: *
662: * @param AuthID AuthoID being added to transaction
663: *
664: * @throws IOException Not used
665: */
666: public void addAuthorization(String AuthID) throws IOException {
667: // we don't need this callback
668: }
669:
670: /**
671: * Will make lock stale on commit
672: *
673: * @throws IOException If anything goes wrong
674: */
675: public void commit() throws IOException {
676: release();
677: }
678:
679: /**
680: * Will make lock stale on rollback
681: *
682: * @throws IOException If anything goes wrong
683: */
684: public void rollback() throws IOException {
685: release();
686: }
687:
688: /**
689: * Will make lock stale if removed from Transaction
690: *
691: * @param transaction Transaction on putState, or null on removeState
692: */
693: public void setTransaction(Transaction transaction) {
694: if (transaction == null) {
695: release();
696: }
697:
698: this .transaction = transaction;
699: }
700:
701: public String toString() {
702: return "TranasctionLock(" + !isExpired() + ")";
703: }
704: }
705:
706: /**
707: * Class referenced by featureID in locks( typeName).
708: *
709: * <p>
710: * FeatureLock is the request - MemoryLock is the result.
711: * </p>
712: *
713: * @author Jody Garnett, Refractions Reasearch Inc.
714: */
715: class MemoryLock implements Lock {
716: String authID;
717: long duration;
718: long expiry;
719:
720: MemoryLock(FeatureLock lock) {
721: this (lock.getAuthorization(), lock.getDuration());
722: }
723:
724: MemoryLock(String id, long length) {
725: authID = id;
726: this .duration = length;
727: expiry = System.currentTimeMillis() + length;
728: }
729:
730: public boolean isMatch(String id) {
731: return authID.equals(id);
732: }
733:
734: public void refresh() {
735: expiry = System.currentTimeMillis() + duration;
736: }
737:
738: public void release() {
739: }
740:
741: public boolean isExpired() {
742: if (duration == 0) {
743: return false; // perma lock
744: }
745:
746: long now = System.currentTimeMillis();
747:
748: return now >= expiry;
749: }
750:
751: public boolean isAuthorized(Transaction transaction) {
752: //LOGGER.info("checking authorization on " + this.toString() + ", "
753: // + ((transaction != Transaction.AUTO_COMMIT)
754: // ? transaction.getAuthorizations().toString() : "autocommit"));
755:
756: return (transaction != Transaction.AUTO_COMMIT)
757: && transaction.getAuthorizations().contains(authID);
758: }
759:
760: public String toString() {
761: if (duration == 0) {
762: return "MemoryLock(" + authID + "|PermaLock)";
763: }
764:
765: long now = System.currentTimeMillis();
766: long delta = (expiry - now);
767: long dur = duration;
768:
769: return "MemoryLock(" + authID + "|" + delta + "ms|" + dur
770: + "ms)";
771: }
772: }
773: }
|