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 */
017package org.apache.commons.transaction.memory;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.Map;
025import java.util.Set;
026
027import 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 */
048public 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}