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 }