/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.api; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import javax.naming.NamingException; import javax.transaction.Status; import javax.transaction.Synchronization; import javax.transaction.Transaction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.runtime.api.J2EEContainerDescriptor; import org.nuxeo.runtime.datasource.ConnectionHelper; import org.nuxeo.runtime.transaction.TransactionHelper; /** * Wrapper around a CoreSession that gives it transactional behavior. * <p> * Transactional behavior: * <ul> * <li>notifies the event service on transaction start/stop</li> * <li>throws RollbackException</li> * </ul> */ public class TransactionalCoreSessionWrapper implements InvocationHandler, Synchronization { private static final Log log = LogFactory.getLog(TransactionalCoreSessionWrapper.class); private static final Class<?>[] INTERFACES = new Class[] { CoreSession.class }; private final CoreSession session; /** * Per-thread flag with transaction status: * <ul> * <li>{@code null}: outside transaction</li> * <li>{@code TRUE}: in a transaction</li> * </ul> */ private final ThreadLocal<Transaction> threadBound = new ThreadLocal<Transaction>(); protected TransactionalCoreSessionWrapper(CoreSession session) { this.session = session; } public static CoreSession wrap(CoreSession session) { try { TransactionHelper.lookupTransactionManager(); } catch (NamingException e) { // no transactions, do not wrap return session; } ClassLoader cl = session.getClass().getClassLoader(); return (CoreSession) Proxy.newProxyInstance(cl, INTERFACES, new TransactionalCoreSessionWrapper(session)); } protected void checkTxActiveRequired(Method m) { if (threadBound.get() != null) { return; // tx is active, no ckeck needed } if (J2EEContainerDescriptor.getSelected() == null) { return; // not in container } // TODO add annotation on core session api for marking non // transactional API final String name = m.getName(); if ("getSessionId".equals(name)) { return; } if ("connect".equals(name)) { return; } if ("close".equals(name)) { return; } if ("destroy".equals(name)) { return; } log.warn("CoreSession." + name + " invoked without a transaction," + " check debug logs for more information"); if (log.isDebugEnabled()) { log.debug("CoreSession." + name + " invoked without a transaction", new Throwable()); } } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Transaction main = threadBound.get(); if (main == null) { // first call in thread try { main = TransactionHelper.lookupTransactionManager().getTransaction(); if (main != null) { if (main.getStatus() == Status.STATUS_ACTIVE) { // register last, we want post-commit stuff to be // executed after everything else is committed ConnectionHelper.registerSynchronizationLast(this); threadBound.set(main); } } } catch (NamingException e) { // no transaction manager, ignore } catch (Exception e) { log.error("Error on transaction synchronizer registration", e); } checkTxActiveRequired(method); } try { return method.invoke(session, args); } catch (Throwable t) { if (t instanceof InvocationTargetException) { Throwable tt = ((InvocationTargetException) t).getTargetException(); if (tt != null) { t = tt; } } if (TransactionHelper.isTransactionActive() && needsRollback(method, t)) { TransactionHelper.setTransactionRollbackOnly(); if (!(t instanceof ConcurrentUpdateException)) { // don't log a WARN for ConcurrentUpdateException // because often this will be retried by the Work framework // log is still available at DEBUG level log.warn("Setting transaction ROLLBACK ONLY due to exception" + " (check DEBUG logs for stacktrace): " + t); } if (log.isDebugEnabled()) { log.debug( "Setting transaction ROLLBACK ONLY due to exception: " + t, t); } } throw t; } } protected boolean needsRollback(Method method, Throwable t) { for (Annotation annotation : method.getAnnotations()) { if (annotation.annotationType() == NoRollbackOnException.class) { return false; } } return true; } @Override public void beforeCompletion() { } @Override public void afterCompletion(int status) { Transaction current = null; try { current = TransactionHelper.lookupTransactionManager().getTransaction(); } catch (Exception e) { throw new RuntimeException("no tx", e); } Transaction main = threadBound.get(); if (main.equals(current)) { threadBound.remove(); } } @Override public String toString() { return this.getClass().getSimpleName() + '(' + session + ')'; } }