diff --git a/envers/pom.xml b/envers/pom.xml index a5360e9373..9ea5adf4a5 100644 --- a/envers/pom.xml +++ b/envers/pom.xml @@ -118,6 +118,12 @@ javassist test + + + org.hibernate + hibernate-testing + test + @@ -137,6 +143,11 @@ hibernate-entitymanager ${version} + + org.hibernate + hibernate-testing + ${version} + org.hibernate hibernate-tools diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/AuditProcess.java b/envers/src/main/java/org/hibernate/envers/synchronization/AuditProcess.java index 6f71e87e97..2462833148 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/AuditProcess.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/AuditProcess.java @@ -37,11 +37,14 @@ import org.hibernate.envers.tools.Pair; import org.hibernate.FlushMode; import org.hibernate.Session; +import javax.transaction.Synchronization; + /** * @author Adam Warski (adam at warski dot org) */ -public class AuditProcess implements BeforeTransactionCompletionProcess { +public class AuditProcess implements BeforeTransactionCompletionProcess, Synchronization { private final RevisionInfoGenerator revisionInfoGenerator; + private final SessionImplementor session; private final LinkedList workUnits; private final Queue undoQueue; @@ -49,8 +52,9 @@ public class AuditProcess implements BeforeTransactionCompletionProcess { private Object revisionData; - public AuditProcess(RevisionInfoGenerator revisionInfoGenerator) { + public AuditProcess(RevisionInfoGenerator revisionInfoGenerator, SessionImplementor session) { this.revisionInfoGenerator = revisionInfoGenerator; + this.session = session; workUnits = new LinkedList(); undoQueue = new LinkedList(); @@ -153,4 +157,12 @@ public class AuditProcess implements BeforeTransactionCompletionProcess { session.flush(); } } + + // Synchronization methods + + public void beforeCompletion() { + doBeforeTransactionCompletion(session); + } + + public void afterCompletion(int status) { } } diff --git a/envers/src/main/java/org/hibernate/envers/synchronization/AuditProcessManager.java b/envers/src/main/java/org/hibernate/envers/synchronization/AuditProcessManager.java index 5d2875a846..d756b99b37 100644 --- a/envers/src/main/java/org/hibernate/envers/synchronization/AuditProcessManager.java +++ b/envers/src/main/java/org/hibernate/envers/synchronization/AuditProcessManager.java @@ -52,10 +52,22 @@ public class AuditProcessManager { AuditProcess auditProcess = auditProcesses.get(transaction); if (auditProcess == null) { // No worries about registering a transaction twice - a transaction is single thread - auditProcess = new AuditProcess(revisionInfoGenerator); + auditProcess = new AuditProcess(revisionInfoGenerator, session); auditProcesses.put(transaction, auditProcess); + /* + * HHH-5315: the process must be both a BeforeTransactionCompletionProcess and a TX Synchronization. + * + * In a resource-local tx env, the process is called after the flush, and populates the audit tables. + * Also, any exceptions that occur during that are propagated (if a Synchronization was used, the exceptions + * would be eaten). + * + * In a JTA env, the before transaction completion is called before the flush, so not all changes are yet + * written. However, Synchronization-s do propagate exceptions, so they can be safely used. + */ session.getActionQueue().registerProcess(auditProcess); + session.getTransaction().registerSynchronization(auditProcess); + session.getActionQueue().registerProcess(new AfterTransactionCompletionProcess() { public void doAfterTransactionCompletion(boolean success, SessionImplementor session) { auditProcesses.remove(transaction); diff --git a/envers/src/test/java/org/hibernate/envers/test/AbstractEntityTest.java b/envers/src/test/java/org/hibernate/envers/test/AbstractEntityTest.java index fe93c59fd4..c3431f11ed 100644 --- a/envers/src/test/java/org/hibernate/envers/test/AbstractEntityTest.java +++ b/envers/src/test/java/org/hibernate/envers/test/AbstractEntityTest.java @@ -27,9 +27,13 @@ import java.io.IOException; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; +import org.hibernate.cfg.Environment; +import org.hibernate.ejb.AvailableSettings; import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; import org.hibernate.envers.event.AuditEventListener; +import org.hibernate.testing.tm.ConnectionProviderImpl; +import org.hibernate.testing.tm.TransactionManagerLookupImpl; import org.testng.annotations.*; import org.hibernate.ejb.Ejb3Configuration; @@ -89,11 +93,15 @@ public abstract class AbstractEntityTest { initListeners(); } + cfg.configure("hibernate.test.cfg.xml"); + if (auditStrategy != null && !"".equals(auditStrategy)) { cfg.setProperty("org.hibernate.envers.audit_strategy", auditStrategy); - } + } + + // Separate database for each test class + cfg.setProperty("hibernate.connection.url", "jdbc:h2:mem:" + this.getClass().getName()); - cfg.configure("hibernate.test.cfg.xml"); configure(cfg); emf = cfg.buildEntityManagerFactory(); @@ -117,4 +125,10 @@ public abstract class AbstractEntityTest { public Ejb3Configuration getCfg() { return cfg; } + + protected void addJTAConfig(Ejb3Configuration cfg) { + cfg.setProperty("connection.provider_class", ConnectionProviderImpl.class.getName()); + cfg.setProperty(Environment.TRANSACTION_MANAGER_STRATEGY, TransactionManagerLookupImpl.class.getName()); + cfg.setProperty(AvailableSettings.TRANSACTION_TYPE, "JTA"); + } } diff --git a/envers/src/test/java/org/hibernate/envers/test/integration/jta/JtaExceptionListener.java b/envers/src/test/java/org/hibernate/envers/test/integration/jta/JtaExceptionListener.java new file mode 100644 index 0000000000..e4453c34b6 --- /dev/null +++ b/envers/src/test/java/org/hibernate/envers/test/integration/jta/JtaExceptionListener.java @@ -0,0 +1,74 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Middleware LLC. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.envers.test.integration.jta; + +import org.hibernate.ejb.Ejb3Configuration; +import org.hibernate.envers.test.AbstractEntityTest; +import org.hibernate.envers.test.entities.StrTestEntity; +import org.hibernate.envers.test.integration.reventity.ExceptionListenerRevEntity; +import org.hibernate.testing.tm.SimpleJtaTransactionManagerImpl; +import org.testng.annotations.Test; + +import javax.persistence.EntityManager; + +/** + * Same as {@link org.hibernate.envers.test.integration.reventity.ExceptionListener}, but in a JTA environment. + * @author Adam Warski (adam at warski dot org) + */ +public class JtaExceptionListener extends AbstractEntityTest { + public void configure(Ejb3Configuration cfg) { + cfg.addAnnotatedClass(StrTestEntity.class); + cfg.addAnnotatedClass(ExceptionListenerRevEntity.class); + + addJTAConfig(cfg); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testTransactionRollback() throws Exception { + SimpleJtaTransactionManagerImpl.getInstance().begin(); + + // Trying to persist an entity - however the listener should throw an exception, so the entity + // shouldn't be persisted + newEntityManager(); + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + StrTestEntity te = new StrTestEntity("x"); + em.persist(te); + + SimpleJtaTransactionManagerImpl.getInstance().commit(); + } + + @Test(dependsOnMethods = "testTransactionRollback") + public void testDataNotPersisted() throws Exception { + SimpleJtaTransactionManagerImpl.getInstance().begin(); + + // Checking if the entity became persisted + newEntityManager(); + EntityManager em = getEntityManager(); + Long count = (Long) em.createQuery("select count(s) from StrTestEntity s where s.str = 'x'").getSingleResult(); + assert count == 0l; + + SimpleJtaTransactionManagerImpl.getInstance().commit(); + } +} diff --git a/envers/src/test/java/org/hibernate/envers/test/integration/jta/JtaTransaction.java b/envers/src/test/java/org/hibernate/envers/test/integration/jta/JtaTransaction.java new file mode 100644 index 0000000000..052cd2c605 --- /dev/null +++ b/envers/src/test/java/org/hibernate/envers/test/integration/jta/JtaTransaction.java @@ -0,0 +1,63 @@ +package org.hibernate.envers.test.integration.jta; + +import org.hibernate.ejb.Ejb3Configuration; +import org.hibernate.envers.test.AbstractEntityTest; +import org.hibernate.envers.test.entities.IntTestEntity; +import org.hibernate.testing.tm.SimpleJtaTransactionManagerImpl; +import org.testng.annotations.Test; + +import javax.persistence.EntityManager; +import java.util.Arrays; + +/** + * Same as {@link org.hibernate.envers.test.integration.basic.Simple}, but in a JTA environment. + * @author Adam Warski (adam at warski dot org) + */ +public class JtaTransaction extends AbstractEntityTest { + private Integer id1; + + public void configure(Ejb3Configuration cfg) { + cfg.addAnnotatedClass(IntTestEntity.class); + + addJTAConfig(cfg); + } + + @Test + public void initData() throws Exception { + SimpleJtaTransactionManagerImpl.getInstance().begin(); + + newEntityManager(); + EntityManager em = getEntityManager(); + em.joinTransaction(); + IntTestEntity ite = new IntTestEntity(10); + em.persist(ite); + id1 = ite.getId(); + + SimpleJtaTransactionManagerImpl.getInstance().commit(); + + // + + SimpleJtaTransactionManagerImpl.getInstance().begin(); + + newEntityManager(); + em = getEntityManager(); + ite = em.find(IntTestEntity.class, id1); + ite.setNumber(20); + + SimpleJtaTransactionManagerImpl.getInstance().commit(); + } + + @Test(dependsOnMethods = "initData") + public void testRevisionsCounts() throws Exception { + assert Arrays.asList(1, 2).equals(getAuditReader().getRevisions(IntTestEntity.class, id1)); + } + + @Test(dependsOnMethods = "initData") + public void testHistoryOfId1() { + IntTestEntity ver1 = new IntTestEntity(10, id1); + IntTestEntity ver2 = new IntTestEntity(20, id1); + + assert getAuditReader().find(IntTestEntity.class, id1, 1).equals(ver1); + assert getAuditReader().find(IntTestEntity.class, id1, 2).equals(ver2); + } +} diff --git a/envers/src/test/resources/hibernate.test.cfg.xml b/envers/src/test/resources/hibernate.test.cfg.xml index 423f61c0cd..1d00e9c76d 100644 --- a/envers/src/test/resources/hibernate.test.cfg.xml +++ b/envers/src/test/resources/hibernate.test.cfg.xml @@ -12,10 +12,10 @@ true org.hibernate.dialect.H2Dialect - jdbc:h2:mem:envers org.h2.Driver sa + @@ -46,4 +46,4 @@ --> - \ No newline at end of file + diff --git a/envers/src/test/resources/testng.xml b/envers/src/test/resources/testng.xml index 97371278b7..5a94ee0574 100644 --- a/envers/src/test/resources/testng.xml +++ b/envers/src/test/resources/testng.xml @@ -38,6 +38,7 @@ +