001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.commons.transaction.memory;
018:
019: import java.util.ArrayList;
020: import java.util.Collection;
021: import java.util.Collections;
022: import java.util.HashSet;
023: import java.util.Iterator;
024: import java.util.Map;
025: import java.util.Set;
026:
027: import javax.transaction.Status;
028:
029: /**
030: * Wrapper that adds transactional control to all kinds of maps that implement the {@link Map} interface.
031: * This wrapper has rather weak isolation, but is simply, neven blocks and commits will never fail for logical
032: * reasons.
033: * <br>
034: * Start a transaction by calling {@link #startTransaction()}. Then perform the normal actions on the map and
035: * finally either call {@link #commitTransaction()} to make your changes permanent or {@link #rollbackTransaction()} to
036: * undo them.
037: * <br>
038: * <em>Caution:</em> Do not modify values retrieved by {@link #get(Object)} as this will circumvent the transactional mechanism.
039: * Rather clone the value or copy it in a way you see fit and store it back using {@link #put(Object, Object)}.
040: * <br>
041: * <em>Note:</em> This wrapper guarantees isolation level <code>READ COMMITTED</code> only. I.e. as soon a value
042: * is committed in one transaction it will be immediately visible in all other concurrent transactions.
043: *
044: * @version $Id: TransactionalMapWrapper.java 493628 2007-01-07 01:42:48Z joerg $
045: * @see OptimisticMapWrapper
046: * @see PessimisticMapWrapper
047: */
048: public class TransactionalMapWrapper implements Map, Status {
049:
050: /** The map wrapped. */
051: protected Map wrapped;
052:
053: /** Factory to be used to create temporary maps for transactions. */
054: protected MapFactory mapFactory;
055: /** Factory to be used to create temporary sets for transactions. */
056: protected SetFactory setFactory;
057:
058: private ThreadLocal activeTx = new ThreadLocal();
059:
060: /**
061: * Creates a new transactional map wrapper. Temporary maps and sets to store transactional
062: * data will be instances of {@link java.util.HashMap} and {@link java.util.HashSet}.
063: *
064: * @param wrapped map to be wrapped
065: */
066: public TransactionalMapWrapper(Map wrapped) {
067: this (wrapped, new HashMapFactory(), new HashSetFactory());
068: }
069:
070: /**
071: * Creates a new transactional map wrapper. Temporary maps and sets to store transactional
072: * data will be created and disposed using {@link MapFactory} and {@link SetFactory}.
073: *
074: * @param wrapped map to be wrapped
075: * @param mapFactory factory for temporary maps
076: * @param setFactory factory for temporary sets
077: */
078: public TransactionalMapWrapper(Map wrapped, MapFactory mapFactory,
079: SetFactory setFactory) {
080: this .wrapped = Collections.synchronizedMap(wrapped);
081: this .mapFactory = mapFactory;
082: this .setFactory = setFactory;
083: }
084:
085: /**
086: * Checks if any write operations have been performed inside this transaction.
087: *
088: * @return <code>true</code> if no write opertation has been performed inside the current transaction,
089: * <code>false</code> otherwise
090: */
091: public boolean isReadOnly() {
092: TxContext txContext = getActiveTx();
093:
094: if (txContext == null) {
095: throw new IllegalStateException("Active thread "
096: + Thread.currentThread()
097: + " not associated with a transaction!");
098: }
099:
100: return txContext.readOnly;
101: }
102:
103: /**
104: * Checks whether this transaction has been marked to allow a rollback as the only
105: * valid outcome. This can be set my method {@link #markTransactionForRollback()} or might
106: * be set internally be any fatal error. Once a transaction is marked for rollback there
107: * is no way to undo this. A transaction that is marked for rollback can not be committed,
108: * also rolled back.
109: *
110: * @return <code>true</code> if this transaction has been marked for a roll back
111: * @see #markTransactionForRollback()
112: */
113: public boolean isTransactionMarkedForRollback() {
114: TxContext txContext = getActiveTx();
115:
116: if (txContext == null) {
117: throw new IllegalStateException("Active thread "
118: + Thread.currentThread()
119: + " not associated with a transaction!");
120: }
121:
122: return (txContext.status == Status.STATUS_MARKED_ROLLBACK);
123: }
124:
125: /**
126: * Marks the current transaction to allow only a rollback as valid outcome.
127: *
128: * @see #isTransactionMarkedForRollback()
129: */
130: public void markTransactionForRollback() {
131: TxContext txContext = getActiveTx();
132:
133: if (txContext == null) {
134: throw new IllegalStateException("Active thread "
135: + Thread.currentThread()
136: + " not associated with a transaction!");
137: }
138:
139: txContext.status = Status.STATUS_MARKED_ROLLBACK;
140: }
141:
142: /**
143: * Suspends the transaction associated to the current thread. I.e. the associated between the
144: * current thread and the transaction is deleted. This is useful when you want to continue the transaction
145: * in another thread later. Call {@link #resumeTransaction(TxContext)} - possibly in another thread than the current -
146: * to resume work on the transaction.
147: * <br><br>
148: * <em>Caution:</em> When calling this method the returned identifier
149: * for the transaction is the only remaining reference to the transaction, so be sure to remember it or
150: * the transaction will be eventually deleted (and thereby rolled back) as garbage.
151: *
152: * @return an identifier for the suspended transaction, will be needed to later resume the transaction by
153: * {@link #resumeTransaction(TxContext)}
154: *
155: * @see #resumeTransaction(TxContext)
156: */
157: public TxContext suspendTransaction() {
158: TxContext txContext = getActiveTx();
159:
160: if (txContext == null) {
161: throw new IllegalStateException("Active thread "
162: + Thread.currentThread()
163: + " not associated with a transaction!");
164: }
165:
166: txContext.suspended = true;
167: setActiveTx(null);
168: return txContext;
169: }
170:
171: /**
172: * Resumes a transaction in the current thread that has previously been suspened by {@link #suspendTransaction()}.
173: *
174: * @param suspendedTx the identifier for the transaction to be resumed, delivered by {@link #suspendTransaction()}
175: *
176: * @see #suspendTransaction()
177: */
178: public void resumeTransaction(TxContext suspendedTx) {
179: if (getActiveTx() != null) {
180: throw new IllegalStateException("Active thread "
181: + Thread.currentThread()
182: + " already associated with a transaction!");
183: }
184:
185: if (suspendedTx == null) {
186: throw new IllegalStateException("No transaction to resume!");
187: }
188:
189: if (!suspendedTx.suspended) {
190: throw new IllegalStateException(
191: "Transaction to resume needs to be suspended!");
192: }
193:
194: suspendedTx.suspended = false;
195: setActiveTx(suspendedTx);
196: }
197:
198: /**
199: * Returns the state of the current transaction.
200: *
201: * @return state of the current transaction as decribed in the {@link Status} interface.
202: */
203: public int getTransactionState() {
204: TxContext txContext = getActiveTx();
205:
206: if (txContext == null) {
207: return STATUS_NO_TRANSACTION;
208: }
209: return txContext.status;
210: }
211:
212: /**
213: * Starts a new transaction and associates it with the current thread. All subsequent changes in the same
214: * thread made to the map are invisible from other threads until {@link #commitTransaction()} is called.
215: * Use {@link #rollbackTransaction()} to discard your changes. After calling either method there will be
216: * no transaction associated to the current thread any longer.
217: * <br><br>
218: * <em>Caution:</em> Be careful to finally call one of those methods,
219: * as otherwise the transaction will lurk around for ever.
220: *
221: * @see #commitTransaction()
222: * @see #rollbackTransaction()
223: */
224: public void startTransaction() {
225: if (getActiveTx() != null) {
226: throw new IllegalStateException("Active thread "
227: + Thread.currentThread()
228: + " already associated with a transaction!");
229: }
230: setActiveTx(new TxContext());
231: }
232:
233: /**
234: * Discards all changes made in the current transaction and deletes the association between the current thread
235: * and the transaction.
236: *
237: * @see #startTransaction()
238: * @see #commitTransaction()
239: */
240: public void rollbackTransaction() {
241: TxContext txContext = getActiveTx();
242:
243: if (txContext == null) {
244: throw new IllegalStateException("Active thread "
245: + Thread.currentThread()
246: + " not associated with a transaction!");
247: }
248:
249: // simply forget about tx
250: txContext.dispose();
251: setActiveTx(null);
252: }
253:
254: /**
255: * Commits all changes made in the current transaction and deletes the association between the current thread
256: * and the transaction.
257: *
258: * @see #startTransaction()
259: * @see #rollbackTransaction()
260: */
261: public void commitTransaction() {
262: TxContext txContext = getActiveTx();
263:
264: if (txContext == null) {
265: throw new IllegalStateException("Active thread "
266: + Thread.currentThread()
267: + " not associated with a transaction!");
268: }
269:
270: if (txContext.status == Status.STATUS_MARKED_ROLLBACK) {
271: throw new IllegalStateException("Active thread "
272: + Thread.currentThread()
273: + " is marked for rollback!");
274: }
275:
276: txContext.merge();
277: txContext.dispose();
278: setActiveTx(null);
279: }
280:
281: //
282: // Map methods
283: //
284:
285: /**
286: * @see Map#clear()
287: */
288: public void clear() {
289: TxContext txContext = getActiveTx();
290: if (txContext != null) {
291: txContext.clear();
292: } else {
293: wrapped.clear();
294: }
295: }
296:
297: /**
298: * @see Map#size()
299: */
300: public int size() {
301: TxContext txContext = getActiveTx();
302: if (txContext != null) {
303: return txContext.size();
304: } else {
305: return wrapped.size();
306: }
307: }
308:
309: /**
310: * @see Map#isEmpty()
311: */
312: public boolean isEmpty() {
313: TxContext txContext = getActiveTx();
314: if (txContext == null) {
315: return wrapped.isEmpty();
316: } else {
317: return txContext.isEmpty();
318: }
319: }
320:
321: /**
322: * @see Map#containsKey(java.lang.Object)
323: */
324: public boolean containsKey(Object key) {
325: return keySet().contains(key);
326: }
327:
328: /**
329: * @see Map#containsValue(java.lang.Object)
330: */
331: public boolean containsValue(Object value) {
332: TxContext txContext = getActiveTx();
333:
334: if (txContext == null) {
335: return wrapped.containsValue(value);
336: } else {
337: return values().contains(value);
338: }
339: }
340:
341: /**
342: * @see Map#values()
343: */
344: public Collection values() {
345:
346: TxContext txContext = getActiveTx();
347:
348: if (txContext == null) {
349: return wrapped.values();
350: } else {
351: // XXX expensive :(
352: Collection values = new ArrayList();
353: for (Iterator it = keySet().iterator(); it.hasNext();) {
354: Object key = it.next();
355: Object value = get(key);
356: // XXX we have no isolation, so get entry might have been deleted in the meantime
357: if (value != null) {
358: values.add(value);
359: }
360: }
361: return values;
362: }
363: }
364:
365: /**
366: * @see Map#putAll(java.util.Map)
367: */
368: public void putAll(Map map) {
369: TxContext txContext = getActiveTx();
370:
371: if (txContext == null) {
372: wrapped.putAll(map);
373: } else {
374: for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
375: Map.Entry entry = (Map.Entry) it.next();
376: txContext.put(entry.getKey(), entry.getValue());
377: }
378: }
379: }
380:
381: /**
382: * @see Map#entrySet()
383: */
384: public Set entrySet() {
385: TxContext txContext = getActiveTx();
386: if (txContext == null) {
387: return wrapped.entrySet();
388: } else {
389: Set entrySet = new HashSet();
390: // XXX expensive :(
391: for (Iterator it = keySet().iterator(); it.hasNext();) {
392: Object key = it.next();
393: Object value = get(key);
394: // XXX we have no isolation, so get entry might have been deleted in the meantime
395: if (value != null) {
396: entrySet.add(new HashEntry(key, value));
397: }
398: }
399: return entrySet;
400: }
401: }
402:
403: /**
404: * @see Map#keySet()
405: */
406: public Set keySet() {
407: TxContext txContext = getActiveTx();
408:
409: if (txContext == null) {
410: return wrapped.keySet();
411: } else {
412: return txContext.keys();
413: }
414: }
415:
416: /**
417: * @see Map#get(java.lang.Object)
418: */
419: public Object get(Object key) {
420: TxContext txContext = getActiveTx();
421:
422: if (txContext != null) {
423: return txContext.get(key);
424: } else {
425: return wrapped.get(key);
426: }
427: }
428:
429: /**
430: * @see Map#remove(java.lang.Object)
431: */
432: public Object remove(Object key) {
433: TxContext txContext = getActiveTx();
434:
435: if (txContext == null) {
436: return wrapped.remove(key);
437: } else {
438: Object oldValue = get(key);
439: txContext.remove(key);
440: return oldValue;
441: }
442: }
443:
444: /**
445: * @see Map#put(java.lang.Object, java.lang.Object)
446: */
447: public Object put(Object key, Object value) {
448: TxContext txContext = getActiveTx();
449:
450: if (txContext == null) {
451: return wrapped.put(key, value);
452: } else {
453: Object oldValue = get(key);
454: txContext.put(key, value);
455: return oldValue;
456: }
457:
458: }
459:
460: protected TxContext getActiveTx() {
461: return (TxContext) activeTx.get();
462: }
463:
464: protected void setActiveTx(TxContext txContext) {
465: activeTx.set(txContext);
466: }
467:
468: // mostly copied from org.apache.commons.collections.map.AbstractHashedMap
469: protected static class HashEntry implements Map.Entry {
470: /** The key */
471: protected Object key;
472: /** The value */
473: protected Object value;
474:
475: protected HashEntry(Object key, Object value) {
476: this .key = key;
477: this .value = value;
478: }
479:
480: public Object getKey() {
481: return key;
482: }
483:
484: public Object getValue() {
485: return value;
486: }
487:
488: public Object setValue(Object value) {
489: Object old = this .value;
490: this .value = value;
491: return old;
492: }
493:
494: public boolean equals(Object obj) {
495: if (obj == this ) {
496: return true;
497: }
498: if (!(obj instanceof Map.Entry)) {
499: return false;
500: }
501: Map.Entry other = (Map.Entry) obj;
502: return (getKey() == null ? other.getKey() == null
503: : getKey().equals(other.getKey()))
504: && (getValue() == null ? other.getValue() == null
505: : getValue().equals(other.getValue()));
506: }
507:
508: public int hashCode() {
509: return (getKey() == null ? 0 : getKey().hashCode())
510: ^ (getValue() == null ? 0 : getValue().hashCode());
511: }
512:
513: public String toString() {
514: return new StringBuffer().append(getKey()).append('=')
515: .append(getValue()).toString();
516: }
517: }
518:
519: public class TxContext {
520: protected Set deletes;
521: protected Map changes;
522: protected Map adds;
523: protected int status;
524: protected boolean cleared;
525: protected boolean readOnly;
526: protected boolean suspended = false;
527:
528: protected TxContext() {
529: deletes = setFactory.createSet();
530: changes = mapFactory.createMap();
531: adds = mapFactory.createMap();
532: status = Status.STATUS_ACTIVE;
533: cleared = false;
534: readOnly = true;
535: }
536:
537: protected Set keys() {
538: Set keySet = new HashSet();
539: if (!cleared) {
540: keySet.addAll(wrapped.keySet());
541: keySet.removeAll(deletes);
542: }
543: keySet.addAll(adds.keySet());
544: return keySet;
545: }
546:
547: protected Object get(Object key) {
548:
549: if (deletes.contains(key)) {
550: // reflects that entry has been deleted in this tx
551: return null;
552: }
553:
554: if (changes.containsKey(key)) {
555: return changes.get(key);
556: }
557:
558: if (adds.containsKey(key)) {
559: return adds.get(key);
560: }
561:
562: if (cleared) {
563: return null;
564: } else {
565: // not modified in this tx
566: return wrapped.get(key);
567: }
568: }
569:
570: protected void put(Object key, Object value) {
571: try {
572: readOnly = false;
573: deletes.remove(key);
574: if (wrapped.containsKey(key)) {
575: changes.put(key, value);
576: } else {
577: adds.put(key, value);
578: }
579: } catch (RuntimeException e) {
580: status = Status.STATUS_MARKED_ROLLBACK;
581: throw e;
582: } catch (Error e) {
583: status = Status.STATUS_MARKED_ROLLBACK;
584: throw e;
585: }
586: }
587:
588: protected void remove(Object key) {
589:
590: try {
591: readOnly = false;
592: changes.remove(key);
593: adds.remove(key);
594: if (wrapped.containsKey(key) && !cleared) {
595: deletes.add(key);
596: }
597: } catch (RuntimeException e) {
598: status = Status.STATUS_MARKED_ROLLBACK;
599: throw e;
600: } catch (Error e) {
601: status = Status.STATUS_MARKED_ROLLBACK;
602: throw e;
603: }
604: }
605:
606: protected int size() {
607: int size = (cleared ? 0 : wrapped.size());
608:
609: size -= deletes.size();
610: size += adds.size();
611:
612: return size;
613: }
614:
615: protected void clear() {
616: readOnly = false;
617: cleared = true;
618: deletes.clear();
619: changes.clear();
620: adds.clear();
621: }
622:
623: protected boolean isEmpty() {
624: return (size() == 0);
625: }
626:
627: protected void merge() {
628: if (!readOnly) {
629:
630: if (cleared) {
631: wrapped.clear();
632: }
633:
634: wrapped.putAll(changes);
635: wrapped.putAll(adds);
636:
637: for (Iterator it = deletes.iterator(); it.hasNext();) {
638: Object key = it.next();
639: wrapped.remove(key);
640: }
641: }
642: }
643:
644: protected void dispose() {
645: setFactory.disposeSet(deletes);
646: deletes = null;
647: mapFactory.disposeMap(changes);
648: changes = null;
649: mapFactory.disposeMap(adds);
650: adds = null;
651: status = Status.STATUS_NO_TRANSACTION;
652: }
653: }
654: }
|