diff --git a/.travis.yml b/.travis.yml index 24f65e32..821c91f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,4 @@ addons: language: java jdk: - oraclejdk8 - - oraclejdk7 - - openjdk6 + \ No newline at end of file diff --git a/btm-cdi/doc/CdiTransactions.png b/btm-cdi/doc/CdiTransactions.png new file mode 100644 index 00000000..f15282f6 Binary files /dev/null and b/btm-cdi/doc/CdiTransactions.png differ diff --git a/btm-cdi/doc/CdiTransactions.puml b/btm-cdi/doc/CdiTransactions.puml new file mode 100644 index 00000000..b78465de --- /dev/null +++ b/btm-cdi/doc/CdiTransactions.puml @@ -0,0 +1,98 @@ +@startuml + +actor client as c +participant TransactionalBean as s +participant TInterceptor as ti +participant TFrameStack as ts +participant TransactionInfo as tif +participant emdelegate as emd +participant tm +participant em +participant pfactory as pf +participant query1 as q1 +participant query2 as q2 + +c -> s: call +activate s +s -> ti: manageTransaction +activate ti +ti -> ts: pushTra(attribute) +create tif +ts -> tif: new +ts -> tm: begin +tm --> ts: ok +ts --> ti: ok +ti -> s: invoke +activate s +s -> emd: createQuery +activate emd +emd -> pf: getTransactional +activate pf +pf -> ts: getEntityManager(this) +activate ts +ts -> ts: searchEmInTopTInfo +ts -> pf: createEntityManager +create em +pf -> em: new +pf --> ts: em +ts -> em: joinTransaction +ts -> tif: register(em) +ts --> pf: ok +deactivate ts +pf --> emd: em +deactivate pf +emd -> em: createQuery +create q1 +em -> q1: createQuery +em --> emd: q1 +emd --> s: q1 +deactivate emd +s -> q1: execute +activate q1 +q1 --> s: results +deactivate q1 +destroy q1 +s -> emd: createQuery +activate emd +emd -> pf: getTransactional +activate pf +pf -> ts: getEntityManager(this) +activate ts +ts -> ts: searchEmInTopTInfo +ts --> pf: em +deactivate ts +pf --> emd: em +deactivate pf +emd -> em: createQuery +create q2 +em -> q2: createQuery +em --> emd: q2 +emd --> s: q2 +deactivate emd +s -> q2: execute +activate q2 +q2 --> s: results +deactivate q2 +destroy q2 +s --> ti: return +deactivate s +activate ti +ti -> ts: popTra +activate ts +ts -> tm: commit +tm -> em: flush +em --> tm: ok +tm --> ts: ok +ts -> em: close +em --> ts: ok +destroy em +ts --> tif: close +destroy tif +ts --> ti: ok +deactivate ts +deactivate ti +ti --> s: return from intercept +deactivate ti +s --> c: return +deactivate s +@enduml diff --git a/btm-cdi/pom.xml b/btm-cdi/pom.xml new file mode 100644 index 00000000..d3006a6d --- /dev/null +++ b/btm-cdi/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + org.codehaus.btm + btm-parent + 3.0.0-SNAPSHOT + + btm-cdi + Bitronix Transaction Manager :: CDI Integration + + + 1.0.1.Final + 4.2.2.Final + 4.3.1.Final + 1.7.21 + + + + org.codehaus.btm + btm + + + + javax.transaction + jta + provided + + + + org.codehaus.btm + btm + test-jar + test + + + org.jboss.weld.se + weld-se-core + + + 2.3.5.Final + + + junit + junit + test + + + org.mockito + mockito-all + test + + + javax.inject + javax.inject + test + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + + + javax + javaee-api + 7.0 + + + javax.enterprise + cdi-api + 1.2 + + + org.jboss.spec.javax.transaction + jboss-transaction-api_1.1_spec + RELEASE + + + org.jglue.cdi-unit + cdi-unit + 3.1.4 + + + com.h2database + h2 + test + 1.4.196 + + + org.hibernate + hibernate-entitymanager + ${version.org.hibernate} + + + org.hibernate + hibernate-validator + ${version.org.hibernate.validator} + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + ${version.org.hibernate.api} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-source-plugin + + + + + diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/CdiTInterceptor.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/CdiTInterceptor.java new file mode 100644 index 00000000..04b49504 --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/CdiTInterceptor.java @@ -0,0 +1,109 @@ +package bitronix.tm.integration.cdi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + + +import javax.ejb.ApplicationException; +import javax.ejb.EJBException; +import javax.inject.Inject; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; +import javax.transaction.RollbackException; +import javax.transaction.TransactionManager; +import javax.transaction.Transactional; + +@Interceptor +@CdiTransactional +public class CdiTInterceptor { + + Logger logger = LoggerFactory.getLogger(CdiTInterceptor.class); + + @Inject + TransactionManager tm; + + @AroundInvoke + public Object manageTransaction(InvocationContext ctx) throws Exception { + final Class declaringClass = ctx.getMethod().getDeclaringClass(); + TFrameStack ts = new TFrameStack(); + TransactionInfo lastTransactionInfo = ts.topTransaction(); + Transactional.TxType attribute = null; + Transactional classTransactional = + declaringClass.getAnnotation( + Transactional.class); + Transactional transactionMethod = ctx.getMethod().getAnnotation(Transactional.class); + + Class[] rollbackon = null; + Class[] dontRollBackOn = null; + + if (transactionMethod != null) { + attribute = transactionMethod.value(); + rollbackon = transactionMethod.rollbackOn(); + dontRollBackOn = transactionMethod.dontRollbackOn(); + + } else if (classTransactional != null) { + if (classTransactional != null) { + attribute = classTransactional.value(); + rollbackon = classTransactional.rollbackOn(); + dontRollBackOn = classTransactional.dontRollbackOn(); + } else { + attribute = Transactional.TxType.REQUIRED; + } + } + if (attribute == null) { + logger.error("CdiTransactionalInterceptor should not be used at this class: {}", declaringClass.getName()); + return ctx.proceed(); + } else { + boolean passThroughRollbackException = true; + try { + logger.info("Thread {} L{} changing from {} to {} xid: {} in {}.{}", + Thread.currentThread().getId(), ts.currentLevel(), + ts.currentTxType(), + attribute, MDC.get("XID"), declaringClass.getSimpleName(), ctx.getMethod().getName()); + ts.pushTransaction(attribute); + return ctx.proceed(); + } catch (Throwable ex) { + logger.info("Thread {} L{} Exception {} in {} xid: {} in {}.{}", + Thread.currentThread().getId(), ts.currentLevel(), + ex.getClass().getSimpleName(), attribute, MDC.get("XID"), declaringClass.getSimpleName(), + ctx.getMethod().getName()); + boolean doRollback = !isSubOrClassOfAny(ex.getClass(), dontRollBackOn) + && (isSubOrClassOfAny(ex.getClass(), rollbackon) || ex instanceof RuntimeException); + if (doRollback) { + passThroughRollbackException = false; + tm.rollback(); + } + + throw ex; + } finally { + logger.info("Thread {} L{} finally in {} xid: {} in {}.{}", + Thread.currentThread().getId(), ts.currentLevel(), attribute, MDC.get("XID"), declaringClass.getSimpleName(), + ctx.getMethod().getName()); + try { + ts.popTransaction(); + } catch (RollbackException rbe) { + if (passThroughRollbackException) { + throw rbe; + } + } finally { + logger.info("Thread {} L{} done {} back to {} xid: {} in {}.{}", + Thread.currentThread().getId(), ts.topTransaction(), attribute, + lastTransactionInfo == null ? "undefined" : lastTransactionInfo.currentTxType, + MDC.get("XID"), declaringClass.getSimpleName(), ctx.getMethod().getName()); + } + } + } + } + + public boolean isSubOrClassOfAny(Class c, Class[] classes) { + for (Class clazz: classes) { + if (clazz.isAssignableFrom(c)) { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/CdiTransactional.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/CdiTransactional.java new file mode 100644 index 00000000..3a3e2c8f --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/CdiTransactional.java @@ -0,0 +1,16 @@ +package bitronix.tm.integration.cdi; + +import javax.interceptor.InterceptorBinding; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author aschoerk + */ +@InterceptorBinding +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CdiTransactional { +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EjbTInterceptor.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EjbTInterceptor.java new file mode 100644 index 00000000..d63e85d6 --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EjbTInterceptor.java @@ -0,0 +1,159 @@ +package bitronix.tm.integration.cdi; + +/** + * @author aschoerk + */ + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import javax.ejb.ApplicationException; +import javax.ejb.EJBException; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.ejb.TransactionManagement; +import javax.ejb.TransactionManagementType; +import javax.inject.Inject; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; + +@Interceptor +@EjbTransactional +public class EjbTInterceptor { + + private final Logger logger = + LoggerFactory.getLogger(EjbTInterceptor.class); + + @Inject + TransactionManager tm; + + + + @AroundInvoke + public Object manageTransaction(InvocationContext ctx) throws Exception { + + final Class declaringClass = ctx.getMethod().getDeclaringClass(); + Class targetClass = getTargetClass(ctx); + boolean beanManaged = isBeanManaged(declaringClass) || isBeanManaged(targetClass); + TFrameStack ts = new TFrameStack(); + TransactionInfo lastTransactionInfo = ts.topTransaction(); + + TransactionAttributeType attribute; + if (beanManaged) { + attribute = TransactionAttributeType.NOT_SUPPORTED; + } else { + TransactionAttribute transaction = + declaringClass.getAnnotation( + TransactionAttribute.class); + TransactionAttribute transactionMethod = ctx.getMethod().getAnnotation(TransactionAttribute.class); + + if (transactionMethod != null) { + attribute = transactionMethod.value(); + } else if (transaction != null) { + attribute = transaction.value(); + } else { + attribute = TransactionAttributeType.REQUIRED; + } + } + + + boolean passThroughRollbackException = true; + try { + logger.info("Thread {} L{} changing from {} to {} xid: {} in {}.{}", + Thread.currentThread().getId(), ts.currentLevel(), + ts.currentType(), + attribute, MDC.get("XID"), declaringClass.getSimpleName(), ctx.getMethod().getName()); + ts.pushTransaction(attribute); + return ctx.proceed(); + } catch (Throwable ex) { + logger.info("Thread {} L{} Exception {} in {} xid: {} in {}.{}", + Thread.currentThread().getId(), ts.currentLevel(), + ex.getClass().getSimpleName(), attribute, MDC.get("XID"), declaringClass.getSimpleName(), + ctx.getMethod().getName()); + if (beanManaged) { + if (ex instanceof RuntimeException) { + throw new EJBException((RuntimeException) ex); + } else { + throw ex; + } + } + ApplicationException applicationException = findApplicationException(ex); + boolean doRollback = + applicationException != null ? applicationException.rollback() : ex instanceof RuntimeException; + + if (doRollback) { + passThroughRollbackException = false; + tm.rollback(); + } + + if (applicationException == null && ex instanceof RuntimeException) { + throw new EJBException((RuntimeException) ex); + } else { + throw ex; + } + } finally { + logger.info("Thread {} L{} finally in {} xid: {} in {}.{}", + Thread.currentThread().getId(), ts.currentLevel(), attribute, MDC.get("XID"), declaringClass.getSimpleName(), + ctx.getMethod().getName()); + try { + ts.popTransaction(); + } catch (RollbackException rbe) { + if (passThroughRollbackException) { + throw rbe; + } + } finally { + logger.info("Thread {} L{} done {} back to {} xid: {} in {}.{}", + Thread.currentThread().getId(), ts.topTransaction(), attribute, + lastTransactionInfo == null ? "undefined" : lastTransactionInfo.currentTransactionAttributeType, + MDC.get("XID"), declaringClass.getSimpleName(), ctx.getMethod().getName()); + } + } + } + + + private boolean traActive() throws SystemException { + return tm.getStatus() != Status.STATUS_NO_TRANSACTION; + } + + private Class getTargetClass(InvocationContext ctx) { + final Object target = ctx.getTarget(); + if (target == null) + return null; + Class res = target.getClass(); + if (res.getName().endsWith("WeldSubclass")) + return res.getSuperclass(); + else + return res; + + } + + private ApplicationException findApplicationException(Throwable ex) { + // search for applicationexception + Class tmp = ex.getClass(); + ApplicationException applicationException = null; + while (!tmp.equals(Throwable.class)) { + applicationException = tmp.getAnnotation(ApplicationException.class); + if (applicationException != null) { + break; + } + tmp = tmp.getSuperclass(); + } + if (applicationException != null && (tmp.equals(ex.getClass()) || applicationException.inherited())) { + return applicationException; + } + return null; + } + + private boolean isBeanManaged(Class declaringClass) { + return declaringClass != null + && declaringClass.getAnnotation(TransactionManagement.class) != null + && declaringClass.getAnnotation(TransactionManagement.class).value() == TransactionManagementType.BEAN; + } + +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EjbTransactional.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EjbTransactional.java new file mode 100644 index 00000000..66a18699 --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EjbTransactional.java @@ -0,0 +1,16 @@ +package bitronix.tm.integration.cdi; + +import javax.interceptor.InterceptorBinding; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author aschoerk + */ +@InterceptorBinding +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface EjbTransactional { +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EntityManagerDelegate.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EntityManagerDelegate.java new file mode 100644 index 00000000..2a85c17c --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EntityManagerDelegate.java @@ -0,0 +1,312 @@ +package bitronix.tm.integration.cdi; + +import javax.persistence.EntityGraph; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityTransaction; +import javax.persistence.FlushModeType; +import javax.persistence.LockModeType; +import javax.persistence.Query; +import javax.persistence.StoredProcedureQuery; +import javax.persistence.TransactionRequiredException; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaDelete; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.CriteriaUpdate; +import javax.persistence.metamodel.Metamodel; +import java.util.List; +import java.util.Map; + +/** + * Used to delegate EntityManager actions to the current EntityManager of the Thread, as it is defined according + * to Initialization and Transaction-Context + * Created by aschoerk2 on 3/2/14. + */ +@SuppressWarnings("ClassWithTooManyMethods") +class EntityManagerDelegate implements EntityManager { + + private final SqlPersistenceFactory entityManagerStore; + + EntityManagerDelegate(SqlPersistenceFactory entityManagerStore) { + this.entityManagerStore = entityManagerStore; + } + + private EntityManager getEmbeddedEntityManager() { + /* + * make sure the transaction context is correctly started, if necessary, then return the workable EntityManager + * of the thread. + */ + try { + return entityManagerStore.getTransactional(false); + } catch (TransactionRequiredException e) { + throw new RuntimeException("not expected exception: ", e); + } + } + + + private EntityManager getEmbeddedEntityManager(boolean expectTransaction) { + /* + * make sure the transaction context is correctly started, if necessary, then return the workable EntityManager of the thread. + */ + return entityManagerStore.getTransactional(expectTransaction); + } + + @Override + public void persist(final Object entity) { + getEmbeddedEntityManager(true).persist(entity); + } + + @Override + public T merge(final T entity) { + return getEmbeddedEntityManager(true).merge(entity); + } + + @Override + public void remove(final Object entity) { + getEmbeddedEntityManager(true).remove(entity); + } + + @Override + public T find(final Class entityClass, final Object primaryKey) { + return getEmbeddedEntityManager().find(entityClass, primaryKey); + } + + @Override + public T find(final Class entityClass, final Object primaryKey, final Map properties) { + return getEmbeddedEntityManager().find(entityClass, primaryKey, properties); + } + + @Override + public T find(final Class entityClass, final Object primaryKey, final LockModeType lockMode) { + return getEmbeddedEntityManager().find(entityClass, primaryKey, lockMode); + } + + @Override + public T find(final Class entityClass, final Object primaryKey, final LockModeType lockMode, + final Map properties) { + return getEmbeddedEntityManager().find(entityClass, primaryKey, lockMode, properties); + } + + @Override + public T getReference(final Class entityClass, final Object primaryKey) { + return getEmbeddedEntityManager().getReference(entityClass, primaryKey); + } + + @Override + public void flush() { + getEmbeddedEntityManager(true).flush(); + } + + @Override + public FlushModeType getFlushMode() { + return getEmbeddedEntityManager().getFlushMode(); + } + + @Override + public void setFlushMode(final FlushModeType flushMode) { + getEmbeddedEntityManager().setFlushMode(flushMode); + } + + @Override + public void lock(final Object entity, final LockModeType lockMode) { + getEmbeddedEntityManager(true).lock(entity, lockMode); + } + + @Override + public void lock(final Object entity, final LockModeType lockMode, final Map properties) { + getEmbeddedEntityManager(true).lock(entity, lockMode, properties); + } + + @Override + public void refresh(final Object entity) { + getEmbeddedEntityManager(true).refresh(entity); + } + + @Override + public void refresh(final Object entity, final Map properties) { + getEmbeddedEntityManager(true).refresh(entity, properties); + } + + @Override + public void refresh(final Object entity, final LockModeType lockMode) { + getEmbeddedEntityManager(true).refresh(entity, lockMode); + } + + @Override + public void refresh(final Object entity, final LockModeType lockMode, final Map properties) { + getEmbeddedEntityManager(true).refresh(entity, lockMode, properties); + } + + @Override + public void clear() { + getEmbeddedEntityManager().clear(); + } + + @Override + public void detach(final Object entity) { + getEmbeddedEntityManager().detach(entity); + } + + @Override + public boolean contains(final Object entity) { + return getEmbeddedEntityManager().contains(entity); + } + + @Override + public LockModeType getLockMode(final Object entity) { + return getEmbeddedEntityManager(true).getLockMode(entity); + } + + @Override + public void setProperty(final String propertyName, final Object value) { + getEmbeddedEntityManager().setProperty(propertyName, value); + } + + @Override + public Map getProperties() { + return getEmbeddedEntityManager().getProperties(); + } + + @Override + public Query createQuery(final String qlString) { + return getEmbeddedEntityManager().createQuery(qlString); + } + + @Override + public TypedQuery createQuery(final CriteriaQuery criteriaQuery) { + return getEmbeddedEntityManager().createQuery(criteriaQuery); + } + + @Override + public Query createQuery(CriteriaUpdate criteriaUpdate) { + return getEmbeddedEntityManager().createQuery(criteriaUpdate); + } + + @Override + public Query createQuery(CriteriaDelete criteriaDelete) { + return getEmbeddedEntityManager().createQuery(criteriaDelete); + } + + @Override + public TypedQuery createQuery(final String qlString, final Class resultClass) { + return getEmbeddedEntityManager().createQuery(qlString, resultClass); + } + + @Override + public Query createNamedQuery(final String name) { + return getEmbeddedEntityManager().createNamedQuery(name); + } + + @Override + public TypedQuery createNamedQuery(final String name, final Class resultClass) { + return getEmbeddedEntityManager().createNamedQuery(name, resultClass); + } + + @Override + public Query createNativeQuery(final String sqlString) { + return getEmbeddedEntityManager().createNativeQuery(sqlString); + } + + @SuppressWarnings("rawtypes") + @Override + public Query createNativeQuery(final String sqlString, final Class resultClass) { + return getEmbeddedEntityManager().createNativeQuery(sqlString, resultClass); + } + + @Override + public Query createNativeQuery(final String sqlString, final String resultSetMapping) { + return getEmbeddedEntityManager().createNativeQuery(sqlString, resultSetMapping); + } + + @Override + public StoredProcedureQuery createNamedStoredProcedureQuery(String name) { + return getEmbeddedEntityManager().createNamedStoredProcedureQuery(name); + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName) { + return getEmbeddedEntityManager().createStoredProcedureQuery(procedureName); + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, Class[] resultClasses) { + return getEmbeddedEntityManager().createStoredProcedureQuery(procedureName, resultClasses); + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, String... resultSetMappings) { + return getEmbeddedEntityManager().createStoredProcedureQuery(procedureName, resultSetMappings); + } + + @Override + public void joinTransaction() { + getEmbeddedEntityManager().joinTransaction(); + } + + @Override + public boolean isJoinedToTransaction() { + return getEmbeddedEntityManager().isJoinedToTransaction(); + } + + @Override + public T unwrap(final Class cls) { + return getEmbeddedEntityManager().unwrap(cls); + } + + @Override + public Object getDelegate() { + return getEmbeddedEntityManager().getDelegate(); + } + + @Override + public void close() { + getEmbeddedEntityManager().close(); + } + + @Override + public boolean isOpen() { + return getEmbeddedEntityManager().isOpen(); + } + + @Override + public EntityTransaction getTransaction() { + return null; + } + + @Override + public EntityManagerFactory getEntityManagerFactory() { + return getEmbeddedEntityManager().getEntityManagerFactory(); + } + + @Override + public CriteriaBuilder getCriteriaBuilder() { + return getEmbeddedEntityManager().getCriteriaBuilder(); + } + + @Override + public Metamodel getMetamodel() { + return getEmbeddedEntityManager().getMetamodel(); + } + + @Override + public EntityGraph createEntityGraph(Class rootType) { + return getEmbeddedEntityManager().createEntityGraph(rootType); + } + + @Override + public EntityGraph createEntityGraph(String graphName) { + return getEmbeddedEntityManager().createEntityGraph(graphName); + } + + @Override + public EntityGraph getEntityGraph(String graphName) { + return getEmbeddedEntityManager().getEntityGraph(graphName); + } + + @Override + public List> getEntityGraphs(Class entityClass) { + return getEmbeddedEntityManager().getEntityGraphs(entityClass); + } + +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EntityManagerInfo.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EntityManagerInfo.java new file mode 100644 index 00000000..9a06c581 --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/EntityManagerInfo.java @@ -0,0 +1,11 @@ +package bitronix.tm.integration.cdi; + +import javax.persistence.EntityManager; + +/** + * @author aschoerk + */ +public class EntityManagerInfo { + String persistenceUnit; + EntityManager em; +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/PlatformTransactionManager.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/PlatformTransactionManager.java new file mode 100644 index 00000000..7d494c96 --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/PlatformTransactionManager.java @@ -0,0 +1,61 @@ +package bitronix.tm.integration.cdi; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Produces; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + + +import bitronix.tm.BitronixTransactionManager; +import bitronix.tm.TransactionManagerServices; +import org.jglue.cdiunit.ProducesAlternative; + +/** + * Bitronix-specific Spring PlatformTransactionManager implementation. + * + * @author Marcus Klimstra (CGI) + */ +@ApplicationScoped +public class PlatformTransactionManager { + + private BitronixTransactionManager transactionManager;; + + public PlatformTransactionManager() { + this.transactionManager = TransactionManagerServices.getTransactionManager(); + } + + @PostConstruct + public void postConstructPlatformTM() { + // System.clearProperty("java.naming.factory.initial"); + } + + @PreDestroy + public void preDestroyPlatfromTM() { + transactionManager.shutdown(); + transactionManager = null; + } + + @Produces + @ProducesAlternative + @Alternative + protected UserTransaction retrieveUserTransaction() { + return new TUserTransaction(); + } + + @Produces + protected TransactionManager retrieveTransactionManager() { + return transactionManager; + } + + @Produces + protected Object retrieveTransactionSynchronizationRegistry() { + return transactionManager; + } + + public void destroy() throws Exception { + transactionManager.shutdown(); + } +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/PoolingDataSourceFactoryBean.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/PoolingDataSourceFactoryBean.java new file mode 100644 index 00000000..be47cb5e --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/PoolingDataSourceFactoryBean.java @@ -0,0 +1,50 @@ +package bitronix.tm.integration.cdi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import bitronix.tm.resource.common.ResourceBean; +import bitronix.tm.resource.jdbc.PoolingDataSource; +import bitronix.tm.utils.PropertyUtils; + +/** + * FactoryBean for PoolingDataSource to correctly manage its lifecycle when used + * with Spring. + * + * @author Marcus Klimstra (CGI) + */ +public class PoolingDataSourceFactoryBean extends ResourceBean { + + private static final Logger log = LoggerFactory.getLogger(PoolingDataSourceFactoryBean.class); + private static final long serialVersionUID = 8283399886348754184L; + + private PoolingDataSource ds; + + public Class getObjectType() { + return PoolingDataSource.class; + } + + public boolean isSingleton() { + return true; + } + + public PoolingDataSource getObject() throws Exception { + if (ds == null) { + ds = new PoolingDataSource(); + PropertyUtils.setProperties(ds, PropertyUtils.getProperties(this)); + + + log.debug("Initializing PoolingDataSource with id '{}'", ds.getUniqueName()); + ds.init(); + } + return ds; + } + + public void destroy() throws Exception { + if (ds != null) { + log.debug("Closing PoolingDataSource with id '{}'", ds.getUniqueName()); + ds.close(); + ds = null; + } + } +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/SqlPersistenceFactory.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/SqlPersistenceFactory.java new file mode 100644 index 00000000..bc3a2b0c --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/SqlPersistenceFactory.java @@ -0,0 +1,139 @@ +package bitronix.tm.integration.cdi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.sql.DataSource; +import javax.transaction.TransactionManager; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashSet; + +/** + * @author aschoerk + */ +public abstract class SqlPersistenceFactory { + @Inject + TransactionManager tm; + + private final Logger logger = LoggerFactory.getLogger(SqlPersistenceFactory.class); + + private static final HashSet PERSISTENCE_UNIT_NAMES = new HashSet<>(); + private EntityManagerFactory emf = null; + + protected DataSource ds; + + /** + * allow to reset between Tests. + */ + private static void clearPersistenceUnitNames() { + PERSISTENCE_UNIT_NAMES.clear(); + } + + public abstract String getPersistenceUnitName(); + + protected abstract DataSource createDataSource(); + + + public EntityManagerFactory getEmf() { + return emf; + } + + protected void createEntityManagerFactory() { + if (emf == null) { + ds = createDataSource(); + emf = Persistence.createEntityManagerFactory(getPersistenceUnitName()); + } + } + /** + * prepare EntityManagerFactory + */ + @PostConstruct + public void construct() { + logger.info("creating persistence factory {}", getPersistenceUnitName()); + synchronized (PERSISTENCE_UNIT_NAMES) { + if (PERSISTENCE_UNIT_NAMES.contains(getPersistenceUnitName())) { + throw new RuntimeException("Repeated construction of currently existing PersistenceFactory for " + getPersistenceUnitName()); + } else { + createEntityManagerFactory(); + PERSISTENCE_UNIT_NAMES.add(getPersistenceUnitName()); + } + } + } + + /** + * make sure all connections will be closed + */ + @PreDestroy + public void destroy() { + logger.info("destroying persistence factory {}", getPersistenceUnitName()); + synchronized (PERSISTENCE_UNIT_NAMES) { + if (!PERSISTENCE_UNIT_NAMES.contains(getPersistenceUnitName())) { + throw new RuntimeException("Expected PersistenceFactory for " + getPersistenceUnitName()); + } else { + if (emf != null && emf.isOpen()) { + emf.close(); + emf = null; + } + PERSISTENCE_UNIT_NAMES.remove(getPersistenceUnitName()); + } + clearPersistenceUnitNames(); + } + if (ds != null) { + try { + Method closeMethod = ds.getClass().getMethod("close"); + closeMethod.invoke(ds); + } catch (NoSuchMethodException e) { + + } catch (IllegalAccessException e) { + + } catch (InvocationTargetException e) { + + } + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + SqlPersistenceFactory that = (SqlPersistenceFactory) obj; + + return getPersistenceUnitName() != null ? getPersistenceUnitName().equals(that.getPersistenceUnitName()) + : that.getPersistenceUnitName() == null; + } + + @Override + public int hashCode() { + return getPersistenceUnitName() != null ? getPersistenceUnitName().hashCode() : 0; + } + + /** + * returns EntityManager, to be injected and used so that the current threadSpecific context is correctly handled + * + * @return the EntityManager as it is returnable by producers. + */ + public EntityManager produceEntityManager() { + return new EntityManagerDelegate(this); + } + + + public EntityManager getTransactional(boolean expectTransaction) { + return new TFrameStack().getEntityManager(this, expectTransaction); + } + + public DataSource getDatasource() { + return ds; + } +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TFrameStack.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TFrameStack.java new file mode 100644 index 00000000..2da466fb --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TFrameStack.java @@ -0,0 +1,205 @@ +package bitronix.tm.integration.cdi; + +import bitronix.tm.TransactionManagerServices; + +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.TransactionRequiredException; +import javax.transaction.*; + +/** + * The logic necessary to handle stacking of transactions according ejb-transactionattributetypes. + * Later: should be able to handle bean managed transactions as well (UserTransaction). + * + * @author aschoerk + */ +public class TFrameStack { + + private static ThreadLocal transactionInfoThreadLocal = new ThreadLocal<>(); + + private final TransactionManager tm = TransactionManagerServices.getTransactionManager(); + + public TransactionInfo topTransaction() { + return transactionInfoThreadLocal.get(); + } + + public int currentLevel() { + TransactionInfo tt = topTransaction(); + return tt == null ? 0 : tt.level; + } + + public TransactionAttributeType currentType() { + TransactionInfo tt = topTransaction(); + return tt == null ? null : tt.currentTransactionAttributeType; + } + + public Transactional.TxType currentTxType() { + TransactionInfo tt = topTransaction(); + return tt == null ? null : tt.currentTxType; + } + + public void commitTransaction() throws HeuristicRollbackException, RollbackException, InvalidTransactionException, HeuristicMixedException, SystemException { + popTransaction(true, false); + } + public void rollbackTransaction() throws HeuristicRollbackException, RollbackException, InvalidTransactionException, HeuristicMixedException, SystemException { + popTransaction(true, true); + } + + public void popTransaction() throws HeuristicRollbackException, RollbackException, InvalidTransactionException, HeuristicMixedException, SystemException { + popTransaction(false, false); + } + + public boolean isUserTransaction() { + final TransactionInfo transactionInfo = transactionInfoThreadLocal.get(); + return transactionInfo != null ? transactionInfo.userTransaction : false; + } + + + public void popTransaction(boolean expectNewTra, boolean rollback) throws HeuristicRollbackException, RollbackException, HeuristicMixedException, SystemException, InvalidTransactionException { + TransactionInfo transactionInfo = transactionInfoThreadLocal.get(); + transactionInfoThreadLocal.set(transactionInfo.previous); + try { + if (expectNewTra && !transactionInfo.newTra) + throw new IllegalStateException("expected new tra-transaction-frame on stack"); + if (transactionInfo.newTra || transactionInfo.suspended != null) { + if (rollback) + tm.rollback(); + else + tm.commit(); + for (EntityManagerInfo ei: transactionInfo.entityManagers) { + ei.em.close(); + } + } + } + finally { + if (transactionInfo.suspended != null) { + tm.resume(transactionInfo.suspended); + } + } + } + + public void pushUserTransaction() throws SystemException, NotSupportedException { + final TransactionInfo previousTransactionInfo = topTransaction(); + TransactionInfo transactionInfo = new TransactionInfo(previousTransactionInfo); + transactionInfo.currentTransactionAttributeType = null; + if (traActive()) { + transactionInfo.suspended = tm.suspend(); + } + tm.begin(); + transactionInfo.newTra = true; + transactionInfo.setUserTransaction(); + transactionInfoThreadLocal.set(transactionInfo); + + } + + public void pushTransaction(Transactional.TxType attributeType) throws SystemException, NotSupportedException { + final TransactionInfo previousTransactionInfo = topTransaction(); + TransactionInfo transactionInfo = new TransactionInfo(previousTransactionInfo); + transactionInfo.currentTxType = attributeType; + switch (attributeType) { + case MANDATORY: + if (!traActive()) + throw new TransactionRequiredException("Mandatory Transaction"); + break; + case REQUIRED: + if (!traActive()) { + tm.begin(); + transactionInfo.newTra = true; + } + break; + case REQUIRES_NEW: + if (traActive()) { + transactionInfo.suspended = tm.suspend(); + } + tm.begin(); + transactionInfo.newTra = true; + break; + case SUPPORTS: + break; + case NOT_SUPPORTED: + if (traActive()) { + transactionInfo.suspended = tm.suspend(); + } + break; + case NEVER: + if (traActive()) + throw new TransactionRequiredException("Transaction is not allowed"); + break; + } + transactionInfoThreadLocal.set(transactionInfo); + } + + public void pushTransaction(TransactionAttributeType attributeType) throws SystemException, NotSupportedException { + + final TransactionInfo previousTransactionInfo = topTransaction(); + TransactionInfo transactionInfo = new TransactionInfo(previousTransactionInfo); + transactionInfo.currentTransactionAttributeType = attributeType; + switch (attributeType) { + case MANDATORY: + if (!traActive()) + throw new TransactionRequiredException("Mandatory Transaction"); + break; + case REQUIRED: + if (!traActive()) { + tm.begin(); + transactionInfo.newTra = true; + } + break; + case REQUIRES_NEW: + if (traActive()) { + transactionInfo.suspended = tm.suspend(); + } + tm.begin(); + transactionInfo.newTra = true; + break; + case SUPPORTS: + break; + case NOT_SUPPORTED: + if (traActive()) { + transactionInfo.suspended = tm.suspend(); + } + break; + case NEVER: + if (traActive()) + throw new TransactionRequiredException("Transaction is not allowed"); + break; + } + transactionInfoThreadLocal.set(transactionInfo); + } + + private boolean traActive() { + try { + return tm.getStatus() != Status.STATUS_NO_TRANSACTION; + } catch (SystemException e) { + throw new RuntimeException("simulated ejb", e); + } + } + + public EntityManager getEntityManager(SqlPersistenceFactory sqlPersistenceFactory, boolean expectTransaction) { + if (expectTransaction && !traActive()) { + throw new TransactionRequiredException("ejb simulation"); + } + String name = sqlPersistenceFactory.getPersistenceUnitName(); + TransactionInfo info = topTransaction(); + while (info != null) { + if (info.newTra || info.suspended != null) + break; + info = info.previous; + } + if (info != null) { + for (EntityManagerInfo ei: info.entityManagers) { + if (ei.persistenceUnit.equals(name)) { + return ei.em; + } + } + EntityManager result = sqlPersistenceFactory.getEmf().createEntityManager(); + if (traActive()) { + result.joinTransaction(); + } + return result; + } else { + assert !traActive(); + return sqlPersistenceFactory.getEmf().createEntityManager(); + } + } +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TUserTransaction.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TUserTransaction.java new file mode 100644 index 00000000..5ac77b89 --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TUserTransaction.java @@ -0,0 +1,222 @@ +package bitronix.tm.integration.cdi; + +import bitronix.tm.TransactionManagerServices; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.InvalidTransactionException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +/** + * @author aschoerk + */ +public class TUserTransaction implements UserTransaction { + + TransactionManager tm; + + TFrameStack ts; + + public TUserTransaction() { + this.tm = TransactionManagerServices.getTransactionManager(); + this.ts = new TFrameStack(); + } + + /** + * Create a new transaction and associate it with the current thread. + * + * @exception NotSupportedException Thrown if the thread is already + * associated with a transaction and the Transaction Manager + * implementation does not support nested transactions. + * + * @exception SystemException Thrown if the transaction manager + * encounters an unexpected error condition. + * + */ + @Override + public void begin() throws NotSupportedException, SystemException { + if (ts.isUserTransaction()) + throw new IllegalStateException("Already UserTransaction running"); + ts.pushUserTransaction(); + } + + /** + * Complete the transaction associated with the current thread. When this + * method completes, the thread is no longer associated with a transaction. + * + * @exception RollbackException Thrown to indicate that + * the transaction has been rolled back rather than committed. + * + * @exception HeuristicMixedException Thrown to indicate that a heuristic + * decision was made and that some relevant updates have been committed + * while others have been rolled back. + * + * @exception HeuristicRollbackException Thrown to indicate that a + * heuristic decision was made and that all relevant updates have been + * rolled back. + * + * @exception SecurityException Thrown to indicate that the thread is + * not allowed to commit the transaction. + * + * @exception IllegalStateException Thrown if the current thread is + * not associated with a transaction. + * + * @exception SystemException Thrown if the transaction manager + * encounters an unexpected error condition. + * + */ + @Override + public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { + try { + if (!ts.isUserTransaction()) + throw new IllegalStateException("No UserTransaction"); + ts.commitTransaction(); + } catch (InvalidTransactionException e) { + throw new RuntimeException(e); + } + } + + /** + * Obtain the status of the transaction associated with the current thread. + * + * @return The transaction status. If no transaction is associated with + * the current thread, this method returns the Status.NoTransaction + * value. + * + * @exception SystemException Thrown if the transaction manager + * encounters an unexpected error condition. + * + */ + @Override + public int getStatus() throws SystemException { + return tm.getStatus(); + } + + /** + * Get the transaction object that represents the transaction + * context of the calling thread. + * + * @return the Transaction object representing the + * transaction associated with the calling thread. + * + * @exception SystemException Thrown if the transaction manager + * encounters an unexpected error condition. + * + */ + public Transaction getTransaction() throws SystemException { + return tm.getTransaction(); + } + + /** + * Resume the transaction context association of the calling thread + * with the transaction represented by the supplied Transaction object. + * When this method returns, the calling thread is associated with the + * transaction context specified. + * + * @param tobj The Transaction object that represents the + * transaction to be resumed. + * + * @exception InvalidTransactionException Thrown if the parameter + * transaction object contains an invalid transaction. + * + * @exception IllegalStateException Thrown if the thread is already + * associated with another transaction. + * + * @exception SystemException Thrown if the transaction manager + * encounters an unexpected error condition. + */ + public void resume(Transaction tobj) throws InvalidTransactionException, IllegalStateException, SystemException { + if (!ts.isUserTransaction()) + throw new IllegalStateException("No UserTransaction"); + tm.resume(tobj); + } + + /** + * Roll back the transaction associated with the current thread. When this + * method completes, the thread is no longer associated with a + * transaction. + * + * @exception SecurityException Thrown to indicate that the thread is + * not allowed to roll back the transaction. + * + * @exception IllegalStateException Thrown if the current thread is + * not associated with a transaction. + * + * @exception SystemException Thrown if the transaction manager + * encounters an unexpected error condition. + * + */ + @Override + public void rollback() throws IllegalStateException, SecurityException, SystemException { + try { + if (!ts.isUserTransaction()) + throw new IllegalStateException("No UserTransaction"); + ts.rollbackTransaction(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Modify the transaction associated with the current thread such that + * the only possible outcome of the transaction is to roll back the + * transaction. + * + * @exception IllegalStateException Thrown if the current thread is + * not associated with a transaction. + * + * @exception SystemException Thrown if the transaction manager + * encounters an unexpected error condition. + * + */ + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + if (!ts.isUserTransaction()) + throw new IllegalStateException("No UserTransaction"); + tm.setRollbackOnly(); + } + + /** + * Modify the timeout value that is associated with transactions started + * by the current thread with the begin method. + * + *

If an application has not called this method, the transaction + * service uses some default value for the transaction timeout. + * + * @param seconds The value of the timeout in seconds. If the value is zero, + * the transaction service restores the default value. If the value + * is negative a SystemException is thrown. + * + * @exception SystemException Thrown if the transaction manager + * encounters an unexpected error condition. + * + */ + @Override + public void setTransactionTimeout(int seconds) throws SystemException { + tm.setTransactionTimeout(seconds); + } + + /** + * Suspend the transaction currently associated with the calling + * thread and return a Transaction object that represents the + * transaction context being suspended. If the calling thread is + * not associated with a transaction, the method returns a null + * object reference. When this method returns, the calling thread + * is not associated with a transaction. + * + * @return Transaction object representing the suspended transaction. + * + * @exception SystemException Thrown if the transaction manager + * encounters an unexpected error condition. + * + */ + public Transaction suspend() throws SystemException { + if (!ts.isUserTransaction()) + throw new IllegalStateException("No UserTransaction"); + return tm.suspend(); + } +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TransactionInfo.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TransactionInfo.java new file mode 100644 index 00000000..12c912ca --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TransactionInfo.java @@ -0,0 +1,43 @@ +package bitronix.tm.integration.cdi; + +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.transaction.Transaction; +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; + +/** + * @author aschoerk + */ +public class TransactionInfo { + + static class TAttribute { + TransactionAttributeType ejbAttribue; + Transactional.TxType txType; + boolean userTransaction; + } + + public TransactionInfo(TransactionInfo previous) { + this.previous = previous; + this.level = previous != null ? previous.level + 1 : 0; + } + + List entityManagers = new ArrayList<>(); + Transaction suspended; // fetching of entitymanagers: only new ones + boolean newTra = false; // if true: tra has been begin, entitymanagers joined, pop means: need to commit! + TransactionAttributeType currentTransactionAttributeType; + Transactional.TxType currentTxType; + TAttribute tAttribute; + TransactionInfo previous; + boolean userTransaction; + int level; + + public void setUserTransaction() { + this.userTransaction = true; + } + + public boolean isUserTransaction() { + return userTransaction; + } +} diff --git a/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TransactionalCdiExtension.java b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TransactionalCdiExtension.java new file mode 100644 index 00000000..63f30071 --- /dev/null +++ b/btm-cdi/src/main/java/bitronix/tm/integration/cdi/TransactionalCdiExtension.java @@ -0,0 +1,131 @@ +package bitronix.tm.integration.cdi; + +import org.apache.deltaspike.core.util.metadata.builder.AnnotatedTypeBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import javax.ejb.MessageDriven; +import javax.ejb.Singleton; +import javax.ejb.Stateful; +import javax.ejb.Stateless; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.*; +import javax.enterprise.util.AnnotationLiteral; +import javax.transaction.Transactional; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class TransactionalCdiExtension implements Extension { + + private static final Logger log = LoggerFactory.getLogger(TransactionalCdiExtension.class); + + AnnotationLiteral EJBTRALITERAL = new AnnotationLiteral () { + + private static final long serialVersionUID = -6529647818427562781L; + }; + + AnnotationLiteral CDITRALITERAL = new AnnotationLiteral () { + + private static final long serialVersionUID = 6942136472219373737L; + }; + + void processAnnotatedType(@Observes ProcessAnnotatedType pat) { + final AnnotatedType annotatedType = pat.getAnnotatedType(); + boolean interceptForEjbTransactions = false; + boolean interceptForCdiTransactions = false; + if (annotatedType.isAnnotationPresent(Stateless.class) + || annotatedType.isAnnotationPresent(Stateful.class) + || annotatedType.isAnnotationPresent(Singleton.class) + || annotatedType.isAnnotationPresent(MessageDriven.class) + ) { + interceptForEjbTransactions = true; + } + for (AnnotatedMethod m: annotatedType.getMethods()) { + if (m.isAnnotationPresent(Transactional.class)) { + interceptForCdiTransactions = true; + } + } + if (annotatedType.isAnnotationPresent(Transactional.class)) { + interceptForCdiTransactions = true; + } + if (interceptForCdiTransactions && interceptForEjbTransactions) { + log.warn("Transactional-Annotation for Ejb ignored {}", annotatedType.getJavaClass().getName()); + interceptForCdiTransactions = false; + } + if (interceptForEjbTransactions || interceptForCdiTransactions) { + final boolean finalInterceptForCdiTransactions = interceptForCdiTransactions; + pat.setAnnotatedType(new AnnotatedType() { + @Override + public Class getJavaClass() { + return annotatedType.getJavaClass(); + } + + @Override + public Set> getConstructors() { + return annotatedType.getConstructors(); + } + + @Override + public Set> getMethods() { + return annotatedType.getMethods(); + } + + @Override + public Set> getFields() { + return annotatedType.getFields(); + } + + @Override + public Type getBaseType() { + return annotatedType.getBaseType(); + } + + @Override + public Set getTypeClosure() { + return annotatedType.getTypeClosure(); + } + + @Override + public T getAnnotation(Class annotationType) { + if (finalInterceptForCdiTransactions) { + if (annotationType.equals(CdiTransactional.class)) + return (T) CDITRALITERAL; + } else { + if (annotationType.equals(EjbTransactional.class)) + return (T) EJBTRALITERAL; + } + return annotatedType.getAnnotation(annotationType); + } + + @Override + public Set getAnnotations() { + Set result = new HashSet<>(annotatedType.getAnnotations()); + if (finalInterceptForCdiTransactions) { + result.add(CDITRALITERAL); + } else { + result.add(EJBTRALITERAL); + + } + return Collections.unmodifiableSet(result); + } + + @Override + public boolean isAnnotationPresent(Class annotationType) { + if (finalInterceptForCdiTransactions) { + if (annotationType.equals(CdiTransactional.class)) + return true; + } else { + if (annotationType.equals(EjbTransactional.class)) + return true; + } + return annotatedType.isAnnotationPresent(annotationType); + } + }); + } + + } +} diff --git a/btm-cdi/src/main/resources/META-INF/beans.xml b/btm-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000..b7fdba87 --- /dev/null +++ b/btm-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,4 @@ + + + diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/cdiintercepted/CDITransactionalJPABean.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/cdiintercepted/CDITransactionalJPABean.java new file mode 100644 index 00000000..f924619d --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/cdiintercepted/CDITransactionalJPABean.java @@ -0,0 +1,89 @@ +package bitronix.tm.integration.cdi.cdiintercepted; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; + +import java.util.Iterator; + +import javax.annotation.Resource; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.sql.DataSource; +import javax.transaction.TransactionManager; +import javax.transaction.Transactional; +import javax.transaction.xa.XAResource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import bitronix.tm.integration.cdi.entities.TestEntity1; +import bitronix.tm.mock.events.EventRecorder; +import bitronix.tm.mock.events.XAResourceCommitEvent; +import bitronix.tm.mock.events.XAResourceEndEvent; +import bitronix.tm.mock.events.XAResourcePrepareEvent; +import bitronix.tm.mock.events.XAResourceStartEvent; + +@Transactional +public class CDITransactionalJPABean { + + private static final Logger log = LoggerFactory.getLogger(CDITransactionalJPABean.class); + + @Resource(name = "h2DataSource") + private DataSource dataSource; + + @Inject + TransactionManager tm; + + @Inject + EntityManager em; + + + @Transactional(value = Transactional.TxType.REQUIRES_NEW) + public void insertTestEntityInNewTra() throws Exception { + em.persist(new TestEntity1()); + } + + @Transactional(value = Transactional.TxType.REQUIRES_NEW) + public void insertTestEntityInNewTraAndSetRollbackOnly() throws Exception { + em.persist(new TestEntity1()); + tm.setRollbackOnly(); + } + + @Transactional(value = Transactional.TxType.REQUIRED) + public void insertTestEntityInRequired() throws Exception { + em.persist(new TestEntity1()); + } + + public long countTestEntity() throws Exception { + Long result = em.createQuery("select count(e) from TestEntity1 e", Long.class).getSingleResult(); + return result; + } + + public void verifyEvents(int count) { + if (log.isDebugEnabled()) { + log.debug(EventRecorder.dumpToString()); + } + + Iterator it = EventRecorder.iterateEvents(); + + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMNOFLAGS, ((XAResourceStartEvent) it.next()).getFlag()); + } + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMSUCCESS, ((XAResourceEndEvent) it.next()).getFlag()); + } + if (count > 1) { + for (int i = 0; i < count; i++) { + assertEquals(XAResource.XA_OK, ((XAResourcePrepareEvent) it.next()).getReturnCode()); + } + } + for (int i = 0; i < count; i++) { + assertEquals(count == 1, ((XAResourceCommitEvent) it.next()).isOnePhase()); + } + + assertFalse(it.hasNext()); + } +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/cdiintercepted/H2CdiTransactionalTest.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/cdiintercepted/H2CdiTransactionalTest.java new file mode 100644 index 00000000..68a70d95 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/cdiintercepted/H2CdiTransactionalTest.java @@ -0,0 +1,138 @@ +package bitronix.tm.integration.cdi.cdiintercepted; + +import javax.inject.Inject; +import javax.sql.DataSource; +import javax.transaction.RollbackException; +import javax.transaction.UserTransaction; + +import org.jglue.cdiunit.AdditionalClasses; +import org.jglue.cdiunit.AdditionalPackages; +import org.jglue.cdiunit.CdiRunner; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import bitronix.tm.integration.cdi.CdiTInterceptor; +import bitronix.tm.integration.cdi.EjbTInterceptor; +import bitronix.tm.integration.cdi.PlatformTransactionManager; +import bitronix.tm.integration.cdi.TransactionalCdiExtension; + +/** + * @author aschoerk + */ +@RunWith(CdiRunner.class) +@AdditionalClasses({ + H2PersistenceFactory.class, + CdiTInterceptor.class, TransactionalCdiExtension.class}) +@AdditionalPackages(PlatformTransactionManager.class) +public class H2CdiTransactionalTest { + + static Logger log = LoggerFactory.getLogger("testlogger"); + + @BeforeClass + public static void loginit() { + log.info("log"); + } + + @Inject + PlatformTransactionManager platformTransactionManager; + + @Inject + UserTransaction tm; + + @Inject + DataSource dataSource; + + @Before + public void initContext() throws Exception { + } + + @Inject + CDITransactionalJPABean jpaBean; + + @Test + public void testTraMethod() throws Exception { + + jpaBean.insertTestEntityInNewTra(); + Assert.assertEquals(1L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + tm.rollback(); + Assert.assertEquals(1L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + tm.rollback(); + Assert.assertEquals(2L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + insertTestEntityInNewTraAndRollback(); + tm.commit(); + Assert.assertEquals(4L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + insertTestEntityInNewTraAndRollback(); + tm.commit(); + Assert.assertEquals(5L, jpaBean.countTestEntity()); + tm.begin(); + insertTestEntityInNewTraAndRollback(); + jpaBean.insertTestEntityInRequired(); + tm.commit(); + Assert.assertEquals(6L, jpaBean.countTestEntity()); + tm.begin(); + insertTestEntityInNewTraAndRollback(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + tm.rollback(); + Assert.assertEquals(7L, jpaBean.countTestEntity()); + + tm.begin(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + insertTestEntityInNewTraAndSetRollbackOnly(); + tm.commit(); + Assert.assertEquals(9L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + insertTestEntityInNewTraAndSetRollbackOnly(); + tm.commit(); + Assert.assertEquals(10L, jpaBean.countTestEntity()); + tm.begin(); + insertTestEntityInNewTraAndSetRollbackOnly(); + jpaBean.insertTestEntityInRequired(); + tm.commit(); + Assert.assertEquals(11L, jpaBean.countTestEntity()); + tm.begin(); + insertTestEntityInNewTraAndSetRollbackOnly(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + tm.rollback(); + Assert.assertEquals(12L, jpaBean.countTestEntity()); + + } + + private void insertTestEntityInNewTraAndSetRollbackOnly() throws Exception { + try { + jpaBean.insertTestEntityInNewTraAndSetRollbackOnly(); + Assert.fail("Expected Rollbackexception during commit of new tra"); + } catch (RollbackException ex){ + + } + } + + private void insertTestEntityInNewTraAndRollback() throws Exception { + try { + jpaBean.insertTestEntityInNewTraAndSetRollbackOnly(); + Assert.fail("expected rollbackexception"); + } + catch (RollbackException rbe) { + + } + } + +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/cdiintercepted/H2PersistenceFactory.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/cdiintercepted/H2PersistenceFactory.java new file mode 100644 index 00000000..01208922 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/cdiintercepted/H2PersistenceFactory.java @@ -0,0 +1,62 @@ +package bitronix.tm.integration.cdi.cdiintercepted; + +import java.util.Properties; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.persistence.EntityManager; +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import bitronix.tm.integration.cdi.SqlPersistenceFactory; +import bitronix.tm.resource.jdbc.PoolingDataSource; + +/** + * @author aschoerk + */ +@ApplicationScoped +public class H2PersistenceFactory extends SqlPersistenceFactory { + + Logger log = LoggerFactory.getLogger("H2PersistenceFactory"); + + public H2PersistenceFactory() { + } + + + @Override + public String getPersistenceUnitName() { + return "btm-cdi-test-h2-pu"; + } + + @Produces + public EntityManager newEm() { + return produceEntityManager(); + } + + + @Produces + @ApplicationScoped + protected DataSource createDataSource() { + if (ds != null) + return ds; + log.info("creating datasource"); + PoolingDataSource res = new PoolingDataSource(); + res.setClassName("org.h2.jdbcx.JdbcDataSource"); + Properties driverProperties = res.getDriverProperties(); + driverProperties.setProperty("URL", "jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=0"); + driverProperties.setProperty("user","sa"); + driverProperties.setProperty("password",""); + res.setUniqueName("jdbc/btm-cdi-test-h2"); + res.setMinPoolSize(1); + res.setMaxPoolSize(10); + res.setAllowLocalTransactions(true); // to allow autocommitmode + res.init(); + log.info("created datasource"); + return res; + } + + + +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/ejbintercepted/EJBTransactionalJPABean.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/ejbintercepted/EJBTransactionalJPABean.java new file mode 100644 index 00000000..212c6988 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/ejbintercepted/EJBTransactionalJPABean.java @@ -0,0 +1,86 @@ +package bitronix.tm.integration.cdi.ejbintercepted; + +import bitronix.tm.integration.cdi.entities.TestEntity1; +import bitronix.tm.mock.events.EventRecorder; +import bitronix.tm.mock.events.XAResourceCommitEvent; +import bitronix.tm.mock.events.XAResourceEndEvent; +import bitronix.tm.mock.events.XAResourcePrepareEvent; +import bitronix.tm.mock.events.XAResourceStartEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Resource; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.sql.DataSource; +import javax.transaction.TransactionManager; +import javax.transaction.xa.XAResource; +import java.util.Iterator; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; + +@Stateless +public class EJBTransactionalJPABean { + + private static final Logger log = LoggerFactory.getLogger(EJBTransactionalJPABean.class); + + @Resource(name = "h2DataSource") + private DataSource dataSource; + + @Inject + TransactionManager tm; + + @Inject + EntityManager em; + + + @TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW) + public void insertTestEntityInNewTra() throws Exception { + em.persist(new TestEntity1()); + } + + @TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW) + public void insertTestEntityInNewTraAndSetRollbackOnly() throws Exception { + em.persist(new TestEntity1()); + tm.setRollbackOnly(); + } + + @TransactionAttribute(value = TransactionAttributeType.REQUIRED) + public void insertTestEntityInRequired() throws Exception { + em.persist(new TestEntity1()); + } + + public long countTestEntity() throws Exception { + Long result = em.createQuery("select count(e) from TestEntity1 e", Long.class).getSingleResult(); + return result; + } + + public void verifyEvents(int count) { + if (log.isDebugEnabled()) { + log.debug(EventRecorder.dumpToString()); + } + + Iterator it = EventRecorder.iterateEvents(); + + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMNOFLAGS, ((XAResourceStartEvent) it.next()).getFlag()); + } + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMSUCCESS, ((XAResourceEndEvent) it.next()).getFlag()); + } + if (count > 1) { + for (int i = 0; i < count; i++) { + assertEquals(XAResource.XA_OK, ((XAResourcePrepareEvent) it.next()).getReturnCode()); + } + } + for (int i = 0; i < count; i++) { + assertEquals(count == 1, ((XAResourceCommitEvent) it.next()).isOnePhase()); + } + + assertFalse(it.hasNext()); + } +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/ejbintercepted/H2EjbTransactionalTest.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/ejbintercepted/H2EjbTransactionalTest.java new file mode 100644 index 00000000..76f39cda --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/ejbintercepted/H2EjbTransactionalTest.java @@ -0,0 +1,137 @@ +package bitronix.tm.integration.cdi.ejbintercepted; + +import org.jglue.cdiunit.AdditionalClasses; +import org.jglue.cdiunit.AdditionalPackages; +import org.jglue.cdiunit.CdiRunner; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.sql.DataSource; +import javax.transaction.RollbackException; +import javax.transaction.UserTransaction; + +import bitronix.tm.integration.cdi.EjbTInterceptor; +import bitronix.tm.integration.cdi.PlatformTransactionManager; +import bitronix.tm.integration.cdi.TransactionalCdiExtension; + +/** + * @author aschoerk + */ +@RunWith(CdiRunner.class) +@AdditionalClasses({ + H2PersistenceFactory.class, + EjbTInterceptor.class, TransactionalCdiExtension.class}) +@AdditionalPackages(PlatformTransactionManager.class) +public class H2EjbTransactionalTest { + + static Logger log = LoggerFactory.getLogger("testlogger"); + + @BeforeClass + public static void loginit() { + log.info("log"); + } + + @Inject + PlatformTransactionManager platformTransactionManager; + + @Inject + UserTransaction tm; + + @Inject + DataSource dataSource; + + @Before + public void initContext() throws Exception { + } + + @Inject + EJBTransactionalJPABean jpaBean; + + @Test + public void testTraMethod() throws Exception { + + jpaBean.insertTestEntityInNewTra(); + Assert.assertEquals(1L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + tm.rollback(); + Assert.assertEquals(1L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + tm.rollback(); + Assert.assertEquals(2L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + insertTestEntityInNewTraAndRollback(); + tm.commit(); + Assert.assertEquals(4L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + insertTestEntityInNewTraAndRollback(); + tm.commit(); + Assert.assertEquals(5L, jpaBean.countTestEntity()); + tm.begin(); + insertTestEntityInNewTraAndRollback(); + jpaBean.insertTestEntityInRequired(); + tm.commit(); + Assert.assertEquals(6L, jpaBean.countTestEntity()); + tm.begin(); + insertTestEntityInNewTraAndRollback(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + tm.rollback(); + Assert.assertEquals(7L, jpaBean.countTestEntity()); + + tm.begin(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + insertTestEntityInNewTraAndSetRollbackOnly(); + tm.commit(); + Assert.assertEquals(9L, jpaBean.countTestEntity()); + tm.begin(); + jpaBean.insertTestEntityInRequired(); + insertTestEntityInNewTraAndSetRollbackOnly(); + tm.commit(); + Assert.assertEquals(10L, jpaBean.countTestEntity()); + tm.begin(); + insertTestEntityInNewTraAndSetRollbackOnly(); + jpaBean.insertTestEntityInRequired(); + tm.commit(); + Assert.assertEquals(11L, jpaBean.countTestEntity()); + tm.begin(); + insertTestEntityInNewTraAndSetRollbackOnly(); + jpaBean.insertTestEntityInRequired(); + jpaBean.insertTestEntityInNewTra(); + tm.rollback(); + Assert.assertEquals(12L, jpaBean.countTestEntity()); + + } + + private void insertTestEntityInNewTraAndSetRollbackOnly() throws Exception { + try { + jpaBean.insertTestEntityInNewTraAndSetRollbackOnly(); + Assert.fail("Expected Rollbackexception during commit of new tra"); + } catch (RollbackException ex){ + + } + } + + private void insertTestEntityInNewTraAndRollback() throws Exception { + try { + jpaBean.insertTestEntityInNewTraAndSetRollbackOnly(); + Assert.fail("expected rollbackexception"); + } + catch (RollbackException rbe) { + + } + } + +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/ejbintercepted/H2PersistenceFactory.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/ejbintercepted/H2PersistenceFactory.java new file mode 100644 index 00000000..2dc8e4c9 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/ejbintercepted/H2PersistenceFactory.java @@ -0,0 +1,64 @@ +package bitronix.tm.integration.cdi.ejbintercepted; + +import bitronix.tm.integration.cdi.SqlPersistenceFactory; +import bitronix.tm.resource.jdbc.PoolingDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.sql.DataSource; +import javax.transaction.TransactionManager; +import java.util.Properties; + +/** + * @author aschoerk + */ +@ApplicationScoped +public class H2PersistenceFactory extends SqlPersistenceFactory { + + Logger log = LoggerFactory.getLogger("H2PersistenceFactory"); + + public H2PersistenceFactory() { + } + + + @Override + public String getPersistenceUnitName() { + return "btm-cdi-test-h2-pu"; + } + + @Produces + public EntityManager newEm() { + return produceEntityManager(); + } + + + @Produces + @ApplicationScoped + protected DataSource createDataSource() { + if (ds != null) + return ds; + log.info("creating datasource"); + PoolingDataSource res = new PoolingDataSource(); + res.setClassName("org.h2.jdbcx.JdbcDataSource"); + Properties driverProperties = res.getDriverProperties(); + driverProperties.setProperty("URL", "jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=0"); + driverProperties.setProperty("user","sa"); + driverProperties.setProperty("password",""); + res.setUniqueName("jdbc/btm-cdi-test-h2"); + res.setMinPoolSize(1); + res.setMaxPoolSize(10); + res.setAllowLocalTransactions(true); // to allow autocommitmode + res.init(); + log.info("created datasource"); + return res; + } + + + +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/entities/TestEntity1.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/entities/TestEntity1.java new file mode 100644 index 00000000..d2b4a0c2 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/entities/TestEntity1.java @@ -0,0 +1,50 @@ +package bitronix.tm.integration.cdi.entities; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * @author aschoerk + */ +@Entity +@Table(name = "test_entity_1") +public class TestEntity1 { + @Id + @GeneratedValue(strategy = GenerationType.TABLE) + private Long id; + + private String stringAttribute; + + private int intAttribute; + + public TestEntity1() { + + } + + public Long getId() { + return id; + } + + public void setId(long idP) { + this.id = idP; + } + + public String getStringAttribute() { + return stringAttribute; + } + + public void setStringAttribute(String stringAttributeP) { + this.stringAttribute = stringAttributeP; + } + + public int getIntAttribute() { + return intAttribute; + } + + public void setIntAttribute(int intAttributeP) { + this.intAttribute = intAttributeP; + } +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/DataSource1.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/DataSource1.java new file mode 100644 index 00000000..d623ef0f --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/DataSource1.java @@ -0,0 +1,22 @@ +package bitronix.tm.integration.cdi.nonintercepted; + +import javax.enterprise.inject.Alternative; + +import bitronix.tm.integration.cdi.PoolingDataSourceFactoryBean; + +/** + * @author aschoerk + */ +@Alternative +public class DataSource1 extends PoolingDataSourceFactoryBean { + + private static final long serialVersionUID = 6581338365140914540L; + + public DataSource1() { + super(); + setClassName("bitronix.tm.mock.resource.jdbc.MockitoXADataSource"); + setUniqueName("btm-cdi-test-ds1"); + setMinPoolSize(1); + setMaxPoolSize(3); + } +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/DataSource2.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/DataSource2.java new file mode 100644 index 00000000..2bb2f17a --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/DataSource2.java @@ -0,0 +1,28 @@ +package bitronix.tm.integration.cdi.nonintercepted; + +import javax.enterprise.inject.Alternative; +import java.util.Properties; + +import bitronix.tm.integration.cdi.PoolingDataSourceFactoryBean; + +/** + * @author aschoerk + */ +@Alternative +public class DataSource2 extends PoolingDataSourceFactoryBean { + + private static final long serialVersionUID = 6581338365140914540L; + + public DataSource2() { + super(); + setClassName("bitronix.tm.mock.resource.jdbc.MockitoXADataSource"); + setUniqueName("btm-cdi-test-ds2"); + setMinPoolSize(1); + setMaxPoolSize(2); + setAutomaticEnlistingEnabled(true); + setUseTmJoin(false); + Properties driverProperties = new Properties(); + driverProperties.setProperty("loginTimeout", "5"); + setDriverProperties(driverProperties); + } +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/JPABean.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/JPABean.java new file mode 100644 index 00000000..ca0a250c --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/JPABean.java @@ -0,0 +1,155 @@ +package bitronix.tm.integration.cdi.nonintercepted; + +import bitronix.tm.integration.cdi.entities.TestEntity1; +import bitronix.tm.mock.events.EventRecorder; +import bitronix.tm.mock.events.XAResourceCommitEvent; +import bitronix.tm.mock.events.XAResourceEndEvent; +import bitronix.tm.mock.events.XAResourcePrepareEvent; +import bitronix.tm.mock.events.XAResourceStartEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.xa.XAResource; +import java.util.Iterator; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; + +class JPABean { + + private static final Logger log = LoggerFactory.getLogger(JPABean.class); + + @Inject + TransactionManager tm; + + @Inject + EntityManagerFactory entityManagerFactory; + + class CloseableEm implements AutoCloseable { + private final EntityManager em; + + public CloseableEm(EntityManager em) { + this.em = em; + } + + public EntityManager getEm() { + return em; + } + + @Override + public void close() throws Exception { + em.close(); + } + } + + CloseableEm getEm() { + EntityManager result = entityManagerFactory.createEntityManager(); + result.joinTransaction(); + return new CloseableEm(result); + } + + public void insertTestEntityInNewTra() throws Exception { + Transaction suspendedTransaction = tm.suspend(); + + tm.begin(); + try (CloseableEm em = getEm()){ + em.getEm().persist(new TestEntity1()); + } finally { + tm.commit(); + if (suspendedTransaction != null) + tm.resume(suspendedTransaction); + } + } + + public void insertTestEntityInNewTraAndRollback() throws Exception { + Transaction suspendedTransaction = tm.suspend(); + + tm.begin(); + try (CloseableEm em = getEm()){ + em.getEm().persist(new TestEntity1()); + } finally { + tm.rollback(); + if (suspendedTransaction != null) + tm.resume(suspendedTransaction); + } + } + + public void insertTestEntityInNewTraAndSetRollbackOnly() throws Exception { + Transaction suspendedTransaction = tm.suspend(); + + tm.begin(); + try (CloseableEm em = getEm()){ + em.getEm().persist(new TestEntity1()); + tm.setRollbackOnly(); + } finally { + try { + tm.commit(); + + } catch (RollbackException ex) { + + } + + if (suspendedTransaction != null) + tm.resume(suspendedTransaction); + } + } + + public void insertTestEntityInRequired() throws Exception { + boolean encloseInTra = tm.getStatus() == Status.STATUS_NO_TRANSACTION ? true : false; + if (encloseInTra) { + tm.begin(); + } + try (CloseableEm em = getEm()){ + em.getEm().persist(new TestEntity1()); + } finally { + if (encloseInTra) + tm.commit(); + } + } + + public long countTestEntity() throws Exception { + boolean encloseInTra = tm.getStatus() == Status.STATUS_NO_TRANSACTION ? true : false; + if (encloseInTra) { + tm.begin(); + } + try (CloseableEm em = getEm()) { + Long result = em.getEm().createQuery("select count(e) from TestEntity1 e", Long.class).getSingleResult(); + return result; + } finally { + if (encloseInTra) + tm.commit(); + } + } + + public void verifyEvents(int count) { + if (log.isDebugEnabled()) { + log.debug(EventRecorder.dumpToString()); + } + + Iterator it = EventRecorder.iterateEvents(); + + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMNOFLAGS, ((XAResourceStartEvent) it.next()).getFlag()); + } + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMSUCCESS, ((XAResourceEndEvent) it.next()).getFlag()); + } + if (count > 1) { + for (int i = 0; i < count; i++) { + assertEquals(XAResource.XA_OK, ((XAResourcePrepareEvent) it.next()).getReturnCode()); + } + } + for (int i = 0; i < count; i++) { + assertEquals(count == 1, ((XAResourceCommitEvent) it.next()).isOnePhase()); + } + + assertFalse(it.hasNext()); + } +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/PlatformTransactionManagerTest.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/PlatformTransactionManagerTest.java new file mode 100644 index 00000000..30589603 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/PlatformTransactionManagerTest.java @@ -0,0 +1,45 @@ +package bitronix.tm.integration.cdi.nonintercepted; + +import javax.inject.Inject; + +import org.jglue.cdiunit.ActivatedAlternatives; +import org.jglue.cdiunit.AdditionalClasses; +import org.jglue.cdiunit.CdiRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import bitronix.tm.integration.cdi.PlatformTransactionManager; +import bitronix.tm.mock.events.EventRecorder; + +@RunWith(CdiRunner.class) +@AdditionalClasses({PlatformTransactionManager.class}) +@ActivatedAlternatives({DataSource1.class}) +public class PlatformTransactionManagerTest { + + private static final Logger log = LoggerFactory.getLogger(PlatformTransactionManagerTest.class); + + @Inject + private TransactionalBean bean; + + @Before @After + public void clearEvents() { + EventRecorder.clear(); + } + + @After + public void logEvents() { + if (log.isDebugEnabled()) { + log.debug(EventRecorder.dumpToString()); + } + } + + @Test + public void testTransactionalMethod() throws Exception { + bean.doSomethingTransactional(1); + bean.verifyEvents(1); + } +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/PoolingDataSourceFactoryBeanTest.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/PoolingDataSourceFactoryBeanTest.java new file mode 100644 index 00000000..30872042 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/PoolingDataSourceFactoryBeanTest.java @@ -0,0 +1,49 @@ +package bitronix.tm.integration.cdi.nonintercepted; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; + +import java.sql.Connection; + +import javax.inject.Inject; + +import org.jglue.cdiunit.ActivatedAlternatives; +import org.jglue.cdiunit.CdiRunner; +import org.junit.Test; +import org.junit.runner.RunWith; + +import junit.framework.Assert; + +@RunWith(CdiRunner.class) +@ActivatedAlternatives({DataSource2.class}) +public class PoolingDataSourceFactoryBeanTest { + + @Inject + private DataSource2 dataSource2; + + @Test + public void validateProperties() { + Assert.assertEquals("btm-cdi-test-ds2", dataSource2.getUniqueName()); + Assert.assertEquals("bitronix.tm.mock.resource.jdbc.MockitoXADataSource", dataSource2.getClassName()); + Assert.assertEquals(1, dataSource2.getMinPoolSize()); + Assert.assertEquals(2, dataSource2.getMaxPoolSize()); + Assert.assertEquals(true, dataSource2.getAutomaticEnlistingEnabled()); + Assert.assertEquals(false, dataSource2.getUseTmJoin()); + Assert.assertEquals(60, dataSource2.getMaxIdleTime()); // default value not overridden in bean configuration + Assert.assertEquals("5", dataSource2.getDriverProperties().get("loginTimeout")); + } + + @Test + public void validateConnection() throws Exception { + Connection connection = null; + try { + connection = dataSource2.getObject().getConnection(); + assertNotNull(connection); + } + finally { + if (connection != null) { + connection.close(); + } + } + } +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/Resources.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/Resources.java new file mode 100644 index 00000000..834a6899 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/Resources.java @@ -0,0 +1,88 @@ +package bitronix.tm.integration.cdi.nonintercepted; + +import bitronix.tm.resource.jdbc.PoolingDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.sql.DataSource; +import javax.transaction.TransactionManager; +import java.util.Properties; + +/** + * @author aschoerk + */ +@ApplicationScoped +class Resources { + + Logger log = LoggerFactory.getLogger("ResourcesLogger"); + + public Resources() { + } + + @PreDestroy + public void preDestroyResources() { + if (ds != null) { + ds.close(); + ds = null; + } + } + + @Inject + TransactionManager tm; + + private PoolingDataSource ds; + + private EntityManagerFactory emf = null; + + protected void createEntityManagerFactory() { + if (emf == null) { + if (ds == null) + createDataSource(); + emf = Persistence.createEntityManagerFactory("btm-cdi-test-h2-pu"); + } + } + + DataSource getDs() { + if (ds == null) + createDataSource(); + return ds; + } + + @Produces + EntityManagerFactory produceEntityManagerFactory() { + createEntityManagerFactory(); + return emf; + } + + @Produces + @ApplicationScoped + DataSource createDataSource() { + if (ds != null) + return ds; + log.info("creating datasource"); + PoolingDataSource res = new PoolingDataSource(); + res.setClassName("org.h2.jdbcx.JdbcDataSource"); + Properties driverProperties = res.getDriverProperties(); + driverProperties.setProperty("URL", "jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=0"); + driverProperties.setProperty("user","sa"); + driverProperties.setProperty("password",""); + res.setUniqueName("jdbc/btm-cdi-test-h2"); + res.setMinPoolSize(1); + res.setMaxPoolSize(3); + res.setAllowLocalTransactions(true); + res.init(); + log.info("created datasource"); + ds = res; + return res; + } + + + +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/TransactionalBean.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/TransactionalBean.java new file mode 100644 index 00000000..f1105a93 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/TransactionalBean.java @@ -0,0 +1,77 @@ +package bitronix.tm.integration.cdi.nonintercepted; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; + +import java.sql.Connection; +import java.util.Iterator; + +import javax.inject.Inject; +import javax.transaction.TransactionManager; +import javax.transaction.xa.XAResource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import bitronix.tm.integration.cdi.PoolingDataSourceFactoryBean; +import bitronix.tm.mock.events.EventRecorder; +import bitronix.tm.mock.events.XAResourceCommitEvent; +import bitronix.tm.mock.events.XAResourceEndEvent; +import bitronix.tm.mock.events.XAResourcePrepareEvent; +import bitronix.tm.mock.events.XAResourceStartEvent; + +public class TransactionalBean { + + private static final Logger log = LoggerFactory.getLogger(TransactionalBean.class); + + @Inject + private PoolingDataSourceFactoryBean dataSource; + + @Inject + TransactionManager transactionManager; + + public void doSomethingTransactional(int count) throws Exception { + transactionManager.begin(); + log.info("From transactional method, claiming {} connection(s)", count); + + Connection[] connections = new Connection[count]; + try { + for (int i = 0; i < count; i++) { + connections[i] = dataSource.getObject().getConnection(); + connections[i].createStatement(); + } + } finally { + for (int i = 0; i < count; i++) { + if (connections[i] != null) { + connections[i].close(); + } + } + } + transactionManager.commit(); + } + + public void verifyEvents(int count) { + if (log.isDebugEnabled()) { + log.debug(EventRecorder.dumpToString()); + } + + Iterator it = EventRecorder.iterateEvents(); + + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMNOFLAGS, ((XAResourceStartEvent) it.next()).getFlag()); + } + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMSUCCESS, ((XAResourceEndEvent) it.next()).getFlag()); + } + if (count > 1) { + for (int i = 0; i < count; i++) { + assertEquals(XAResource.XA_OK, ((XAResourcePrepareEvent) it.next()).getReturnCode()); + } + } + for (int i = 0; i < count; i++) { + assertEquals(count == 1, ((XAResourceCommitEvent) it.next()).isOnePhase()); + } + + assertFalse(it.hasNext()); + } +} diff --git a/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/TransactionalTest.java b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/TransactionalTest.java new file mode 100644 index 00000000..f02ecac7 --- /dev/null +++ b/btm-cdi/src/test/java/bitronix/tm/integration/cdi/nonintercepted/TransactionalTest.java @@ -0,0 +1,132 @@ +package bitronix.tm.integration.cdi.nonintercepted; + +import org.jglue.cdiunit.AdditionalClasses; +import org.jglue.cdiunit.CdiRunner; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.transaction.TransactionManager; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +import bitronix.tm.integration.cdi.PlatformTransactionManager; +import bitronix.tm.integration.cdi.TransactionalCdiExtension; + +/** + * @author aschoerk + */ +@RunWith(CdiRunner.class) +@AdditionalClasses({PlatformTransactionManager.class, Resources.class, TransactionalCdiExtension.class}) +public class TransactionalTest { + + static Logger log = LoggerFactory.getLogger("testlogger"); + + @BeforeClass + public static void loginit() { + log.info("log"); + } + + + @Inject + TransactionManager tm; + + @Inject + Resources resources; + + @Before + public void initContext() throws Exception { + } + + @Test + public void test() throws Exception { + tm.begin(); + + try (Connection connection = resources.getDs().getConnection()) { + try (Statement stmt = connection.createStatement()) { + stmt.execute("create table t (a varchar(200), b integer)"); + stmt.execute("insert into t (a, b) values ('a', 1 )"); + stmt.execute("insert into t (a, b) values ('b', 2 )"); + stmt.execute("select * from t"); + try (ResultSet result = stmt.getResultSet()) { + assert (result.next()); + assert (result.next()); + assert (!result.next()); + } + } + } + tm.commit(); + } + + @Inject + JPABean JPABean; + + @Test + public void testTraMethod() throws Exception { + + JPABean.insertTestEntityInNewTra(); + Assert.assertEquals(1L, JPABean.countTestEntity()); + tm.begin(); + JPABean.insertTestEntityInRequired(); + tm.rollback(); + Assert.assertEquals(1L, JPABean.countTestEntity()); + tm.begin(); + JPABean.insertTestEntityInRequired(); + JPABean.insertTestEntityInNewTra(); + tm.rollback(); + Assert.assertEquals(2L, JPABean.countTestEntity()); + tm.begin(); + JPABean.insertTestEntityInRequired(); + JPABean.insertTestEntityInNewTra(); + JPABean.insertTestEntityInNewTraAndRollback(); + tm.commit(); + Assert.assertEquals(4L, JPABean.countTestEntity()); + tm.begin(); + JPABean.insertTestEntityInRequired(); + JPABean.insertTestEntityInNewTraAndRollback(); + tm.commit(); + Assert.assertEquals(5L, JPABean.countTestEntity()); + tm.begin(); + JPABean.insertTestEntityInNewTraAndRollback(); + JPABean.insertTestEntityInRequired(); + tm.commit(); + Assert.assertEquals(6L, JPABean.countTestEntity()); + tm.begin(); + JPABean.insertTestEntityInNewTraAndRollback(); + JPABean.insertTestEntityInRequired(); + JPABean.insertTestEntityInNewTra(); + tm.rollback(); + Assert.assertEquals(7L, JPABean.countTestEntity()); + + tm.begin(); + JPABean.insertTestEntityInRequired(); + JPABean.insertTestEntityInNewTra(); + JPABean.insertTestEntityInNewTraAndSetRollbackOnly(); + tm.commit(); + Assert.assertEquals(9L, JPABean.countTestEntity()); + tm.begin(); + JPABean.insertTestEntityInRequired(); + JPABean.insertTestEntityInNewTraAndSetRollbackOnly(); + tm.commit(); + Assert.assertEquals(10L, JPABean.countTestEntity()); + tm.begin(); + JPABean.insertTestEntityInNewTraAndSetRollbackOnly(); + JPABean.insertTestEntityInRequired(); + tm.commit(); + Assert.assertEquals(11L, JPABean.countTestEntity()); + tm.begin(); + JPABean.insertTestEntityInNewTraAndSetRollbackOnly(); + JPABean.insertTestEntityInRequired(); + JPABean.insertTestEntityInNewTra(); + tm.rollback(); + Assert.assertEquals(12L, JPABean.countTestEntity()); + + } + +} diff --git a/btm-cdi/src/test/resources/META-INF/beans.xml b/btm-cdi/src/test/resources/META-INF/beans.xml new file mode 100644 index 00000000..b7fdba87 --- /dev/null +++ b/btm-cdi/src/test/resources/META-INF/beans.xml @@ -0,0 +1,4 @@ + + + diff --git a/btm-cdi/src/test/resources/META-INF/persistence.xml b/btm-cdi/src/test/resources/META-INF/persistence.xml new file mode 100644 index 00000000..793d1d1a --- /dev/null +++ b/btm-cdi/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,21 @@ + + + + jdbc/btm-cdi-test-h2 + + + + + + + + + + + + + + diff --git a/btm-cdi/src/test/resources/bitronix-default-config.properties b/btm-cdi/src/test/resources/bitronix-default-config.properties new file mode 100644 index 00000000..9ad95e92 --- /dev/null +++ b/btm-cdi/src/test/resources/bitronix-default-config.properties @@ -0,0 +1,6 @@ +bitronix.tm.serverId=btm-cdi-test +# for testing don't use logs +bitronix.tm.journal=null +bitronix.tm.timer.gracefulShutdownInterval=0 +# bitronix.tm.journal.disk.logPart1Filename=target/btm1.tlog +# bitronix.tm.journal.disk.logPart2Filename=target/btm2.tlog diff --git a/btm-cdi/src/test/resources/jndi.properties b/btm-cdi/src/test/resources/jndi.properties new file mode 100644 index 00000000..5d3c598d --- /dev/null +++ b/btm-cdi/src/test/resources/jndi.properties @@ -0,0 +1 @@ +java.naming.factory.initial=bitronix.tm.jndi.BitronixInitialContextFactory \ No newline at end of file diff --git a/btm-cdi/src/test/resources/log4j.xml b/btm-cdi/src/test/resources/log4j.xml new file mode 100644 index 00000000..77d51869 --- /dev/null +++ b/btm-cdi/src/test/resources/log4j.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/btm-cdi/src/test/resources/test-context.xml b/btm-cdi/src/test/resources/test-context.xml new file mode 100644 index 00000000..7f939027 --- /dev/null +++ b/btm-cdi/src/test/resources/test-context.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + 5 + + + + + + + + + + diff --git a/btm-spring/pom.xml b/btm-spring/pom.xml index f73adf74..09f3237e 100644 --- a/btm-spring/pom.xml +++ b/btm-spring/pom.xml @@ -9,6 +9,12 @@ btm-spring Bitronix Transaction Manager :: Spring Integration + + 1.0.1.Final + 4.2.7.Final + 4.3.1.Final + 1.7.21 + org.codehaus.btm @@ -62,21 +68,53 @@ spring-context test + + org.springframework + spring-orm + test + org.springframework spring-test test + org.slf4j - jcl-over-slf4j + slf4j-api + + + org.slf4j + slf4j-log4j12 test + ${slf4j.version} ch.qos.logback logback-classic test + + com.h2database + h2 + test + 1.4.196 + + + org.hibernate + hibernate-entitymanager + ${version.org.hibernate} + + + org.hibernate + hibernate-validator + ${version.org.hibernate.validator} + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + ${version.org.hibernate.api} + diff --git a/btm-spring/src/test/java/bitronix/tm/integration/spring/JPATest.java b/btm-spring/src/test/java/bitronix/tm/integration/spring/JPATest.java new file mode 100644 index 00000000..7395677d --- /dev/null +++ b/btm-spring/src/test/java/bitronix/tm/integration/spring/JPATest.java @@ -0,0 +1,32 @@ +package bitronix.tm.integration.spring; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.sql.DataSource; + +/** + * @author aschoerk + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:test-context.xml") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class JPATest { + + @Resource(name = "h2DataSource") + private DataSource dataSource; + + @Inject + TransactionalJPABean transactionalJPABean; + + @Test + public void test() { + transactionalJPABean.insertTestEntityInNewTra(); + transactionalJPABean.insertTestEntityInRequired(); + } +} diff --git a/btm-spring/src/test/java/bitronix/tm/integration/spring/LifecycleTest.java b/btm-spring/src/test/java/bitronix/tm/integration/spring/LifecycleTest.java index 31577046..7d289e93 100644 --- a/btm-spring/src/test/java/bitronix/tm/integration/spring/LifecycleTest.java +++ b/btm-spring/src/test/java/bitronix/tm/integration/spring/LifecycleTest.java @@ -46,7 +46,7 @@ public void testLifecycle() throws SQLException { // make sure that a refresh closes all connections int totalPoolSize = getTotalPoolSize(applicationContext); assertEquals(3, totalPoolSize); - + applicationContext.refresh(); verifyConnectionsClosed(totalPoolSize); @@ -66,7 +66,8 @@ public void testLifecycle() throws SQLException { private int getTotalPoolSize(ApplicationContext applicationContext) { int totalPoolSize = 0; for (PoolingDataSource ds : applicationContext.getBeansOfType(PoolingDataSource.class).values()) { - totalPoolSize += ds.getTotalPoolSize(); + if (!ds.getAllowLocalTransactions()) + totalPoolSize += ds.getTotalPoolSize(); } return totalPoolSize; } diff --git a/btm-spring/src/test/java/bitronix/tm/integration/spring/TestEntity1.java b/btm-spring/src/test/java/bitronix/tm/integration/spring/TestEntity1.java new file mode 100644 index 00000000..9d04800d --- /dev/null +++ b/btm-spring/src/test/java/bitronix/tm/integration/spring/TestEntity1.java @@ -0,0 +1,50 @@ +package bitronix.tm.integration.spring; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * @author aschoerk + */ +@Entity +@Table(name = "test_entity_1") +public class TestEntity1 { + @Id + @GeneratedValue(strategy = GenerationType.TABLE) + private Long id; + + private String stringAttribute; + + private int intAttribute; + + public TestEntity1() { + + } + + public Long getId() { + return id; + } + + public void setId(long idP) { + this.id = idP; + } + + public String getStringAttribute() { + return stringAttribute; + } + + public void setStringAttribute(String stringAttributeP) { + this.stringAttribute = stringAttributeP; + } + + public int getIntAttribute() { + return intAttribute; + } + + public void setIntAttribute(int intAttributeP) { + this.intAttribute = intAttributeP; + } +} diff --git a/btm-spring/src/test/java/bitronix/tm/integration/spring/TransactionalBean.java b/btm-spring/src/test/java/bitronix/tm/integration/spring/TransactionalBean.java index 232b619b..bb6c845f 100644 --- a/btm-spring/src/test/java/bitronix/tm/integration/spring/TransactionalBean.java +++ b/btm-spring/src/test/java/bitronix/tm/integration/spring/TransactionalBean.java @@ -7,6 +7,7 @@ import java.sql.SQLException; import java.util.Iterator; +import javax.annotation.Resource; import javax.inject.Inject; import javax.sql.DataSource; import javax.transaction.xa.XAResource; @@ -25,7 +26,7 @@ public class TransactionalBean { private static final Logger log = LoggerFactory.getLogger(TransactionalBean.class); - @Inject + @Resource(name = "dataSource1") private DataSource dataSource; @Transactional diff --git a/btm-spring/src/test/java/bitronix/tm/integration/spring/TransactionalJPABean.java b/btm-spring/src/test/java/bitronix/tm/integration/spring/TransactionalJPABean.java new file mode 100644 index 00000000..75c4bbe3 --- /dev/null +++ b/btm-spring/src/test/java/bitronix/tm/integration/spring/TransactionalJPABean.java @@ -0,0 +1,98 @@ +package bitronix.tm.integration.spring; + +import bitronix.tm.mock.events.EventRecorder; +import bitronix.tm.mock.events.XAResourceCommitEvent; +import bitronix.tm.mock.events.XAResourceEndEvent; +import bitronix.tm.mock.events.XAResourcePrepareEvent; +import bitronix.tm.mock.events.XAResourceStartEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.sql.DataSource; +import javax.transaction.xa.XAResource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Iterator; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; + +public class TransactionalJPABean { + + private static final Logger log = LoggerFactory.getLogger(TransactionalJPABean.class); + + @Resource(name = "h2DataSource") + private DataSource dataSource; + + + @PersistenceContext(name = "btm-cdi-test-h2-pu") + private EntityManager em; + + @Inject + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean; + + EntityManager getEm() { + return em; + } + + @Transactional + public void doSomethingTransactional(int count) throws SQLException { + log.info("From transactional method, claiming {} connection(s)", count); + + Connection[] connections = new Connection[count]; + try { + for (int i = 0; i < count; i++) { + connections[i] = dataSource.getConnection(); + connections[i].createStatement(); + } + } finally { + for (int i = 0; i < count; i++) { + if (connections[i] != null) { + connections[i].close(); + } + } + } + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void insertTestEntityInNewTra() { + getEm().persist(new TestEntity1()); + } + + @Transactional(propagation = Propagation.REQUIRED) + public void insertTestEntityInRequired() { + getEm().persist(new TestEntity1()); + } + + public void verifyEvents(int count) { + if (log.isDebugEnabled()) { + log.debug(EventRecorder.dumpToString()); + } + + Iterator it = EventRecorder.iterateEvents(); + + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMNOFLAGS, ((XAResourceStartEvent) it.next()).getFlag()); + } + for (int i = 0; i < count; i++) { + assertEquals(XAResource.TMSUCCESS, ((XAResourceEndEvent) it.next()).getFlag()); + } + if (count > 1) { + for (int i = 0; i < count; i++) { + assertEquals(XAResource.XA_OK, ((XAResourcePrepareEvent) it.next()).getReturnCode()); + } + } + for (int i = 0; i < count; i++) { + assertEquals(count == 1, ((XAResourceCommitEvent) it.next()).isOnePhase()); + } + + assertFalse(it.hasNext()); + } +} diff --git a/btm-spring/src/test/resources/META-INF/persistence.xml b/btm-spring/src/test/resources/META-INF/persistence.xml new file mode 100644 index 00000000..811dbe2d --- /dev/null +++ b/btm-spring/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,22 @@ + + + + jdbc/btm-spring-test-h2 + bitronix.tm.integration.spring.TestEntity1 + + + + + + + + + + + + + + diff --git a/btm-spring/src/test/resources/create-schema.sql b/btm-spring/src/test/resources/create-schema.sql new file mode 100644 index 00000000..36a6b02e --- /dev/null +++ b/btm-spring/src/test/resources/create-schema.sql @@ -0,0 +1,5 @@ +create table a (a varchar 200); +create table b (a varchar 200); + +create table hibernate_sequences (sequence_name varchar(200), sequence_next_hi_value bigint); +insert into hibernate_sequences (sequence_name, sequence_next_hi_value) values ('test_entity_1', 1); diff --git a/btm-spring/src/test/resources/import.sql b/btm-spring/src/test/resources/import.sql new file mode 100644 index 00000000..36a6b02e --- /dev/null +++ b/btm-spring/src/test/resources/import.sql @@ -0,0 +1,5 @@ +create table a (a varchar 200); +create table b (a varchar 200); + +create table hibernate_sequences (sequence_name varchar(200), sequence_next_hi_value bigint); +insert into hibernate_sequences (sequence_name, sequence_next_hi_value) values ('test_entity_1', 1); diff --git a/btm-spring/src/test/resources/jndi.properties b/btm-spring/src/test/resources/jndi.properties new file mode 100644 index 00000000..7896d013 --- /dev/null +++ b/btm-spring/src/test/resources/jndi.properties @@ -0,0 +1,2 @@ +# java.naming.factory.initial=bitronix.tm.jndi.BitronixInitialContextFactory +java.naming.factory.initial=org.jglue.cdiunit.internal.naming.CdiUnitContextFactory diff --git a/btm-spring/src/test/resources/test-context.xml b/btm-spring/src/test/resources/test-context.xml index fde7fe45..6ca37841 100644 --- a/btm-spring/src/test/resources/test-context.xml +++ b/btm-spring/src/test/resources/test-context.xml @@ -31,9 +31,46 @@ + + + + + + + + + + + jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=0 + sa + + + + + + + + + + + + + create-drop + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 56106589..91478a06 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,7 @@ btm-jetty9-lifecycle btm-tomcat55-lifecycle btm-spring + btm-cdi btm-docs @@ -180,6 +181,11 @@ spring-tx ${spring.version} + + org.springframework + spring-orm + ${spring.version} + org.springframework spring-test