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, SetFactory setFactory) {
079            this.wrapped = Collections.synchronizedMap(wrapped);
080            this.mapFactory = mapFactory;
081            this.setFactory = setFactory;
082        }
083    
084        /**
085         * Checks if any write operations have been performed inside this transaction.
086         * 
087         * @return <code>true</code> if no write opertation has been performed inside the current transaction,
088         * <code>false</code> otherwise
089         */
090        public boolean isReadOnly() {
091            TxContext txContext = getActiveTx();
092    
093            if (txContext == null) {
094                throw new IllegalStateException(
095                    "Active thread " + Thread.currentThread() + " not associated with a transaction!");
096            }
097    
098            return txContext.readOnly;
099        }
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    }