Coverage Report - org.apache.commons.transaction.memory.TransactionalMapWrapper
 
Classes in this File Line Coverage Branch Coverage Complexity
TransactionalMapWrapper
85%
111/130
70%
41/58
2.952
TransactionalMapWrapper$HashEntry
64%
11/17
30%
6/20
2.952
TransactionalMapWrapper$TxContext
84%
67/79
92%
24/26
2.952
 
 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  20
     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  6
         this(wrapped, new HashMapFactory(), new HashSetFactory());
 68  6
     }
 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  20
     public TransactionalMapWrapper(Map wrapped, MapFactory mapFactory, SetFactory setFactory) {
 79  20
         this.wrapped = Collections.synchronizedMap(wrapped);
 80  20
         this.mapFactory = mapFactory;
 81  20
         this.setFactory = setFactory;
 82  20
     }
 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  6
         TxContext txContext = getActiveTx();
 92  
 
 93  6
         if (txContext == null) {
 94  0
             throw new IllegalStateException(
 95  
                 "Active thread " + Thread.currentThread() + " not associated with a transaction!");
 96  
         }
 97  
 
 98  6
         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  6
         TxContext txContext = getActiveTx();
 113  
 
 114  6
         if (txContext == null) {
 115  0
             throw new IllegalStateException(
 116  
                 "Active thread " + Thread.currentThread() + " not associated with a transaction!");
 117  
         }
 118  
 
 119  6
         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  3
         TxContext txContext = getActiveTx();
 129  
 
 130  3
         if (txContext == null) {
 131  0
             throw new IllegalStateException(
 132  
                 "Active thread " + Thread.currentThread() + " not associated with a transaction!");
 133  
         }
 134  
 
 135  3
         txContext.status = Status.STATUS_MARKED_ROLLBACK;
 136  3
     }
 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  6
         TxContext txContext = getActiveTx();
 155  
 
 156  6
         if (txContext == null) {
 157  0
             throw new IllegalStateException(
 158  
                 "Active thread " + Thread.currentThread() + " not associated with a transaction!");
 159  
         }
 160  
 
 161  6
         txContext.suspended = true;
 162  6
         setActiveTx(null);
 163  6
         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  6
         if (getActiveTx() != null) {
 175  0
             throw new IllegalStateException(
 176  
                 "Active thread " + Thread.currentThread() + " already associated with a transaction!");
 177  
         }
 178  
 
 179  6
         if (suspendedTx == null) {
 180  0
             throw new IllegalStateException("No transaction to resume!");
 181  
         }
 182  
 
 183  6
         if (!suspendedTx.suspended) {
 184  0
             throw new IllegalStateException("Transaction to resume needs to be suspended!");
 185  
         }
 186  
 
 187  6
         suspendedTx.suspended = false;
 188  6
         setActiveTx(suspendedTx);
 189  6
     }
 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  9
         TxContext txContext = getActiveTx();
 198  
 
 199  9
         if (txContext == null) {
 200  6
             return STATUS_NO_TRANSACTION;
 201  
         }
 202  3
         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  11
         if (getActiveTx() != null) {
 219  0
             throw new IllegalStateException(
 220  
                 "Active thread " + Thread.currentThread() + " already associated with a transaction!");
 221  
         }
 222  11
         setActiveTx(new TxContext());
 223  11
     }
 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  52
         TxContext txContext = getActiveTx();
 234  
 
 235  52
         if (txContext == null) {
 236  0
             throw new IllegalStateException(
 237  
                 "Active thread " + Thread.currentThread() + " not associated with a transaction!");
 238  
         }
 239  
 
 240  
         // simply forget about tx
 241  52
         txContext.dispose();
 242  52
         setActiveTx(null);
 243  52
     }
 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  35
         TxContext txContext = getActiveTx();
 254  
 
 255  35
         if (txContext == null) {
 256  0
             throw new IllegalStateException(
 257  
                 "Active thread " + Thread.currentThread() + " not associated with a transaction!");
 258  
         }
 259  
 
 260  35
         if (txContext.status == Status.STATUS_MARKED_ROLLBACK) {
 261  2
             throw new IllegalStateException("Active thread " + Thread.currentThread() + " is marked for rollback!");
 262  
         }
 263  
 
 264  33
         txContext.merge();
 265  33
         txContext.dispose();
 266  33
         setActiveTx(null);
 267  33
     }
 268  
 
 269  
     //
 270  
     // Map methods
 271  
     // 
 272  
 
 273  
     /**
 274  
      * @see Map#clear() 
 275  
      */
 276  
     public void clear() {
 277  3
         TxContext txContext = getActiveTx();
 278  3
         if (txContext != null) {
 279  3
             txContext.clear();
 280  
         } else {
 281  0
             wrapped.clear();
 282  
         }
 283  3
     }
 284  
 
 285  
     /**
 286  
      * @see Map#size() 
 287  
      */
 288  
     public int size() {
 289  30
         TxContext txContext = getActiveTx();
 290  30
         if (txContext != null) {
 291  21
             return txContext.size();
 292  
         } else {
 293  9
             return wrapped.size();
 294  
         }
 295  
     }
 296  
 
 297  
     /**
 298  
      * @see Map#isEmpty() 
 299  
      */
 300  
     public boolean isEmpty() {
 301  12
         TxContext txContext = getActiveTx();
 302  12
         if (txContext == null) {
 303  9
             return wrapped.isEmpty();
 304  
         } else {
 305  3
             return txContext.isEmpty();
 306  
         }
 307  
     }
 308  
 
 309  
     /**
 310  
      * @see Map#containsKey(java.lang.Object) 
 311  
      */
 312  
     public boolean containsKey(Object key) {
 313  54
         return keySet().contains(key);
 314  
     }
 315  
 
 316  
     /**
 317  
      * @see Map#containsValue(java.lang.Object) 
 318  
      */
 319  
     public boolean containsValue(Object value) {
 320  36
         TxContext txContext = getActiveTx();
 321  
 
 322  36
         if (txContext == null) {
 323  15
             return wrapped.containsValue(value);
 324  
         } else {
 325  21
             return values().contains(value);
 326  
         }
 327  
     }
 328  
 
 329  
     /**
 330  
      * @see Map#values() 
 331  
      */
 332  
     public Collection values() {
 333  
 
 334  27
         TxContext txContext = getActiveTx();
 335  
 
 336  27
         if (txContext == null) {
 337  3
             return wrapped.values();
 338  
         } else {
 339  
             // XXX expensive :(
 340  24
             Collection values = new ArrayList();
 341  24
             for (Iterator it = keySet().iterator(); it.hasNext();) {
 342  87
                 Object key = it.next();
 343  87
                 Object value = get(key);
 344  
                 // XXX we have no isolation, so get entry might have been deleted in the meantime
 345  87
                 if (value != null) {
 346  87
                     values.add(value);
 347  
                 }
 348  87
             }
 349  24
             return values;
 350  
         }
 351  
     }
 352  
 
 353  
     /**
 354  
      * @see Map#putAll(java.util.Map) 
 355  
      */
 356  
     public void putAll(Map map) {
 357  0
         TxContext txContext = getActiveTx();
 358  
 
 359  0
         if (txContext == null) {
 360  0
             wrapped.putAll(map);
 361  
         } else {
 362  0
             for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
 363  0
                 Map.Entry entry = (Map.Entry) it.next();
 364  0
                 txContext.put(entry.getKey(), entry.getValue());
 365  0
             }
 366  
         }
 367  0
     }
 368  
 
 369  
     /**
 370  
      * @see Map#entrySet() 
 371  
      */
 372  
     public Set entrySet() {
 373  6
         TxContext txContext = getActiveTx();
 374  6
         if (txContext == null) {
 375  3
             return wrapped.entrySet();
 376  
         } else {
 377  3
             Set entrySet = new HashSet();
 378  
             // XXX expensive :(
 379  3
             for (Iterator it = keySet().iterator(); it.hasNext();) {
 380  12
                 Object key = it.next();
 381  12
                 Object value = get(key);
 382  
                 // XXX we have no isolation, so get entry might have been deleted in the meantime
 383  12
                 if (value != null) {
 384  12
                     entrySet.add(new HashEntry(key, value));
 385  
                 }
 386  12
             }
 387  3
             return entrySet;
 388  
         }
 389  
     }
 390  
 
 391  
     /**
 392  
      * @see Map#keySet() 
 393  
      */
 394  
     public Set keySet() {
 395  87
         TxContext txContext = getActiveTx();
 396  
 
 397  87
         if (txContext == null) {
 398  27
             return wrapped.keySet();
 399  
         } else {
 400  60
             return txContext.keys();
 401  
         }
 402  
     }
 403  
 
 404  
     /**
 405  
      * @see Map#get(java.lang.Object) 
 406  
      */
 407  
     public Object get(Object key) {
 408  295
         TxContext txContext = getActiveTx();
 409  
 
 410  295
         if (txContext != null) {
 411  272
             return txContext.get(key);
 412  
         } else {
 413  23
             return wrapped.get(key);
 414  
         }
 415  
     }
 416  
 
 417  
     /**
 418  
      * @see Map#remove(java.lang.Object) 
 419  
      */
 420  
     public Object remove(Object key) {
 421  21
         TxContext txContext = getActiveTx();
 422  
 
 423  21
         if (txContext == null) {
 424  3
             return wrapped.remove(key);
 425  
         } else {
 426  18
             Object oldValue = get(key);
 427  18
             txContext.remove(key);
 428  18
             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  101
         TxContext txContext = getActiveTx();
 437  
 
 438  101
         if (txContext == null) {
 439  22
             return wrapped.put(key, value);
 440  
         } else {
 441  79
             Object oldValue = get(key);
 442  79
             txContext.put(key, value);
 443  79
             return oldValue;
 444  
         }
 445  
 
 446  
     }
 447  
 
 448  
     protected TxContext getActiveTx() {
 449  981
         return (TxContext) activeTx.get();
 450  
     }
 451  
 
 452  
     protected void setActiveTx(TxContext txContext) {
 453  182
         activeTx.set(txContext);
 454  182
     }
 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  12
         protected HashEntry(Object key, Object value) {
 464  12
             this.key = key;
 465  12
             this.value = value;
 466  12
         }
 467  
 
 468  
         public Object getKey() {
 469  72
             return key;
 470  
         }
 471  
 
 472  
         public Object getValue() {
 473  36
             return value;
 474  
         }
 475  
 
 476  
         public Object setValue(Object value) {
 477  0
             Object old = this.value;
 478  0
             this.value = value;
 479  0
             return old;
 480  
         }
 481  
 
 482  
         public boolean equals(Object obj) {
 483  6
             if (obj == this) {
 484  0
                 return true;
 485  
             }
 486  6
             if (!(obj instanceof Map.Entry)) {
 487  0
                 return false;
 488  
             }
 489  6
             Map.Entry other = (Map.Entry) obj;
 490  6
             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  12
             return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode());
 496  
         }
 497  
 
 498  
         public String toString() {
 499  0
             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  85
         protected boolean suspended = false;
 511  
 
 512  85
         protected TxContext() {
 513  85
             deletes = setFactory.createSet();
 514  85
             changes = mapFactory.createMap();
 515  85
             adds = mapFactory.createMap();
 516  85
             status = Status.STATUS_ACTIVE;
 517  85
             cleared = false;
 518  85
             readOnly = true;
 519  85
         }
 520  
 
 521  
         protected Set keys() {
 522  60
             Set keySet = new HashSet();
 523  60
             if (!cleared) {
 524  60
                 keySet.addAll(wrapped.keySet());
 525  60
                 keySet.removeAll(deletes);
 526  
             }
 527  60
             keySet.addAll(adds.keySet());
 528  60
             return keySet;
 529  
         }
 530  
 
 531  
         protected Object get(Object key) {
 532  
 
 533  181
             if (deletes.contains(key)) {
 534  
                 // reflects that entry has been deleted in this tx 
 535  2
                 return null;
 536  
             }
 537  
 
 538  179
             if(changes.containsKey(key)){
 539  7
                 return changes.get(key);
 540  
             }
 541  
 
 542  172
             if(adds.containsKey(key)){
 543  38
                 return adds.get(key);
 544  
             }
 545  
 
 546  134
             if (cleared) {
 547  6
                 return null;
 548  
             } else {
 549  
                 // not modified in this tx
 550  128
                 return wrapped.get(key);
 551  
             }
 552  
         }
 553  
 
 554  
         protected void put(Object key, Object value) {
 555  
             try {
 556  79
                 readOnly = false;
 557  79
                 deletes.remove(key);
 558  79
                 if (wrapped.containsKey(key)) {
 559  43
                     changes.put(key, value);
 560  
                 } else {
 561  36
                     adds.put(key, value);
 562  
                 }
 563  0
             } catch (RuntimeException e) {
 564  0
                 status = Status.STATUS_MARKED_ROLLBACK;
 565  0
                 throw e;
 566  0
             } catch (Error e) {
 567  0
                 status = Status.STATUS_MARKED_ROLLBACK;
 568  0
                 throw e;
 569  79
             }
 570  79
         }
 571  
 
 572  
         protected void remove(Object key) {
 573  
 
 574  
             try {
 575  18
                 readOnly = false;
 576  18
                 changes.remove(key);
 577  18
                 adds.remove(key);
 578  18
                 if (wrapped.containsKey(key) && !cleared) {
 579  12
                     deletes.add(key);
 580  
                 }
 581  0
             } catch (RuntimeException e) {
 582  0
                 status = Status.STATUS_MARKED_ROLLBACK;
 583  0
                 throw e;
 584  0
             } catch (Error e) {
 585  0
                 status = Status.STATUS_MARKED_ROLLBACK;
 586  0
                 throw e;
 587  18
             }
 588  18
         }
 589  
 
 590  
         protected int size() {
 591  24
             int size = (cleared ? 0 : wrapped.size());
 592  
 
 593  24
             size -= deletes.size();
 594  24
             size += adds.size();
 595  
 
 596  24
             return size;
 597  
         }
 598  
 
 599  
         protected void clear() {
 600  3
             readOnly = false;
 601  3
             cleared = true;
 602  3
             deletes.clear();
 603  3
             changes.clear();
 604  3
             adds.clear();
 605  3
         }
 606  
 
 607  
         protected boolean isEmpty() {
 608  3
             return (size() == 0); 
 609  
         }
 610  
 
 611  
         protected void merge() {
 612  33
             if (!readOnly) {
 613  
 
 614  31
                 if (cleared) {
 615  3
                     wrapped.clear();
 616  
                 }
 617  
 
 618  31
                 wrapped.putAll(changes);
 619  31
                 wrapped.putAll(adds);
 620  
 
 621  31
                 for (Iterator it = deletes.iterator(); it.hasNext();) {
 622  9
                     Object key = it.next();
 623  9
                     wrapped.remove(key);
 624  9
                 }
 625  
             }
 626  33
         }
 627  
 
 628  
         protected void dispose() {
 629  146
             setFactory.disposeSet(deletes);
 630  146
             deletes = null;
 631  146
             mapFactory.disposeMap(changes);
 632  146
             changes = null;
 633  146
             mapFactory.disposeMap(adds);
 634  146
             adds = null;
 635  146
             status = Status.STATUS_NO_TRANSACTION;
 636  146
         }
 637  
     }
 638  
 }