diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/ContextSourceTransactionManager.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/ContextSourceTransactionManager.java new file mode 100644 index 00000000..6b587e52 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/ContextSourceTransactionManager.java @@ -0,0 +1,133 @@ +package org.springframework.ldap.support.transaction; + +import javax.naming.NamingException; +import javax.naming.directory.DirContext; + +import org.springframework.ldap.ContextSource; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * TransactionManager for managing LDAP transactions. Since transactions are not + * supported in the LDAP protocol, this class and its collaborators aims to + * provide compensating transactions instead. TODO: improve javadoc. + * + * @author Mattias Arthursson + */ +public class ContextSourceTransactionManager extends + AbstractPlatformTransactionManager { + + private ContextSource contextSource; + + /** + * Set the ContextSource to work on. The supplied ContextSource must be of the type abstract + * + * @param contextSource + * the ContextSource to work on. + */ + public void setContextSource(ContextSource contextSource) { + this.contextSource = contextSource; + } + + public ContextSource getContextSource() { + return contextSource; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doGetTransaction() + */ + protected Object doGetTransaction() throws TransactionException { + DirContextHolder contextHolder = (DirContextHolder) TransactionSynchronizationManager + .getResource(this.contextSource); + ContextSourceTransactionObject txObject = new ContextSourceTransactionObject( + contextHolder); + return txObject; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doBegin(java.lang.Object, + * org.springframework.transaction.TransactionDefinition) + */ + protected void doBegin(Object transaction, TransactionDefinition definition) + throws TransactionException { + ContextSourceTransactionObject txObject = (ContextSourceTransactionObject) transaction; + + if (txObject.getContextHolder() == null) { + DirContext newCtx = getContextSource().getReadOnlyContext(); + DirContextHolder contextHolder = new DirContextHolder(newCtx); + + txObject.setContextHolder(contextHolder); + TransactionSynchronizationManager.bindResource(getContextSource(), + contextHolder); + } + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus) + */ + protected void doCommit(DefaultTransactionStatus status) + throws TransactionException { + // Nothing much to do here. + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doRollback(org.springframework.transaction.support.DefaultTransactionStatus) + */ + protected void doRollback(DefaultTransactionStatus status) + throws TransactionException { + // Perform compensating transaction cleanup using information stored in + // ContextHolder. + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doCleanupAfterCompletion(java.lang.Object) + */ + protected void doCleanupAfterCompletion(Object transaction) { + ContextSourceTransactionObject txObject = (ContextSourceTransactionObject) transaction; + TransactionSynchronizationManager.unbindResource(contextSource); + DirContext ctx = txObject.getContextHolder().getCtx(); + + try { + ctx.close(); + } catch (NamingException e) { + e.printStackTrace(); + } + + txObject.getContextHolder().clear(); + } + + /** + * Transaction object for ContextSourceTransactionManager. + * + * @author Mattias Arthursson + */ + static class ContextSourceTransactionObject { + private DirContextHolder contextHolder; + + public ContextSourceTransactionObject(DirContextHolder contextHolder) { + this.contextHolder = contextHolder; + } + + public DirContextHolder getContextHolder() { + return contextHolder; + } + + public void setContextHolder(DirContextHolder contextHolder) { + this.contextHolder = contextHolder; + } + } + +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/DirContextHolder.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/DirContextHolder.java new file mode 100644 index 00000000..1f6ea1ed --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/DirContextHolder.java @@ -0,0 +1,28 @@ +package org.springframework.ldap.support.transaction; + +import javax.naming.directory.DirContext; + +import org.springframework.transaction.support.ResourceHolderSupport; + +public class DirContextHolder extends ResourceHolderSupport { + private DirContext ctx; + + private CompensatingTransactionDataManager transactionDataManager; + + public void setCtx(DirContext ctx) { + this.ctx = ctx; + } + + public DirContextHolder(DirContext ctx) { + this.ctx = ctx; + } + + public DirContext getCtx() { + return ctx; + } + + public void clear() { + super.clear(); + + } +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/DirContextProxy.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/DirContextProxy.java new file mode 100644 index 00000000..15c632a8 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/DirContextProxy.java @@ -0,0 +1,7 @@ +package org.springframework.ldap.support.transaction; + +import javax.naming.directory.DirContext; + +public interface DirContextProxy { + DirContext getTargetContext(); +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/LdapCompensatingTransactionDataManager.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/LdapCompensatingTransactionDataManager.java new file mode 100644 index 00000000..9bd9ada1 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/LdapCompensatingTransactionDataManager.java @@ -0,0 +1,89 @@ +package org.springframework.ldap.support.transaction; + +import java.util.Stack; + +import javax.naming.directory.DirContext; + +import org.springframework.dao.DataAccessException; +import org.springframework.ldap.ContextSource; +import org.springframework.ldap.LdapOperations; +import org.springframework.ldap.LdapTemplate; + +public class LdapCompensatingTransactionDataManager implements + CompensatingTransactionDataManager { + + private Stack rollbackOperations = new Stack(); + + private LdapOperations ldapOperations; + + public LdapCompensatingTransactionDataManager(DirContext ctx) { + this.ldapOperations = new LdapTemplate(new SingleContextSource(ctx)); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.support.CompensatingTransactionDataManager#operationPerformed(java.lang.String, + * java.lang.Object[]) + */ + public void operationPerformed(String operation, Object[] params) { + CompensatingTransactionRecordingOperation recordingOperation = getRecordingOperation(operation); + rollbackOperations.push(recordingOperation.performOperation(params)); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.support.CompensatingTransactionDataManager#rollback() + */ + public void rollback() { + while (!rollbackOperations.isEmpty()) { + CompensatingTransactionRollbackOperation rollbackOperation = (CompensatingTransactionRollbackOperation) rollbackOperations + .pop(); + rollbackOperation.rollback(); + } + } + + /** + * Get the rollback operations. Package protected for testing purposes. + * + * @return the rollback operations. + */ + Stack getRollbackOperations() { + return rollbackOperations; + } + + /** + * Set the rollback operations. Package protected - for testing purposes + * only. + * + * @param rollbackOperations + * the rollback operations. + */ + void setRollbackOperations(Stack rollbackOperations) { + this.rollbackOperations = rollbackOperations; + } + + protected CompensatingTransactionRecordingOperation getRecordingOperation( + String operation) { + return null; + } + + static class SingleContextSource implements ContextSource { + private DirContext ctx; + + public SingleContextSource(DirContext ctx) { + this.ctx = ctx; + } + + public DirContext getReadOnlyContext() throws DataAccessException { + return ctx; + } + + public DirContext getReadWriteContext() throws DataAccessException { + return ctx; + } + + } + +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/LdapUtils.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/LdapUtils.java new file mode 100644 index 00000000..644510b8 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/LdapUtils.java @@ -0,0 +1,41 @@ +package org.springframework.ldap.support.transaction; + +import javax.naming.Name; + +import org.springframework.ldap.support.DistinguishedName; +import org.springframework.util.Assert; + +/** + * Common helper methods for Ldap operations. + * + * @author Mattias Arthursson + */ +public class LdapUtils { + /** + * Not to be instantiated. + */ + private LdapUtils() { + } + + /** + * Get the first parameter in the argument list as a Name. + * + * @param args + * arguments supplied to a ldap operation. + * @return a Name representation of the first argument, or the Name itself + * if it is a name. + */ + public static Name getFirstArgumentAsName(Object[] args) { + Assert.notEmpty(args); + + if (args[0] instanceof String) { + return new DistinguishedName((String) args[0]); + } else if (args[0] instanceof Name) { + return (Name) args[0]; + } else { + throw new IllegalArgumentException( + "First argument needs to be a Name or a String representation thereof"); + } + } + +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/NullRollbackOperation.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/NullRollbackOperation.java new file mode 100644 index 00000000..f31bd862 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/NullRollbackOperation.java @@ -0,0 +1,15 @@ +package org.springframework.ldap.support.transaction; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class NullRollbackOperation implements + CompensatingTransactionRollbackOperation { + + private static Log log = LogFactory.getLog(NullRollbackOperation.class); + + public void rollback() { + log.info("Rolling back null operation"); + } + +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/RebindRecordingOperation.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/RebindRecordingOperation.java new file mode 100644 index 00000000..7d2cfa1d --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/RebindRecordingOperation.java @@ -0,0 +1,36 @@ +package org.springframework.ldap.support.transaction; + +import javax.naming.Name; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.LdapOperations; +import org.springframework.ldap.support.DirContextAdapter; + +public class RebindRecordingOperation implements + CompensatingTransactionRecordingOperation { + + private static Log log = LogFactory.getLog(RebindRecordingOperation.class); + + private LdapOperations ldapOperations; + + public RebindRecordingOperation(LdapOperations ldapOperations) { + this.ldapOperations = ldapOperations; + } + + public CompensatingTransactionRollbackOperation performOperation( + Object[] args) { + Name dn = LdapUtils.getFirstArgumentAsName(args); + + try { + DirContextAdapter ctx = (DirContextAdapter) ldapOperations + .lookup(dn); + return new RebindRollbackOperation(ldapOperations, ctx); + } catch (Exception e) { + log.warn( + "Failed to create rollback operation, dn " + dn.toString(), + e); + return new NullRollbackOperation(); + } + } +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/RebindRollbackOperation.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/RebindRollbackOperation.java new file mode 100644 index 00000000..987946fd --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/RebindRollbackOperation.java @@ -0,0 +1,53 @@ +package org.springframework.ldap.support.transaction; + +import javax.naming.Name; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.LdapOperations; +import org.springframework.ldap.support.DirContextOperations; + +public class RebindRollbackOperation implements + CompensatingTransactionRollbackOperation { + + private static Log log = LogFactory.getLog(RebindRollbackOperation.class); + + private LdapOperations ldapOperations; + + private DirContextOperations dirContextOperations; + + public RebindRollbackOperation(LdapOperations ldapOperations, + DirContextOperations dirContextOperations) { + this.ldapOperations = ldapOperations; + this.dirContextOperations = dirContextOperations; + } + + /** + * Get the targegt DirContextOperations. Package private for testing + * purposes. + * + * @return the DirContextOperations. + */ + DirContextOperations getDirContextOperations() { + return dirContextOperations; + } + + /** + * Get the LdapOperations. Package private for testing purposes. + * + * @return the LdapOperations. + */ + LdapOperations getLdapOperations() { + return ldapOperations; + } + + public void rollback() { + Name dn = dirContextOperations.getDn(); + try { + ldapOperations.rebind(dn, dirContextOperations, null); + } catch (Exception e) { + log.warn("Failed to rollback operation, dn: " + dn, e); + } + } + +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/RebindRollbackOperationTest.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/RebindRollbackOperationTest.java new file mode 100644 index 00000000..0458b177 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/RebindRollbackOperationTest.java @@ -0,0 +1,63 @@ +package org.springframework.ldap.support.transaction; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.ldap.LdapOperations; +import org.springframework.ldap.support.DirContextAdapter; +import org.springframework.ldap.support.DirContextOperations; +import org.springframework.ldap.support.DistinguishedName; + +public class RebindRollbackOperationTest extends TestCase { + + private MockControl ldapOperationsControl; + + private LdapOperations ldapOperationsMock; + + private MockControl dirContextOperationsControl; + + private DirContextOperations dirContextOperationsMock; + + protected void setUp() throws Exception { + ldapOperationsControl = MockControl.createControl(LdapOperations.class); + ldapOperationsMock = (LdapOperations) ldapOperationsControl.getMock(); + + dirContextOperationsControl = MockControl + .createControl(DirContextOperations.class); + dirContextOperationsMock = (DirContextOperations) dirContextOperationsControl + .getMock(); + } + + protected void tearDown() throws Exception { + ldapOperationsControl = null; + ldapOperationsMock = null; + + dirContextOperationsControl = null; + dirContextOperationsMock = null; + } + + protected void replay() { + ldapOperationsControl.replay(); + dirContextOperationsControl.replay(); + } + + protected void verify() { + ldapOperationsControl.verify(); + dirContextOperationsControl.verify(); + } + + public void testRollback() { + RebindRollbackOperation tested = new RebindRollbackOperation( + ldapOperationsMock, dirContextOperationsMock); + + DistinguishedName expectedName = new DistinguishedName("cn=john doe"); + dirContextOperationsControl.expectAndReturn(dirContextOperationsMock + .getDn(), expectedName); + ldapOperationsMock.rebind(expectedName, dirContextOperationsMock, null); + + replay(); + // perform test + tested.rollback(); + verify(); + } +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/TransactionAwareContextSourceProxy.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/TransactionAwareContextSourceProxy.java new file mode 100644 index 00000000..f5f521f2 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/TransactionAwareContextSourceProxy.java @@ -0,0 +1,102 @@ +package org.springframework.ldap.support.transaction; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import javax.naming.NamingException; +import javax.naming.directory.DirContext; + +import org.springframework.dao.DataAccessException; +import org.springframework.ldap.ContextSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class TransactionAwareContextSourceProxy implements ContextSource { + + private ContextSource target; + + public TransactionAwareContextSourceProxy(ContextSource target) { + this.target = target; + } + + public DirContext getReadOnlyContext() throws DataAccessException { + DirContextHolder contextHolder = (DirContextHolder) TransactionSynchronizationManager + .getResource(target); + DirContext ctx = null; + + if (contextHolder != null) { + ctx = contextHolder.getCtx(); + } + + if (ctx == null) { + ctx = target.getReadOnlyContext(); + if (contextHolder != null) { + contextHolder.setCtx(ctx); + } + } + return getTransactionAwareDirContextProxy(ctx, target); + } + + private DirContext getTransactionAwareDirContextProxy(DirContext context, + ContextSource target) { + return (DirContext) Proxy + .newProxyInstance(DirContextProxy.class.getClassLoader(), + new Class[] { DirContextProxy.class }, + new TransactionAwareDirContextInvocationHandler( + context, target)); + + } + + public DirContext getReadWriteContext() throws DataAccessException { + throw new UnsupportedOperationException("Not implemented yet"); + } + + static class TransactionAwareDirContextInvocationHandler implements + InvocationHandler { + private DirContext target; + + private ContextSource contextSource; + + public TransactionAwareDirContextInvocationHandler(DirContext target, + ContextSource contextSource) { + this.target = target; + this.contextSource = contextSource; + } + + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + + if (method.getName().equals("getTargetContext")) { + return target; + } else if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } else if (method.getName().equals("hashCode")) { + // Use hashCode of Connection proxy. + return new Integer(hashCode()); + } else if (method.getName().equals("close")) { + doCloseConnection(target, contextSource); + return null; + } + + try { + return method.invoke(target, args); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } + } + + private void doCloseConnection(DirContext context, + ContextSource contextSource) throws NamingException { + DirContextHolder transactionContextHolder = (DirContextHolder) TransactionSynchronizationManager + .getResource(contextSource); + if (transactionContextHolder == null + || transactionContextHolder.getCtx() != context) { + // This is not the transactional context or the transaction is + // no longer active - we should close it. + context.close(); + } + } + } +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/UnbindRollbackOperation.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/UnbindRollbackOperation.java new file mode 100644 index 00000000..0881338c --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/UnbindRollbackOperation.java @@ -0,0 +1,53 @@ +package org.springframework.ldap.support.transaction; + +import javax.naming.Name; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.LdapOperations; + +public class UnbindRollbackOperation implements + CompensatingTransactionRollbackOperation { + private static Log log = LogFactory.getLog(UnbindRollbackOperation.class); + + private LdapOperations ldapOperations; + + private Name dn; + + public UnbindRollbackOperation(LdapOperations ldapOperations, Name dn) { + this.ldapOperations = ldapOperations; + this.dn = dn; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.support.transaction.CompensatingTransactionRollbackOperation#rollback() + */ + public void rollback() { + try { + ldapOperations.unbind(dn); + } catch (Exception e) { + log.warn("Failed to rollback, dn:" + dn.toString(), e); + } + } + + /** + * Get the DN. Package private for testing purposes. + * + * @return the target DN. + */ + Name getDn() { + return dn; + } + + /** + * Get the LdapOperations. Package private for testing purposes. + * + * @return the LdapOperations. + */ + LdapOperations getLdapOperations() { + return ldapOperations; + } + +} diff --git a/sandbox/src/main/java/org/springframework/ldap/support/transaction/UnbindRollbackOperationTest.java b/sandbox/src/main/java/org/springframework/ldap/support/transaction/UnbindRollbackOperationTest.java new file mode 100644 index 00000000..cbe0cab5 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/ldap/support/transaction/UnbindRollbackOperationTest.java @@ -0,0 +1,44 @@ +package org.springframework.ldap.support.transaction; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.ldap.LdapOperations; +import org.springframework.ldap.support.DistinguishedName; + +public class UnbindRollbackOperationTest extends TestCase { + private MockControl ldapOperationsControl; + + private LdapOperations ldapOperationsMock; + + protected void setUp() throws Exception { + ldapOperationsControl = MockControl.createControl(LdapOperations.class); + ldapOperationsMock = (LdapOperations) ldapOperationsControl.getMock(); + } + + protected void tearDown() throws Exception { + ldapOperationsControl = null; + ldapOperationsMock = null; + } + + protected void replay() { + ldapOperationsControl.replay(); + } + + protected void verify() { + ldapOperationsControl.verify(); + } + + public void testRollback() { + DistinguishedName expectedDn = new DistinguishedName("cn=john doe"); + UnbindRollbackOperation tested = new UnbindRollbackOperation(ldapOperationsMock, expectedDn); + + ldapOperationsMock.unbind(expectedDn); + + replay(); + //perform teste + tested.rollback(); + verify(); + } + +}