/** * Copyright (C) 2012 Stephan Classen * Based on guice-perist (Copyright (C) 2010 Google, Inc.) * * 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 com.github.sclassen.guicejpa; import static com.google.common.base.Preconditions.checkNotNull; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Map; import javax.persistence.EntityManager; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import com.google.common.collect.MapMaker; /** * Abstract super class for all @{@link Transactional} annotation interceptors. * * @author Stephan Classen */ abstract class AbstractTxnInterceptor implements MethodInterceptor { // ---- Members /** Unit of work. */ private final UnitOfWork unitOfWork; /** Provider for {@link EntityManager}. */ private final EntityManagerProvider emProvider; /** Annotation of the persistence unit this interceptor belongs to. */ private final Class<? extends Annotation> puAnntoation; /** cache for {@link Transactional} annotations per method. */ private final Map<Method, Transactional> transactionalCache = new MapMaker().weakKeys().makeMap(); /** Translator for PersistenceException's. */ private final PersistenceExceptionTranslator<?> peTranslator; // ---- Constructor /** * Constructor. * * @param emProvider the provider for {@link EntityManager}. Must not be {@code null}. * @param unitOfWork the unit of work. Must not be {@code null}. * @param puAnntoation the annotation of the persistence unit the interceptor belongs to. * May be {@code null}. * @param peTranslator the {@link PersistenceExceptionTranslator}. Can be {@code null}. */ public AbstractTxnInterceptor(EntityManagerProvider emProvider, UnitOfWork unitOfWork, Class<? extends Annotation> puAnntoation, PersistenceExceptionTranslator<?> peTranslator) { checkNotNull(unitOfWork); checkNotNull(emProvider); this.unitOfWork = unitOfWork; this.emProvider = emProvider; this.puAnntoation = puAnntoation; this.peTranslator = peTranslator; } // ---- Methods /** * {@inheritDoc} */ @Override public final Object invoke(MethodInvocation methodInvocation) throws Throwable { if (!transactionCoversThisPersistenceUnit(methodInvocation)) { return methodInvocation.proceed(); } final boolean weStartedTheUnitOfWork = !unitOfWork.isActive(); if (weStartedTheUnitOfWork) { unitOfWork.begin(); } try { final EntityManager em = emProvider.get(); final TransactionFacade transactionFacade = getTransactionFacade(em); return invoke(methodInvocation, transactionFacade); } catch (RuntimeException e) { if (peTranslator != null) { RuntimeException te = peTranslator.translateExceptionIfPossible(e); if (te != null) { throw te; } } throw e; } finally { if (weStartedTheUnitOfWork) { unitOfWork.end(); } } } /** * Check whether the persistence unit of this interceptor participates in the transaction or not. * * @param methodInvocation the original method invocation. * @return {@code true} if the persistence unit participates in the transaction * {@code false} otherwise. */ private boolean transactionCoversThisPersistenceUnit(MethodInvocation methodInvocation) { if (null == puAnntoation) { return true; } final Transactional localTransaction = readTransactionMetadata(methodInvocation); final Class<? extends Annotation>[] units = localTransaction.onUnits(); if (null == units || 0 == units.length) { return true; } final int n = units.length; for (int i = 0; i < n; i++) { if (puAnntoation.equals(units[i])) { return true; } } return false; } /** * Returns the transaction facade for the given entity manager. * * @param em the entity manager. * @return the transaction facade. Never {@code null}. */ protected abstract TransactionFacade getTransactionFacade(final EntityManager em); /** * Invoke the original method surrounded by a transaction. * * @param methodInvocation the original method invocation. * @param transactionFacade the facade to the underling resource local or jta transaction. * @return the result of the invocation of the original method. * @throws Throwable if an exception occurs during the call to the original method. */ private Object invoke(MethodInvocation methodInvocation, TransactionFacade transactionFacade) throws Throwable { transactionFacade.begin(); final Object result = doTransactional(methodInvocation, transactionFacade); transactionFacade.commit(); return result; } /** * Invoke the original method assuming a transaction has already been started. * This method is responsible of calling rollback if necessary. * * @param methodInvocation the original method invocation. * @param transactionFacade the facade to the underlying resource local or jta transaction. * @return the result of the invocation of the original method. * @throws Throwable if an exception occurs during the call to the original method. */ private Object doTransactional(MethodInvocation methodInvocation, TransactionFacade transactionFacade) throws Throwable { try { return methodInvocation.proceed(); } catch (Throwable e) { final Transactional t = readTransactionMetadata(methodInvocation); if (rollbackIsNecessary(t, e)) { transactionFacade.rollback(); } else { transactionFacade.commit(); } // In any case: throw the original exception. throw e; } } /** * Reads the @{@link Transactional} of a given method invocation. * * @param methodInvocation the method invocation for which to obtain the @{@link Transactional}. * @return the @{@link Transactional} of the given method invocation. Never {@code null}. */ private Transactional readTransactionMetadata(MethodInvocation methodInvocation) { final Method method = methodInvocation.getMethod(); Transactional result; result = transactionalCache.get(method); if (null == result) { result = method.getAnnotation(Transactional.class); if (null == result) { final Class<?> targetClass = methodInvocation.getThis().getClass(); result = targetClass.getAnnotation(Transactional.class); } if (null == result) { result = DefaultTransactional.class.getAnnotation(Transactional.class); } transactionalCache.put(method, result); } return result; } /** * Returns True if a rollback is necessary. * * @param transactional The metadata annotation of the method. * @param e The exception to test for rollback. * @return {@code true} if a rollback is necessary, {@code false} otherwise. */ private boolean rollbackIsNecessary(Transactional transactional, Throwable e) { for (Class<? extends Exception> rollbackOn : transactional.rollbackOn()) { if (rollbackOn.isInstance(e)) { for (Class<? extends Exception> ignore : transactional.ignore()) { if (ignore.isInstance(e)) { return false; } } return true; } } return false; } /** Helper class for obtaining the default of @{@link Transactional}. */ @Transactional private static class DefaultTransactional { } }