/*
* 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 + ')';
}
}