/* * Copyright 2006-2007 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.batch.support.transaction; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.framework.ProxyFactory; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import org.springframework.transaction.support.TransactionSynchronizationManager; /** * <p> * Factory for transaction aware objects (like lists, sets, maps). If a * transaction is active when a method is called on an instance created by the * factory, it makes a copy of the target object and carries out all operations * on the copy. Only when the transaction commits is the target re-initialised * with the copy. * </p> * * <p> * Works well with collections and maps for testing transactional behaviour * without needing a database. The base implementation handles lists, sets and * maps. Subclasses can implement {@link #begin(Object)} and * {@link #commit(Object, Object)} to provide support for other resources. * </p> * * <p> * Generally not intended for multi-threaded use, but the * {@link #createAppendOnlyTransactionalMap() append only version} of * collections gives isolation between threads operating on different keys in a * map, provided they only append to the map. (Threads are limited to removing * entries that were created in the same transaction.) * </p> * * @author Dave Syer * */ public class TransactionAwareProxyFactory<T> { private final T target; private final boolean appendOnly; private TransactionAwareProxyFactory(T target) { this(target, false); } private TransactionAwareProxyFactory(T target, boolean appendOnly) { super(); this.target = target; this.appendOnly = appendOnly; } /** * Make a copy of the target that can be used inside a transaction to * isolate changes from the original. Also called from the factory * constructor to isolate the target from the original value passed in. * * @param target the target object (List, Set or Map) * @return an independent copy */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected final T begin(T target) { // Unfortunately in Java 5 this method has to synchronized // (works OK without in Java 6). synchronized (target) { if (target instanceof List) { if (appendOnly) { return (T) new ArrayList(); } return (T) new ArrayList((List) target); } else if (target instanceof Set) { if (appendOnly) { return (T) new HashSet(); } return (T) new HashSet((Set) target); } else if (target instanceof Map) { if (appendOnly) { return (T) new HashMap(); } return (T) new HashMap((Map) target); } else { throw new UnsupportedOperationException("Cannot copy target for this type: " + target.getClass()); } } } /** * Take the working copy state and commit it back to the original target. * The target then reflects all the changes applied to the copy during a * transaction. * * @param copy the working copy. * @param target the original target of the factory. */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected void commit(T copy, T target) { // Unfortunately in Java 5 this method has to be synchronized // (works OK without in Java 6). synchronized (target) { if (target instanceof Collection) { if (!appendOnly) { ((Collection) target).clear(); } ((Collection) target).addAll((Collection) copy); } else { if (!appendOnly) { ((Map) target).clear(); } ((Map) target).putAll((Map) copy); } } } private T createInstance() { synchronized (target) { ProxyFactory factory = new ProxyFactory(target); factory.addAdvice(new TransactionAwareInterceptor()); @SuppressWarnings("unchecked") T instance = (T) factory.getProxy(); return instance; } } public static <K, V> Map<K, V> createTransactionalMap() { return new TransactionAwareProxyFactory<ConcurrentHashMap<K, V>>(new ConcurrentHashMap<K, V>()).createInstance(); } public static <K, V> Map<K, V> createTransactionalMap(Map<K, V> map) { return new TransactionAwareProxyFactory<ConcurrentHashMap<K, V>>(new ConcurrentHashMap<K, V>(map)).createInstance(); } public static <K, V> ConcurrentMap<K, V> createAppendOnlyTransactionalMap() { return new TransactionAwareProxyFactory<ConcurrentHashMap<K, V>>(new ConcurrentHashMap<K, V>(), true).createInstance(); } public static <T> Set<T> createAppendOnlyTransactionalSet() { return new TransactionAwareProxyFactory<CopyOnWriteArraySet<T>>(new CopyOnWriteArraySet<T>(), true).createInstance(); } public static <T> Set<T> createTransactionalSet() { return new TransactionAwareProxyFactory<CopyOnWriteArraySet<T>>(new CopyOnWriteArraySet<T>()).createInstance(); } public static <T> Set<T> createTransactionalSet(Set<T> set) { return new TransactionAwareProxyFactory<CopyOnWriteArraySet<T>>(new CopyOnWriteArraySet<T>(set)).createInstance(); } public static <T> List<T> createAppendOnlyTransactionalList() { return new TransactionAwareProxyFactory<CopyOnWriteArrayList<T>>(new CopyOnWriteArrayList<T>(), true).createInstance(); } public static <T> List<T> createTransactionalList() { return new TransactionAwareProxyFactory<CopyOnWriteArrayList<T>>(new CopyOnWriteArrayList<T>()).createInstance(); } public static <T> List<T> createTransactionalList(List<T> list) { return new TransactionAwareProxyFactory<CopyOnWriteArrayList<T>>(new CopyOnWriteArrayList<T>(list)).createInstance(); } private class TargetSynchronization extends TransactionSynchronizationAdapter { private final T cache; private final Object key; public TargetSynchronization(Object key, T cache) { super(); this.cache = cache; this.key = key; } @Override public void afterCompletion(int status) { super.afterCompletion(status); if (status == TransactionSynchronization.STATUS_COMMITTED) { synchronized (target) { commit(cache, target); } } TransactionSynchronizationManager.unbindResource(key); } } private class TransactionAwareInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { if (!TransactionSynchronizationManager.isActualTransactionActive()) { return invocation.proceed(); } T cache; if (!TransactionSynchronizationManager.hasResource(this)) { cache = begin(target); TransactionSynchronizationManager.bindResource(this, cache); TransactionSynchronizationManager.registerSynchronization(new TargetSynchronization(this, cache)); } else { @SuppressWarnings("unchecked") T retrievedCache = (T) TransactionSynchronizationManager.getResource(this); cache = retrievedCache; } Object result = invocation.getMethod().invoke(cache, invocation.getArguments()); if (appendOnly) { String methodName = invocation.getMethod().getName(); if ((result == null && methodName.equals("get")) || (Boolean.FALSE.equals(result) && (methodName.startsWith("contains")) || (Boolean.TRUE .equals(result) && methodName.startsWith("isEmpty")))) { // In appendOnly mode the result of a get might not be // in the cache... return invocation.proceed(); } if (result instanceof Collection<?>) { HashSet<Object> set = new HashSet<Object>((Collection<?>) result); set.addAll((Collection<?>) invocation.proceed()); result = set; } } return result; } } }