/*
* Bean Testing.
*
* 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 info.novatec.beantest.transactions;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.LockTimeoutException;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.QueryTimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Transactional interceptor to provide basic transaction propagation.
* <p>
* <b>Note</b> This implementation is intentionally not thread-safe, because unit tests are usually run in one thread. <br>
* If you try to run unit tests in parallel, unexpected behavior may occur.
* <p>
* Alternatively the Apache Deltaspike JPA module can be used. The JPA module provides more advanced transaction handling.
* However, this implementation should suffice for testing purposes.
*
* @author Carlos Barragan (carlos.barragan@novatec-gmbh.de)
*/
@Interceptor
@Transactional
public class TransactionalInterceptor {
/**
* Exceptions that should not cause the transaction to rollback according to Java EE Documentation.
* (http://docs.oracle.com/javaee/6/api/javax/persistence/PersistenceException.html)
*/
private static final Set<Class<?>> NO_ROLLBACK_EXCEPTIONS=new HashSet<Class<?>>(Arrays.asList(
NonUniqueResultException.class,
NoResultException.class,
QueryTimeoutException.class,
LockTimeoutException.class));
@Inject
@PersistenceContext
EntityManager em;
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionalInterceptor.class);
private static int INTERCEPTOR_COUNTER = 0;
@AroundInvoke
public Object manageTransaction(InvocationContext ctx) throws Exception {
EntityTransaction transaction = em.getTransaction();
if (!transaction.isActive()) {
transaction.begin();
LOGGER.debug("Transaction started");
}
INTERCEPTOR_COUNTER++;
Object result = null;
try {
result = ctx.proceed();
} catch (Exception e) {
if (isFirstInterceptor()) {
markRollbackTransaction(e);
}
throw e;
} finally {
processTransaction();
}
return result;
}
/**
* Commits the current transaction if it is not already marked as rollback via the {@link EntityTransaction#getRollbackOnly()} method.
* In that case, a rollback will be executed.
*/
private void processTransaction() throws Exception {
EntityTransaction transaction = em.getTransaction();
try {
if (em.isOpen() && transaction.isActive() && isFirstInterceptor()) {
if (transaction.getRollbackOnly()) {
transaction.rollback();
LOGGER.debug("Transaction was rollbacked");
} else {
transaction.commit();
LOGGER.debug("Transaction committed");
}
em.clear();
}
} catch (Exception e) {
LOGGER.warn("Error when trying to commit transaction: {0}", e);
throw e;
} finally {
INTERCEPTOR_COUNTER--;
}
}
/**
* Marks the transaction for rollback via {@link EntityTransaction#setRollbackOnly()}.
*/
private void markRollbackTransaction(Exception exception) throws Exception {
try {
if (em.isOpen() && em.getTransaction().isActive() && shouldExceptionCauseRollback(exception)) {
em.getTransaction().setRollbackOnly();
}
} catch (Exception e) {
LOGGER.warn("Error when trying to roll back the transaction: {0}", e);
throw e;
}
}
private static boolean isFirstInterceptor() {
return INTERCEPTOR_COUNTER -1 == 0;
}
private static boolean shouldExceptionCauseRollback(Exception e ) {
return ! NO_ROLLBACK_EXCEPTIONS.contains(e.getClass());
}
}