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}