From febecf8d8f60a33439e1c75e852e55e5491a2721 Mon Sep 17 00:00:00 2001
From: Mattias Arthursson
+ * The actual operations performed by the respective methods of this class might
+ * not be what would originally be expected. E.g. one would expect that the
+ * {@link #performOperation()} method of a
+ * CompensatingTransactionOperationExecutor implementation would actually delete
+ * the entry, leaving it for the {@link #rollback()} method to recreate it using
+ * data from the original entry. In an LDAP system for instance this will not be
+ * possible, because it might not be possible to retrieve all the stored data
+ * from the original entry. In that case, the {@link #performOperation()} method
+ * will instead move the entry to a temporary location and leave it for the
+ * {@link #commit()} method to actually remove the entry.
+ *
+ * @author Mattias Arthursson
+ */
+public interface CompensatingTransactionOperationExecutor {
+ /**
+ * Rollback the operation, restoring state of the target as it was before
+ * the operation was performed using the information supplied on creation of
+ * this instance.
+ */
+ public void rollback();
+
+ /**
+ * Commit the operation. In many cases this will not require any work at all
+ * to be performed. However in some cases there will be interesting stuff to
+ * do. See class description for elaboration on this.
+ */
+ public void commit();
+
+ /**
+ * Perform the operation. This will most often require performing the
+ * recorded operation, but in some cases the actual operation performed by
+ * this method might be something else. See class description for
+ * elaboration on this.
+ */
+ public void performOperation();
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationFactory.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationFactory.java
new file mode 100644
index 00000000..2864776d
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction;
+
+/**
+ * Factory interface for creating
+ * {@link CompensatingTransactionOperationRecorder} objects based on operation
+ * method names.
+ *
+ * @author Mattias Arthursson
+ * @see DefaultCompensatingTransactionOperationManager
+ */
+public interface CompensatingTransactionOperationFactory {
+ /**
+ * Create an appropriate {@link CompensatingTransactionOperationRecorder}
+ * instance corresponding to the supplied method name.
+ *
+ * @param method
+ * the method name to create a
+ * {@link CompensatingTransactionOperationRecorder} for.
+ * @return a new {@link CompensatingTransactionOperationRecorder} instance.
+ */
+ public CompensatingTransactionOperationRecorder createRecordingOperation(
+ String method);
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationManager.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationManager.java
new file mode 100644
index 00000000..197902e3
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationManager.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction;
+
+/**
+ * A CompensatingTransactionOperationManager implementation records and performs
+ * operations that are to be performed within a compensating transaction and
+ * keeps track of compensating actions necessary for rolling back each
+ * individual operation.
+ *
+ * @author Mattias Arthursson
+ *
+ */
+public interface CompensatingTransactionOperationManager {
+ /**
+ * Indicates that the supplied operation (method name) is to be performed.
+ * This method is responsible for recording the current state (prior to the
+ * operation), performing the operation, and storing the necessary
+ * information to roll back or commit the performed operation.
+ *
+ * @param operation
+ * The method to be invoked.
+ * @param args
+ * Arguments supplied to the method.
+ */
+ public void performOperation(String operation, Object[] args);
+
+ /**
+ * Rollback all recorded operations, by performing each of the recorded
+ * rollback operations.
+ */
+ public void rollback();
+
+ /**
+ * Commit all recorded operations. In many cases this means doing nothing,
+ * but in some cases some temporary data will need to be removed.
+ */
+ public void commit();
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationRecorder.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationRecorder.java
new file mode 100644
index 00000000..3c715f39
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationRecorder.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction;
+
+/**
+ * An implementation of this interface is responsible for recording data and
+ * supplying a {@link CompensatingTransactionOperationExecutor} to be invoked
+ * for execution and compensating transaction management of the operation.
+ * Recording of an operation should not fail (throwing an Exception), but rather
+ * log the result.
+ *
+ * @author Mattias Arthursson
+ */
+public interface CompensatingTransactionOperationRecorder {
+ /**
+ * Record information about the operation performed and return a
+ * corresponding {@link CompensatingTransactionOperationExecutor} to be used
+ * if the operation would need to be rolled back.
+ *
+ * @param args
+ * The arguments that have been sent to the operation.
+ * @return A {@link CompensatingTransactionOperationExecutor} to be used if
+ * the recorded operation should need to be rolled back.
+ */
+ public CompensatingTransactionOperationExecutor recordOperation(
+ Object[] args);
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/DefaultCompensatingTransactionOperationManager.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/DefaultCompensatingTransactionOperationManager.java
new file mode 100644
index 00000000..16af2734
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/DefaultCompensatingTransactionOperationManager.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction;
+
+import java.util.Stack;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Default implementation of {@link CompensatingTransactionOperationManager}.
+ * Manages a stack of {@link CompensatingTransactionOperationExecutor} objects
+ * and manages rollback of these in the reverse order.
+ *
+ * @author Mattias Arthursson
+ */
+public class DefaultCompensatingTransactionOperationManager implements
+ CompensatingTransactionOperationManager {
+
+ private static Log log = LogFactory
+ .getLog(DefaultCompensatingTransactionOperationManager.class);
+
+ private Stack rollbackOperations = new Stack();
+
+ private CompensatingTransactionOperationFactory operationFactory;
+
+ /**
+ * Set the {@link CompensatingTransactionOperationFactory} to use.
+ *
+ * @param operationFactory
+ * the {@link CompensatingTransactionOperationFactory}.
+ */
+ public DefaultCompensatingTransactionOperationManager(
+ CompensatingTransactionOperationFactory operationFactory) {
+ this.operationFactory = operationFactory;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationManager#operationPerformed(java.lang.String,
+ * java.lang.Object[])
+ */
+ public void performOperation(String operation, Object[] args) {
+ CompensatingTransactionOperationRecorder recorder = operationFactory
+ .createRecordingOperation(operation);
+ CompensatingTransactionOperationExecutor executor = recorder
+ .recordOperation(args);
+
+ executor.performOperation();
+
+ // Don't push the executor until the actual operation passed.
+ rollbackOperations.push(executor);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationManager#rollback()
+ */
+ public void rollback() {
+ log.debug("Performing rollback");
+ while (!rollbackOperations.isEmpty()) {
+ CompensatingTransactionOperationExecutor rollbackOperation = (CompensatingTransactionOperationExecutor) rollbackOperations
+ .pop();
+ rollbackOperation.rollback();
+ }
+ }
+
+ /**
+ * Get the rollback operations. Package protected for testing purposes.
+ *
+ * @return the rollback operations.
+ */
+ protected 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;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationManager#commit()
+ */
+ public void commit() {
+ log.debug("Performing rollback");
+ // TODO: Should this really be done in reverse order?
+ while (!rollbackOperations.isEmpty()) {
+ CompensatingTransactionOperationExecutor rollbackOperation = (CompensatingTransactionOperationExecutor) rollbackOperations
+ .pop();
+ rollbackOperation.commit();
+ }
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/BindOperationExecutor.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/BindOperationExecutor.java
new file mode 100644
index 00000000..43b326fe
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/BindOperationExecutor.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+import javax.naming.directory.Attributes;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+
+/**
+ * A {@link CompensatingTransactionOperationExecutor} to manage a bind
+ * operation. Performs a bind in {@link #performOperation()}, a corresponding
+ * unbind in {@link #rollback()}, and nothing in {@link #commit()}.
+ *
+ * @author Mattias Arthursson
+ */
+public class BindOperationExecutor implements
+ CompensatingTransactionOperationExecutor {
+ private static Log log = LogFactory.getLog(BindOperationExecutor.class);
+
+ private LdapOperations ldapOperations;
+
+ private Name dn;
+
+ private Object originalObject;
+
+ private Attributes originalAttributes;
+
+ /**
+ * Constructor.
+ *
+ * @param ldapOperations
+ * {@link LdapOperations} to use for performing the rollback
+ * operation.
+ * @param dn
+ * DN of the entry to be unbound.
+ * @param originalObject
+ * original value sent to the 'object' parameter of the bind
+ * operation.
+ * @param originalAttributes
+ * original value sent to the 'attributes' parameter of the bind
+ * operation.
+ */
+ public BindOperationExecutor(LdapOperations ldapOperations, Name dn,
+ Object originalObject, Attributes originalAttributes) {
+ this.ldapOperations = ldapOperations;
+ this.dn = dn;
+ this.originalObject = originalObject;
+ this.originalAttributes = originalAttributes;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#rollback()
+ */
+ public void rollback() {
+ try {
+ ldapOperations.unbind(dn);
+ } catch (Exception e) {
+ log.warn("Failed to rollback, dn:" + dn.toString(), e);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#commit()
+ */
+ public void commit() {
+ log.debug("Nothing to do in commit for bind operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#performOperation()
+ */
+ public void performOperation() {
+ log.debug("Performing bind operation");
+ ldapOperations.bind(dn, originalObject, originalAttributes);
+ }
+
+ /**
+ * 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;
+ }
+
+ Attributes getOriginalAttributes() {
+ return originalAttributes;
+ }
+
+ Object getOriginalObject() {
+ return originalObject;
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/BindOperationRecorder.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/BindOperationRecorder.java
new file mode 100644
index 00000000..f7e52b3e
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/BindOperationRecorder.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+import javax.naming.directory.Attributes;
+
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+
+/**
+ * A {@link CompensatingTransactionOperationRecorder} to manage LDAP bind
+ * operations. The corresponding
+ * {@link CompensatingTransactionOperationExecutor} is
+ * {@link BindOperationExecutor}.
+ *
+ * @author Mattias Arthursson
+ */
+public class BindOperationRecorder implements
+ CompensatingTransactionOperationRecorder {
+
+ private LdapOperations ldapOperations;
+
+ /**
+ * Constructor.
+ *
+ * @param ldapOperations
+ * {@link LdapOperations} to use for supplying to the
+ * corresponding rollback operation.
+ */
+ public BindOperationRecorder(LdapOperations ldapOperations) {
+ this.ldapOperations = ldapOperations;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationRecorder#recordOperation(java.lang.Object[])
+ */
+ public CompensatingTransactionOperationExecutor recordOperation(
+ Object[] args) {
+ if (args == null || args.length != 3) {
+ throw new IllegalArgumentException(
+ "Invalid arguments for bind operation");
+ }
+ Name dn = LdapUtils.getFirstArgumentAsName(args);
+ Object object = args[1];
+ Attributes attributes = null;
+ if (args[2] != null && !(args[2] instanceof Attributes)) {
+ throw new IllegalArgumentException(
+ "Invalid third argument to bind operation");
+ } else if (args[2] != null) {
+ attributes = (Attributes) args[2];
+ }
+
+ return new BindOperationExecutor(ldapOperations, dn, object,
+ attributes);
+ }
+
+ /**
+ * Get the LdapOperations. For testing purposes.s
+ *
+ * @return the LdapOperations.
+ */
+ LdapOperations getLdapOperations() {
+ return ldapOperations;
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceAndDataSourceTransactionManager.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceAndDataSourceTransactionManager.java
new file mode 100644
index 00000000..5f1720b4
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceAndDataSourceTransactionManager.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionException;
+import org.springframework.transaction.TransactionSuspensionNotSupportedException;
+import org.springframework.transaction.support.DefaultTransactionStatus;
+
+/**
+ * A Transaction Manager to manage LDAP and JDBC operations within the same
+ * transaction. Note that even though the same logical transaction is used, this
+ * is not a JTA XA transaction; no two-phase commit will be performed,
+ * and thus commit and rollback may result in unexpected results.
+ *
+ * @author Mattias Arthursson
+ */
+public class ContextSourceAndDataSourceTransactionManager extends
+ DataSourceTransactionManager {
+
+ private static final long serialVersionUID = 6832868697460384648L;
+
+ private ContextSourceTransactionManagerDelegate ldapManagerDelegate = new ContextSourceTransactionManagerDelegate();
+
+ public ContextSourceAndDataSourceTransactionManager() {
+ super();
+ // Override the default behaviour.
+ setNestedTransactionAllowed(false);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.jdbc.datasource.DataSourceTransactionManager#isExistingTransaction(java.lang.Object)
+ */
+ protected boolean isExistingTransaction(Object transaction) {
+ ContextSourceAndDataSourceTransactionObject actualTransactionObject = (ContextSourceAndDataSourceTransactionObject) transaction;
+
+ return super.isExistingTransaction(actualTransactionObject
+ .getDataSourceTransactionObject());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.jdbc.datasource.DataSourceTransactionManager#doGetTransaction()
+ */
+ protected Object doGetTransaction() throws TransactionException {
+ Object dataSourceTransactionObject = super.doGetTransaction();
+ Object contextSourceTransactionObject = ldapManagerDelegate
+ .doGetTransaction();
+
+ return new ContextSourceAndDataSourceTransactionObject(
+ contextSourceTransactionObject, dataSourceTransactionObject);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin(java.lang.Object,
+ * org.springframework.transaction.TransactionDefinition)
+ */
+ protected void doBegin(Object transaction, TransactionDefinition definition)
+ throws TransactionException {
+ ContextSourceAndDataSourceTransactionObject actualTransactionObject = (ContextSourceAndDataSourceTransactionObject) transaction;
+
+ super.doBegin(actualTransactionObject.getDataSourceTransactionObject(),
+ definition);
+ ldapManagerDelegate.doBegin(actualTransactionObject
+ .getLdapTransactionObject(), definition);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion(java.lang.Object)
+ */
+ protected void doCleanupAfterCompletion(Object transaction) {
+ ContextSourceAndDataSourceTransactionObject actualTransactionObject = (ContextSourceAndDataSourceTransactionObject) transaction;
+
+ super.doCleanupAfterCompletion(actualTransactionObject
+ .getDataSourceTransactionObject());
+ ldapManagerDelegate.doCleanupAfterCompletion(actualTransactionObject
+ .getLdapTransactionObject());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.jdbc.datasource.DataSourceTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus)
+ */
+ protected void doCommit(DefaultTransactionStatus status)
+ throws TransactionException {
+
+ ContextSourceAndDataSourceTransactionObject actualTransactionObject = (ContextSourceAndDataSourceTransactionObject) status
+ .getTransaction();
+
+ try {
+ super.doCommit(new DefaultTransactionStatus(actualTransactionObject
+ .getDataSourceTransactionObject(), status
+ .isNewTransaction(), status.isNewSynchronization(), status
+ .isReadOnly(), status.isDebug(), status
+ .getSuspendedResources()));
+ } catch (TransactionException ex) {
+ if (isRollbackOnCommitFailure()) {
+ logger.debug("Failed to commit db resource, rethrowing", ex);
+ // If we are to rollback on commit failure, just rethrow the
+ // exception - this will cause a rollback to be performed on
+ // both resources.
+ throw ex;
+ } else {
+ logger
+ .warn("Failed to commit and resource is rollbackOnCommit not set -"
+ + " proceeding to commit ldap resource.");
+ }
+ }
+ ldapManagerDelegate.doCommit(new DefaultTransactionStatus(
+ actualTransactionObject.getLdapTransactionObject(), status
+ .isNewTransaction(), status.isNewSynchronization(),
+ status.isReadOnly(), status.isDebug(), status
+ .getSuspendedResources()));
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.jdbc.datasource.DataSourceTransactionManager#doRollback(org.springframework.transaction.support.DefaultTransactionStatus)
+ */
+ protected void doRollback(DefaultTransactionStatus status)
+ throws TransactionException {
+ ContextSourceAndDataSourceTransactionObject actualTransactionObject = (ContextSourceAndDataSourceTransactionObject) status
+ .getTransaction();
+
+ super.doRollback(new DefaultTransactionStatus(actualTransactionObject
+ .getDataSourceTransactionObject(), status.isNewTransaction(),
+ status.isNewSynchronization(), status.isReadOnly(), status
+ .isDebug(), status.getSuspendedResources()));
+ ldapManagerDelegate.doRollback(new DefaultTransactionStatus(
+ actualTransactionObject.getLdapTransactionObject(), status
+ .isNewTransaction(), status.isNewSynchronization(),
+ status.isReadOnly(), status.isDebug(), status
+ .getSuspendedResources()));
+ }
+
+ public ContextSource getContextSource() {
+ return ldapManagerDelegate.getContextSource();
+ }
+
+ public void setContextSource(ContextSource contextSource) {
+ ldapManagerDelegate.setContextSource(contextSource);
+ }
+
+ protected void setRenamingStrategy(
+ TempEntryRenamingStrategy renamingStrategy) {
+ ldapManagerDelegate.setRenamingStrategy(renamingStrategy);
+ }
+
+ private class ContextSourceAndDataSourceTransactionObject {
+ private Object ldapTransactionObject;
+
+ private Object dataSourceTransactionObject;
+
+ public ContextSourceAndDataSourceTransactionObject(
+ Object ldapTransactionObject, Object dataSourceTransactionObject) {
+ this.ldapTransactionObject = ldapTransactionObject;
+ this.dataSourceTransactionObject = dataSourceTransactionObject;
+ }
+
+ public Object getDataSourceTransactionObject() {
+ return dataSourceTransactionObject;
+ }
+
+ public Object getLdapTransactionObject() {
+ return ldapTransactionObject;
+ }
+ }
+
+ protected Object doSuspend(Object transaction) throws TransactionException {
+ throw new TransactionSuspensionNotSupportedException(
+ "Transaction manager [" + getClass().getName()
+ + "] does not support transaction suspension");
+ }
+
+ protected void doResume(Object transaction, Object suspendedResources)
+ throws TransactionException {
+ throw new TransactionSuspensionNotSupportedException(
+ "Transaction manager [" + getClass().getName()
+ + "] does not support transaction suspension");
+ }
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManager.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManager.java
new file mode 100644
index 00000000..1e179fab
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManager.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+import org.springframework.ldap.transaction.DefaultCompensatingTransactionOperationManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionException;
+import org.springframework.transaction.support.AbstractPlatformTransactionManager;
+import org.springframework.transaction.support.DefaultTransactionStatus;
+
+/**
+ * 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, i.e. should a transaction need to
+ * be rolled back this TransactionManager will try to restore the original
+ * original state using information recorded prior to each operation.
+ *
+ * NOTE: The transactions provided by this TransactionManager are all
+ * client side and are by no means 'real' transactions, in the sense
+ * that we know them in the ordinary database world, e.g.:
+ *
+ *
+ *
+ * While the points above should be noted and considered, the compensating + * transaction approach will be perfectly sufficient for all but the most + * unfortunate of circumstances, particularly considering the total absence of + * transaction support which is normally the case working against LDAP servers. + *
+ *+ * An LDAP transaction is tied to a {@link ContextSource}, to be supplied to + * the {@link #setContextSource(ContextSource)} method. While the actual + * ContextSource used by the target LdapTemplate instance needs to be of the + * type {@link TransactionAwareContextSourceProxy}, the ContextSource supplied + * to this class should be the actual target ContextSource. + *
+ *+ * Using this TransactionManager along with + * {@link TransactionAwareContextSourceProxy} all modifying operations (bind, + * unbind, rebind, rename, modifyAttributes) in a transaction will be + * intercepted. Each modification has its corresponding + * {@link CompensatingTransactionOperationRecorder}, which collects the + * information necessary to perform a rollback and produces a + * {@link CompensatingTransactionOperationExecutor} which is then used to + * execute the actual operation and is later called for performing the commit or + * rollback. + *
+ *+ * For several of the operations, performing a rollback is pretty + * straightforward. E.g. in order to roll back a rename operation it will only + * be required to rename the entry back to its original position. For other + * operations however, it's a bit more complicated. E.g. an unbind operation is + * not possible to roll back by simply binding the entry back with the + * attributes retrieved from the original entry. This is because it might not be + * possible to get all the information from the original entry. Consequently, + * the {@link UnbindOperationExecutor} will move the original entry to a + * temporary location in its performOperation() method. In the commit() method + * we already know that everything went well, so we're free to unbind the entry, + * but the rollback operation will be to rename the entry back to its original + * location. The same behaviour is used for rebind() operations. The operation + * of calculating a temporary location for an entry is delegated to a + * {@link TempEntryRenamingStrategy} (default + * {@link DefaultTempEntryRenamingStrategy}), specified in + * {@link #setRenamingStrategy(TempEntryRenamingStrategy)}. + *
+ *+ * The actual work of this Transaction Manager is delegated to a + * {@link ContextSourceTransactionManagerDelegate}. This is because the exact + * same logic needs to be used if we want to wrap a JDBC and LDAP transaction in + * the same logical transaction. + *
+ * + * @author Mattias Arthursson + * + * @see ContextSourceAndDataSourceTransactionManager + * @see ContextSourceTransactionManagerDelegate + * @see DefaultCompensatingTransactionOperationManager + * @see TempEntryRenamingStrategy + * @see TransactionAwareContextSourceProxy + */ +public class ContextSourceTransactionManager extends + AbstractPlatformTransactionManager { + + private static final long serialVersionUID = 7138208218687237856L; + + private ContextSourceTransactionManagerDelegate delegate = new ContextSourceTransactionManagerDelegate(); + + /* + * (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 { + delegate.doBegin(transaction, definition); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doCleanupAfterCompletion(java.lang.Object) + */ + protected void doCleanupAfterCompletion(Object transaction) { + delegate.doCleanupAfterCompletion(transaction); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus) + */ + protected void doCommit(DefaultTransactionStatus status) + throws TransactionException { + delegate.doCommit(status); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doGetTransaction() + */ + protected Object doGetTransaction() throws TransactionException { + return delegate.doGetTransaction(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doRollback(org.springframework.transaction.support.DefaultTransactionStatus) + */ + protected void doRollback(DefaultTransactionStatus status) + throws TransactionException { + delegate.doRollback(status); + } + + /** + * Get the ContextSource. + * + * @return the contextSource. + * @see ContextSourceTransactionManagerDelegate#getContextSource() + */ + public ContextSource getContextSource() { + return delegate.getContextSource(); + } + + /** + * Set the ContextSource. + * + * @param contextSource + * the ContextSource. + * @see ContextSourceTransactionManagerDelegate#setContextSource(ContextSource) + */ + public void setContextSource(ContextSource contextSource) { + delegate.setContextSource(contextSource); + } + + /** + * Set the {@link TempEntryRenamingStrategy}. + * + * @param renamingStrategy + * the Renaming Strategy. + * @see ContextSourceTransactionManagerDelegate#setRenamingStrategy(TempEntryRenamingStrategy) + */ + public void setRenamingStrategy(TempEntryRenamingStrategy renamingStrategy) { + delegate.setRenamingStrategy(renamingStrategy); + } +} diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManagerDelegate.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManagerDelegate.java new file mode 100644 index 00000000..c96f436c --- /dev/null +++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManagerDelegate.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ldap.transaction.core; + +import javax.naming.NamingException; +import javax.naming.directory.DirContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.core.ContextSource; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * This delegate performs all the work for the + * {@link ContextSourceTransactionManager}. The work is delegated in order to + * be able to perform the exact same work for the LDAP part in + * {@link ContextSourceAndDataSourceTransactionManager}. + * + * @author Mattias Arthursson + * @see ContextSourceTransactionManager + * @see ContextSourceAndDataSourceTransactionManager + */ +public class ContextSourceTransactionManagerDelegate { + private static Log log = LogFactory + .getLog(ContextSourceTransactionManager.class); + + private ContextSource contextSource; + + private TempEntryRenamingStrategy renamingStrategy = new DefaultTempEntryRenamingStrategy(); + + /** + * Set the ContextSource to work on. Even though the actual ContextSource + * sent to the LdapTemplate instance should be a + * {@link TransactionAwareContextSourceProxy}, the one sent to this method + * should be the target of that proxy. If it is not, the target will be + * extracted and used instead. + * + * @param contextSource + * the ContextSource to work on. + */ + public void setContextSource(ContextSource contextSource) { + if (contextSource instanceof TransactionAwareContextSourceProxy) { + TransactionAwareContextSourceProxy proxy = (TransactionAwareContextSourceProxy) contextSource; + this.contextSource = proxy.getTarget(); + } else { + this.contextSource = contextSource; + } + } + + public ContextSource getContextSource() { + return contextSource; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doGetTransaction() + */ + public 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) + */ + public 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, + renamingStrategy); + + txObject.setContextHolder(contextHolder); + TransactionSynchronizationManager.bindResource(getContextSource(), + contextHolder); + } + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doCommit(org.springframework.transaction.support.DefaultTransactionStatus) + */ + public void doCommit(DefaultTransactionStatus status) + throws TransactionException { + ContextSourceTransactionObject txObject = (ContextSourceTransactionObject) status + .getTransaction(); + txObject.getContextHolder().getTransactionDataManager().commit(); + + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doRollback(org.springframework.transaction.support.DefaultTransactionStatus) + */ + public void doRollback(DefaultTransactionStatus status) + throws TransactionException { + ContextSourceTransactionObject txObject = (ContextSourceTransactionObject) status + .getTransaction(); + txObject.getContextHolder().getTransactionDataManager().rollback(); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#doCleanupAfterCompletion(java.lang.Object) + */ + public void doCleanupAfterCompletion(Object transaction) { + log.debug("Cleaning stored ContextHolder"); + TransactionSynchronizationManager.unbindResource(contextSource); + + ContextSourceTransactionObject txObject = (ContextSourceTransactionObject) transaction; + DirContext ctx = txObject.getContextHolder().getCtx(); + + try { + log.debug("Closing target context"); + ctx.close(); + } catch (NamingException e) { + e.printStackTrace(); + } + + txObject.getContextHolder().clear(); + } + + /** + * Set the {@link TempEntryRenamingStrategy} to be used when renaming + * temporary entries in unbind and rebind operations. Default value is a + * {@link DefaultTempEntryRenamingStrategy}. + * + * @param renamingStrategy + * the {@link TempEntryRenamingStrategy} to use. + */ + public void setRenamingStrategy(TempEntryRenamingStrategy renamingStrategy) { + this.renamingStrategy = renamingStrategy; + } + +} diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceTransactionObject.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceTransactionObject.java new file mode 100644 index 00000000..3dac7d5b --- /dev/null +++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ContextSourceTransactionObject.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ldap.transaction.core; + +/** + * Transaction object for ContextSourceTransactionManager. Keeps a reference to + * the {@link DirContextHolder} associated with the current transaction. + * + * @author Mattias Arthursson + */ +public class ContextSourceTransactionObject { + private DirContextHolder contextHolder; + + /** + * Constructor. + * + * @param contextHolder + * the DirContextHolder associated with the current transaction. + */ + public ContextSourceTransactionObject(DirContextHolder contextHolder) { + this.contextHolder = contextHolder; + } + + /** + * Get the DirContextHolder. + * + * @return the DirContextHolder. + */ + public DirContextHolder getContextHolder() { + return contextHolder; + } + + /** + * Set the DirContextHolder associated with the current transaction.s + * + * @param contextHolder + * the DirContextHolder associated with the current transaction. + */ + public void setContextHolder(DirContextHolder contextHolder) { + this.contextHolder = contextHolder; + } +} \ No newline at end of file diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DefaultTempEntryRenamingStrategy.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DefaultTempEntryRenamingStrategy.java new file mode 100644 index 00000000..5c31e0f8 --- /dev/null +++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DefaultTempEntryRenamingStrategy.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ldap.transaction.core; + +import java.util.List; + +import javax.naming.Name; + +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapRdn; +import org.springframework.ldap.core.LdapRdnComponent; + +/** + * Default implementation of {@link TempEntryRenamingStrategy}. This + * implementation simply adds "_temp" to the leftmost (least siginificant part) + * of the name, e.g.:
+ * cn=john doe, ou=company1, c=SE
+ *
+ *
+ * becomes
+ *
+ *
+ * cn=john doe_temp, ou=company1, c=SE
+ * .
+ * + * Note that using this strategy means that the entry remains in virtually the + * same location as where it originally resided. This means that searches later + * in the same transaction might return references to the temporary entry even + * though it should have been removed or rebound. + * + * @author Mattias Arthursson + */ +public class DefaultTempEntryRenamingStrategy implements + TempEntryRenamingStrategy { + + /** + * The default temp entry suffix, "_temp". + */ + public static final String DEFAULT_TEMP_SUFFIX = "_temp"; + + private String tempSuffix = DEFAULT_TEMP_SUFFIX; + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.support.transaction.TempEntryRenamingStrategy#getTemporaryName(javax.naming.Name) + */ + public Name getTemporaryName(Name originalName) { + DistinguishedName temporaryName = new DistinguishedName(originalName); + List names = temporaryName.getNames(); + LdapRdn rdn = (LdapRdn) names.get(names.size() - 1); + LdapRdnComponent rdnComponent = rdn.getComponent(); + String value = rdnComponent.getValue(); + rdnComponent.setValue(value + DEFAULT_TEMP_SUFFIX); + + return temporaryName; + } + + /** + * Get the suffix that will be used for renaming temporary entries. + * + * @return the suffix. + */ + public String getTempSuffix() { + return tempSuffix; + } + + /** + * Set the suffix to use for renaming temporary entries. Default value is + * {@link #DEFAULT_TEMP_SUFFIX}. + * + * @param tempSuffix + * the suffix. + */ + public void setTempSuffix(String tempSuffix) { + this.tempSuffix = tempSuffix; + } + +} diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DifferentSubtreeTempEntryRenamingStrategy.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DifferentSubtreeTempEntryRenamingStrategy.java new file mode 100644 index 00000000..2191b50a --- /dev/null +++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DifferentSubtreeTempEntryRenamingStrategy.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ldap.transaction.core; + +import java.util.List; + +import javax.naming.Name; + +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapRdn; +import org.springframework.ldap.core.LdapRdnComponent; + +/** + * A {@link TempEntryRenamingStrategy} that moves the entry to a different + * subtree than the original entry. The specified subtree needs to be present in + * the LDAP tree; it will not be created and operations using this strategy will + * fail if it is not in place. However, this strategy is preferrable to + * {@link DefaultTempEntryRenamingStrategy}, as it makes it possible to have + * searches have the expected result even though the temporary entry still exits + * until the end of the transaction. + *
+ * Example: If the specified subtreeNode is
+ * ou=tempEntries and the originalName is
+ * cn=john doe, ou=company1, c=SE, the result of
+ * {@link #getTemporaryName(Name)} will be
+ * cn=john doe1, ou=tempEntries. The "1" suffix is a
+ * sequence number needed to prevent potential collisions in the temporary
+ * storage.
+ *
+ * @author Mattias Arthursson
+ *
+ */
+public class DifferentSubtreeTempEntryRenamingStrategy implements
+ TempEntryRenamingStrategy {
+
+ private Name subtreeNode;
+
+ private static int nextSequenceNo = 1;
+
+ public DifferentSubtreeTempEntryRenamingStrategy(Name subtreeNode) {
+ this.subtreeNode = subtreeNode;
+ }
+
+ public Name getSubtreeNode() {
+ return subtreeNode;
+ }
+
+ public void setSubtreeNode(Name subtreeNode) {
+ this.subtreeNode = subtreeNode;
+ }
+
+ int getNextSequenceNo() {
+ return nextSequenceNo;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.TempEntryRenamingStrategy#getTemporaryName(javax.naming.Name)
+ */
+ public Name getTemporaryName(Name originalName) {
+ DistinguishedName tempName = new DistinguishedName(originalName);
+ List names = tempName.getNames();
+ LdapRdn rdn = (LdapRdn) names.get(names.size() - 1);
+ LdapRdnComponent component = rdn.getComponent();
+
+ LdapRdn newRdn;
+ synchronized (this) {
+ newRdn = new LdapRdn(component.getKey(), component.getValue()
+ + nextSequenceNo++);
+ }
+
+ DistinguishedName newName = new DistinguishedName(subtreeNode);
+ newName.add(newRdn);
+ return newName;
+ }
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DirContextHolder.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DirContextHolder.java
new file mode 100644
index 00000000..ccf88221
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DirContextHolder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.directory.DirContext;
+
+import org.springframework.ldap.transaction.CompensatingTransactionOperationFactory;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationManager;
+import org.springframework.ldap.transaction.DefaultCompensatingTransactionOperationManager;
+import org.springframework.transaction.support.ResourceHolderSupport;
+
+/**
+ * Keeps track of the transaction DirContext. The same DirContext instance will
+ * be reused throughout a transaction. Also keeps a
+ * {@link CompensatingTransactionOperationManager}, responsible for performing
+ * operations and keeping track of all changes and storing information necessary
+ * for commit or rollback.
+ *
+ * @author Mattias Arthursson
+ *
+ */
+public class DirContextHolder extends ResourceHolderSupport {
+ private DirContext ctx;
+
+ private CompensatingTransactionOperationManager transactionDataManager;
+
+ private CompensatingTransactionOperationFactory operationFactory;
+
+ private TempEntryRenamingStrategy renamingStrategy;
+
+ /**
+ * Constructor.
+ *
+ * @param ctx
+ * The DirContext associated with the current transaction.
+ */
+ public DirContextHolder(DirContext ctx,
+ TempEntryRenamingStrategy renamingStrategy) {
+ this.ctx = ctx;
+ this.renamingStrategy = renamingStrategy;
+ this.transactionDataManager = new DefaultCompensatingTransactionOperationManager(
+ createOperationFactory());
+ }
+
+ /**
+ * Set the DirContext associated with the current transaction.
+ *
+ * @param ctx
+ * The DirContext associated with the current transaction.
+ */
+ public void setCtx(DirContext ctx) {
+ this.ctx = ctx;
+ this.transactionDataManager = new DefaultCompensatingTransactionOperationManager(
+ createOperationFactory());
+ }
+
+ /**
+ * Factory method to create a
+ * {@link CompensatingTransactionOperationFactory} using the settings and
+ * current state of this object.
+ *
+ * @return a new {@link LdapCompensatingTransactionOperationFactory}
+ * referncing the current transaction context.
+ */
+ private CompensatingTransactionOperationFactory createOperationFactory() {
+ return new LdapCompensatingTransactionOperationFactory(ctx,
+ renamingStrategy);
+ }
+
+ /**
+ * Return the DirContext associated with the current transaction.
+ */
+ public DirContext getCtx() {
+ return ctx;
+ }
+
+ public void clear() {
+ super.clear();
+ transactionDataManager = null;
+ operationFactory = null;
+ }
+
+ /**
+ * Get the CompensatingTransactionOperationManager to handle the data for
+ * the current transaction.
+ *
+ * @return the CompensatingTransactionOperationManager.
+ */
+ public CompensatingTransactionOperationManager getTransactionDataManager() {
+ return transactionDataManager;
+ }
+
+ /**
+ * Set the CompensatingTransactionOperationManager. For testing purposes
+ * only.
+ *
+ * @param transactionDataManager
+ * the CompensatingTransactionOperationManager to use.
+ */
+ void setTransactionDataManager(
+ CompensatingTransactionOperationManager transactionDataManager) {
+ this.transactionDataManager = transactionDataManager;
+ }
+
+ public CompensatingTransactionOperationFactory getOperationFactory() {
+ return operationFactory;
+ }
+
+ public TempEntryRenamingStrategy getRenamingStrategy() {
+ return renamingStrategy;
+ }
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DirContextProxy.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DirContextProxy.java
new file mode 100644
index 00000000..0ff0d744
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/DirContextProxy.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.directory.DirContext;
+
+/**
+ * Helper interface to be able to get hold of the target DirContext from proxies
+ * created by {@link TransactionAwareContextSourceProxy}.
+ *
+ * @author Mattias Arthursson
+ */
+public interface DirContextProxy extends DirContext {
+ /**
+ * Get the target DirContext of the proxy.
+ *
+ * @return the target DirContext.
+ */
+ DirContext getTargetContext();
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/LdapCompensatingTransactionOperationFactory.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/LdapCompensatingTransactionOperationFactory.java
new file mode 100644
index 00000000..527bfa3e
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/LdapCompensatingTransactionOperationFactory.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import javax.naming.directory.DirContext;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.dao.DataAccessException;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationFactory;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+
+/**
+ * {@link CompensatingTransactionOperationRecorder} implementation for LDAP
+ * operations.
+ *
+ * @author Mattias Arthursson
+ */
+public class LdapCompensatingTransactionOperationFactory implements
+ CompensatingTransactionOperationFactory {
+ private static Log log = LogFactory
+ .getLog(LdapCompensatingTransactionOperationFactory.class);
+
+ private LdapOperations ldapOperations;
+
+ private TempEntryRenamingStrategy renamingStrategy;
+
+ /**
+ * Constructor.
+ *
+ * @param ctx
+ * The transactional DirContext.
+ */
+ public LdapCompensatingTransactionOperationFactory(DirContext ctx,
+ TempEntryRenamingStrategy renamingStrategy) {
+ this.ldapOperations = new LdapTemplate(new SingleContextSource(ctx));
+ this.renamingStrategy = renamingStrategy;
+ }
+
+ public CompensatingTransactionOperationRecorder createRecordingOperation(
+ String operation) {
+ if (StringUtils.equals(operation, LdapUtils.BIND_METHOD_NAME)) {
+ log.debug("Bind operation recorded");
+ return new BindOperationRecorder(ldapOperations);
+ } else if (StringUtils.equals(operation, LdapUtils.REBIND_METHOD_NAME)) {
+ log.debug("Rebind operation recorded");
+ return new RebindOperationRecorder(ldapOperations, renamingStrategy);
+ } else if (StringUtils.equals(operation, LdapUtils.RENAME_METHOD_NAME)) {
+ log.debug("Rename operation recorded");
+ return new RenameOperationRecorder(ldapOperations);
+ } else if (StringUtils.equals(operation,
+ LdapUtils.MODIFY_ATTRIBUTES_METHOD_NAME)) {
+ return new ModifyAttributesOperationRecorder(ldapOperations);
+ } else if (StringUtils.equals(operation, LdapUtils.UNBIND_METHOD_NAME)) {
+ return new UnbindOperationRecorder(ldapOperations, renamingStrategy);
+ }
+
+ log
+ .warn("No suitable CompensatingTransactionOperationRecorder found for method "
+ + operation + ". Operation will not be transacted.");
+ return new NullOperationRecorder();
+ }
+
+ /**
+ * A {@link ContextSource} implementation using returning
+ * {@link NonClosingDirContextInvocationHandler} proxies on the same
+ * DirContext instance for each call.
+ *
+ * @author Mattias Arthursson
+ */
+ static class SingleContextSource implements ContextSource {
+ private DirContext ctx;
+
+ /**
+ * Constructor.
+ *
+ * @param ctx
+ * the target DirContext.
+ */
+ public SingleContextSource(DirContext ctx) {
+ this.ctx = ctx;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.ContextSource#getReadOnlyContext()
+ */
+ public DirContext getReadOnlyContext() throws DataAccessException {
+ return getNonClosingDirContextProxy(ctx);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.ContextSource#getReadWriteContext()
+ */
+ public DirContext getReadWriteContext() throws DataAccessException {
+ return getNonClosingDirContextProxy(ctx);
+ }
+
+ private DirContext getNonClosingDirContextProxy(DirContext context) {
+ return (DirContext) Proxy.newProxyInstance(DirContextProxy.class
+ .getClassLoader(), new Class[] { DirContextProxy.class },
+ new NonClosingDirContextInvocationHandler(context));
+
+ }
+ }
+
+ /**
+ * A proxy for DirContext forwarding all operation to the target DirContext,
+ * but making sure that no close operations will be
+ * performed.
+ *
+ * @author Mattias Arthursson
+ */
+ public static class NonClosingDirContextInvocationHandler implements
+ InvocationHandler {
+
+ private DirContext target;
+
+ public NonClosingDirContextInvocationHandler(DirContext target) {
+ this.target = target;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object,
+ * java.lang.reflect.Method, java.lang.Object[])
+ */
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+
+ String methodName = method.getName();
+ if (methodName.equals("getTargetContext")) {
+ return target;
+ } else if (methodName.equals("equals")) {
+ // Only consider equal when proxies are identical.
+ return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE);
+ } else if (methodName.equals("hashCode")) {
+ // Use hashCode of Connection proxy.
+ return new Integer(proxy.hashCode());
+ } else if (methodName.equals("close")) {
+ // Never close the target context, as this class will only be
+ // used for operations concerning the compensating transactions.
+ return null;
+ }
+
+ try {
+ return method.invoke(target, args);
+ } catch (InvocationTargetException e) {
+ throw e.getTargetException();
+ }
+ }
+ }
+
+ void setLdapOperations(LdapOperations ldapOperations) {
+ this.ldapOperations = ldapOperations;
+ }
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/LdapUtils.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/LdapUtils.java
new file mode 100644
index 00000000..8182b3c5
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/LdapUtils.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import javax.naming.Name;
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationManager;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.springframework.util.Assert;
+
+/**
+ * Common helper methods for Ldap operations.
+ *
+ * @author Mattias Arthursson
+ */
+public class LdapUtils {
+ private static Log log = LogFactory.getLog(LdapUtils.class);
+
+ public static final String REBIND_METHOD_NAME = "rebind";
+
+ public static final String BIND_METHOD_NAME = "bind";
+
+ public static final String RENAME_METHOD_NAME = "rename";
+
+ public static final String UNBIND_METHOD_NAME = "unbind";
+
+ public static final String MODIFY_ATTRIBUTES_METHOD_NAME = "modifyAttributes";
+
+ /**
+ * 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);
+
+ Object firstArg = args[0];
+ return getArgumentAsName(firstArg);
+ }
+
+ /**
+ * Get the argument as a Name.
+ *
+ * @param arg
+ * an argument supplied to an Ldap operation.
+ * @return a Name representation of the argument, or the Name itself if it
+ * is a Name.
+ */
+ public static Name getArgumentAsName(Object arg) {
+ if (arg instanceof String) {
+ return new DistinguishedName((String) arg);
+ } else if (arg instanceof Name) {
+ return (Name) arg;
+ } else {
+ throw new IllegalArgumentException(
+ "First argument needs to be a Name or a String representation thereof");
+ }
+ }
+
+ /**
+ * Close the supplied context, but only if it is not associated with the
+ * current transaction.
+ *
+ * @param context
+ * the DirContext to close.
+ * @param contextSource
+ * the ContextSource bound to the transaction.
+ * @throws NamingException
+ */
+ public static void doCloseConnection(DirContext context,
+ ContextSource contextSource) throws NamingException {
+ DirContextHolder transactionContextHolder = (DirContextHolder) TransactionSynchronizationManager
+ .getResource(contextSource);
+ if (transactionContextHolder == null
+ || transactionContextHolder.getCtx() != context) {
+ log.debug("Closing context");
+ // This is not the transactional context or the transaction is
+ // no longer active - we should close it.
+ context.close();
+ } else {
+ log.debug("Leaving transactional context open");
+ }
+ }
+
+ /**
+ * Check whether the supplied method is a method for which transactions is
+ * supported (and which should be recorded for possible rollback later).
+ *
+ * @param methodName
+ * name of the method to check.
+ * @return true if this is a supported transaction operation,
+ * false otherwise.
+ */
+ public static boolean isSupportedWriteTransactionOperation(String methodName) {
+ return (StringUtils.equals(methodName, BIND_METHOD_NAME)
+ || StringUtils.equals(methodName, REBIND_METHOD_NAME)
+ || StringUtils.equals(methodName, RENAME_METHOD_NAME)
+ || StringUtils
+ .equals(methodName, MODIFY_ATTRIBUTES_METHOD_NAME) || StringUtils
+ .equals(methodName, UNBIND_METHOD_NAME));
+
+ }
+
+ /**
+ * Perform the specified operation, storing the state prior to the
+ * operation, to enable commit/rollback later.
+ *
+ * @param contextSource
+ * the ContextSource we are operating on.
+ * @param method
+ * name of the method to be invoked.
+ * @param args
+ * arguments with which the operation is invoked.
+ */
+ public static void performOperation(ContextSource contextSource,
+ Method method, Object[] args) throws Throwable {
+ DirContextHolder transactionContextHolder = (DirContextHolder) TransactionSynchronizationManager
+ .getResource(contextSource);
+ if (transactionContextHolder != null) {
+
+ CompensatingTransactionOperationManager transactionDataManager = transactionContextHolder
+ .getTransactionDataManager();
+ transactionDataManager.performOperation(method.getName(), args);
+ } else {
+ // Perform the target operation
+ try {
+ method.invoke(transactionContextHolder.getCtx(), args);
+ } catch (InvocationTargetException e) {
+ throw e.getTargetException();
+ }
+ }
+ }
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationExecutor.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationExecutor.java
new file mode 100644
index 00000000..f8c4bcdb
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationExecutor.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+import javax.naming.directory.ModificationItem;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+
+/**
+ * A {@link CompensatingTransactionOperationExecutor} to manage a
+ * modifyAttributes operation. Performs a
+ * modifyAttributes in {@link #performOperation()}, a negating
+ * modifyAttributes in {@link #rollback()}, and nothing in {@link #commit()}.
+ *
+ * @author Mattias Arthursson
+ */
+public class ModifyAttributesOperationExecutor implements
+ CompensatingTransactionOperationExecutor {
+
+ private static Log log = LogFactory
+ .getLog(ModifyAttributesOperationExecutor.class);
+
+ private LdapOperations ldapOperations;
+
+ private Name dn;
+
+ private ModificationItem[] compensatingModifications;
+
+ private ModificationItem[] actualModifications;
+
+ /**
+ * Constructor.
+ *
+ * @param ldapOperations
+ * The {@link LdapOperations} to use to perform the rollback
+ * operation.
+ * @param dn
+ * the DN of the target entry.
+ * @param actualModifications
+ * the actual modificationItems that were sent to the
+ * modifyAttributes operation.
+ * @param compensatingModifications
+ * the ModificationItems to undo the recorded operation.
+ */
+ public ModifyAttributesOperationExecutor(LdapOperations ldapOperations,
+ Name dn, ModificationItem[] actualModifications,
+ ModificationItem[] compensatingModifications) {
+ this.ldapOperations = ldapOperations;
+ this.dn = dn;
+ this.actualModifications = actualModifications;
+ this.compensatingModifications = compensatingModifications;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#rollback()
+ */
+ public void rollback() {
+ try {
+ log.debug("Rolling back modifyAttributes operation");
+ ldapOperations.modifyAttributes(dn, compensatingModifications);
+ } catch (Exception e) {
+ log
+ .warn("Failed to rollback ModifyAttributes operation, dn: "
+ + dn);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#commit()
+ */
+ public void commit() {
+ log.debug("Nothing to do in commit for modifyAttributes");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#performOperation()
+ */
+ public void performOperation() {
+ log.debug("Performing modifyAttributes operation");
+ ldapOperations.modifyAttributes(dn, actualModifications);
+ }
+
+ Name getDn() {
+ return dn;
+ }
+
+ LdapOperations getLdapOperations() {
+ return ldapOperations;
+ }
+
+ ModificationItem[] getActualModifications() {
+ return actualModifications;
+ }
+
+ ModificationItem[] getCompensatingModifications() {
+ return compensatingModifications;
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationRecorder.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationRecorder.java
new file mode 100644
index 00000000..24209663
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationRecorder.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.naming.Name;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+
+import org.springframework.ldap.core.AttributesMapper;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link CompensatingTransactionOperationRecorder} keeping track of
+ * modifyAttributes operations, creating corresponding
+ * {@link ModifyAttributesOperationExecutor} instances for rollback.
+ *
+ * @author Mattias Arthursson
+ */
+public class ModifyAttributesOperationRecorder implements
+ CompensatingTransactionOperationRecorder {
+
+ private LdapOperations ldapOperations;
+
+ public ModifyAttributesOperationRecorder(LdapOperations ldapOperations) {
+ this.ldapOperations = ldapOperations;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationRecorder#recordOperation(java.lang.Object[])
+ */
+ public CompensatingTransactionOperationExecutor recordOperation(
+ Object[] args) {
+ Assert.notNull(args);
+ Name dn = LdapUtils.getFirstArgumentAsName(args);
+ if (args.length != 2 || !(args[1] instanceof ModificationItem[])) {
+ throw new IllegalArgumentException(
+ "Unexpected arguments to ModifyAttributes operation");
+ }
+
+ ModificationItem[] incomingModifications = (ModificationItem[]) args[1];
+
+ Set set = new HashSet();
+ for (int i = 0; i < incomingModifications.length; i++) {
+ set.add(incomingModifications[i].getAttribute().getID());
+ }
+
+ // Get the current values of all referred Attributes.
+ String[] attributeNameArray = (String[]) set.toArray(new String[set
+ .size()]);
+ Attributes currentAttributes = (Attributes) ldapOperations.lookup(dn,
+ attributeNameArray, getAttributesMapper());
+
+ // Get a compensating ModificationItem for each of the incoming
+ // modification.
+ ModificationItem[] rollbackItems = new ModificationItem[incomingModifications.length];
+ for (int i = 0; i < incomingModifications.length; i++) {
+ rollbackItems[i] = getCompensatingModificationItem(
+ currentAttributes, incomingModifications[i]);
+ }
+
+ return new ModifyAttributesOperationExecutor(ldapOperations, dn,
+ incomingModifications, rollbackItems);
+ }
+
+ /**
+ * Get an {@link AttributesMapper} that just returns the supplied
+ * Attributes.
+ *
+ * @return the {@link AttributesMapper} to use for getting the current
+ * Attributes of the target DN.
+ */
+ AttributesMapper getAttributesMapper() {
+ return new AttributesMapper() {
+ public Object mapFromAttributes(Attributes attributes)
+ throws NamingException {
+ return attributes;
+ }
+ };
+ }
+
+ /**
+ * Get a ModificationItem to use for rollback of the supplied modification.
+ *
+ * @param originalAttributes
+ * All Attributes of the target DN that are affected of any of
+ * the ModificationItems.
+ * @param modificationItem
+ * the ModificationItem to create a rollback item for.
+ * @return A ModificationItem to use for rollback of the supplied
+ * ModificationItem.
+ */
+ protected ModificationItem getCompensatingModificationItem(
+ Attributes originalAttributes, ModificationItem modificationItem) {
+ Attribute modificationAttribute = modificationItem.getAttribute();
+ Attribute originalAttribute = originalAttributes
+ .get(modificationAttribute.getID());
+
+ if (modificationItem.getModificationOp() == DirContext.REMOVE_ATTRIBUTE) {
+ if (modificationAttribute.size() == 0) {
+ // If the modification attribute size it means that the
+ // Attribute should be removed entirely - we should store a
+ // ModificationItem to restore all present values for rollback.
+ return new ModificationItem(DirContext.ADD_ATTRIBUTE,
+ (Attribute) originalAttribute.clone());
+ } else {
+ // The rollback modification will be to re-add the removed
+ // attribute values.
+ return new ModificationItem(DirContext.ADD_ATTRIBUTE,
+ (Attribute) modificationAttribute.clone());
+ }
+ } else if (modificationItem.getModificationOp() == DirContext.REPLACE_ATTRIBUTE) {
+ if (originalAttribute != null) {
+ return new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
+ (Attribute) originalAttribute.clone());
+ } else {
+ // The attribute doesn't previously exist - the rollback
+ // operation will be to remove the attribute.
+ return new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
+ new BasicAttribute(modificationAttribute.getID()));
+ }
+ } else {
+ // An ADD_ATTRIBUTE operation
+ if (originalAttribute == null) {
+ // The attribute doesn't previously exist - the rollback
+ // operation will be to remove the attribute.
+ return new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
+ new BasicAttribute(modificationAttribute.getID()));
+ } else {
+ // The attribute does exist before - we should store the
+ // previous value and it should be used for replacing in
+ // rollback.
+ return new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
+ (Attribute) originalAttribute.clone());
+ }
+ }
+ }
+
+ LdapOperations getLdapOperations() {
+ return ldapOperations;
+ }
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/NullOperationExecutor.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/NullOperationExecutor.java
new file mode 100644
index 00000000..dd04d62e
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/NullOperationExecutor.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+
+/**
+ * A {@link CompensatingTransactionOperationExecutor} that performs nothing.
+ *
+ * @author Mattias Arthursson
+ */
+public class NullOperationExecutor implements
+ CompensatingTransactionOperationExecutor {
+
+ private static Log log = LogFactory.getLog(NullOperationExecutor.class);
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#rollback()
+ */
+ public void rollback() {
+ log.info("Rolling back null operation");
+ }
+
+ public void commit() {
+ log.info("Committing back null operation");
+ }
+
+ public void performOperation() {
+ log.info("Performing null operation");
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/NullOperationRecorder.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/NullOperationRecorder.java
new file mode 100644
index 00000000..7e2f82ff
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/NullOperationRecorder.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationManager;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+
+/**
+ * A {@link CompensatingTransactionOperationRecorder} performing nothing,
+ * returning a {@link NullOperationExecutor} regardless of the input. Instances
+ * of this class will be created if the
+ * {@link CompensatingTransactionOperationManager} cannot determine any appropriate
+ * {@link CompensatingTransactionOperationRecorder} for the current operation.
+ *
+ * @author Mattias Arthursson
+ *
+ */
+public class NullOperationRecorder implements
+ CompensatingTransactionOperationRecorder {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationRecorder#recordOperation(java.lang.Object[])
+ */
+ public CompensatingTransactionOperationExecutor recordOperation(
+ Object[] args) {
+ return new NullOperationExecutor();
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RebindOperationExecutor.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RebindOperationExecutor.java
new file mode 100644
index 00000000..1f105a4d
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RebindOperationExecutor.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+import javax.naming.directory.Attributes;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+
+/**
+ * A {@link CompensatingTransactionOperationExecutor} to manage a rebind
+ * operation. The methods in this class do not behave as expected, since it
+ * might be impossible to retrieve all the original attributes from the entry.
+ * Instead this class performs a rename in {@link #performOperation()},
+ * a negating rename in {@link #rollback()}, and the {@link #commit()}
+ * operation unbinds the original entry from its temporary location and binds a
+ * new entry to the original location using the attributes supplied to the
+ * original rebind opertaion.
+ *
+ * @author Mattias Arthursson
+ */
+public class RebindOperationExecutor implements
+ CompensatingTransactionOperationExecutor {
+
+ private static Log log = LogFactory.getLog(RebindOperationExecutor.class);
+
+ private LdapOperations ldapOperations;
+
+ private Name originalDn;
+
+ private Name temporaryDn;
+
+ private Object originalObject;
+
+ private Attributes originalAttributes;
+
+ /**
+ * Constructor.
+ *
+ * @param ldapOperations
+ * the {@link LdapOperations} to use to perform the rollback.
+ * @param originalDn
+ * The original DN of the entry to bind.
+ * @param temporaryDn
+ * The temporary DN of the entry.
+ * @param originalObject
+ * Original 'object' parameter sent to the rebind operation.
+ * @param originalAttributes
+ * Original 'attributes' parameter sent to the rebind operation
+ */
+ public RebindOperationExecutor(LdapOperations ldapOperations,
+ Name originalDn, Name temporaryDn, Object originalObject,
+ Attributes originalAttributes) {
+ this.ldapOperations = ldapOperations;
+ this.originalDn = originalDn;
+ this.temporaryDn = temporaryDn;
+ this.originalObject = originalObject;
+ this.originalAttributes = originalAttributes;
+ }
+
+ /**
+ * Get the LdapOperations. Package private for testing purposes.
+ *
+ * @return the LdapOperations.
+ */
+ LdapOperations getLdapOperations() {
+ return ldapOperations;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#rollback()
+ */
+ public void rollback() {
+ log.debug("Rolling back rebind operation");
+ try {
+ ldapOperations.unbind(originalDn);
+ ldapOperations.rename(temporaryDn, originalDn);
+ } catch (Exception e) {
+ log.warn("Failed to rollback operation, dn: " + originalDn
+ + "; temporary DN: " + temporaryDn, e);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#commit()
+ */
+ public void commit() {
+ log.debug("Committing rebind operation");
+ ldapOperations.unbind(temporaryDn);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#performOperation()
+ */
+ public void performOperation() {
+ log.debug("Performing rebind operation - "
+ + "renaming original entry and "
+ + "binding new contents to entry.");
+ ldapOperations.rename(originalDn, temporaryDn);
+ ldapOperations.bind(originalDn, originalObject, originalAttributes);
+ }
+
+ Attributes getOriginalAttributes() {
+ return originalAttributes;
+ }
+
+ Name getOriginalDn() {
+ return originalDn;
+ }
+
+ Object getOriginalObject() {
+ return originalObject;
+ }
+
+ Name getTemporaryDn() {
+ return temporaryDn;
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RebindOperationRecorder.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RebindOperationRecorder.java
new file mode 100644
index 00000000..709974c6
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RebindOperationRecorder.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+import javax.naming.directory.Attributes;
+
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+
+/**
+ * A {@link CompensatingTransactionOperationRecorder} keeping track of a rebind
+ * operation. Creates {@link RebindOperationExecutor} objects in
+ * {@link #recordOperation(Object[])}.
+ *
+ * @author Mattias Arthursson
+ */
+public class RebindOperationRecorder implements
+ CompensatingTransactionOperationRecorder {
+
+ private LdapOperations ldapOperations;
+
+ private TempEntryRenamingStrategy renamingStrategy;
+
+ /**
+ * Constructor.
+ *
+ * @param ldapOperations
+ * {@link LdapOperations} to use for getting the rollback
+ * information and supply to the {@link RebindOperationExecutor}.
+ * @param the
+ * {@link TempEntryRenamingStrategy} to use for generating temp
+ * DNs.
+ */
+ public RebindOperationRecorder(LdapOperations ldapOperations,
+ TempEntryRenamingStrategy renamingStrategy) {
+ this.ldapOperations = ldapOperations;
+ this.renamingStrategy = renamingStrategy;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationRecorder#recordOperation(java.lang.Object[])
+ */
+ public CompensatingTransactionOperationExecutor recordOperation(
+ Object[] args) {
+ if (args == null || args.length != 3) {
+ throw new IllegalArgumentException(
+ "Invalid arguments for bind operation");
+ }
+ Name dn = LdapUtils.getFirstArgumentAsName(args);
+ Object object = args[1];
+ Attributes attributes = null;
+ if (args[2] != null && !(args[2] instanceof Attributes)) {
+ throw new IllegalArgumentException(
+ "Invalid third argument to bind operation");
+ } else if (args[2] != null) {
+ attributes = (Attributes) args[2];
+ }
+
+ Name temporaryName = renamingStrategy.getTemporaryName(dn);
+
+ return new RebindOperationExecutor(ldapOperations, dn, temporaryName,
+ object, attributes);
+ }
+
+ /**
+ * Get the LdapOperations. For testing purposes.
+ *
+ * @return the LdapOperations.
+ */
+ LdapOperations getLdapOperations() {
+ return ldapOperations;
+ }
+
+ public TempEntryRenamingStrategy getRenamingStrategy() {
+ return renamingStrategy;
+ }
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RenameOperationExecutor.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RenameOperationExecutor.java
new file mode 100644
index 00000000..af14b3f7
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RenameOperationExecutor.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+
+/**
+ * A {@link CompensatingTransactionOperationExecutor} to manage a rename
+ * operation. Performs a rename operation in {@link #performOperation()}, a
+ * negating rename in {@link #rollback()}, and nothing in {@link #commit()}.
+ *
+ * @author Mattias Arthursson
+ *
+ */
+public class RenameOperationExecutor implements
+ CompensatingTransactionOperationExecutor {
+
+ private static Log log = LogFactory.getLog(RenameOperationExecutor.class);
+
+ private LdapOperations ldapOperations;
+
+ private Name newDn;
+
+ private Name originalDn;
+
+ /**
+ * Constructor.
+ *
+ * @param ldapOperations
+ * The {@link LdapOperations} to use for performing the rollback
+ * operation.
+ * @param originalDn
+ * DN that the entry was moved from in the recorded operation.
+ * @param newDn
+ * DN that the entry has been moved to in the recorded operation.
+ */
+ public RenameOperationExecutor(LdapOperations ldapOperations,
+ Name originalDn, Name newDn) {
+ this.ldapOperations = ldapOperations;
+ this.originalDn = originalDn;
+ this.newDn = newDn;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#rollback()
+ */
+ public void rollback() {
+ log.debug("Rolling back rename operation");
+ try {
+ ldapOperations.rename(newDn, originalDn);
+ } catch (Exception e) {
+ log.warn("Unable to rollback rename operation. " + "originalDn: "
+ + newDn + "; newDn: " + originalDn);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#commit()
+ */
+ public void commit() {
+ log.debug("Nothing to do in commit for rename operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#performOperation()
+ */
+ public void performOperation() {
+ log.debug("Performing rename operation");
+ ldapOperations.rename(originalDn, newDn);
+ }
+
+ Name getNewDn() {
+ return newDn;
+ }
+
+ LdapOperations getLdapOperations() {
+ return ldapOperations;
+ }
+
+ Name getOriginalDn() {
+ return originalDn;
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RenameOperationRecorder.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RenameOperationRecorder.java
new file mode 100644
index 00000000..fa8ccbf4
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/RenameOperationRecorder.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link CompensatingTransactionOperationRecorder} for keeping track of
+ * rename operations. Creates {@link RebindOperationExecutor} objects for
+ * rolling back.
+ *
+ * @author Mattias Arthursson
+ *
+ */
+public class RenameOperationRecorder implements
+ CompensatingTransactionOperationRecorder {
+
+ private static Log log = LogFactory.getLog(RenameOperationRecorder.class);
+
+ private LdapOperations ldapOperations;
+
+ /**
+ * Constructor.
+ *
+ * @param ldapOperations
+ * The {@link LdapOperations} to supply to the created
+ * {@link RebindOperationExecutor} objects.
+ */
+ public RenameOperationRecorder(LdapOperations ldapOperations) {
+ this.ldapOperations = ldapOperations;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationRecorder#recordOperation(java.lang.Object[])
+ */
+ public CompensatingTransactionOperationExecutor recordOperation(
+ Object[] args) {
+ log.debug("Storing rollback information for rename operation");
+ Assert.notEmpty(args);
+ if (args.length != 2) {
+ // This really shouldn't happen.
+ throw new IllegalArgumentException("Illegal argument length");
+ }
+ Name oldDn = LdapUtils.getArgumentAsName(args[0]);
+ Name newDn = LdapUtils.getArgumentAsName(args[1]);
+ return new RenameOperationExecutor(ldapOperations, oldDn, newDn);
+ }
+
+ LdapOperations getLdapOperations() {
+ return ldapOperations;
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/TempEntryRenamingStrategy.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/TempEntryRenamingStrategy.java
new file mode 100644
index 00000000..869378f8
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/TempEntryRenamingStrategy.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+
+/**
+ * Interface for different strategies to rename temporary entries for unbind and
+ * rebind operations.
+ *
+ * @author Mattias Arthursson
+ */
+public interface TempEntryRenamingStrategy {
+
+ /**
+ * Get a temporary name for the current entry to be renamed to.
+ *
+ * @param originalName
+ * The original name of the entry.
+ * @return The name to which the entry should be temporarily renamed
+ * according to this strategy.
+ */
+ public Name getTemporaryName(Name originalName);
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/TransactionAwareContextSourceProxy.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/TransactionAwareContextSourceProxy.java
new file mode 100644
index 00000000..283810ff
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/TransactionAwareContextSourceProxy.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import java.lang.reflect.Proxy;
+
+import javax.naming.directory.DirContext;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+/**
+ * A proxy for ContextSource to make sure that the returned DirContext objects
+ * are aware of the surrounding transactions, making sure that the DirContext is
+ * not closed during the transaction and that all modifying operations are
+ * recorded, keeping track of the corresponding rollback operations. All
+ * returned DirContext instances will be of the type
+ * {@link TransactionAwareDirContextInvocationHandler}.
+ *
+ * @author Mattias Arthursson
+ */
+public class TransactionAwareContextSourceProxy implements ContextSource {
+ private ContextSource target;
+
+ /**
+ * Constructor.
+ *
+ * @param target
+ * the target ContextSource.
+ */
+ public TransactionAwareContextSourceProxy(ContextSource target) {
+ this.target = target;
+ }
+
+ /**
+ * Get the target ContextSource.
+ *
+ * @return the target ContextSource.
+ */
+ public ContextSource getTarget() {
+ return target;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.core.ContextSource#getReadOnlyContext()
+ */
+ public DirContext getReadOnlyContext() throws DataAccessException {
+ return getReadWriteContext();
+ }
+
+ private DirContext getTransactionAwareDirContextProxy(DirContext context,
+ ContextSource target) {
+ return (DirContext) Proxy
+ .newProxyInstance(DirContextProxy.class.getClassLoader(),
+ new Class[] { DirContextProxy.class },
+ new TransactionAwareDirContextInvocationHandler(
+ context, target));
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.core.ContextSource#getReadWriteContext()
+ */
+ public DirContext getReadWriteContext() throws DataAccessException {
+ DirContextHolder contextHolder = (DirContextHolder) TransactionSynchronizationManager
+ .getResource(target);
+ DirContext ctx = null;
+
+ if (contextHolder != null) {
+ ctx = contextHolder.getCtx();
+ }
+
+ if (ctx == null) {
+ ctx = target.getReadWriteContext();
+ if (contextHolder != null) {
+ contextHolder.setCtx(ctx);
+ }
+ }
+ return getTransactionAwareDirContextProxy(ctx, target);
+ }
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/TransactionAwareDirContextInvocationHandler.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/TransactionAwareDirContextInvocationHandler.java
new file mode 100644
index 00000000..bbad4ac8
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/TransactionAwareDirContextInvocationHandler.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import javax.naming.directory.DirContext;
+
+import org.springframework.ldap.core.ContextSource;
+
+/**
+ * Proxy implementation for DirContext, making sure that the instance is not
+ * closed during a transaction, and that all modifying operations are recorded,
+ * storing compensating rollback operations for them.
+ *
+ * @author Mattias Arthursson
+ */
+public class TransactionAwareDirContextInvocationHandler implements
+ InvocationHandler {
+
+ private DirContext target;
+
+ private ContextSource contextSource;
+
+ /**
+ * Constructor.
+ *
+ * @param target
+ * The target DirContext.
+ * @param contextSource
+ * The transactional ContextSource, needed to get hold of the
+ * current transaction's {@link DirContextHolder}.
+ */
+ public TransactionAwareDirContextInvocationHandler(DirContext target,
+ ContextSource contextSource) {
+ this.target = target;
+ this.contextSource = contextSource;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object,
+ * java.lang.reflect.Method, java.lang.Object[])
+ */
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+
+ String methodName = method.getName();
+ if (methodName.equals("getTargetContext")) {
+ return target;
+ } else if (methodName.equals("equals")) {
+ // Only consider equal when proxies are identical.
+ return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE);
+ } else if (methodName.equals("hashCode")) {
+ // Use hashCode of Connection proxy.
+ return new Integer(proxy.hashCode());
+ } else if (methodName.equals("close")) {
+ LdapUtils.doCloseConnection(target, contextSource);
+ return null;
+ } else if (LdapUtils.isSupportedWriteTransactionOperation(methodName)) {
+ // Store transaction data and allow operation to proceed.
+ LdapUtils.performOperation(contextSource, method, args);
+ return null;
+ } else {
+ try {
+ return method.invoke(target, args);
+ } catch (InvocationTargetException e) {
+ throw e.getTargetException();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/UnbindOperationExecutor.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/UnbindOperationExecutor.java
new file mode 100644
index 00000000..d47f1127
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/UnbindOperationExecutor.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+
+/**
+ * A {@link CompensatingTransactionOperationExecutor} to manage an unbind
+ * operation. The methods in this class do not behave as expected, since it
+ * might be impossible to retrieve all the original attributes from the entry.
+ * Instead this class performs a rename in {@link #performOperation()},
+ * a negating rename in {@link #rollback()}, and {@link #commit()} unbinds the
+ * entry from its temporary location.
+ *
+ * @author Mattias Arthursson
+ */
+public class UnbindOperationExecutor implements
+ CompensatingTransactionOperationExecutor {
+
+ private static Log log = LogFactory.getLog(UnbindOperationExecutor.class);
+
+ private LdapOperations ldapOperations;
+
+ private Name originalDn;
+
+ private Name temporaryDn;
+
+ /**
+ * Constructor.
+ *
+ * @param ldapOperations
+ * The {@link LdapOperations} to use for performing the rollback
+ * operation.
+ * @param originalDn
+ * The original DN of the entry to be removed.
+ * @param temporaryDn
+ * Temporary DN of the entry to be removed; this is where the
+ * entry is temporarily stored during the transaction.
+ */
+ public UnbindOperationExecutor(LdapOperations ldapOperations,
+ Name originalDn, Name temporaryDn) {
+ this.ldapOperations = ldapOperations;
+ this.originalDn = originalDn;
+ this.temporaryDn = temporaryDn;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#rollback()
+ */
+ public void rollback() {
+ try {
+ ldapOperations.rename(temporaryDn, originalDn);
+ } catch (Exception e) {
+ log.warn("Filed to rollback unbind operation, temporaryDn: "
+ + temporaryDn + "; originalDn: " + originalDn);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#commit()
+ */
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#commit()
+ */
+ public void commit() {
+ log.debug("Committing unbind operation - unbinding temporary entry");
+ ldapOperations.unbind(temporaryDn);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationExecutor#performOperation()
+ */
+ public void performOperation() {
+ log.debug("Performing operation for unbind -"
+ + " renaming to temporary entry.");
+ ldapOperations.rename(originalDn, temporaryDn);
+ }
+
+ LdapOperations getLdapOperations() {
+ return ldapOperations;
+ }
+
+ Name getOriginalDn() {
+ return originalDn;
+ }
+
+ Name getTemporaryDn() {
+ return temporaryDn;
+ }
+
+}
diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/UnbindOperationRecorder.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/UnbindOperationRecorder.java
new file mode 100644
index 00000000..084b20fa
--- /dev/null
+++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/core/UnbindOperationRecorder.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-2007 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+
+/**
+ * {@link CompensatingTransactionOperationRecorder} to keep track of unbind
+ * operations. This class creates {@link UnbindOperationExecutor} objects for
+ * rollback.
+ *
+ * @author Mattias Arthursson
+ */
+public class UnbindOperationRecorder implements
+ CompensatingTransactionOperationRecorder {
+
+ private LdapOperations ldapOperations;
+
+ private TempEntryRenamingStrategy renamingStrategy;
+
+ /**
+ * Constructor.
+ *
+ * @param ldapOperations
+ * {@link LdapOperations} to use for getting the data prior to
+ * unbinding the entry and to supply to the
+ * {@link UnbindOperationExecutor} for rollback.
+ * @param renamingStrategy
+ * the {@link TempEntryRenamingStrategy} to use when generating
+ * DNs for temporary entries.
+ */
+ public UnbindOperationRecorder(LdapOperations ldapOperations,
+ TempEntryRenamingStrategy renamingStrategy) {
+ this.ldapOperations = ldapOperations;
+ this.renamingStrategy = renamingStrategy;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.support.transaction.CompensatingTransactionOperationRecorder#recordOperation(java.lang.Object[])
+ */
+ public CompensatingTransactionOperationExecutor recordOperation(
+ Object[] args) {
+ Name dn = LdapUtils.getFirstArgumentAsName(args);
+ Name temporaryDn = renamingStrategy.getTemporaryName(dn);
+
+ return new UnbindOperationExecutor(ldapOperations, dn, temporaryDn);
+ }
+
+ LdapOperations getLdapOperations() {
+ return ldapOperations;
+ }
+
+ public TempEntryRenamingStrategy getRenamingStrategy() {
+ return renamingStrategy;
+ }
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/DefaultCompensatingTransactionOperationManagerTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/DefaultCompensatingTransactionOperationManagerTest.java
new file mode 100644
index 00000000..f22ca6f3
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/DefaultCompensatingTransactionOperationManagerTest.java
@@ -0,0 +1,116 @@
+package org.springframework.ldap.transaction;
+
+import java.util.Stack;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationFactory;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+import org.springframework.ldap.transaction.DefaultCompensatingTransactionOperationManager;
+
+public class DefaultCompensatingTransactionOperationManagerTest extends
+ TestCase {
+
+ private MockControl operationExecutorControl;
+
+ private CompensatingTransactionOperationExecutor operationExecutorMock;
+
+ private MockControl operationFactoryControl;
+
+ private CompensatingTransactionOperationFactory operationFactoryMock;
+
+ private MockControl operationRecorderControl;
+
+ private CompensatingTransactionOperationRecorder operationRecorderMock;
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ operationExecutorControl = MockControl
+ .createControl(CompensatingTransactionOperationExecutor.class);
+ operationExecutorMock = (CompensatingTransactionOperationExecutor) operationExecutorControl
+ .getMock();
+
+ operationFactoryControl = MockControl
+ .createControl(CompensatingTransactionOperationFactory.class);
+ operationFactoryMock = (CompensatingTransactionOperationFactory) operationFactoryControl
+ .getMock();
+
+ operationRecorderControl = MockControl
+ .createControl(CompensatingTransactionOperationRecorder.class);
+ operationRecorderMock = (CompensatingTransactionOperationRecorder) operationRecorderControl
+ .getMock();
+
+ }
+
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ operationExecutorControl = null;
+ operationExecutorMock = null;
+
+ operationFactoryControl = null;
+ operationFactoryMock = null;
+
+ operationRecorderControl = null;
+ operationRecorderMock = null;
+ }
+
+ protected void replay() {
+ operationExecutorControl.replay();
+ operationFactoryControl.replay();
+ operationRecorderControl.replay();
+ }
+
+ protected void verify() {
+ operationExecutorControl.verify();
+ operationFactoryControl.verify();
+ operationRecorderControl.verify();
+ }
+
+ public void testPerformOperation() {
+ Object[] expectedArgs = new Object[0];
+
+ operationFactoryControl
+ .expectAndReturn(operationFactoryMock
+ .createRecordingOperation("some method"),
+ operationRecorderMock);
+ operationRecorderControl.expectAndReturn(operationRecorderMock
+ .recordOperation(expectedArgs), operationExecutorMock);
+ operationExecutorMock.performOperation();
+
+ DefaultCompensatingTransactionOperationManager tested = new DefaultCompensatingTransactionOperationManager(
+ operationFactoryMock);
+ replay();
+ tested.performOperation("some method", expectedArgs);
+ verify();
+
+ Stack result = tested.getRollbackOperations();
+ assertFalse(result.isEmpty());
+ assertSame(operationExecutorMock, result.peek());
+ }
+
+ public void testRollback() {
+ DefaultCompensatingTransactionOperationManager tested = new DefaultCompensatingTransactionOperationManager(
+ operationFactoryMock);
+ tested.getRollbackOperations().push(operationExecutorMock);
+
+ operationExecutorMock.rollback();
+
+ replay();
+ tested.rollback();
+ verify();
+ }
+
+ public void testCommit() {
+ DefaultCompensatingTransactionOperationManager tested = new DefaultCompensatingTransactionOperationManager(
+ operationFactoryMock);
+ tested.getRollbackOperations().push(operationExecutorMock);
+
+ operationExecutorMock.commit();
+
+ replay();
+ tested.commit();
+ verify();
+ }
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/BindOperationExecutorTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/BindOperationExecutorTest.java
new file mode 100644
index 00000000..466ee492
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/BindOperationExecutorTest.java
@@ -0,0 +1,80 @@
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.directory.BasicAttributes;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.core.BindOperationExecutor;
+
+public class BindOperationExecutorTest 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 testPerformOperation() {
+ DistinguishedName expectedDn = new DistinguishedName("cn=john doe");
+ Object expectedObject = new Object();
+ BasicAttributes expectedAttributes = new BasicAttributes();
+ BindOperationExecutor tested = new BindOperationExecutor(
+ ldapOperationsMock, expectedDn, expectedObject,
+ expectedAttributes);
+
+ ldapOperationsMock.bind(expectedDn, expectedObject, expectedAttributes);
+
+ replay();
+ // perform teste
+ tested.performOperation();
+ verify();
+ }
+
+ public void testCommit() {
+ DistinguishedName expectedDn = new DistinguishedName("cn=john doe");
+ Object expectedObject = new Object();
+ BasicAttributes expectedAttributes = new BasicAttributes();
+ BindOperationExecutor tested = new BindOperationExecutor(
+ ldapOperationsMock, expectedDn, expectedObject,
+ expectedAttributes);
+
+ // Nothing to do here.
+
+ replay();
+ // perform teste
+ tested.commit();
+ verify();
+ }
+
+ public void testRollback() {
+ DistinguishedName expectedDn = new DistinguishedName("cn=john doe");
+ BindOperationExecutor tested = new BindOperationExecutor(
+ ldapOperationsMock, expectedDn, null, null);
+
+ ldapOperationsMock.unbind(expectedDn);
+
+ replay();
+ // perform teste
+ tested.rollback();
+ verify();
+ }
+
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/BindOperationRecorderTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/BindOperationRecorderTest.java
new file mode 100644
index 00000000..8db3074e
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/BindOperationRecorderTest.java
@@ -0,0 +1,83 @@
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.directory.BasicAttributes;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.core.BindOperationExecutor;
+import org.springframework.ldap.transaction.core.BindOperationRecorder;
+
+public class BindOperationRecorderTest 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;
+ }
+
+ public void testRecordOperation_DistinguishedName() {
+ BindOperationRecorder tested = new BindOperationRecorder(
+ ldapOperationsMock);
+ DistinguishedName expectedDn = new DistinguishedName("cn=John Doe");
+
+ Object expectedObject = new Object();
+ BasicAttributes expectedAttributes = new BasicAttributes();
+ // Perform test.
+ CompensatingTransactionOperationExecutor operation = tested
+ .recordOperation(new Object[] { expectedDn, expectedObject,
+ expectedAttributes });
+
+ assertTrue(operation instanceof BindOperationExecutor);
+ BindOperationExecutor rollbackOperation = (BindOperationExecutor) operation;
+ assertSame(expectedDn, rollbackOperation.getDn());
+ assertSame(ldapOperationsMock, rollbackOperation.getLdapOperations());
+ assertSame(expectedObject, rollbackOperation.getOriginalObject());
+ assertSame(expectedAttributes, rollbackOperation
+ .getOriginalAttributes());
+ }
+
+ public void testPerformOperation_String() {
+ BindOperationRecorder tested = new BindOperationRecorder(
+ ldapOperationsMock);
+ String expectedDn = "cn=John Doe";
+
+ Object expectedObject = new Object();
+ BasicAttributes expectedAttributes = new BasicAttributes();
+ // Perform test.
+ CompensatingTransactionOperationExecutor operation = tested
+ .recordOperation(new Object[] { expectedDn, expectedObject,
+ expectedAttributes });
+
+ assertTrue(operation instanceof BindOperationExecutor);
+ BindOperationExecutor rollbackOperation = (BindOperationExecutor) operation;
+ assertEquals(expectedDn, rollbackOperation.getDn().toString());
+ assertSame(ldapOperationsMock, rollbackOperation.getLdapOperations());
+ }
+
+ public void testPerformOperation_Invalid() {
+ BindOperationRecorder tested = new BindOperationRecorder(
+ ldapOperationsMock);
+ Object expectedDn = new Object();
+
+ try {
+ // Perform test.
+ tested.recordOperation(new Object[] { expectedDn });
+ fail("IllegalArgumentException expected");
+ } catch (IllegalArgumentException expected) {
+ assertTrue(true);
+ }
+
+ }
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManagerTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManagerTest.java
new file mode 100644
index 00000000..8846f12f
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManagerTest.java
@@ -0,0 +1,187 @@
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.directory.DirContext;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationManager;
+import org.springframework.ldap.transaction.core.ContextSourceTransactionManager;
+import org.springframework.ldap.transaction.core.ContextSourceTransactionObject;
+import org.springframework.ldap.transaction.core.DirContextHolder;
+import org.springframework.ldap.transaction.core.TempEntryRenamingStrategy;
+import org.springframework.ldap.transaction.core.TransactionAwareContextSourceProxy;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.support.DefaultTransactionStatus;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+public class ContextSourceTransactionManagerTest extends TestCase {
+
+ private MockControl contextSourceControl;
+
+ private ContextSource contextSourceMock;
+
+ private MockControl contextControl;
+
+ private DirContext contextMock;
+
+ private ContextSourceTransactionManager tested;
+
+ private MockControl transactionDefinitionControl;
+
+ private MockControl transactionDataManagerControl;
+
+ private CompensatingTransactionOperationManager transactionDataManagerMock;
+
+ private TransactionDefinition transactionDefinitionMock;
+
+ private MockControl renamingStrategyControl;
+
+ private TempEntryRenamingStrategy renamingStrategyMock;
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ if (TransactionSynchronizationManager.isSynchronizationActive()) {
+ TransactionSynchronizationManager.clearSynchronization();
+ }
+
+ contextSourceControl = MockControl.createControl(ContextSource.class);
+ contextSourceMock = (ContextSource) contextSourceControl.getMock();
+
+ contextControl = MockControl.createControl(DirContext.class);
+ contextMock = (DirContext) contextControl.getMock();
+
+ transactionDefinitionControl = MockControl
+ .createControl(TransactionDefinition.class);
+ transactionDefinitionMock = (TransactionDefinition) transactionDefinitionControl
+ .getMock();
+
+ transactionDataManagerControl = MockControl
+ .createControl(CompensatingTransactionOperationManager.class);
+ transactionDataManagerMock = (CompensatingTransactionOperationManager) transactionDataManagerControl
+ .getMock();
+
+ renamingStrategyControl = MockControl
+ .createControl(TempEntryRenamingStrategy.class);
+ renamingStrategyMock = (TempEntryRenamingStrategy) renamingStrategyControl
+ .getMock();
+
+ tested = new ContextSourceTransactionManager();
+ tested.setContextSource(contextSourceMock);
+ tested.setRenamingStrategy(renamingStrategyMock);
+ }
+
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ contextControl = null;
+ contextMock = null;
+
+ contextSourceControl = null;
+ contextSourceMock = null;
+
+ transactionDefinitionControl = null;
+ transactionDefinitionMock = null;
+
+ transactionDataManagerControl = null;
+ transactionDataManagerMock = null;
+
+ renamingStrategyControl = null;
+ renamingStrategyMock = null;
+
+ if (TransactionSynchronizationManager.isSynchronizationActive()) {
+ TransactionSynchronizationManager.clearSynchronization();
+ }
+ }
+
+ public void testDoGetTransaction() {
+ Object result = tested.doGetTransaction();
+
+ assertNotNull(result);
+ assertTrue(result instanceof ContextSourceTransactionObject);
+ ContextSourceTransactionObject transactionObject = (ContextSourceTransactionObject) result;
+ assertNull(transactionObject.getContextHolder());
+ }
+
+ public void testDoGetTransactionTransactionActive() {
+ DirContextHolder expectedContextHolder = new DirContextHolder(null,
+ null);
+ TransactionSynchronizationManager.bindResource(contextSourceMock,
+ expectedContextHolder);
+ Object result = tested.doGetTransaction();
+ assertSame(expectedContextHolder,
+ ((ContextSourceTransactionObject) result).getContextHolder());
+ }
+
+ public void testDoBegin() {
+ contextSourceControl.expectAndReturn(contextSourceMock
+ .getReadOnlyContext(), contextMock);
+
+ contextSourceControl.replay();
+
+ ContextSourceTransactionObject expectedTransactionObject = new ContextSourceTransactionObject(
+ null);
+ tested.doBegin(expectedTransactionObject, transactionDefinitionMock);
+
+ contextSourceControl.verify();
+
+ DirContextHolder foundContextHolder = (DirContextHolder) TransactionSynchronizationManager
+ .getResource(contextSourceMock);
+ assertSame(contextMock, foundContextHolder.getCtx());
+ assertSame(renamingStrategyMock, foundContextHolder
+ .getRenamingStrategy());
+ }
+
+ public void testDoCommit() {
+ }
+
+ public void testDoRollback() {
+
+ DirContextHolder expectedContextHolder = new DirContextHolder(
+ contextMock, renamingStrategyMock);
+ expectedContextHolder
+ .setTransactionDataManager(transactionDataManagerMock);
+ TransactionSynchronizationManager.bindResource(contextSourceMock,
+ expectedContextHolder);
+
+ transactionDataManagerMock.rollback();
+ transactionDataManagerControl.replay();
+ ContextSourceTransactionObject transactionObject = new ContextSourceTransactionObject(
+ null);
+ transactionObject.setContextHolder(expectedContextHolder);
+ tested.doRollback(new DefaultTransactionStatus(transactionObject,
+ false, false, false, false, null));
+ transactionDataManagerControl.verify();
+ }
+
+ public void testDoCleanupAfterCompletion() throws Exception {
+ DirContextHolder expectedContextHolder = new DirContextHolder(
+ contextMock, renamingStrategyMock);
+ TransactionSynchronizationManager.bindResource(contextSourceMock,
+ expectedContextHolder);
+
+ contextMock.close();
+ contextControl.replay();
+
+ tested.doCleanupAfterCompletion(new ContextSourceTransactionObject(
+ expectedContextHolder));
+
+ contextControl.verify();
+ assertNull(TransactionSynchronizationManager
+ .getResource(contextSourceMock));
+ assertNull(expectedContextHolder.getTransactionDataManager());
+ }
+
+ public void testSetContextSource_Proxy() {
+ TransactionAwareContextSourceProxy proxy = new TransactionAwareContextSourceProxy(
+ contextSourceMock);
+
+ // Perform test
+ tested.setContextSource(proxy);
+ ContextSource result = tested.getContextSource();
+
+ // Verify result
+ assertSame(contextSourceMock, result);
+ }
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/DefaultTempEntryRenamingStrategyTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/DefaultTempEntryRenamingStrategyTest.java
new file mode 100644
index 00000000..934c6d6d
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/DefaultTempEntryRenamingStrategyTest.java
@@ -0,0 +1,34 @@
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.transaction.core.DefaultTempEntryRenamingStrategy;
+
+import junit.framework.TestCase;
+
+public class DefaultTempEntryRenamingStrategyTest extends TestCase {
+
+ public void testGetTemporaryName() {
+ DistinguishedName expectedOriginalName = new DistinguishedName(
+ "cn=john doe, ou=somecompany, c=SE");
+ DefaultTempEntryRenamingStrategy tested = new DefaultTempEntryRenamingStrategy();
+
+ Name result = tested.getTemporaryName(expectedOriginalName);
+ assertEquals("cn=john doe_temp, ou=somecompany, c=SE", result
+ .toString());
+ assertNotSame(expectedOriginalName, result);
+ }
+
+ public void testGetTemporaryDN_MultivalueDN() {
+ DistinguishedName expectedOriginalName = new DistinguishedName(
+ "cn=john doe+sn=doe, ou=somecompany, c=SE");
+ DefaultTempEntryRenamingStrategy tested = new DefaultTempEntryRenamingStrategy();
+
+ Name result = tested.getTemporaryName(expectedOriginalName);
+ assertEquals("cn=john doe_temp+sn=doe, ou=somecompany, c=SE", result
+ .toString());
+ }
+
+
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/DifferentSubtreeTempEntryRenamingStrategyTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/DifferentSubtreeTempEntryRenamingStrategyTest.java
new file mode 100644
index 00000000..8e946c1f
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/DifferentSubtreeTempEntryRenamingStrategyTest.java
@@ -0,0 +1,28 @@
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.transaction.core.DifferentSubtreeTempEntryRenamingStrategy;
+
+import junit.framework.TestCase;
+
+public class DifferentSubtreeTempEntryRenamingStrategyTest extends TestCase {
+
+ public void testGetTemporaryName() {
+ DistinguishedName originalName = new DistinguishedName(
+ "cn=john doe, ou=somecompany, c=SE");
+ DifferentSubtreeTempEntryRenamingStrategy tested = new DifferentSubtreeTempEntryRenamingStrategy(
+ new DistinguishedName("ou=tempEntries"));
+
+ int nextSequenceNo = tested.getNextSequenceNo();
+
+ // Perform test
+ Name result = tested.getTemporaryName(originalName);
+
+ // Verify result
+ assertEquals("cn=john doe" + nextSequenceNo + ", ou=tempEntries",
+ result.toString());
+ }
+
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/LdapCompensatingTransactionOperationFactoryTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/LdapCompensatingTransactionOperationFactoryTest.java
new file mode 100644
index 00000000..e90d0626
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/LdapCompensatingTransactionOperationFactoryTest.java
@@ -0,0 +1,120 @@
+package org.springframework.ldap.transaction.core;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationRecorder;
+import org.springframework.ldap.transaction.core.BindOperationRecorder;
+import org.springframework.ldap.transaction.core.LdapCompensatingTransactionOperationFactory;
+import org.springframework.ldap.transaction.core.ModifyAttributesOperationRecorder;
+import org.springframework.ldap.transaction.core.RebindOperationRecorder;
+import org.springframework.ldap.transaction.core.RenameOperationRecorder;
+import org.springframework.ldap.transaction.core.TempEntryRenamingStrategy;
+import org.springframework.ldap.transaction.core.UnbindOperationRecorder;
+
+public class LdapCompensatingTransactionOperationFactoryTest extends TestCase {
+ private MockControl ldapOperationsControl;
+
+ private LdapOperations ldapOperationsMock;
+
+ private MockControl renamingStrategyControl;
+
+ private TempEntryRenamingStrategy renamingStrategyMock;
+
+ protected void setUp() throws Exception {
+ ldapOperationsControl = MockControl.createControl(LdapOperations.class);
+ ldapOperationsMock = (LdapOperations) ldapOperationsControl.getMock();
+
+ renamingStrategyControl = MockControl
+ .createControl(TempEntryRenamingStrategy.class);
+ renamingStrategyMock = (TempEntryRenamingStrategy) renamingStrategyControl
+ .getMock();
+
+ }
+
+ protected void tearDown() throws Exception {
+ ldapOperationsControl = null;
+ ldapOperationsMock = null;
+
+ renamingStrategyControl = null;
+ renamingStrategyMock = null;
+ }
+
+ protected void replay() {
+ ldapOperationsControl.replay();
+ renamingStrategyControl.replay();
+ }
+
+ protected void verify() {
+ ldapOperationsControl.verify();
+ renamingStrategyControl.verify();
+ }
+
+ public void testGetRecordingOperation_Bind() throws Exception {
+ LdapCompensatingTransactionOperationFactory tested = new LdapCompensatingTransactionOperationFactory(
+ null, renamingStrategyMock);
+ tested.setLdapOperations(ldapOperationsMock);
+
+ CompensatingTransactionOperationRecorder result = tested
+ .createRecordingOperation("bind");
+ assertTrue(result instanceof BindOperationRecorder);
+ BindOperationRecorder bindOperationRecorder = (BindOperationRecorder) result;
+ assertSame(ldapOperationsMock, bindOperationRecorder
+ .getLdapOperations());
+ }
+
+ public void testGetRecordingOperation_Rebind() throws Exception {
+ LdapCompensatingTransactionOperationFactory tested = new LdapCompensatingTransactionOperationFactory(
+ null, renamingStrategyMock);
+ tested.setLdapOperations(ldapOperationsMock);
+
+ CompensatingTransactionOperationRecorder result = tested
+ .createRecordingOperation("rebind");
+ assertTrue(result instanceof RebindOperationRecorder);
+ RebindOperationRecorder rebindOperationRecorder = (RebindOperationRecorder) result;
+ assertSame(ldapOperationsMock, rebindOperationRecorder
+ .getLdapOperations());
+ assertSame(renamingStrategyMock, rebindOperationRecorder
+ .getRenamingStrategy());
+ }
+
+ public void testGetRecordingOperation_Rename() throws Exception {
+ LdapCompensatingTransactionOperationFactory tested = new LdapCompensatingTransactionOperationFactory(
+ null, renamingStrategyMock);
+ tested.setLdapOperations(ldapOperationsMock);
+
+ CompensatingTransactionOperationRecorder result = tested
+ .createRecordingOperation("rename");
+ assertTrue(result instanceof RenameOperationRecorder);
+ RenameOperationRecorder recordingOperation = (RenameOperationRecorder) result;
+ assertSame(ldapOperationsMock, recordingOperation.getLdapOperations());
+ }
+
+ public void testGetRecordingOperation_ModifyAttributes() throws Exception {
+ LdapCompensatingTransactionOperationFactory tested = new LdapCompensatingTransactionOperationFactory(
+ null, renamingStrategyMock);
+ tested.setLdapOperations(ldapOperationsMock);
+
+ CompensatingTransactionOperationRecorder result = tested
+ .createRecordingOperation("modifyAttributes");
+ assertTrue(result instanceof ModifyAttributesOperationRecorder);
+ ModifyAttributesOperationRecorder recordingOperation = (ModifyAttributesOperationRecorder) result;
+ assertSame(ldapOperationsMock, recordingOperation.getLdapOperations());
+ }
+
+ public void testGetRecordingOperation_Unbind() throws Exception {
+ LdapCompensatingTransactionOperationFactory tested = new LdapCompensatingTransactionOperationFactory(
+ null, renamingStrategyMock);
+ tested.setLdapOperations(ldapOperationsMock);
+
+ CompensatingTransactionOperationRecorder result = tested
+ .createRecordingOperation("unbind");
+ assertTrue(result instanceof UnbindOperationRecorder);
+ UnbindOperationRecorder recordingOperation = (UnbindOperationRecorder) result;
+ assertSame(ldapOperationsMock, recordingOperation.getLdapOperations());
+ assertSame(renamingStrategyMock, recordingOperation
+ .getRenamingStrategy());
+ }
+
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationExecutorTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationExecutorTest.java
new file mode 100644
index 00000000..f57c52a9
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationExecutorTest.java
@@ -0,0 +1,94 @@
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.Name;
+import javax.naming.directory.ModificationItem;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.core.ModifyAttributesOperationExecutor;
+
+import junit.framework.TestCase;
+
+public class ModifyAttributesOperationExecutorTest 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 testPerformOperation() {
+ ModificationItem[] expectedCompensatingItems = new ModificationItem[0];
+ ModificationItem[] expectedActualItems = new ModificationItem[0];
+
+ Name expectedDn = new DistinguishedName("cn=john doe");
+
+ ModifyAttributesOperationExecutor tested = new ModifyAttributesOperationExecutor(
+ ldapOperationsMock, expectedDn, expectedActualItems,
+ expectedCompensatingItems);
+
+ ldapOperationsMock.modifyAttributes(expectedDn, expectedActualItems);
+
+ replay();
+ // Perform test
+ tested.performOperation();
+
+ verify();
+ }
+
+ public void testCommit() {
+ ModificationItem[] expectedCompensatingItems = new ModificationItem[0];
+ ModificationItem[] expectedActualItems = new ModificationItem[0];
+
+ Name expectedDn = new DistinguishedName("cn=john doe");
+
+ ModifyAttributesOperationExecutor tested = new ModifyAttributesOperationExecutor(
+ ldapOperationsMock, expectedDn, expectedActualItems,
+ expectedCompensatingItems);
+
+ // No operation here
+
+ replay();
+ // Perform test
+ tested.commit();
+
+ verify();
+ }
+
+ public void testRollback() {
+ ModificationItem[] expectedCompensatingItems = new ModificationItem[0];
+ ModificationItem[] expectedActualItems = new ModificationItem[0];
+
+ Name expectedDn = new DistinguishedName("cn=john doe");
+
+ ModifyAttributesOperationExecutor tested = new ModifyAttributesOperationExecutor(
+ ldapOperationsMock, expectedDn, expectedActualItems,
+ expectedCompensatingItems);
+
+ ldapOperationsMock.modifyAttributes(expectedDn,
+ expectedCompensatingItems);
+
+ replay();
+ // Perform test
+ tested.rollback();
+
+ verify();
+ }
+
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationRecorderTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationRecorderTest.java
new file mode 100644
index 00000000..f9f4b1af
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/ModifyAttributesOperationRecorderTest.java
@@ -0,0 +1,256 @@
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.AttributesMapper;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.core.ModifyAttributesOperationExecutor;
+import org.springframework.ldap.transaction.core.ModifyAttributesOperationRecorder;
+
+public class ModifyAttributesOperationRecorderTest extends TestCase {
+ private MockControl ldapOperationsControl;
+
+ private LdapOperations ldapOperationsMock;
+
+ private MockControl attributesMapperControl;
+
+ private AttributesMapper attributesMapperMock;
+
+ private ModifyAttributesOperationRecorder tested;
+
+ protected void setUp() throws Exception {
+ ldapOperationsControl = MockControl.createControl(LdapOperations.class);
+ ldapOperationsMock = (LdapOperations) ldapOperationsControl.getMock();
+
+ attributesMapperControl = MockControl
+ .createControl(AttributesMapper.class);
+ attributesMapperMock = (AttributesMapper) attributesMapperControl
+ .getMock();
+
+ tested = new ModifyAttributesOperationRecorder(ldapOperationsMock);
+ }
+
+ protected void tearDown() throws Exception {
+ ldapOperationsControl = null;
+ ldapOperationsMock = null;
+
+ attributesMapperControl = null;
+ attributesMapperMock = null;
+
+ tested = null;
+ }
+
+ protected void replay() {
+ ldapOperationsControl.replay();
+ attributesMapperControl.replay();
+ }
+
+ protected void verify() {
+ ldapOperationsControl.verify();
+ attributesMapperControl.verify();
+ }
+
+ public void testRecordOperation() {
+ final ModificationItem incomingItem = new ModificationItem(
+ DirContext.ADD_ATTRIBUTE, new BasicAttribute("attribute1"));
+ ModificationItem[] incomingMods = new ModificationItem[] { incomingItem };
+ final ModificationItem compensatingItem = new ModificationItem(
+ DirContext.ADD_ATTRIBUTE, new BasicAttribute("attribute2"));
+
+ final Attributes expectedAttributes = new BasicAttributes();
+
+ tested = new ModifyAttributesOperationRecorder(ldapOperationsMock) {
+ AttributesMapper getAttributesMapper() {
+ return attributesMapperMock;
+ }
+
+ protected ModificationItem getCompensatingModificationItem(
+ Attributes originalAttributes,
+ ModificationItem modificationItem) {
+ assertSame(expectedAttributes, originalAttributes);
+ assertSame(incomingItem, modificationItem);
+ return compensatingItem;
+ }
+ };
+
+ DistinguishedName expectedName = new DistinguishedName("cn=john doe");
+ ldapOperationsControl.setDefaultMatcher(MockControl.ARRAY_MATCHER);
+ ldapOperationsControl.expectAndReturn(ldapOperationsMock.lookup(
+ expectedName, new String[] { "attribute1" },
+ attributesMapperMock), expectedAttributes);
+
+ replay();
+ // Perform test
+ CompensatingTransactionOperationExecutor operation = tested
+ .recordOperation(new Object[] { expectedName, incomingMods });
+ verify();
+
+ // Verify outcome
+ assertTrue(operation instanceof ModifyAttributesOperationExecutor);
+ ModifyAttributesOperationExecutor rollbackOperation = (ModifyAttributesOperationExecutor) operation;
+ assertSame(expectedName, rollbackOperation.getDn());
+ assertSame(ldapOperationsMock, rollbackOperation.getLdapOperations());
+ assertSame(incomingMods, rollbackOperation.getActualModifications());
+ assertEquals(1, rollbackOperation.getCompensatingModifications().length);
+ assertSame(compensatingItem, rollbackOperation
+ .getCompensatingModifications()[0]);
+ }
+
+ public void testGetCompensatingModificationItem_RemoveFullExistingAttribute()
+ throws NamingException {
+ BasicAttribute attribute = new BasicAttribute("someattr");
+ attribute.add("value1");
+ attribute.add("value2");
+ Attributes attributes = new BasicAttributes();
+ attributes.put(attribute);
+
+ ModificationItem originalItem = new ModificationItem(
+ DirContext.REMOVE_ATTRIBUTE, new BasicAttribute("someattr"));
+
+ // Perform test
+ ModificationItem result = tested.getCompensatingModificationItem(
+ attributes, originalItem);
+
+ // Verify result
+ assertEquals(DirContext.ADD_ATTRIBUTE, result.getModificationOp());
+ Attribute resultAttribute = result.getAttribute();
+ assertEquals("someattr", resultAttribute.getID());
+ Object object = resultAttribute.get(0);
+ assertEquals("value1", object);
+ assertEquals("value2", resultAttribute.get(1));
+ }
+
+ public void testGetCompensatingModificationItem_RemoveTwoAttributeValues()
+ throws NamingException {
+ BasicAttribute attribute = new BasicAttribute("someattr");
+ attribute.add("value1");
+ attribute.add("value2");
+ attribute.add("value3");
+ Attributes attributes = new BasicAttributes();
+ attributes.put(attribute);
+
+ BasicAttribute modificationAttribute = new BasicAttribute("someattr");
+ modificationAttribute.add("value1");
+ modificationAttribute.add("value2");
+ ModificationItem originalItem = new ModificationItem(
+ DirContext.REMOVE_ATTRIBUTE, modificationAttribute);
+
+ // Perform test
+ ModificationItem result = tested.getCompensatingModificationItem(
+ attributes, originalItem);
+
+ // Verify result
+ assertEquals(DirContext.ADD_ATTRIBUTE, result.getModificationOp());
+ Attribute resultAttribute = result.getAttribute();
+ assertEquals("someattr", resultAttribute.getID());
+ Object object = resultAttribute.get(0);
+ assertEquals("value1", object);
+ assertEquals("value2", resultAttribute.get(1));
+ }
+
+ public void testGetCompensatingModificationItem_ReplaceExistingAttribute()
+ throws NamingException {
+ BasicAttribute attribute = new BasicAttribute("someattr");
+ attribute.add("value1");
+ attribute.add("value2");
+ Attributes attributes = new BasicAttributes();
+ attributes.put(attribute);
+
+ BasicAttribute modificationAttribute = new BasicAttribute("someattr");
+ modificationAttribute.add("newvalue1");
+ modificationAttribute.add("newvalue2");
+ ModificationItem originalItem = new ModificationItem(
+ DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("someattr"));
+
+ // Perform test
+ ModificationItem result = tested.getCompensatingModificationItem(
+ attributes, originalItem);
+
+ // Verify result
+ assertEquals(DirContext.REPLACE_ATTRIBUTE, result.getModificationOp());
+ Attribute resultAttribute = result.getAttribute();
+ assertEquals("someattr", resultAttribute.getID());
+ Object object = resultAttribute.get(0);
+ assertEquals("value1", object);
+ assertEquals("value2", resultAttribute.get(1));
+ }
+
+ public void testGetCompensatingModificationItem_ReplaceNonExistingAttribute()
+ throws NamingException {
+ Attributes attributes = new BasicAttributes();
+
+ BasicAttribute modificationAttribute = new BasicAttribute("someattr");
+ modificationAttribute.add("newvalue1");
+ modificationAttribute.add("newvalue2");
+ ModificationItem originalItem = new ModificationItem(
+ DirContext.REPLACE_ATTRIBUTE, modificationAttribute);
+
+ // Perform test
+ ModificationItem result = tested.getCompensatingModificationItem(
+ attributes, originalItem);
+
+ // Verify result
+ assertEquals(DirContext.REMOVE_ATTRIBUTE, result.getModificationOp());
+ Attribute resultAttribute = result.getAttribute();
+ assertEquals("someattr", resultAttribute.getID());
+ assertEquals(0, resultAttribute.size());
+ }
+
+ public void testGetCompensatingModificationItem_AddNonExistingAttribute()
+ throws NamingException {
+ Attributes attributes = new BasicAttributes();
+
+ BasicAttribute modificationAttribute = new BasicAttribute("someattr");
+ modificationAttribute.add("newvalue1");
+ modificationAttribute.add("newvalue2");
+ ModificationItem originalItem = new ModificationItem(
+ DirContext.ADD_ATTRIBUTE, modificationAttribute);
+
+ // Perform test
+ ModificationItem result = tested.getCompensatingModificationItem(
+ attributes, originalItem);
+
+ // Verify result
+ assertEquals(DirContext.REMOVE_ATTRIBUTE, result.getModificationOp());
+ Attribute resultAttribute = result.getAttribute();
+ assertEquals("someattr", resultAttribute.getID());
+ assertEquals(0, resultAttribute.size());
+ }
+
+ public void testGetCompensatingModificationItem_AddExistingAttribute()
+ throws NamingException {
+ BasicAttribute attribute = new BasicAttribute("someattr");
+ attribute.add("value1");
+ attribute.add("value2");
+ Attributes attributes = new BasicAttributes();
+ attributes.put(attribute);
+
+ BasicAttribute modificationAttribute = new BasicAttribute("someattr");
+ modificationAttribute.add("newvalue1");
+ modificationAttribute.add("newvalue2");
+ ModificationItem originalItem = new ModificationItem(
+ DirContext.ADD_ATTRIBUTE, new BasicAttribute("someattr"));
+
+ // Perform test
+ ModificationItem result = tested.getCompensatingModificationItem(
+ attributes, originalItem);
+
+ // Verify result
+ assertEquals(DirContext.REPLACE_ATTRIBUTE, result.getModificationOp());
+ Attribute resultAttribute = result.getAttribute();
+ assertEquals("someattr", resultAttribute.getID());
+ assertEquals("value1", result.getAttribute().get(0));
+ assertEquals("value2", result.getAttribute().get(1));
+ }
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RebindOperationExecutorTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RebindOperationExecutorTest.java
new file mode 100644
index 00000000..759aa280
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RebindOperationExecutorTest.java
@@ -0,0 +1,95 @@
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.directory.BasicAttributes;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.core.RebindOperationExecutor;
+
+public class RebindOperationExecutorTest 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 testPerformOperation() {
+ DistinguishedName expectedOriginalDn = new DistinguishedName(
+ "cn=john doe");
+ DistinguishedName expectedTempDn = new DistinguishedName(
+ "cn=john doe_temp");
+ Object expectedObject = new Object();
+ BasicAttributes expectedAttributes = new BasicAttributes();
+ RebindOperationExecutor tested = new RebindOperationExecutor(
+ ldapOperationsMock, expectedOriginalDn, expectedTempDn,
+ expectedObject, expectedAttributes);
+
+ ldapOperationsMock.rename(expectedOriginalDn, expectedTempDn);
+ ldapOperationsMock.bind(expectedOriginalDn, expectedObject,
+ expectedAttributes);
+
+ replay();
+ // perform test
+ tested.performOperation();
+ verify();
+ }
+
+ public void testCommit() {
+ DistinguishedName expectedOriginalDn = new DistinguishedName(
+ "cn=john doe");
+ DistinguishedName expectedTempDn = new DistinguishedName(
+ "cn=john doe_temp");
+ Object expectedObject = new Object();
+ BasicAttributes expectedAttributes = new BasicAttributes();
+ RebindOperationExecutor tested = new RebindOperationExecutor(
+ ldapOperationsMock, expectedOriginalDn, expectedTempDn,
+ expectedObject, expectedAttributes);
+
+ ldapOperationsMock.unbind(expectedTempDn);
+
+ replay();
+ // perform test
+ tested.commit();
+ verify();
+ }
+
+ public void testRollback() {
+ DistinguishedName expectedOriginalDn = new DistinguishedName(
+ "cn=john doe");
+ DistinguishedName expectedTempDn = new DistinguishedName(
+ "cn=john doe_temp");
+ Object expectedObject = new Object();
+ BasicAttributes expectedAttributes = new BasicAttributes();
+ RebindOperationExecutor tested = new RebindOperationExecutor(
+ ldapOperationsMock, expectedOriginalDn, expectedTempDn,
+ expectedObject, expectedAttributes);
+
+ ldapOperationsMock.unbind(expectedOriginalDn);
+ ldapOperationsMock.rename(expectedTempDn, expectedOriginalDn);
+
+ replay();
+ // perform test
+ tested.rollback();
+ verify();
+ }
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RebindOperationRecorderTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RebindOperationRecorderTest.java
new file mode 100644
index 00000000..774647e3
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RebindOperationRecorderTest.java
@@ -0,0 +1,84 @@
+package org.springframework.ldap.transaction.core;
+
+import javax.naming.directory.BasicAttributes;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.core.RebindOperationExecutor;
+import org.springframework.ldap.transaction.core.RebindOperationRecorder;
+import org.springframework.ldap.transaction.core.TempEntryRenamingStrategy;
+
+public class RebindOperationRecorderTest extends TestCase {
+ private MockControl ldapOperationsControl;
+
+ private LdapOperations ldapOperationsMock;
+
+ private MockControl renamingStrategyControl;
+
+ private TempEntryRenamingStrategy renamingStrategyMock;
+
+ protected void setUp() throws Exception {
+ ldapOperationsControl = MockControl.createControl(LdapOperations.class);
+ ldapOperationsMock = (LdapOperations) ldapOperationsControl.getMock();
+
+ renamingStrategyControl = MockControl
+ .createControl(TempEntryRenamingStrategy.class);
+ renamingStrategyMock = (TempEntryRenamingStrategy) renamingStrategyControl
+ .getMock();
+
+ }
+
+ protected void tearDown() throws Exception {
+ ldapOperationsControl = null;
+ ldapOperationsMock = null;
+
+ renamingStrategyControl = null;
+ renamingStrategyMock = null;
+
+ }
+
+ protected void replay() {
+ ldapOperationsControl.replay();
+ renamingStrategyControl.replay();
+ }
+
+ protected void verify() {
+ ldapOperationsControl.verify();
+ renamingStrategyControl.verify();
+ }
+
+ public void testRecordOperation() {
+ final DistinguishedName expectedDn = new DistinguishedName(
+ "cn=john doe");
+ final DistinguishedName expectedTempDn = new DistinguishedName(
+ "cn=john doe");
+ RebindOperationRecorder tested = new RebindOperationRecorder(
+ ldapOperationsMock, renamingStrategyMock);
+
+ renamingStrategyControl.expectAndReturn(renamingStrategyMock
+ .getTemporaryName(expectedDn), expectedTempDn);
+
+ replay();
+ Object expectedObject = new Object();
+ BasicAttributes expectedAttributes = new BasicAttributes();
+
+ // perform test
+ CompensatingTransactionOperationExecutor result = tested
+ .recordOperation(new Object[] { expectedDn, expectedObject,
+ expectedAttributes });
+ verify();
+
+ assertTrue(result instanceof RebindOperationExecutor);
+ RebindOperationExecutor rollbackOperation = (RebindOperationExecutor) result;
+ assertSame(ldapOperationsMock, rollbackOperation.getLdapOperations());
+ assertSame(expectedDn, rollbackOperation.getOriginalDn());
+ assertSame(expectedTempDn, rollbackOperation.getTemporaryDn());
+ assertSame(expectedObject, rollbackOperation.getOriginalObject());
+ assertSame(expectedAttributes, rollbackOperation
+ .getOriginalAttributes());
+ }
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RenameOperationExecutorTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RenameOperationExecutorTest.java
new file mode 100644
index 00000000..e1296fe4
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RenameOperationExecutorTest.java
@@ -0,0 +1,75 @@
+package org.springframework.ldap.transaction.core;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.core.RenameOperationExecutor;
+
+import junit.framework.TestCase;
+
+public class RenameOperationExecutorTest 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 testPerformOperation() {
+ DistinguishedName expectedNewName = new DistinguishedName("ou=newOu");
+ DistinguishedName expectedOldName = new DistinguishedName("ou=someou");
+ RenameOperationExecutor tested = new RenameOperationExecutor(
+ ldapOperationsMock, expectedOldName, expectedNewName);
+
+ ldapOperationsMock.rename(expectedOldName, expectedNewName);
+
+ replay();
+ // Perform test.
+ tested.performOperation();
+ verify();
+ }
+
+ public void testCommit() {
+ DistinguishedName expectedNewName = new DistinguishedName("ou=newOu");
+ DistinguishedName expectedOldName = new DistinguishedName("ou=someou");
+ RenameOperationExecutor tested = new RenameOperationExecutor(
+ ldapOperationsMock, expectedOldName, expectedNewName);
+
+ // Nothing to do for this operation.
+
+ replay();
+ // Perform test.
+ tested.commit();
+ verify();
+ }
+
+ public void testRollback() {
+ DistinguishedName expectedNewName = new DistinguishedName("ou=newOu");
+ DistinguishedName expectedOldName = new DistinguishedName("ou=someou");
+ RenameOperationExecutor tested = new RenameOperationExecutor(
+ ldapOperationsMock, expectedOldName, expectedNewName);
+
+ ldapOperationsMock.rename(expectedNewName, expectedOldName);
+
+ replay();
+ // Perform test.
+ tested.rollback();
+ verify();
+ }
+
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RenameOperationRecorderTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RenameOperationRecorderTest.java
new file mode 100644
index 00000000..ffc5eb8e
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/RenameOperationRecorderTest.java
@@ -0,0 +1,52 @@
+package org.springframework.ldap.transaction.core;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.core.RenameOperationExecutor;
+import org.springframework.ldap.transaction.core.RenameOperationRecorder;
+
+public class RenameOperationRecorderTest 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 testRecordOperation() {
+ RenameOperationRecorder tested = new RenameOperationRecorder(
+ ldapOperationsMock);
+
+ replay();
+ // Perform test
+ CompensatingTransactionOperationExecutor operation = tested
+ .recordOperation(new Object[] { "ou=someou", "ou=newou" });
+ verify();
+
+ assertTrue(operation instanceof RenameOperationExecutor);
+ RenameOperationExecutor rollbackOperation = (RenameOperationExecutor) operation;
+ assertSame(ldapOperationsMock, rollbackOperation.getLdapOperations());
+ assertEquals("ou=newou", rollbackOperation.getNewDn().toString());
+ assertEquals("ou=someou", rollbackOperation.getOriginalDn().toString());
+ }
+
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/UnbindOperationExecutorTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/UnbindOperationExecutorTest.java
new file mode 100644
index 00000000..4db0726a
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/UnbindOperationExecutorTest.java
@@ -0,0 +1,76 @@
+package org.springframework.ldap.transaction.core;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.core.UnbindOperationExecutor;
+
+public class UnbindOperationExecutorTest 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 testPerformOperation() {
+ DistinguishedName expectedOldName = new DistinguishedName("cn=oldDn");
+ DistinguishedName expectedTempName = new DistinguishedName("cn=newDn");
+ UnbindOperationExecutor tested = new UnbindOperationExecutor(
+ ldapOperationsMock, expectedOldName, expectedTempName);
+
+ ldapOperationsMock.rename(expectedOldName, expectedTempName);
+
+ replay();
+ // Perform test
+ tested.performOperation();
+ verify();
+ }
+
+ public void testCommit() {
+ DistinguishedName expectedOldName = new DistinguishedName("cn=oldDn");
+ DistinguishedName expectedTempName = new DistinguishedName("cn=newDn");
+ UnbindOperationExecutor tested = new UnbindOperationExecutor(
+ ldapOperationsMock, expectedOldName, expectedTempName);
+
+ ldapOperationsMock.unbind(expectedTempName);
+
+ replay();
+ // Perform test
+ tested.commit();
+ verify();
+ }
+
+ public void testRollback() {
+ DistinguishedName expectedOldName = new DistinguishedName("cn=oldDn");
+ DistinguishedName expectedTempName = new DistinguishedName("cn=newDn");
+ UnbindOperationExecutor tested = new UnbindOperationExecutor(
+ ldapOperationsMock, expectedOldName, expectedTempName);
+
+ ldapOperationsMock.rename(expectedTempName, expectedOldName);
+
+ replay();
+ // Perform test
+ tested.rollback();
+ verify();
+ }
+}
diff --git a/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/UnbindOperationRecorderTest.java b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/UnbindOperationRecorderTest.java
new file mode 100644
index 00000000..3f357612
--- /dev/null
+++ b/spring-ldap/src/test/java/org/springframework/ldap/transaction/core/UnbindOperationRecorderTest.java
@@ -0,0 +1,77 @@
+package org.springframework.ldap.transaction.core;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.transaction.CompensatingTransactionOperationExecutor;
+import org.springframework.ldap.transaction.core.TempEntryRenamingStrategy;
+import org.springframework.ldap.transaction.core.UnbindOperationExecutor;
+import org.springframework.ldap.transaction.core.UnbindOperationRecorder;
+
+public class UnbindOperationRecorderTest extends TestCase {
+ private MockControl ldapOperationsControl;
+
+ private LdapOperations ldapOperationsMock;
+
+ private MockControl renamingStrategyControl;
+
+ private TempEntryRenamingStrategy renamingStrategyMock;
+
+ protected void setUp() throws Exception {
+ ldapOperationsControl = MockControl.createControl(LdapOperations.class);
+ ldapOperationsMock = (LdapOperations) ldapOperationsControl.getMock();
+
+ renamingStrategyControl = MockControl
+ .createControl(TempEntryRenamingStrategy.class);
+ renamingStrategyMock = (TempEntryRenamingStrategy) renamingStrategyControl
+ .getMock();
+
+ }
+
+ protected void tearDown() throws Exception {
+ ldapOperationsControl = null;
+ ldapOperationsMock = null;
+
+ renamingStrategyControl = null;
+ renamingStrategyMock = null;
+
+ }
+
+ protected void replay() {
+ ldapOperationsControl.replay();
+ renamingStrategyControl.replay();
+ }
+
+ protected void verify() {
+ ldapOperationsControl.verify();
+ renamingStrategyControl.verify();
+ }
+
+ public void testRecordOperation() {
+ final DistinguishedName expectedTempName = new DistinguishedName(
+ "cn=john doe_temp");
+ final DistinguishedName expectedDn = new DistinguishedName(
+ "cn=john doe");
+ UnbindOperationRecorder tested = new UnbindOperationRecorder(
+ ldapOperationsMock, renamingStrategyMock);
+
+ renamingStrategyControl.expectAndReturn(renamingStrategyMock
+ .getTemporaryName(expectedDn), expectedTempName);
+
+ replay();
+ // Perform test
+ CompensatingTransactionOperationExecutor operation = tested
+ .recordOperation(new Object[] { expectedDn });
+ verify();
+
+ // Verify result
+ assertTrue(operation instanceof UnbindOperationExecutor);
+ UnbindOperationExecutor rollbackOperation = (UnbindOperationExecutor) operation;
+ assertSame(ldapOperationsMock, rollbackOperation.getLdapOperations());
+ assertSame(expectedDn, rollbackOperation.getOriginalDn());
+ assertSame(expectedTempName, rollbackOperation.getTemporaryDn());
+ }
+
+}