diff --git a/spring-ldap/.classpath b/spring-ldap/.classpath index 96ad1b1d..fd7e6474 100644 --- a/spring-ldap/.classpath +++ b/spring-ldap/.classpath @@ -47,5 +47,9 @@ + + + + diff --git a/spring-ldap/ivy.xml b/spring-ldap/ivy.xml index 32ce2960..162fb155 100644 --- a/spring-ldap/ivy.xml +++ b/spring-ldap/ivy.xml @@ -62,6 +62,9 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PROPAGATION_REQUIRES_NEW + + + + + diff --git a/spring-ldap/src/itest/java/conf/ldapTemplateTransactionTestContext.xml b/spring-ldap/src/itest/java/conf/ldapTemplateTransactionTestContext.xml new file mode 100644 index 00000000..fef551bf --- /dev/null +++ b/spring-ldap/src/itest/java/conf/ldapTemplateTransactionTestContext.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PROPAGATION_REQUIRES_NEW + + + + diff --git a/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/ContextSourceAndDataSourceTransactionManagerIntegrationTest.java b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/ContextSourceAndDataSourceTransactionManagerIntegrationTest.java new file mode 100644 index 00000000..89348ea6 --- /dev/null +++ b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/ContextSourceAndDataSourceTransactionManagerIntegrationTest.java @@ -0,0 +1,374 @@ +/* + * 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.sql.ResultSet; +import java.sql.SQLException; + +import javax.naming.NamingException; +import javax.naming.directory.Attributes; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.ldap.LdapServerManager; +import org.springframework.ldap.NameNotFoundException; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Integration tests for {@link ContextSourceAndDataSourceTransactionManager}. + * + * @author Mattias Arthursson + */ +public class ContextSourceAndDataSourceTransactionManagerIntegrationTest extends + AbstractDependencyInjectionSpringContextTests { + + private static Log log = LogFactory + .getLog(ContextSourceAndDataSourceTransactionManagerIntegrationTest.class); + + public ContextSourceAndDataSourceTransactionManagerIntegrationTest() { + setAutowireMode(AbstractDependencyInjectionSpringContextTests.AUTOWIRE_BY_NAME); + } + + private DummyDao dummyDao; + + private LdapTemplate ldapTemplate; + + private JdbcTemplate jdbcTemplate; + + private LdapServerManager ldapServerManager; + + public void setLdapServerManager(LdapServerManager ldapServerManager) { + this.ldapServerManager = ldapServerManager; + } + + public void setLdapTemplate(LdapTemplate ldapTemplate) { + this.ldapTemplate = ldapTemplate; + } + + public void setDummyDao(DummyDao dummyDaoImpl) { + this.dummyDao = dummyDaoImpl; + } + + protected String[] getConfigLocations() { + return new String[] { "conf/ldapAndJdbcTransactionTestContext.xml" }; + } + + protected void onSetUp() throws Exception { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.clearSynchronization(); + } + + ldapServerManager.cleanAndSetup("setup_data.ldif"); + jdbcTemplate + .execute("create table PERSON(fullname VARCHAR, lastname VARCHAR, description VARCHAR)"); + jdbcTemplate.update("insert into PERSON values(?, ?, ?)", new Object[] { + "Some Person", "Person", "Sweden, Company1, Some Person" }); + } + + protected void onTearDown() throws Exception { + jdbcTemplate.execute("drop table PERSON"); + } + + public void testCreateWithException() { + try { + dummyDao.createWithException("Sweden", "company1", + "some testperson", "testperson", "some description"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + log.debug("Verifying result"); + + // Verify that no entry was created + try { + ldapTemplate.lookup("cn=some testperson, ou=company1, c=Sweden"); + fail("NameNotFoundException expected"); + } catch (NameNotFoundException expected) { + assertTrue(true); + } + + try { + jdbcTemplate.queryForObject( + "select * from PERSON where fullname='some testperson'", + new RowMapper() { + public Object mapRow(ResultSet rs, int rowNum) + throws SQLException { + return null; + } + }); + fail("EmptyResultDataAccessException expected"); + } catch (EmptyResultDataAccessException expected) { + assertTrue(true); + } + } + + public void testCreate() { + dummyDao.create("Sweden", "company1", "some testperson", "testperson", + "some description"); + + log.debug("Verifying result"); + Object ldapResult = ldapTemplate + .lookup("cn=some testperson, ou=company1, c=Sweden"); + Object dbResult = jdbcTemplate.queryForObject( + "select * from PERSON where fullname='some testperson'", + new RowMapper() { + public Object mapRow(ResultSet rs, int rowNum) + throws SQLException { + return new Object(); + } + }); + assertNotNull(ldapResult); + assertNotNull(dbResult); + } + + public void testUpdateWithException() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + try { + dummyDao.updateWithException(dn, "Some Person", "Updated Person", + "Updated description"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + log.debug("Verifying result"); + + Object ldapResult = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Person", attributes.get("sn").get()); + assertEquals("Sweden, Company1, Some Person", attributes.get( + "description").get()); + return new Object(); + } + }); + + Object jdbcResult = jdbcTemplate.queryForObject( + "select * from PERSON where fullname=?", + new Object[] { "Some Person" }, new RowMapper() { + public Object mapRow(ResultSet rs, int rowNum) + throws SQLException { + assertEquals("Person", rs.getString("lastname")); + assertEquals("Sweden, Company1, Some Person", rs + .getString("description")); + return new Object(); + } + }); + + assertNotNull(ldapResult); + assertNotNull(jdbcResult); + } + + public void testUpdate() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + dummyDao.update(dn, "Some Person", "Updated Person", + "Updated description"); + + log.debug("Verifying result"); + Object ldapResult = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Updated Person", attributes.get("sn").get()); + assertEquals("Updated description", attributes.get( + "description").get()); + return new Object(); + } + }); + + Object jdbcResult = jdbcTemplate.queryForObject( + "select * from PERSON where fullname=?", + new Object[] { "Some Person" }, new RowMapper() { + public Object mapRow(ResultSet rs, int rowNum) + throws SQLException { + assertEquals("Updated Person", rs.getString("lastname")); + assertEquals("Updated description", rs + .getString("description")); + return new Object(); + } + }); + + assertNotNull(ldapResult); + assertNotNull(jdbcResult); + } + + public void testUpdateAndRenameWithException() { + String dn = "cn=Some Person2,ou=company1,c=Sweden"; + String newDn = "cn=Some Person2,ou=company2,c=Sweden"; + try { + // Perform test + dummyDao.updateAndRenameWithException(dn, newDn, + "Updated description"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + // Verify that entry was not moved. + try { + ldapTemplate.lookup(newDn); + fail("NameNotFoundException expected"); + } catch (NameNotFoundException expected) { + assertTrue(true); + } + + // Verify that original entry was not updated. + Object object = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Sweden, Company1, Some Person2", attributes.get( + "description").get()); + return new Object(); + } + }); + assertNotNull(object); + } + + public void testUpdateAndRename() { + String dn = "cn=Some Person2,ou=company1,c=Sweden"; + String newDn = "cn=Some Person2,ou=company2,c=Sweden"; + // Perform test + dummyDao.updateAndRename(dn, newDn, "Updated description"); + + // Verify that entry was moved and updated. + Object object = ldapTemplate.lookup(newDn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Updated description", attributes.get( + "description").get()); + return new Object(); + } + }); + + assertNotNull(object); + } + + public void testModifyAttributesWithException() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + try { + // Perform test + dummyDao.modifyAttributesWithException(dn, "Updated lastname", + "Updated description"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + // Verify result - check that the operation was properly rolled back + Object result = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Person", attributes.get("sn").get()); + assertEquals("Sweden, Company1, Some Person", attributes.get( + "description").get()); + return new Object(); + } + }); + + assertNotNull(result); + } + + public void testModifyAttributes() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + // Perform test + dummyDao + .modifyAttributes(dn, "Updated lastname", "Updated description"); + + // Verify result - check that the operation was not rolled back + Object result = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Updated lastname", attributes.get("sn").get()); + assertEquals("Updated description", attributes.get( + "description").get()); + return new Object(); + } + }); + + assertNotNull(result); + } + + public void testUnbindWithException() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + try { + // Perform test + dummyDao.unbindWithException(dn, "Some Person"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + // Verify result - check that the operation was properly rolled back + Object ldapResult = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + // Just verify that the entry still exists. + return new Object(); + } + }); + + Object jdbcResult = jdbcTemplate.queryForObject( + "select * from PERSON where fullname=?", + new Object[] { "Some Person" }, new RowMapper() { + public Object mapRow(ResultSet rs, int rowNum) + throws SQLException { + // Just verify that the entry still exists. + return new Object(); + } + }); + + assertNotNull(ldapResult); + assertNotNull(jdbcResult); + } + + public void testUnbind() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + // Perform test + dummyDao.unbind(dn, "Some Person"); + + try { + // Verify result - check that the operation was not rolled back + ldapTemplate.lookup(dn); + fail("NameNotFoundException expected"); + } catch (NameNotFoundException expected) { + assertTrue(true); + } + + try { + jdbcTemplate.queryForObject( + "select * from PERSON where fullname=?", + new Object[] { "Some Person" }, new RowMapper() { + public Object mapRow(ResultSet rs, int rowNum) + throws SQLException { + return null; + } + }); + fail("EmptyResultDataAccessException expected"); + } catch (EmptyResultDataAccessException expected) { + assertTrue(true); + } + } + + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } +} diff --git a/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManagerIntegrationTest.java b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManagerIntegrationTest.java new file mode 100644 index 00000000..3a5d4290 --- /dev/null +++ b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/ContextSourceTransactionManagerIntegrationTest.java @@ -0,0 +1,284 @@ +/* + * 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.Attributes; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.LdapServerManager; +import org.springframework.ldap.NameNotFoundException; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Integration tests for {@link ContextSourceAndDataSourceTransactionManager}. + * + * @author Mattias Arthursson + */ +public class ContextSourceTransactionManagerIntegrationTest extends + AbstractDependencyInjectionSpringContextTests { + + private static Log log = LogFactory + .getLog(ContextSourceTransactionManagerIntegrationTest.class); + + public ContextSourceTransactionManagerIntegrationTest() { + setAutowireMode(AbstractDependencyInjectionSpringContextTests.AUTOWIRE_BY_NAME); + } + + private DummyDao dummyDao; + + private LdapTemplate ldapTemplate; + + private LdapServerManager ldapServerManager; + + public void setLdapServerManager(LdapServerManager ldapServerManager) { + this.ldapServerManager = ldapServerManager; + } + + public void setLdapTemplate(LdapTemplate ldapTemplate) { + this.ldapTemplate = ldapTemplate; + } + + public void setDummyDao(DummyDao dummyDaoImpl) { + this.dummyDao = dummyDaoImpl; + } + + protected String[] getConfigLocations() { + return new String[] { "conf/ldapTemplateTransactionTestContext.xml" }; + } + + protected void onSetUp() throws Exception { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.clearSynchronization(); + } + + ldapServerManager.cleanAndSetup("setup_data.ldif"); + } + + protected void onTearDown() throws Exception { + } + + public void testCreateWithException() { + try { + dummyDao.createWithException("Sweden", "company1", + "some testperson", "testperson", "some description"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + log.debug("Verifying result"); + + // Verify that no entry was created + try { + ldapTemplate.lookup("cn=some testperson, ou=company1, c=Sweden"); + fail("NameNotFoundException expected"); + } catch (NameNotFoundException expected) { + assertTrue(true); + } + } + + public void testCreate() { + dummyDao.create("Sweden", "company1", "some testperson", "testperson", + "some description"); + + log.debug("Verifying result"); + Object ldapResult = ldapTemplate + .lookup("cn=some testperson, ou=company1, c=Sweden"); + assertNotNull(ldapResult); + } + + public void testUpdateWithException() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + try { + dummyDao.updateWithException(dn, "Some Person", "Updated Person", + "Updated description"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + log.debug("Verifying result"); + + Object ldapResult = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Person", attributes.get("sn").get()); + assertEquals("Sweden, Company1, Some Person", attributes.get( + "description").get()); + return new Object(); + } + }); + + assertNotNull(ldapResult); + } + + public void testUpdate() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + dummyDao.update(dn, "Some Person", "Updated Person", + "Updated description"); + + log.debug("Verifying result"); + Object ldapResult = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Updated Person", attributes.get("sn").get()); + assertEquals("Updated description", attributes.get( + "description").get()); + return new Object(); + } + }); + + assertNotNull(ldapResult); + } + + public void testUpdateAndRenameWithException() { + String dn = "cn=Some Person2,ou=company1,c=Sweden"; + String newDn = "cn=Some Person2,ou=company2,c=Sweden"; + try { + // Perform test + dummyDao.updateAndRenameWithException(dn, newDn, + "Updated description"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + // Verify that entry was not moved. + try { + ldapTemplate.lookup(newDn); + fail("NameNotFoundException expected"); + } catch (NameNotFoundException expected) { + assertTrue(true); + } + + // Verify that original entry was not updated. + Object object = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Sweden, Company1, Some Person2", attributes.get( + "description").get()); + return new Object(); + } + }); + assertNotNull(object); + } + + public void testUpdateAndRename() { + String dn = "cn=Some Person2,ou=company1,c=Sweden"; + String newDn = "cn=Some Person2,ou=company2,c=Sweden"; + // Perform test + dummyDao.updateAndRename(dn, newDn, "Updated description"); + + // Verify that entry was moved and updated. + Object object = ldapTemplate.lookup(newDn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Updated description", attributes.get( + "description").get()); + return new Object(); + } + }); + + assertNotNull(object); + } + + public void testModifyAttributesWithException() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + try { + // Perform test + dummyDao.modifyAttributesWithException(dn, "Updated lastname", + "Updated description"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + // Verify result - check that the operation was properly rolled back + Object result = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Person", attributes.get("sn").get()); + assertEquals("Sweden, Company1, Some Person", attributes.get( + "description").get()); + return new Object(); + } + }); + + assertNotNull(result); + } + + public void testModifyAttributes() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + // Perform test + dummyDao + .modifyAttributes(dn, "Updated lastname", "Updated description"); + + // Verify result - check that the operation was not rolled back + Object result = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + assertEquals("Updated lastname", attributes.get("sn").get()); + assertEquals("Updated description", attributes.get( + "description").get()); + return new Object(); + } + }); + + assertNotNull(result); + } + + public void testUnbindWithException() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + try { + // Perform test + dummyDao.unbindWithException(dn, "Some Person"); + fail("DummyException expected"); + } catch (DummyException expected) { + assertTrue(true); + } + + // Verify result - check that the operation was properly rolled back + Object ldapResult = ldapTemplate.lookup(dn, new AttributesMapper() { + public Object mapFromAttributes(Attributes attributes) + throws NamingException { + // Just verify that the entry still exists. + return new Object(); + } + }); + + assertNotNull(ldapResult); + } + + public void testUnbind() { + String dn = "cn=Some Person,ou=company1,c=Sweden"; + // Perform test + dummyDao.unbind(dn, "Some Person"); + + try { + // Verify result - check that the operation was not rolled back + ldapTemplate.lookup(dn); + fail("NameNotFoundException expected"); + } catch (NameNotFoundException expected) { + assertTrue(true); + } + + } +} diff --git a/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/DummyDao.java b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/DummyDao.java new file mode 100644 index 00000000..c6fd53b9 --- /dev/null +++ b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/DummyDao.java @@ -0,0 +1,44 @@ +/* + * 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; + +public interface DummyDao { + void createWithException(String country, String company, String fullname, + String lastname, String description); + + void create(String country, String company, String fullname, + String lastname, String description); + + void update(String dn, String fullname, String lastname, String description); + + void updateWithException(String dn, String fullname, String lastname, + String description); + + void updateAndRename(String dn, String newDn, String description); + + void updateAndRenameWithException(String dn, String newDn, + String description); + + void modifyAttributes(String dn, String lastName, String description); + + void modifyAttributesWithException(String dn, String lastName, + String description); + + void unbind(String dn, String fullname); + + void unbindWithException(String dn, String fullname); + +} diff --git a/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/DummyException.java b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/DummyException.java new file mode 100644 index 00000000..4637e5d8 --- /dev/null +++ b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/DummyException.java @@ -0,0 +1,22 @@ +/* + * 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; + +public class DummyException extends RuntimeException { + public DummyException(String message) { + super(message); + } +} diff --git a/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/DummyServiceImpl.java b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/DummyServiceImpl.java new file mode 100644 index 00000000..272d630e --- /dev/null +++ b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/DummyServiceImpl.java @@ -0,0 +1,24 @@ +/* + * 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; + +public class DummyServiceImpl { + private DummyDao dummyDaoImpl; + + public void setDummyDaoImpl(DummyDao dummyDaoImpl) { + this.dummyDaoImpl = dummyDaoImpl; + } +} diff --git a/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/LdapAndJdbcDummyDaoImpl.java b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/LdapAndJdbcDummyDaoImpl.java new file mode 100644 index 00000000..0b897d5b --- /dev/null +++ b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/LdapAndJdbcDummyDaoImpl.java @@ -0,0 +1,178 @@ +/* + * 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.core.JdbcTemplate; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapTemplate; + +public class LdapAndJdbcDummyDaoImpl implements DummyDao { + private LdapTemplate ldapTemplate; + + private JdbcTemplate jdbcTemplate; + + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void setLdapTemplate(LdapTemplate ldapTemplate) { + this.ldapTemplate = ldapTemplate; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#createWithException(java.lang.String, + * java.lang.String, java.lang.String, java.lang.String, + * java.lang.String) + */ + public void createWithException(String country, String company, + String fullname, String lastname, String description) { + create(country, company, fullname, lastname, description); + throw new DummyException("This method failed"); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#create(java.lang.String, + * java.lang.String, java.lang.String, java.lang.String, + * java.lang.String) + */ + public void create(String country, String company, String fullname, + String lastname, String description) { + DistinguishedName dn = new DistinguishedName(); + dn.add("c", country); + dn.add("ou", company); + dn.add("cn", fullname); + + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setAttributeValues("objectclass", new String[] { "top", "person" }); + ctx.setAttributeValue("cn", fullname); + ctx.setAttributeValue("sn", lastname); + ctx.setAttributeValue("description", description); + ldapTemplate.bind(dn, ctx, null); + jdbcTemplate.update("insert into PERSON values(?, ?, ?)", new Object[] { + fullname, lastname, description }); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#update(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void update(String dn, String fullname, String lastname, + String description) { + DirContextAdapter ctx = (DirContextAdapter) ldapTemplate.lookup(dn); + ctx.setAttributeValue("sn", lastname); + ctx.setAttributeValue("description", description); + ctx.update(); + + ldapTemplate.rebind(dn, ctx, null); + jdbcTemplate + .update( + "update PERSON set lastname=?, description = ? where fullname = ?", + new Object[] { lastname, description, fullname }); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#updateWithException(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void updateWithException(String dn, String fullname, + String lastname, String description) { + update(dn, fullname, lastname, description); + throw new DummyException("This method failed."); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#updateAndRename(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void updateAndRename(String dn, String newDn, String description) { + DirContextAdapter ctx = (DirContextAdapter) ldapTemplate.lookup(dn); + ctx.setAttributeValue("description", description); + ctx.update(); + + ldapTemplate.rebind(dn, ctx, null); + ldapTemplate.rename(dn, newDn); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#updateAndRenameWithException(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void updateAndRenameWithException(String dn, String newDn, + String description) { + updateAndRename(dn, newDn, description); + throw new DummyException("This method failed."); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#modifyAttributes(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void modifyAttributes(String dn, String lastName, String description) { + DirContextAdapter ctx = (DirContextAdapter) ldapTemplate.lookup(dn); + ctx.setAttributeValue("sn", lastName); + ctx.setAttributeValue("description", description); + + ldapTemplate.modifyAttributes(dn, ctx.getModificationItems()); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#modifyAttributesWithException(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void modifyAttributesWithException(String dn, String lastName, + String description) { + modifyAttributes(dn, lastName, description); + throw new DummyException("This method failed."); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#unbind(java.lang.String) + */ + public void unbind(String dn, String fullname) { + ldapTemplate.unbind(dn); + jdbcTemplate.update("delete from PERSON where fullname=?", + new Object[] { fullname }); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#unbindWithException(java.lang.String) + */ + public void unbindWithException(String dn, String fullname) { + unbind(dn, fullname); + throw new DummyException("This operation failed."); + } +} diff --git a/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/LdapDummyDaoImpl.java b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/LdapDummyDaoImpl.java new file mode 100644 index 00000000..b7570b68 --- /dev/null +++ b/spring-ldap/src/itest/java/org/springframework/ldap/transaction/core/LdapDummyDaoImpl.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 org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapTemplate; + +public class LdapDummyDaoImpl implements DummyDao { + private LdapTemplate ldapTemplate; + + public void setLdapTemplate(LdapTemplate ldapTemplate) { + this.ldapTemplate = ldapTemplate; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#createWithException(java.lang.String, + * java.lang.String, java.lang.String, java.lang.String, + * java.lang.String) + */ + public void createWithException(String country, String company, + String fullname, String lastname, String description) { + create(country, company, fullname, lastname, description); + throw new DummyException("This method failed"); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#create(java.lang.String, + * java.lang.String, java.lang.String, java.lang.String, + * java.lang.String) + */ + public void create(String country, String company, String fullname, + String lastname, String description) { + DistinguishedName dn = new DistinguishedName(); + dn.add("c", country); + dn.add("ou", company); + dn.add("cn", fullname); + + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setAttributeValues("objectclass", new String[] { "top", "person" }); + ctx.setAttributeValue("cn", fullname); + ctx.setAttributeValue("sn", lastname); + ctx.setAttributeValue("description", description); + ldapTemplate.bind(dn, ctx, null); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#update(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void update(String dn, String fullname, String lastname, + String description) { + DirContextAdapter ctx = (DirContextAdapter) ldapTemplate.lookup(dn); + ctx.setAttributeValue("sn", lastname); + ctx.setAttributeValue("description", description); + ctx.update(); + + ldapTemplate.rebind(dn, ctx, null); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#updateWithException(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void updateWithException(String dn, String fullname, + String lastname, String description) { + update(dn, fullname, lastname, description); + throw new DummyException("This method failed."); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#updateAndRename(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void updateAndRename(String dn, String newDn, String description) { + DirContextAdapter ctx = (DirContextAdapter) ldapTemplate.lookup(dn); + ctx.setAttributeValue("description", description); + ctx.update(); + + ldapTemplate.rebind(dn, ctx, null); + ldapTemplate.rename(dn, newDn); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#updateAndRenameWithException(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void updateAndRenameWithException(String dn, String newDn, + String description) { + updateAndRename(dn, newDn, description); + throw new DummyException("This method failed."); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#modifyAttributes(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void modifyAttributes(String dn, String lastName, String description) { + DirContextAdapter ctx = (DirContextAdapter) ldapTemplate.lookup(dn); + ctx.setAttributeValue("sn", lastName); + ctx.setAttributeValue("description", description); + + ldapTemplate.modifyAttributes(dn, ctx.getModificationItems()); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#modifyAttributesWithException(java.lang.String, + * java.lang.String, java.lang.String) + */ + public void modifyAttributesWithException(String dn, String lastName, + String description) { + modifyAttributes(dn, lastName, description); + throw new DummyException("This method failed."); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#unbind(java.lang.String) + */ + public void unbind(String dn, String fullname) { + ldapTemplate.unbind(dn); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.ldap.transaction.core.DummyDao#unbindWithException(java.lang.String) + */ + public void unbindWithException(String dn, String fullname) { + unbind(dn, fullname); + throw new DummyException("This operation failed."); + } +} diff --git a/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationExecutor.java b/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationExecutor.java new file mode 100644 index 00000000..7d809398 --- /dev/null +++ b/spring-ldap/src/main/java/org/springframework/ldap/transaction/CompensatingTransactionOperationExecutor.java @@ -0,0 +1,60 @@ +/* + * 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; + +/** + * Responsible for executing a single recorded operation as well as committing + * or rolling it back, depending on the transaction outcome. Instances of this + * interface are constructed by {@link CompensatingTransactionOperationRecorder} + * objects, supplying them with the information necessary for the respective + * operations. + *

+ * 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.: + *

    + *
  • Should the transaction failure be caused by a network failure there is + * no way whatsoever that this TransactionManager can restore the database + * state. In this case all possibilities for rollback will be utterly lost.
  • + *
  • Transaction isolation is not provided, i.e. entries participating in a + * transaction for one client may very well participate in another transaction + * for another client at the same time. Should one of these transaction be + * rolled back, the outcome of this is undetermined, and may in the worst case + * result in total failure.
  • + *
+ *

+ *

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