View Javadoc

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