--- /dev/null
+/*
+ * Copyright 2005-2010 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.wamblee.test.transactions;
+
+import java.util.List;
+
+import javax.transaction.UserTransaction;
+
+/**
+ * Transaction factory implementation that creates {@link SimpleUserTransaction} objects.
+ *
+ * @author Erik Brakkee
+ *
+ */
+public class DefaultUserTransactionFactory implements UserTransactionFactory {
+
+ /**
+ * Constructs the factory.
+ */
+ public DefaultUserTransactionFactory() {
+ // Empty.
+ }
+
+ @Override
+ public UserTransaction create(UserTransactionCallback aCallback,
+ List<TransactionResource> aResources) {
+ return new SimpleUserTransaction(aCallback, aResources.toArray(new TransactionResource[0]));
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2005-2010 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.wamblee.test.transactions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.transaction.UserTransaction;
+
+/**
+ * Simple transaction manager provides a simple mechanism to manage transaction
+ * in test code through the {@link UserTransaction} object.
+ *
+ * @author Erik Brakkee
+ *
+ */
+public class SimpleTransactionManager {
+
+ private UserTransactionFactory factory;
+ private ThreadLocal<UserTransaction> current;
+ private UserTransactionCallback callback;
+ private List<TransactionResource> resources;
+
+ /**
+ * Constructs the transaction manager.
+ *
+ * @param aFactory
+ * Factory to create transactions with.
+ */
+ public SimpleTransactionManager(UserTransactionFactory aFactory) {
+ factory = aFactory;
+ current = new ThreadLocal<UserTransaction>();
+ callback = new UserTransactionCallback() {
+
+ @Override
+ public void transactionFinished() {
+ current.set(null);
+ }
+ };
+ resources = new ArrayList<TransactionResource>();
+ }
+
+ /**
+ * Adds a resource to manage. Adding resources is no longer allowed after
+ * the first transaction has started.
+ *
+ * @param aResource
+ * Resource.
+ */
+ public void addResource(TransactionResource aResource) {
+ resources.add(aResource);
+ }
+
+ /**
+ * Gets a transaction associated with the current thread.
+ *
+ * @return User transaction.
+ */
+ public UserTransaction getTransaction() {
+ UserTransaction transaction = current.get();
+ if (transaction == null) {
+ transaction = factory.create(callback, resources);
+ current.set(transaction);
+ }
+ return transaction;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2005-2010 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.wamblee.test.transactions;
+
+import javax.transaction.HeuristicMixedException;
+import javax.transaction.HeuristicRollbackException;
+import javax.transaction.NotSupportedException;
+import javax.transaction.RollbackException;
+import javax.transaction.Status;
+import javax.transaction.SystemException;
+import javax.transaction.UserTransaction;
+
+/**
+ * Represents a user transaction spanning a number of resources.
+ */
+public class SimpleUserTransaction implements UserTransaction {
+
+ private UserTransactionCallback callback;
+ private TransactionResource[] resources;
+ private Object[] txStates;
+ private int status;
+
+ /**
+ * Construcst the transaction.
+ *
+ * @param aCallback
+ * Callback to use.
+ * @param aResources
+ * Resources to use.
+ */
+ public SimpleUserTransaction(UserTransactionCallback aCallback,
+ TransactionResource<?>... aResources) {
+ callback = aCallback;
+ resources = aResources;
+ txStates = new Object[resources.length];
+ status = Status.STATUS_NO_TRANSACTION;
+ }
+
+ @Override
+ public void begin() throws NotSupportedException, SystemException {
+ if (status == Status.STATUS_ACTIVE) {
+ throw new NotSupportedException(
+ "Nested transactions not supported!");
+ }
+ for (int i = 0; i < resources.length; i++) {
+ txStates[i] = resources[i].begin();
+ }
+ status = Status.STATUS_ACTIVE;
+ }
+
+ @Override
+ public void rollback() throws IllegalStateException, SecurityException,
+ SystemException {
+ if ( status == Status.STATUS_NO_TRANSACTION) {
+ throw new IllegalStateException("Rollback while not in a transaction");
+ }
+ try {
+ for (int i = 0; i < resources.length; i++) {
+ resources[i].rollback(txStates[i]);
+ }
+ } finally {
+ status = Status.STATUS_NO_TRANSACTION;
+ callback.transactionFinished();
+ }
+ }
+
+ @Override
+ public void commit() throws RollbackException, HeuristicMixedException,
+ HeuristicRollbackException, SecurityException, IllegalStateException,
+ SystemException {
+ if (status == Status.STATUS_MARKED_ROLLBACK) {
+ rollback();
+ throw new RollbackException();
+ }
+ if (status != Status.STATUS_ACTIVE) {
+ throw new IllegalStateException("Commit while not in a transaction");
+ }
+ try {
+ boolean committing = true;
+ for (int i = 0; i < resources.length; i++) {
+ try {
+ if (committing) {
+ resources[i].commit(txStates[i]);
+ } else {
+ resources[i].rollback(txStates[i]);
+ }
+ } catch (Exception e) {
+ committing = false;
+ }
+ }
+
+ if (!committing) {
+ throw new HeuristicMixedException("Commit failed");
+ }
+ } finally {
+ status = Status.STATUS_NO_TRANSACTION;
+ callback.transactionFinished();
+ }
+ }
+
+ @Override
+ public int getStatus() throws SystemException {
+ return status;
+ }
+
+ @Override
+ public void setRollbackOnly() throws IllegalStateException, SystemException {
+ if ( status == Status.STATUS_NO_TRANSACTION) {
+ throw new IllegalStateException("setRollbackOnly() while not in a transaction");
+ }
+ status = Status.STATUS_MARKED_ROLLBACK;
+ }
+
+ @Override
+ public void setTransactionTimeout(int aSeconds) throws SystemException {
+ // Ignored.
+ }
+}
--- /dev/null
+/*
+ * Copyright 2005-2010 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.wamblee.test.transactions;
+
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import javax.persistence.EntityManager;
+
+import org.wamblee.test.persistence.JpaBuilder;
+import org.wamblee.test.persistence.JpaBuilder.JpaUnitOfWork;
+
+/**
+ * This utility makes sure that each invocation on a certain interface is
+ * carried out within a JPA unit of work. Note that this is equivalent
+ * to the sementics of a requiresNew transaction attribute.
+ *
+ * Use {@link #getTransactionScopedEntityManager()} to get the transaction
+ * scoped entity manager to pass to services.
+ *
+ *
+ * For example:
+ * <pre>
+ * JpaBuilder builder = ...
+ * TransactionProxyFactory<Service> factory = new TransactionProxyFactory<Service>(
+ * builder, Service.class);
+ * Service service = new JpaService(factory.getTransactionScopedEntityManager());
+ * Service proxy = factory.getProxy(service);
+ * proxy.executeMethod(...);
+ * </pre>
+ * The above example executes the executeMethod() call on the service object within an active transaction.
+ * In the constructor of the service a transaction scoped entity manager is passed.
+ *
+ * @param T
+ * Type of interface to proxy.
+ *
+ * @author Erik Brakkee
+ */
+public class TransactionProxyFactory<T> {
+
+ /**
+ * Executes the call on the service within a new transaction.
+ *
+ * @author Erik Brakkee
+ *
+ * @param <T> Type of the service interface.
+ */
+ private class UnitOfWorkInvocationHandler<T> implements InvocationHandler {
+
+ private T service;
+
+ public UnitOfWorkInvocationHandler(T aService) {
+ service = aService;
+ }
+
+ @Override
+ public Object invoke(Object aProxy, final Method aMethod,
+ final Object[] aArgs) throws Throwable {
+ return TransactionProxyFactory.this.jpaBuilder
+ .execute(new JpaUnitOfWork<Object>() {
+ @Override
+ public Object execute(EntityManager aEm) throws Exception {
+ EntityManager oldEm = ENTITY_MANAGER.get();
+ try {
+ ENTITY_MANAGER.set(aEm);
+ return aMethod.invoke(service, aArgs);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Exception) {
+ throw (Exception) cause;
+ } else if (cause instanceof Error) {
+ throw (Error) cause;
+ }
+ // last resort.
+ throw new RuntimeException(e);
+ } finally {
+ ENTITY_MANAGER.set(oldEm);
+ }
+ }
+ });
+ }
+
+ }
+
+ private static final ThreadSpecificProxyFactory<EntityManager> ENTITY_MANAGER = new ThreadSpecificProxyFactory<EntityManager>(
+ EntityManager.class);
+
+ private JpaBuilder jpaBuilder;
+ private Class<T> clazz;
+
+ /**
+ * Constructs the transaction proxy.
+ *
+ * @param aJpaBuilder
+ */
+ public TransactionProxyFactory(JpaBuilder aJpaBuilder, Class<T> aClass) {
+ jpaBuilder = aJpaBuilder;
+ clazz = aClass;
+ }
+
+ public EntityManager getTransactionScopedEntityManager() {
+ return ENTITY_MANAGER.getProxy();
+ }
+
+ public T getProxy(T aService) {
+ InvocationHandler handler = new UnitOfWorkInvocationHandler<T>(aService);
+ Class proxyClass = Proxy.getProxyClass(clazz.getClassLoader(),
+ new Class[] { clazz });
+ T proxy;
+ try {
+ proxy = (T) proxyClass.getConstructor(
+ new Class[] { InvocationHandler.class }).newInstance(
+ new Object[] { handler });
+ return proxy;
+ } catch (Exception e) {
+ throw new RuntimeException("Could not create proxy for " +
+ clazz.getName(), e);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2005-2010 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.wamblee.test.transactions;
+
+
+/**
+ * Interfaces to be implemented by resource that want to participate in transactions
+ * managed through {@link SimpleTransactionManager}.
+ *
+ * @author Erik Brakkee
+ *
+ * @param <T>
+ */
+public interface TransactionResource<T> {
+
+ /**
+ * Begins a transaction.
+ * @return Object that manages the transaction for the resource.
+ */
+ T begin();
+
+ /**
+ * Rolls back a transaction.
+ * @param aT Object that manages the transaction for the resource.
+ */
+ void rollback(T aT);
+
+ /**
+ * Commits the transaction.
+ * @param aT Object that manages the transaction for the resource.
+ */
+ void commit(T aT);
+
+}
--- /dev/null
+/*
+ * Copyright 2005-2010 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.wamblee.test.transactions;
+
+public interface UserTransactionCallback {
+ void transactionFinished();
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright 2005-2010 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.wamblee.test.transactions;
+
+import java.util.List;
+
+import javax.transaction.UserTransaction;
+
+/**
+ * Factory used to create transactions.
+ *
+ * @author Erik Brakkee
+ *
+ */
+public interface UserTransactionFactory {
+
+ UserTransaction create(UserTransactionCallback aCallback, List<TransactionResource> aResources);
+}