Transaction recording and rollback support for bind and rebind.

This commit is contained in:
Mattias Arthursson
2006-12-26 14:38:56 +00:00
parent 617ff734f2
commit 2fbca76dac
12 changed files with 664 additions and 0 deletions

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,7 @@
package org.springframework.ldap.support.transaction;
import javax.naming.directory.DirContext;
public interface DirContextProxy {
DirContext getTargetContext();
}

View File

@@ -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;
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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");
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}