diff --git a/ldif/ldif-batch/.springBeans b/ldif/ldif-batch/.springBeans new file mode 100644 index 00000000..41209bec --- /dev/null +++ b/ldif/ldif-batch/.springBeans @@ -0,0 +1,13 @@ + + + 1 + + + + + + + + + + diff --git a/ldif/ldif-batch/pom.xml b/ldif/ldif-batch/pom.xml new file mode 100644 index 00000000..b8968cff --- /dev/null +++ b/ldif/ldif-batch/pom.xml @@ -0,0 +1,46 @@ + + + org.springframework.ldap + spring-ldap-parent-tiger + 1.3.1.CI-SNAPSHOT + + 4.0.0 + spring-ldap-ldif-batch + Spring LDAP LDIF Batch + Classes for integration with the Spring Batch Framework + + 2.0.2.RELEASE + + + + org.springframework.batch + spring-batch-core + ${spring-batch.version} + compile + + + org.springframework.batch + spring-batch-infrastructure + ${spring-batch.version} + compile + + + org.springframework.batch + spring-batch-test + ${spring-batch.version} + test + + + org.springframework.ldap + spring-ldap-ldif-core + ${version} + compile + + + junit + junit + 4.4 + test + + + diff --git a/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/LdifAggregator.java b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/LdifAggregator.java new file mode 100644 index 00000000..b4a4ce93 --- /dev/null +++ b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/LdifAggregator.java @@ -0,0 +1,28 @@ +/** + * + */ +package org.springframework.ldap.ldif.batch; + +import org.springframework.batch.item.file.transform.LineAggregator; +import org.springframework.ldap.core.LdapAttributes; + +/** + * The {@link LdifAggregator LdifAggregator} object is an implementation of the {@link org.springframework.batch.item.file.transform.LineAggregator LineAggregator} + * interface for use with a {@link org.springframework.batch.item.file.FlatFileItemWriter FlatFileItemWriter} to write LDIF records to a file. + * + * @author Keith Barlow + * + */ +public class LdifAggregator implements LineAggregator { + + /** + * Returns a {@link java.lang.String String} containing a properly formated LDIF. + * + * @param item LdapAttributes object to convert to string. + * @return string representation of the object LDIF format (in accordance with RFC 2849). + */ + public String aggregate(LdapAttributes item) { + return item.toString(); + } + +} diff --git a/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/LdifReader.java b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/LdifReader.java new file mode 100644 index 00000000..e2cc33e7 --- /dev/null +++ b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/LdifReader.java @@ -0,0 +1,152 @@ +package org.springframework.ldap.ldif.batch; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream; +import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.ldap.core.LdapAttributes; +import org.springframework.ldap.ldif.parser.LdifParser; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * The {@link LdifReader LdifReader} is an adaptation of the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader} + * built around an {@link LdifParser LdifParser}. + *

+ * Unlike the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}, the {@link LdifReader LdifReader} + * does not require a mapper. Instead, this version of the {@link LdifReader LdifReader} simply returns an {@link LdapAttributes LdapAttributes} + * object which can be consumed and manipulated as necessary by {@link org.springframework.batch.item.ItemProcessor ItemProcessor} or any + * output service. Alternatively, the {@link RecordMapper RecordMapper} interface can be implemented and set in a + * {@link MappingLdifReader MappingLdifReader} to map records to objects for return. + *

+ * {@link LdifReader LdifReader} usage is mimics that of the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader} + * for all intensive purposes. Adjustments have been made to process records instead of lines, however. As such, the + * {@link #recordsToSkip recordsToSkip} attribute indicates the number of records from the top of the file that should not be processed. + * Implementations of the {@link RecordCallbackHandler RecordCallbackHandler} interface can be used to execute operations on those skipped records. + *

+ * As with the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}, the {@link #strict strict} option differentiates + * between whether or not to require the resource to exist before processing. In the case of a value set to false, a warning is logged instead of + * an exception being thrown. + * + * @author Keith Barlow + * + */ +public class LdifReader extends AbstractItemCountingItemStreamItemReader + implements ResourceAwareItemReaderItemStream, InitializingBean { + + private static final Log log = LogFactory.getLog(LdifReader.class); + + private Resource resource; + + private LdifParser ldifParser; + + private int recordCount = 0; + + private int recordsToSkip = 0; + + private boolean strict = true; + + private RecordCallbackHandler skippedRecordsCallback; + + public LdifReader() { + setName(ClassUtils.getShortName(LdifReader.class)); + } + + /** + * In strict mode the reader will throw an exception on + * {@link #open(org.springframework.batch.item.ExecutionContext)} if the + * input resource does not exist. + * @param strict false by default + */ + public void setStrict(boolean strict) { + this.strict = strict; + } + + /** + * {@link RecordCallbackHandler RecordCallbackHandler} implementations can be used to take action on skipped records. + * + * @param skippedRecordsCallback will be called for each one of the initial + * skipped lines before any items are read. + */ + public void setSkippedRecordsCallback(RecordCallbackHandler skippedRecordsCallback) { + this.skippedRecordsCallback = skippedRecordsCallback; + } + + /** + * Public setter for the number of lines to skip at the start of a file. Can + * be used if the file contains a header without useful (column name) + * information, and without a comment delimiter at the beginning of the + * lines. + * + * @param recordsToSkip the number of lines to skip + */ + public void setRecordsToSkip(int recordsToSkip) { + this.recordsToSkip = recordsToSkip; + } + + @Override + protected void doClose() throws Exception { + if (ldifParser != null) { + ldifParser.close(); + } + this.recordCount = 0; + } + + @Override + protected void doOpen() throws Exception { + if (resource == null) + throw new IllegalStateException("A resource has not been set."); + + if (!resource.exists()) { + if (strict) { + throw new IllegalStateException("Input resource must exist (reader is in 'strict' mode): "+resource); + } else { + log.warn("Input resource does not exist " + resource.getDescription()); + return; + } + } + + ldifParser.open(); + + for (int i = 0; i < recordsToSkip; i++) { + LdapAttributes record = ldifParser.getRecord(); + if (skippedRecordsCallback != null) { + skippedRecordsCallback.handleRecord(record); + } + } + } + + @Override + protected LdapAttributes doRead() throws Exception { + LdapAttributes attributes = null; + + try { + if (ldifParser != null) { + while (attributes == null && ldifParser.hasMoreRecords()) { + attributes = ldifParser.getRecord(); + } + recordCount++; + } + + return attributes; + + } catch(Exception ex){ + log.error("Parsing error at record " + recordCount + " in resource=" + + resource.getDescription() + ", input=[" + attributes + "]", ex); + throw ex; + } + } + + public void setResource(Resource resource) { + this.resource = resource; + this.ldifParser = new LdifParser(resource); + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(resource, "A resource is required to parse."); + Assert.notNull(ldifParser); + } + +} diff --git a/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/MappingLdifReader.java b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/MappingLdifReader.java new file mode 100644 index 00000000..1b462513 --- /dev/null +++ b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/MappingLdifReader.java @@ -0,0 +1,162 @@ +package org.springframework.ldap.ldif.batch; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream; +import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.ldap.core.LdapAttributes; +import org.springframework.ldap.ldif.parser.LdifParser; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * The {@link MappingLdifReader MappingLdifReader} is an adaptation of the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader} + * built around an {@link LdifParser LdifParser}. It differs from the standard {@link LdifReader LdifReader} in its ability to map + * {@link LdapAttributes LdapAttributes} objects to POJOs. + *

+ * The {@link MappingLdifReader MappingLdifReader} requires an {@link RecordMapper RecordMapper} implementation. If mapping + * is not required, the {@link LdifReader LdifReader} should be used instead. It simply returns an {@link LdapAttributes LdapAttributes} + * object which can be consumed and manipulated as necessary by {@link org.springframework.batch.item.ItemProcessor ItemProcessor} or any + * output service. + *

+ * {@link LdifReader LdifReader} usage is mimics that of the FlatFileItemReader for all intensive purposes. Adjustments have been made to + * process records instead of lines, however. As such, the {@link #recordsToSkip recordsToSkip} attribute indicates the number of records + * from the top of the file that should not be processed. Implementations of the {@link RecordCallbackHandler RecordCallbackHandler} + * interface can be used to execute operations on those skipped records. + *

+ * As with the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}, the {@link #strict strict} option + * differentiates between whether or not to require the resource to exist before processing. In the case of a value set to false, a warning + * is logged instead of an exception being thrown. + * + * @author Keith Barlow + * + */ +public class MappingLdifReader extends AbstractItemCountingItemStreamItemReader + implements ResourceAwareItemReaderItemStream, InitializingBean { + + private static final Log log = LogFactory.getLog(MappingLdifReader.class); + + private Resource resource; + + private LdifParser ldifParser; + + private int recordCount = 0; + + private int recordsToSkip = 0; + + private boolean strict = true; + + private RecordCallbackHandler skippedRecordsCallback; + + private RecordMapper recordMapper; + + public MappingLdifReader() { + setName(ClassUtils.getShortName(MappingLdifReader.class)); + } + + /** + * In strict mode the reader will throw an exception on + * {@link #open(org.springframework.batch.item.ExecutionContext)} if the + * input resource does not exist. + * @param strict false by default + */ + public void setStrict(boolean strict) { + this.strict = strict; + } + + /** + * {@link RecordCallbackHandler RecordCallbackHandler} implementations can be used to take action on skipped records. + * + * @param skippedRecordsCallback will be called for each one of the initial + * skipped lines before any items are read. + */ + public void setSkippedRecordsCallback(RecordCallbackHandler skippedRecordsCallback) { + this.skippedRecordsCallback = skippedRecordsCallback; + } + + /** + * Public setter for the number of lines to skip at the start of a file. Can + * be used if the file contains a header without useful (column name) + * information, and without a comment delimiter at the beginning of the + * lines. + * + * @param recordsToSkip the number of lines to skip + */ + public void setRecordsToSkip(int recordsToSkip) { + this.recordsToSkip = recordsToSkip; + } + + /** + * Setter for object mapper. This property is required to be set. + * @param recordMapper maps record to an object + */ + public void setRecordMapper(RecordMapper recordMapper) { + this.recordMapper = recordMapper; + } + + @Override + protected void doClose() throws Exception { + if (ldifParser != null) { + ldifParser.close(); + } + this.recordCount = 0; + } + + @Override + protected void doOpen() throws Exception { + if (resource == null) + throw new IllegalStateException("A resource has not been set."); + + if (!resource.exists()) { + if (strict) { + throw new IllegalStateException("Input resource must exist (reader is in 'strict' mode): "+resource); + } else { + log.warn("Input resource does not exist " + resource.getDescription()); + return; + } + } + + ldifParser.open(); + + for (int i = 0; i < recordsToSkip; i++) { + LdapAttributes record = ldifParser.getRecord(); + if (skippedRecordsCallback != null) { + skippedRecordsCallback.handleRecord(record); + } + } + } + + @Override + protected T doRead() throws Exception { + LdapAttributes attributes = null; + + try { + if (ldifParser != null) { + while (attributes == null && ldifParser.hasMoreRecords()) { + attributes = ldifParser.getRecord(); + } + recordCount++; + return recordMapper.mapRecord(attributes); + } + + return null; + } catch(Exception ex){ + log.error("Parsing error at record " + recordCount + " in resource=" + + resource.getDescription() + ", input=[" + attributes + "]", ex); + throw ex; + } + } + + public void setResource(Resource resource) { + this.resource = resource; + this.ldifParser = new LdifParser(resource); + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(resource, "A resource is required to parse."); + Assert.notNull(ldifParser); + } + +} diff --git a/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/RecordCallbackHandler.java b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/RecordCallbackHandler.java new file mode 100644 index 00000000..93361b99 --- /dev/null +++ b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/RecordCallbackHandler.java @@ -0,0 +1,24 @@ +/** + * + */ +package org.springframework.ldap.ldif.batch; + +import org.springframework.ldap.core.LdapAttributes; + +/** + * This interface can be used to operate on skipped records in the {@link LdifReader LdifReader} and the + * {@link MappingLdifReader MappingLdifReader}. + * + * @author Keith Barlow + * + */ +public interface RecordCallbackHandler { + + /** + * Execute operations on the supplied record. + * + * @param attributes + */ + void handleRecord(LdapAttributes attributes); + +} diff --git a/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/RecordMapper.java b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/RecordMapper.java new file mode 100644 index 00000000..8ec65a4a --- /dev/null +++ b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/RecordMapper.java @@ -0,0 +1,23 @@ +package org.springframework.ldap.ldif.batch; + +import org.springframework.ldap.core.LdapAttributes; + +/** + * This interface should be implemented to map {@link LdapAttributes LdapAttributes} objects to POJOs. The resulting + * implementations can be used in the {@link MappingLdifReader MappingLdifReader}. + * + * @author Keith Barlow + * + * @param + */ +public interface RecordMapper { + + /** + * Maps an {@link LdapAttributes LdapAttributes} object to the specified type. + * + * @param attributes + * @return object of type T + */ + T mapRecord(LdapAttributes attributes); + +} diff --git a/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/package.html b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/package.html new file mode 100644 index 00000000..9b0d12e9 --- /dev/null +++ b/ldif/ldif-batch/src/main/java/org/springframework/ldap/ldif/batch/package.html @@ -0,0 +1,7 @@ + + + +This package contains the classes required for using the LdifParser with the Spring Batch framework. + + + diff --git a/ldif/ldif-batch/src/test/java/org/springframework/ldap/ldif/batch/LdifReaderTest.java b/ldif/ldif-batch/src/test/java/org/springframework/ldap/ldif/batch/LdifReaderTest.java new file mode 100644 index 00000000..e630055d --- /dev/null +++ b/ldif/ldif-batch/src/test/java/org/springframework/ldap/ldif/batch/LdifReaderTest.java @@ -0,0 +1,69 @@ +package org.springframework.ldap.ldif.batch; + +import static org.junit.Assert.*; + +import java.net.MalformedURLException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.test.AbstractJobTests; +import org.springframework.batch.test.AssertFile; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.Assert; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations={"classpath*:applicationContext-test1.xml"}) +public class LdifReaderTest extends AbstractJobTests { + private static Log log = LogFactory.getLog(LdifReaderTest.class); + + private Resource expected; + private Resource actual; + + public LdifReaderTest() { + try { + expected = new UrlResource("file:src/test/resources/expectedOutput.ldif"); + actual = new UrlResource("file:target/test-outputs/output.ldif"); + } catch (MalformedURLException e) { + log.error(e); + } + } + + @Before + public void checkFiles() { + Assert.isTrue(expected.exists(), "Expected does not exist."); + } + + @Test + public void testValidRun() { + try { + JobExecution jobExecution = this.launchStep("step1"); + + //Ensure job completed successfully. + Assert.isTrue(jobExecution.getExitStatus().equals(ExitStatus.COMPLETED), "Step Execution did not complete normally: " + jobExecution.getExitStatus()); + + //Check output. + Assert.isTrue(actual.exists(), "Actual does not exist."); + AssertFile.assertFileEquals(expected.getFile(), actual.getFile()); + + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void testResourceNotExists() { + JobExecution jobExecution = this.launchStep("step2"); + + Assert.isTrue(jobExecution.getExitStatus().getExitCode().equals("FAILED"), "The job exit status is not FAILED."); + Assert.isTrue(jobExecution.getExitStatus().getExitDescription().contains("Failed to initialize the reader"), "The job failed for the wrong reason."); + } +} diff --git a/ldif/ldif-batch/src/test/java/org/springframework/ldap/ldif/batch/MappingLdifReaderTest.java b/ldif/ldif-batch/src/test/java/org/springframework/ldap/ldif/batch/MappingLdifReaderTest.java new file mode 100644 index 00000000..8f286f6e --- /dev/null +++ b/ldif/ldif-batch/src/test/java/org/springframework/ldap/ldif/batch/MappingLdifReaderTest.java @@ -0,0 +1,69 @@ +package org.springframework.ldap.ldif.batch; + +import static org.junit.Assert.*; + +import java.net.MalformedURLException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.test.AbstractJobTests; +import org.springframework.batch.test.AssertFile; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.Assert; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations={"classpath*:applicationContext-test2.xml"}) +public class MappingLdifReaderTest extends AbstractJobTests { + private static Log log = LogFactory.getLog(MappingLdifReaderTest.class); + + private Resource expected; + private Resource actual; + + public MappingLdifReaderTest() { + try { + expected = new UrlResource("file:src/test/resources/expectedOutput.ldif"); + actual = new UrlResource("file:target/test-outputs/output.ldif"); + } catch (MalformedURLException e) { + log.error(e); + } + } + + @Before + public void checkFiles() { + Assert.isTrue(expected.exists(), "Expected does not exist."); + } + + @Test + public void testValidRun() { + try { + JobExecution jobExecution = this.launchStep("step1"); + + //Ensure job completed successfully. + Assert.isTrue(jobExecution.getExitStatus().equals(ExitStatus.COMPLETED), "Step Execution did not complete normally: " + jobExecution.getExitStatus()); + + //Check output. + Assert.isTrue(actual.exists(), "Actual does not exist."); + AssertFile.assertFileEquals(expected.getFile(), actual.getFile()); + + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void testResourceNotExists() { + JobExecution jobExecution = this.launchStep("step2"); + + Assert.isTrue(jobExecution.getExitStatus().getExitCode().equals("FAILED"), "The job exit status is not FAILED."); + Assert.isTrue(jobExecution.getExitStatus().getExitDescription().contains("Failed to initialize the reader"), "The job failed for the wrong reason."); + } +} diff --git a/ldif/ldif-batch/src/test/java/org/springframework/ldap/ldif/batch/MyMapper.java b/ldif/ldif-batch/src/test/java/org/springframework/ldap/ldif/batch/MyMapper.java new file mode 100644 index 00000000..300fa592 --- /dev/null +++ b/ldif/ldif-batch/src/test/java/org/springframework/ldap/ldif/batch/MyMapper.java @@ -0,0 +1,22 @@ +/** + * + */ +package org.springframework.ldap.ldif.batch; + +import org.springframework.ldap.core.LdapAttributes; + +/** + * This default implementation simply returns the LdapAttributes object and is only intended for test. As its not required + * to return an object of a specific type to make the MappingLdifReader implementation work, this basic setting is sufficient + * to demonstrate its function. + * + * @author Keith Barlow + * + */ +public class MyMapper implements RecordMapper { + + public LdapAttributes mapRecord(LdapAttributes attributes) { + return attributes; + } + +} diff --git a/ldif/ldif-batch/src/test/resources/applicationContext-test1.xml b/ldif/ldif-batch/src/test/resources/applicationContext-test1.xml new file mode 100644 index 00000000..85a718bb --- /dev/null +++ b/ldif/ldif-batch/src/test/resources/applicationContext-test1.xml @@ -0,0 +1,53 @@ + + + + + + + + org.springframework.ldap.ldif.InvalidAttributeFormatException + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ldif/ldif-batch/src/test/resources/applicationContext-test2.xml b/ldif/ldif-batch/src/test/resources/applicationContext-test2.xml new file mode 100644 index 00000000..bdeb800d --- /dev/null +++ b/ldif/ldif-batch/src/test/resources/applicationContext-test2.xml @@ -0,0 +1,57 @@ + + + + + + + + org.springframework.ldap.ldif.InvalidAttributeFormatException + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ldif/ldif-batch/src/test/resources/expectedOutput.ldif b/ldif/ldif-batch/src/test/resources/expectedOutput.ldif new file mode 100644 index 00000000..1170dff2 --- /dev/null +++ b/ldif/ldif-batch/src/test/resources/expectedOutput.ldif @@ -0,0 +1,75 @@ +dn: cn=Bjorn Jensen,ou=Accounting,dc=airius,dc=com +telephonenumber: +1 408 555 1212 +objectclass: top +objectclass: person +objectclass: organizationalPerson +sn: Jensen +cn: Bjorn Jensen + +dn: cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com +telephonenumber: +1 408 555 1212 +uid: bjensen +description: Babs is a big sailing fan, and travels extensively in search of perfect sailing conditions. +objectclass: top +objectclass: person +objectclass: organizationalPerson +title: Product Manager, Rod and Reel Division +sn: Jensen +cn: Barbara Jensen +cn: Barbara J Jensen +cn: Babs Jensen + +dn: cn=Gern Jensen,ou=Product Testing,dc=airius,dc=com +telephonenumber: +1 408 555 1212 +uid: gernj +description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVlIGlzIGJhc2UtNjQtZW5j +b2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdGVyIGluIGl0IChhIENSKS4NICBC +eSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQgb3V0IG1vcmUu +objectclass: top +objectclass: person +objectclass: organizationalPerson +sn: Jensen +cn: Gern Jensen +cn: Gern O Jensen + +dn:: b3U95Za25qWt6YOoLG89QWlyaXVz +ou:: 5Za25qWt6YOo +ou:: 44GI44GE44GO44KH44GG44G2 +ou: Sales +description: Japanese office +objectclass: top +objectclass: organizationalUnit + +dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz +givenname:: 44Ot44OJ44OL44O8 +givenname:: 44KN44Gp44Gr44O8 +givenname: Rodney +sn:: 5bCP56yg5Y6f +sn:: 44GK44GM44GV44KP44KJ +sn: Ogasawara +userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM= +mail: rogasawara@airius.co.jp +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +uid: rogasawara +preferredlanguage: ja +cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +cn:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA== +cn: Rodney Ogasawara +title:: 5Za25qWt6YOoIOmDqOmVtw== +title:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg== +title: Sales, Director + +dn: cn=Horatio Jensen,ou=Product Testing,dc=airius,dc=com +telephonenumber: +1 408 555 1212 +uid: hjensen +jpegphoto:< file:///usr/local/directory/photos/hjensen.jpg +objectclass: top +objectclass: person +objectclass: organizationalPerson +sn: Jensen +cn: Horatio Jensen +cn: Horatio N Jensen + diff --git a/ldif/ldif-batch/src/test/resources/log4j.properties b/ldif/ldif-batch/src/test/resources/log4j.properties new file mode 100644 index 00000000..2a0545bb --- /dev/null +++ b/ldif/ldif-batch/src/test/resources/log4j.properties @@ -0,0 +1,12 @@ +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.layout=org.apache.log4j.PatternLayout + +log4j.appender.console.layout.ConversionPattern=%d [%t] %-5p %c - %m%n + +log4j.logger.org.springframework.test=INFO, console +log4j.logger.org.springframework.beans=INFO, console +log4j.logger.org.springframework.context=INFO, console +log4j.logger.org.springframework.transaction=INFO, console +log4j.logger.org.springframework.aop=INFO, console +log4j.logger.org.springframework.batch=INFO, console +log4j.logger.org.springframework.ldap.ldif=DEBUG, console \ No newline at end of file diff --git a/ldif/ldif-batch/src/test/resources/test.ldif b/ldif/ldif-batch/src/test/resources/test.ldif new file mode 100644 index 00000000..c885861f --- /dev/null +++ b/ldif/ldif-batch/src/test/resources/test.ldif @@ -0,0 +1,236 @@ +version: 1 + +# +#Examples of LDAP Data Interchange Format +# + +# +#Example 1: An simple LDAP file with two entries +# + +dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Barbara Jensen +cn: Barbara J Jensen +cn: Babs Jensen +sn: Jensen +uid: bjensen +telephonenumber: +1 408 555 1212 +description: A big sailing fan. + +dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Bjorn Jensen +sn: Jensen +telephonenumber: +1 408 555 1212 + + +# +# Example 2: A file containing an entry with a folded attribute value +# + +dn:cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com +objectclass:top +objectclass:person +objectclass:organizationalPerson +cn:Barbara Jensen +cn:Barbara J Jensen +cn:Babs Jensen +sn:Jensen +uid:bjensen +telephonenumber:+1 408 555 1212 +description:Babs is a big sailing fan, and travels extensively in sea + rch of perfect sailing conditions. +title:Product Manager, Rod and Reel Division + +# +# Example 3: A file containing a base-64-encoded value +# + +dn: cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Gern Jensen +cn: Gern O Jensen +sn: Jensen +uid: gernj +telephonenumber: +1 408 555 1212 +description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl + IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG + VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg + b3V0IG1vcmUu + + +# +# Example 4: A file containing an entries with UTF-8-encoded attribute +# values, including language tags. Comments indicate the contents +# of UTF-8-encoded attributes and distinguished names. +# + +dn:: b3U95Za25qWt6YOoLG89QWlyaXVz +# dn:: ou=,o=Airius +objectclass: top +objectclass: organizationalUnit +ou:: 5Za25qWt6YOo +# ou:: +ou;lang-ja:: 5Za25qWt6YOo +# ou;lang-ja:: +ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2 +# ou;lang-ja:: +ou;lang-en: Sales +description: Japanese office + +dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz +# dn:: uid=,ou=,o=Airius +userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM= +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +uid: rogasawara +mail: rogasawara@airius.co.jp +givenname;lang-ja:: 44Ot44OJ44OL44O8 +# givenname;lang-ja:: +sn;lang-ja:: 5bCP56yg5Y6f +# sn;lang-ja:: +cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +# cn;lang-ja:: +title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw== +# title;lang-ja:: +preferredlanguage: ja +givenname:: 44Ot44OJ44OL44O8 +# givenname:: +sn:: 5bCP56yg5Y6f +# sn:: +cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +# cn:: +title:: 5Za25qWt6YOoIOmDqOmVtw== +# title:: +givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8 +# givenname;lang-ja;phonetic:: +# +sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ +# sn;lang-ja;phonetic:: +cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA== +# cn;lang-ja;phonetic:: +title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg== +# title;lang-ja;phonetic:: +# +givenname;lang-en: Rodney +sn;lang-en: Ogasawara +cn;lang-en: Rodney Ogasawara +title;lang-en: Sales, Director + + +# +# Example 5: An LDIF file containing an invalid attribute +# + +dn: cn=Harry Jacobs, ou=Product Development, dc=airius, dc=com +objectClass: top +objectClass: person +cn: Harry Jacobs +description: :A big sailing fan. + + +# +# Example 6: A file containing a reference to an external file +# + +dn: cn=Horatio Jensen, ou=Product Testing, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Horatio Jensen +cn: Horatio N Jensen +sn: Jensen +uid: hjensen +telephonenumber: +1 408 555 1212 +jpegphoto:< file:///usr/local/directory/photos/hjensen.jpg + + +# +# Example 7: A file containing a series of change records and comments +# + +# Add a new entry +dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com +changetype: add +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Fiona Jensen +sn: Jensen +uid: fiona +telephonenumber: +1 408 555 1212 +jpegphoto:< file:///usr/local/directory/photos/fiona.jpg + +# Delete an existing entry +dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com +changetype: delete + +# Modify an entry's relative distinguished name +dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com +changetype: modrdn +newrdn: cn=Paula Jensen +deleteoldrdn: 1 + +# Rename an entry and move all of its children to a new location in +# the directory tree (only implemented by LDAPv3 servers). +dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com +changetype: modrdn +newrdn: ou=Product Development Accountants +deleteoldrdn: 0 +newsuperior: ou=Accounting, dc=airius, dc=com + + +# Modify an entry: add an additional value to the postaladdress +# attribute, completely delete the description attribute, replace +# the telephonenumber attribute with two values, and delete a specific +# value from the facsimiletelephonenumber attribute +dn: cn=Paula Jensen, ou=Product Development, dc=airius, dc=com +changetype: modify +add: postaladdress +postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086 +- + +delete: description +- +replace: telephonenumber +telephonenumber: +1 408 555 1234 +telephonenumber: +1 408 555 5678 +- +delete: facsimiletelephonenumber +facsimiletelephonenumber: +1 408 555 9876 +- + +# Modify an entry: replace the postaladdress attribute with an empty +# set of values (which will cause the attribute to be removed), and +# delete the entire description attribute. Note that the first will +# always succeed, while the second will only succeed if at least +# one value for the description attribute is present. +dn: cn=Ingrid Jensen, ou=Product Support, dc=airius, dc=com +changetype: modify +replace: postaladdress +- +delete: description +- + + +# +# Example 8: An LDIF file containing a change record with a control +# + +# Delete an entry. The operation will attach the LDAPv3 +# Tree Delete Control defined in [9]. The criticality +# field is "true" and the controlValue field is +# absent, as required by [9]. +dn: ou=Product Development, dc=airius, dc=com +control: 1.2.840.113556.1.4.805 true +changetype: delete + diff --git a/ldif/ldif-core/.springBeans b/ldif/ldif-core/.springBeans new file mode 100644 index 00000000..41209bec --- /dev/null +++ b/ldif/ldif-core/.springBeans @@ -0,0 +1,13 @@ + + + 1 + + + + + + + + + + diff --git a/ldif/ldif-core/pom.xml b/ldif/ldif-core/pom.xml new file mode 100644 index 00000000..ce5a176c --- /dev/null +++ b/ldif/ldif-core/pom.xml @@ -0,0 +1,40 @@ + + + org.springframework.ldap + spring-ldap-parent-tiger + 1.3.1.CI-SNAPSHOT + + 4.0.0 + spring-ldap-ldif-core + Spring LDAP LDIF Core + LDIFParser and supporting classes. + + + commons-lang + commons-lang + compile + + + junit + junit + 4.4 + test + + + log4j + log4j + test + + + org.springframework + spring-beans + compile + + + org.springframework.ldap + spring-ldap-core + ${version} + compile + + + diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/core/LdapAttribute.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/core/LdapAttribute.java new file mode 100644 index 00000000..d59a0eb9 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/core/LdapAttribute.java @@ -0,0 +1,225 @@ +/* + * Copyright 2005-2009 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.core; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.naming.directory.BasicAttribute; + +/** + * Extends {@link javax.naming.directory.BasicAttribute} to add support for + * options as defined in RFC2849. + *

+ * While uncommon, options can be used to specify additional descriptors for + * the attribute. Options are backed by a {@link java.util.HashSet} of + * {@link java.lang.String}. + * + * @author Keith Barlow + * + */ +public class LdapAttribute extends BasicAttribute { + + private static final long serialVersionUID = -5263905906016179429L; + + /** + * Holds the attributes options. + */ + protected Set options = new HashSet(); + + /** + * Creates an unordered attribute with the specified ID. + * + * @param id {@link java.lang.String} ID of the attribute. + */ + public LdapAttribute(String id) { + super(id); + } + + /** + * Creates an unordered attribute with the specified ID and value. + * + * @param id {@link java.lang.String} ID of the attribute. + * @param value Attribute value. + */ + public LdapAttribute(String id, Object value) { + super(id, value); + } + + /** + * Creates an unordered attribute with the specified ID, value, and options. + * + * @param id {@link java.lang.String} ID of the attribute. + * @param value Attribute value. + * @param options {@link java.util.Collection} of {@link java.lang.String} attribute options. + */ + public LdapAttribute(String id, Object value, Collection options) { + super(id, value); + this.options.addAll(options); + } + + /** + * Creates an attribute with the specified ID whose values may be ordered. + * + * @param id {@link java.lang.String} ID of the attribute. + * @param ordered boolean indicating whether or not the attributes values are ordered. + */ + public LdapAttribute(String id, boolean ordered) { + super(id, ordered); + } + + /** + * Creates an attribute with the specified ID and options whose values may be ordered. + * + * @param id {@link java.lang.String} ID of the attribute. + * @param options {@link java.util.Collection} of {@link java.lang.String} attribute options. + * @param ordered boolean indicating whether or not the attributes values are ordered. + */ + public LdapAttribute(String id, Collection options, boolean ordered) { + super(id, ordered); + this.options.addAll(options); + } + + /** + * Creates an attribute with the specified ID and value whose values may be ordered. + * + * @param id {@link java.lang.String} ID of the attribute. + * @param value Attribute value. + * @param ordered boolean indicating whether or not the attributes values are ordered. + */ + public LdapAttribute(String id, Object value, boolean ordered) { + super(id, value, ordered); + } + + /** + * Creates an attribute with the specified ID, value, and options whose values may be ordered. + * + * @param id {@link java.lang.String} ID of the attribute. + * @param value Attribute value. + * @param options {@link java.util.Collection} of {@link java.lang.String} attribute options. + * @param ordered boolean indicating whether or not the attributes values are ordered. + */ + public LdapAttribute(String id, Object value, Collection options, boolean ordered) { + super(id, value, ordered); + this.options.addAll(options); + } + + /** + * Get options. + * + * @return returns a {@link java.util.Set} of {@link java.lang.String} + */ + public Set getOptions() { + return this.options; + } + + /** + * Set options. + * + * @param options {@link java.util.Set} of {@link java.lang.String} + */ + public void setOptions(Set options) { + this.options = options; + } + + /** + * Add an option. + * + * @param option {@link java.lang.String} option. + * @return boolean indication successful addition of option. + */ + public boolean addOption(String option) { + return this.options.add(option); + } + + /** + * Add all values in the collection to the options. + * + * @param options {@link java.util.Collection} of {@link java.lang.String} values. + * @return boolean indication successful addition of options. + */ + public boolean addAllOptions(Collection options) { + return this.options.addAll(options); + } + + /** + * Clears all stored options. + */ + public void clearOptions() { + this.options.clear(); + } + + /** + * Checks for existence of a particular option on the set. + * + * @param option {@link java.lang.String} option. + * @return boolean indicating result. + */ + public boolean contains(String option) { + return this.options.contains(option); + } + + /** + * Checks for existence of a series of options on the set. + * + * @param options {@link java.util.Collection} of {@link java.lang.String} options. + * @return boolean indicating result. + */ + public boolean containsAll(Collection options) { + return this.options.containsAll(options); + } + + /** + * Tests for the presence of options. + * + * @return boolean indicating result. + */ + public boolean hasOptions() { + return !options.isEmpty(); + } + + /** + * Removes an option from the the set. + * + * @param option {@link java.lang.String} option. + * @return boolean indicating successful removal of option. + */ + public boolean removeOption(String option) { + return this.options.remove(options); + } + + /** + * Removes all options listed in the supplied set. + * + * @param options {@link java.util.Collection} of {@link java.lang.String} options. + * @return boolean indicating successful removal of options. + */ + public boolean removeAllOptions(Collection options) { + return this.options.removeAll(options); + } + + /** + * Removes any options not on the set of supplied options. + * + * @param options {@link java.util.Collection} of {@link java.lang.String} options. + * @return boolean indicating successful retention of options. + */ + public boolean retainAllOptions(Collection options) { + return this.options.retainAll(options); + } + +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/core/LdapAttributes.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/core/LdapAttributes.java new file mode 100644 index 00000000..9f101c9b --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/core/LdapAttributes.java @@ -0,0 +1,211 @@ +/* + * Copyright 2005-2009 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.core; + +import java.net.URI; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.BasicAttributes; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.core.DistinguishedName; + +import sun.misc.BASE64Encoder; + +/** + * Extends {@link javax.naming.directory.BasicAttributes} to add specialized support + * for DNs. + *

+ * While DNs appear to be and can be treated as attributes, they have a special + * meaning in that they define the address to which the object is bound. DNs must + * conform to special formating rules and are typically required to be handled + * separately from other attributes. + *

+ * This class makes this distinction between the DN and other + * attributes prominent and apparent. + * + * @author Keith Barlow + * + */ +public class LdapAttributes extends BasicAttributes { + + private static final long serialVersionUID = 97903297123869138L; + + private static Log log = LogFactory.getLog(LdapAttributes.class); + + private static final String SAFE_CHAR = "[\\p{ASCII}&&[^\\x00\\x0A\\x0D]]"; //Any ASCII except NUL, LF, and CR + + private static final String SAFE_INIT_CHAR = "[\\p{ASCII}&&[^ \\x00\\x0A\\x0D\\x3A\\x3C]]"; //Any ASCII except NUL, LF, CR, SPACE, colon, and less-than + + /** + * Distinguished name to which the object is bound. + */ + protected DistinguishedName dn = new DistinguishedName(); + + /** + * Default constructor. + */ + public LdapAttributes() { + + } + + /** + * Creates an LdapAttributes object with the specified DN. + * + * @param dn The {@link org.springframework.ldap.core.DistinguishedName} to which this object is bound. + */ + public LdapAttributes(DistinguishedName dn) { + super(); + this.dn = dn; + } + + /** + * Constructor for specifying whether or not the object is case sensitive. + * + * @param ignoreCase boolean indicator. + */ + public LdapAttributes(boolean ignoreCase) { + super(ignoreCase); + } + + /** + * Creates an LdapAttributes object with the specified DN and case sensitivity setting. + * + * @param dn The {@link org.springframework.ldap.core.DistinguishedName} to which this object is bound. + * @param ignoreCase boolean indicator. + */ + public LdapAttributes(DistinguishedName dn, boolean ignoreCase) { + super(ignoreCase); + this.dn = dn; + } + + /** + * Creates an LdapAttributes object with the specified attribute. + * + * @param attrID {@link java.lang.String} ID of the attribute. + * @param val Value of the attribute. + */ + public LdapAttributes(String attrID, Object val) { + put(new LdapAttribute(attrID, val)); + } + + /** + * Creates an LdapAttributes object with the specifying attribute and value and case sensitivity setting. + * + * @param dn The {@link org.springframework.ldap.core.DistinguishedName} to which this object is bound. + * @param attrID {@link java.lang.String} ID of the attribute. + * @param val Value of the attribute. + */ + public LdapAttributes(DistinguishedName dn, String attrID, Object val) { + this.dn = dn; + put(new LdapAttribute(attrID, val)); + } + + /** + * Creates an LdapAttributes object with the specifying attribute and value and case sensitivity setting. + * + * @param attrID {@link java.lang.String} ID of the attribute. + * @param val Value of the attribute. + * @param ignoreCase boolean indicator. + */ + public LdapAttributes(String attrID, Object val, boolean ignoreCase) { + put(new LdapAttribute(attrID, val, ignoreCase)); + } + + /** + * Creates an LdapAttributes object for the supplied DN with the attribute specified. + * + * @param dn The {@link org.springframework.ldap.core.DistinguishedName} to which this object is bound. + * @param attrID {@link java.lang.String} ID of the attribute. + * @param val Value of the attribute. + * @param ignoreCase boolean indicator. + */ + public LdapAttributes(DistinguishedName dn, String attrID, Object val, boolean ignoreCase) { + this.dn = dn; + put(new LdapAttribute(attrID, val, ignoreCase)); + } + + /** + * Returns the distinguished name to which the object is bound. + * + * @return {@link org.springframework.ldap.core.DistinguishedName} specifying the name to which the object is bound. + */ + public DistinguishedName getDN() { + return dn; + } + + /** + * Sets the distinguished name of the object. + * + * @param dn {@link org.springframework.ldap.core.DistinguishedName} specifying the name to which the object is bound. + */ + public void setDN(DistinguishedName dn) { + this.dn = dn; + } + + /** + * Returns a string representation of the object in LDIF format. + * + * @return {@link java.lang.String} formated to RFC2849 LDIF specifications. + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + + try { + + DistinguishedName dn = getDN(); + + if (!dn.toString().matches(SAFE_INIT_CHAR + SAFE_CHAR + "*")) { + sb.append("dn:: " + new BASE64Encoder().encode(dn.toString().getBytes()) + "\n"); + } else { + sb.append("dn: " + getDN() + "\n"); + } + + NamingEnumeration attributes = getAll(); + + while (attributes.hasMore()) { + Attribute attribute = attributes.next(); + NamingEnumeration values = attribute.getAll(); + + while (values.hasMore()) { + Object value = values.next(); + + if (value instanceof String) + sb.append(attribute.getID() + ": " + (String) value + "\n"); + + else if (value instanceof byte[]) + sb.append(attribute.getID() + ":: " + new BASE64Encoder().encode((byte[]) value) + "\n"); + + else if (value instanceof URI) + sb.append(attribute.getID() + ":< " + (URI) value + "\n"); + + else { + sb.append(attribute.getID() + ": " + value + "\n"); + } + } + } + + } catch (NamingException e) { + log.error("Error formating attributes for output.", e); + sb = new StringBuilder(); + } + + return sb.toString(); + } +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/InvalidAttributeFormatException.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/InvalidAttributeFormatException.java new file mode 100644 index 00000000..1e85ce0b --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/InvalidAttributeFormatException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2005-2009 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.ldif; + +import org.springframework.ldap.NamingException; + +/** + * Thrown whenever a parsed attribute does not conform to LDAP specifications. + * + * @author Keith Barlow + * + */ +public class InvalidAttributeFormatException extends NamingException { + + private static final long serialVersionUID = -4529380160785322985L; + + /** + * @param msg + */ + public InvalidAttributeFormatException(String msg) { + super(msg); + } + + /** + * @param cause + */ + public InvalidAttributeFormatException(Throwable cause) { + super(cause); + } + + /** + * @param msg + * @param cause + */ + public InvalidAttributeFormatException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/InvalidRecordFormatException.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/InvalidRecordFormatException.java new file mode 100644 index 00000000..3615ceed --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/InvalidRecordFormatException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2005-2009 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.ldif; + +import org.springframework.ldap.NamingException; + +/** + * Thrown whenever a parsed record does not conform to LDAP specifications. + * + * @author Keith Barlow + * + */ +public class InvalidRecordFormatException extends NamingException { + + private static final long serialVersionUID = -5047874723621065139L; + + /** + * @param msg + */ + public InvalidRecordFormatException(String msg) { + super(msg); + } + + /** + * @param cause + */ + public InvalidRecordFormatException(Throwable cause) { + super(cause); + } + + /** + * @param msg + * @param cause + */ + public InvalidRecordFormatException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/package.html b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/package.html new file mode 100644 index 00000000..4ebf8a53 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/package.html @@ -0,0 +1,10 @@ + + + +The base package for Spring LDAPs LDIF parser implementation. +

+Classes declared in this package include the new base types for +LDAP objects as well as exception types for the LDIF parser. + + + diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/parser/LdifParser.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/parser/LdifParser.java new file mode 100644 index 00000000..2e1fd272 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/parser/LdifParser.java @@ -0,0 +1,363 @@ +/* + * Copyright 2005-2009 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.ldif.parser; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.NoSuchElementException; + +import javax.naming.NamingException; +import javax.naming.directory.Attribute; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapAttributes; +import org.springframework.ldap.ldif.InvalidRecordFormatException; +import org.springframework.ldap.ldif.support.AttributeValidationPolicy; +import org.springframework.ldap.ldif.support.DefaultAttributeValidationPolicy; +import org.springframework.ldap.ldif.support.LineIdentifier; +import org.springframework.ldap.ldif.support.SeparatorPolicy; +import org.springframework.ldap.schema.DefaultSchemaSpecification; +import org.springframework.ldap.schema.Specification; +import org.springframework.util.Assert; + +/** + * The {@link LdifParser LdifParser} is the main class of the {@link org.springframework.ldap.ldif} package. + * This class reads lines from a resource and assembles them into an {@link LdapAttributes LdapAttributes} object. + * The {@link LdifParser LdifParser} does ignores changetype LDIF entries as their usefulness in the + * context of an application has yet to be determined. + *

+ * Design
+ * {@link LdifParser LdifParser} provides the main interface for operation but requires three supporting classes to + * enable operation: + *

+ * Together, these 4 classes read from the resource line by line and translate the data into objects for use. + *

+ * Usage
+ * {@link #getRecord() getRecord()} reads the next available record from the resource. Lines are read and + * passed to the {@link SeparatorPolicy SeparatorPolicy} for interpretation. The parser continues to read + * lines and appends them to the buffer until it encounters the start of a new attribute or an end of record + * delimiter. When the new attribute or end of record is encountered, the buffer is passed to the + * {@link AttributeValidationPolicy AttributeValidationPolicy} which ensures the buffer conforms to a valid + * attribute definition as defined in RFC2849 and returns an {@link org.springframework.ldap.core.LdapAttribute LdapAttribute} object + * which is then added to the record, an {@link LdapAttributes LdapAttributes} object. Upon encountering the + * end of record, the record is validated by the {@link Specification Specification} policy and, + * if valid, returned to the requester. + *

+ * NOTE: By default, objects are not validated. If validation is required, + * an appropriate specification object must be set. + *

+ * The parser requires the resource to be {@link #open() open()} prior to an invocation of {@link #getRecord() getRecord()}. + * {@link #hasMoreRecords() hasMoreRecords()} can be used to loop over the resource until all records have been + * retrieved. Likewise, the {@link #reset() reset()} method will reset the resource. + *

+ * Objects implementing the {@link javax.naming.directory.Attributes Attributes} interface are required to support a case sensitivity setting + * which controls whether or not the attribute IDs of the object are case sensitive. The {@link #caseInsensitive caseInsensitive} + * setting of the {@link LdifParser LdifParser} is passed to the constructor of any {@link javax.naming.directory.Attributes Attributes} created. The + * default value for this setting is true so that case insensitive objects are created. + * + * @author Keith Barlow + * + */ +public class LdifParser implements Parser, InitializingBean { + + private static final Log log = LogFactory.getLog(LdifParser.class); + + /** + * The resource to parse. + */ + private Resource resource; + + /** + * A BufferedReader to read the file. + */ + private BufferedReader reader; + + /** + * The SeparatorPolicy to use for interpreting attributes from the lines of the resource. + */ + private SeparatorPolicy separatorPolicy = new SeparatorPolicy(); + + /** + * The AttributeValidationPolicy to use to interpret attributes. + */ + private AttributeValidationPolicy attributePolicy = new DefaultAttributeValidationPolicy(); + + /** + * The RecordSpecification for validating records produced. + */ + private Specification specification = new DefaultSchemaSpecification(); + + /** + * This setting is used to control the case sensitivity of LdapAttribute objects returned by the parser. + */ + private boolean caseInsensitive = true; + + /** + * Default constructor. + */ + public LdifParser() { + + } + + /** + * Creates a LdifParser with the indicated case sensitivity setting. + * + * @param caseInsensitive Case sensitivity setting for LdapAttributes objects returned by the parser. + */ + public LdifParser(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + } + + /** + * Creates an LdifParser for the specified resource with the provided case sensitivity setting. + * + * @param resource The resource to parse. + * @param caseInsensitive Case sensitivity setting for LdapAttributes objects returned by the parser. + */ + public LdifParser(Resource resource, boolean caseInsensitive) { + this.resource = resource; + this.caseInsensitive = caseInsensitive; + } + + /** + * Convenience constructor for resource specification. + * + * @param resource The resource to parse. + */ + public LdifParser(Resource resource) { + this.resource = resource; + } + + /** + * Convenience constructor: accepts a File object. + * + * @param file The file to parse. + */ + public LdifParser(File file) { + this.resource = new FileSystemResource(file); + } + + /** + * Set the separator policy. + * + * The default separator policy should suffice for most needs. + * + * @param separatorPolicy Separator policy. + */ + public void setSeparatorPolicy(SeparatorPolicy separatorPolicy) { + this.separatorPolicy = separatorPolicy; + } + + /** + * Policy object enforcing the rules for acceptable attributes. + * + * @param avPolicy Attribute validation policy. + */ + public void setAttributeValidationPolicy(AttributeValidationPolicy avPolicy) { + this.attributePolicy = avPolicy; + } + + /** + * Policy object for enforcing rules to acceptable LDAP objects. + * + * This policy may be used to enforce schema restrictions. + * @param specification + */ + public void setRecordSpecification(Specification specification) { + this.specification = specification; + } + + public void setResource(Resource resource) { + this.resource = resource; + } + + public void setCaseInsensitive(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + } + + public void open() throws IOException { + Assert.notNull(resource, "Resource must be set."); + reader = new BufferedReader(new InputStreamReader(resource.getInputStream())); + } + + public boolean isReady() throws IOException { + return reader.ready(); + } + + public void close() throws IOException { + if (resource.isOpen()) + reader.close(); + } + + public void reset() throws IOException { + Assert.notNull(reader, "A reader has not been obtained."); + reader.reset(); + } + + public boolean hasMoreRecords() throws IOException { + return reader.ready(); + } + + public LdapAttributes getRecord() throws IOException { + Assert.notNull(reader, "A reader must be obtained: parser not open."); + + if (!reader.ready()) { + log.debug("Reader not ready!"); + return null; + } + + LdapAttributes record = new LdapAttributes(caseInsensitive); + StringBuilder builder = new StringBuilder(); + + String line = reader.readLine(); + + while(true) { + + LineIdentifier identifier = separatorPolicy.assess(line); + + switch(identifier) { + case NewRecord: + log.trace("Starting new record."); + //Start new record. + builder = new StringBuilder(line); + + break; + + case Control: + log.trace("'control' encountered."); + + //Log WARN and discard record. + log.warn("LDIF change records have no implementation: record will be ignored."); + builder = null; + record = null; + + break; + + case ChangeType: + log.trace("'changetype' encountered."); + + //Log WARN and discard record. + log.warn("LDIF change records have no implementation: record will be ignored."); + builder = null; + record = null; + + break; + + case Attribute: + //flush buffer. + addAttributeToRecord(builder.toString(), record); + + log.trace("Starting new attribute."); + //Start new attribute. + builder = new StringBuilder(line); + + break; + + case Continuation: + log.trace("...appending line to buffer."); + //Append line to buffer. + builder.append(line.replaceFirst(" ", "")); + + break; + + case EndOfRecord: + log.trace("...done parsing record. (EndOfRecord)"); + + //Validate record and return. + if (record == null) return null; + else { + try { + //flush buffer. + addAttributeToRecord(builder.toString(), record); + + if (specification.isSatisfiedBy(record)) { + log.debug("record parsed:\n" + record); + return record; + + } else { + throw new InvalidRecordFormatException("Record [dn: " + record.getDN() + "] does not conform to specification."); + } + } catch(NamingException e) { + log.error(e); + return null; + } + } + + default: + //Take no action -- applies to VersionIdentifier, Comments, and voided records. + } + + line = reader.readLine(); + + } + + } + + private void addAttributeToRecord(String buffer, LdapAttributes record) { + try { + if (StringUtils.isNotEmpty(buffer) && record != null) { + //Validate previous attribute and add to record. + Attribute attribute = attributePolicy.parse(buffer); + + if (attribute.getID().equalsIgnoreCase("dn")) { + log.trace("...adding DN to record."); + + String dn; + if (attribute.get() instanceof byte[]) { + dn = new String((byte[]) attribute.get()); + } else { + dn = (String) attribute.get(); + } + + record.setDN(new DistinguishedName(dn)); + + } else { + log.trace("...adding attribute to record."); + Attribute attr = record.get(attribute.getID()); + + if (attr != null) { + attr.add(attribute.get()); + } else { + record.put(attribute); + } + } + } + } catch (NamingException e) { + log.error(e); + } catch (NoSuchElementException e) { + log.error(e); + } + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(resource, "A resource to parse is required."); + Assert.isTrue(resource.exists(), resource.getDescription() + ": resource does not exist!"); + Assert.isTrue(resource.isReadable(), "Resource is not readable."); + } + +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/parser/Parser.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/parser/Parser.java new file mode 100644 index 00000000..88b1b285 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/parser/Parser.java @@ -0,0 +1,90 @@ +/* + * Copyright 2005-2009 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.ldif.parser; + +import java.io.IOException; + +import javax.naming.directory.Attributes; + +import org.springframework.core.io.Resource; + +/** + * The Parser interface represents the required methods to be implemented by parser utilities. + * These methods are the base set of methods needed to provide parsing ability. + * + * @author Keith Barlow + */ +public interface Parser { + + /** + * Sets the resource to parse. + * + * @param resource The resource to parse. + */ + public void setResource(Resource resource); + + /** + * Sets the control parameter for specifying case sensitivity on creation of the {@link Attributes} object. + * + * @param caseInsensitive The resource to parse. + */ + public void setCaseInsensitive(boolean caseInsensitive); + + /** + * Opens the resource: the resource must be opened prior to parsing. + * + * @throws IOException if a problem is encountered while trying to open the resource. + */ + public void open() throws IOException; + + /** + * Closes the resource after parsing. + * + * @throws IOException if a problem is encountered while trying to close the resource. + */ + public void close() throws IOException; + + /** + * Resets the line read parser. + * + * @throws Exception if a problem is encountered while trying to reset the resource. + */ + public void reset() throws IOException; + + /** + * True if the resource contains more records; false otherwise. + * + * @return boolean indicating whether or not the end of record has been reached. + * @throws IOException if a problem is encountered while trying to validate the resource is ready. + */ + public boolean hasMoreRecords() throws IOException; + + /** + * Parses the next record from the resource. + * + * @return LdapAttributes object representing the record parsed. + * @throws IOException if a problem is encountered while trying to read from the resource. + */ + public Attributes getRecord() throws IOException; + + /** + * Indicates whether or not the parser is ready to to return results. + * + * @return boolean indicator + * @throws IOException if there is a problem with the underlying resource. + */ + public boolean isReady() throws IOException; +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/parser/package.html b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/parser/package.html new file mode 100644 index 00000000..09952d1f --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/parser/package.html @@ -0,0 +1,7 @@ + + + +This package contains the parser classes and interfaces. + + + diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/AttributeValidationPolicy.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/AttributeValidationPolicy.java new file mode 100644 index 00000000..6953fff0 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/AttributeValidationPolicy.java @@ -0,0 +1,36 @@ +/* + * Copyright 2005-2009 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.ldif.support; + +import javax.naming.directory.Attribute; + +/** + * Interface defining the required methods for AttributeValidationPolicies. + * + * @author Keith Barlow + * + */ +public interface AttributeValidationPolicy { + + /** + * Validates attribute contained in the buffer and returns an LdapAttribute. + * + * @param buffer Buffer containing the line parsed from the resource. + * @return LdapAttribute representing the attribute parsed. + */ + Attribute parse(String buffer); + +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/DefaultAttributeValidationPolicy.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/DefaultAttributeValidationPolicy.java new file mode 100644 index 00000000..0a7db512 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/DefaultAttributeValidationPolicy.java @@ -0,0 +1,368 @@ +/* + * Copyright 2005-2009 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.ldif.support; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.naming.directory.Attribute; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.core.LdapAttribute; +import org.springframework.ldap.ldif.InvalidAttributeFormatException; + +import sun.misc.BASE64Decoder; + +/** + * Ensures the buffer represents a valid attribute as defined by RFC2849. + * + * Meets the standards imposed by RFC 2849 for the "LDAP Data Interchange Format (LDIF) + * - Technical Specification". + * + * Special attention is called to URL support: RFC 2849 requires that + * LDIFs support URLs as defined in 1738; however, RFC 1738 has been updated by several RFCs including + * RFC 1808, RFC 2396, and RFC 3986 (which obsoleted the formers). Unsupported features of this + * implementation of URL identification include query strings and fragments in HTTP URLs. + * + * @author Keith Barlow + * + */ +@SuppressWarnings("unused") +public class DefaultAttributeValidationPolicy implements AttributeValidationPolicy { + + private static Log log = LogFactory.getLog(DefaultAttributeValidationPolicy.class); + + /** + * Pattern Declarations. + */ + + //General Definitions + private static final String DIGIT = "\\p{Digit}"; + + private static final String LOW_ALPHA = "\\p{Lower}"; + + private static final String HIGH_ALPHA = "\\p{Upper}"; + + private static final String ALPHA = "\\p{Alpha}"; + + private static final String ALPHANUM = "\\p{Alnum}"; + + private static final String HEX = "\\p{XDigit}"; + + private static final String SAFE = "[\\x24\\x2D\\x5F\\x2E\\x2B]"; //$|-|_|.|+ + + private static final String EXTRA = "[\\x21\\x2A\\x27\\x7B\\x7D\\x2C]"; //!|*|'|(|)|, + + private static final String PUNCTUATION = "[\\x3C\\x3E\\x23\\x25\\x22]"; //<|>|#|%|" + + private static final String ESCAPE = "%" + HEX + "{2}"; + + private static final String RESERVED = "[\\x3B\\x2F\\x3F\\x3A\\x40\\x26\\x3D]"; //;|/|?|:|@|&|= + + private static final String UNRESERVED = "[" + ALPHA + DIGIT + SAFE + EXTRA + "]"; + + private static final String UCHAR = "(?:" + UNRESERVED + "|" + ESCAPE + ")"; + + private static final String XCHAR = "(?:" + UNRESERVED + "|" + RESERVED + "|" + ESCAPE + ")"; + + private static final String DIGITS = DIGIT + "+"; + + //Standard LDAP Attribute Definitions + private static final String ATTRIBUTE_SEPARATOR = ":"; + + private static final String OPTION_SEPARATOR = ";"; + + private static final String BASE64_INDICATOR = ":"; + + private static final String URL_INDICATOR = "<"; + + private static final String ATTRIBUTE_TYPE_CHARS = ALPHA + DIGIT + "-"; + + private static final String LDAP_OID = "[[0-9]|[1-9][0-9]+][\\.(?:[0-9]|[1-9][0-9]+)]+"; + + private static final String OPTION = "[" + ATTRIBUTE_TYPE_CHARS + "]+"; + + private static final String OPTIONS = "[" + OPTION_SEPARATOR + OPTION + "]*"; + + private static final String ATTRIBUTE_TYPE = LDAP_OID + "|" + ALPHANUM + "[" + ATTRIBUTE_TYPE_CHARS + "]*"; + + private static final String ATTRIBUTE_DESCRIPTION = "(" + ATTRIBUTE_TYPE + ")(" + OPTIONS + ")"; + + private static final String SAFE_CHAR = "[\\p{ASCII}&&[^\\x00\\x0A\\x0D]]"; //Any ASCII except NUL, LF, and CR + + private static final String SAFE_INIT_CHAR = "[\\p{ASCII}&&[^ \\x00\\x0A\\x0D\\x3A\\x3C]]"; //Any ASCII except NUL, LF, CR, SPACE, colon, and less-than + + private static final String SAFE_STRING = "(" + SAFE_INIT_CHAR + SAFE_CHAR + "*)"; + + private static final String FILL = "[ ]*"; //Any number of spaces + + //BASE64 Definitions + private static final String BASE64_CHAR = "[\\x2B\\x2F\\x30-\\x39\\x3D\\x41-\\x5A\\x61-\\x7A]"; //+, /, 0-9, -, A-Z, a-z + + private static final String BASE64_STRING = "(" + BASE64_CHAR + "*)"; + + //URL Components + private static final String USER = "[" + UCHAR + "\\x3B\\x3F\\x26\\x3D]*"; //UCHAR|;|?|&|= + + private static final String PASSWORD = "[" + UCHAR + "\\x3B\\x3F\\x26\\x3D]*"; //UCHAR|;|?|&|= + + private static final String DOMAINLABEL = ALPHANUM + "|" + ALPHANUM + "[" + ALPHANUM + "-]*" + ALPHANUM; + + private static final String TOPLABEL = ALPHA + "|" + ALPHA + "[" + ALPHANUM + "-]*" + ALPHANUM; + + private static final String HOSTNAME = "(?:" + DOMAINLABEL + "\\.)*" + TOPLABEL; + + private static final String IPADDRESS = "(?:" + DIGIT + "{1,3}\\.){3}" + DIGIT + "{1,3}"; + + private static final String HOST = "(?:" + HOSTNAME + "|" + IPADDRESS + ")"; + + private static final String PORT = DIGITS; + + private static final String HOSTPORT = HOST + "(?::" + PORT + ")?"; + + private static final String URLPATH = XCHAR + "*"; + + private static final String LOGIN = "(?:" + USER + "(?::" + PASSWORD + ")?@)?" + HOSTPORT; + + //URL Definitions + private static final String SCHEME = "[" + LOW_ALPHA + DIGIT + "\\x2B\\x2D\\x2E]+"; + + private static final String IP_SCHEMEPART = "//" + LOGIN + "(?:/" + URLPATH + ")?"; + + private static final String SCHEMEPART = "(?:" + XCHAR + "*|" + IP_SCHEMEPART + ")"; + + private static final String GENERIC_URL = SCHEME + ":" + SCHEMEPART; + + //HTTP Definition + private static final String HSEGMENT = "[" + UCHAR + "\\x3A\\x3B\\x26\\x3D\\x40]*"; //UCHAR|:|;|&|=|@ + + private static final String HPATH = HSEGMENT + "[/" + HSEGMENT + "]*"; + + private static final String SEARCH = HSEGMENT; + + private static final String HTTP_URL = "http://" + HOSTPORT + "(?:/" + HPATH + "(?:\\x3F" + SEARCH + ")?)?"; + + //FTP + private static final String FSEGMENT = "[" + UCHAR + "\\x3F\\x3A\\x26\\x3D\\x40]*"; //UCHAR|?|:|&|=|@ + + private static final String FPATH = FSEGMENT + "[/" + FSEGMENT + "]*"; + + private static final String FTPTYPE = "[AIDaid]"; + + private static final String FTP_URL = "ftp://" + LOGIN + "(?:/" + FPATH + "(?:;type=" + FTPTYPE + ")?)?"; + + //NEWS + private static final String GROUP = ALPHA + "[" + ALPHA + DIGIT + "\\x2D\\x2E\\x2B\\x5F]*"; //ALPHA [ALPHA|DIGIT|-|.|+|_]* + + private static final String ARTICLE = "[" + UCHAR + "\\x3A\\x3B\\x2F\\x3F\\x26\\x3D]@" + HOST; //[UCHAR|;|/|?|:|&|=]@HOST + + private static final String GROUPPART = "(?:\\x2A|" + GROUP + "|" + ARTICLE + ")"; + + private static final String NEWS_URL = "news:" + GROUPPART; + + //NNTP + private static final String NNTP_URL = "nntp://" + HOSTPORT + "/" + GROUP + "/" + DIGITS; + + //TELNET + private static final String TELNET_URL = "telnet://" + LOGIN + "[/]?"; + + //GOPHER + private static final String GTYPE = XCHAR; + + private static final String SELECTOR = XCHAR + "*"; + + private static final String GOPHER_STRING = XCHAR + "*"; + + private static final String GOPHER_URL = "gopher://" + HOSTPORT + "(?:/(?:" + GTYPE + "(?:" + SELECTOR + "(?:%09" + SEARCH + "(?:%09" + GOPHER_STRING + ")?)?)?)?)?"; + + //WAIS + private static final String WPATH = UCHAR + "*"; + + private static final String WTYPE = UCHAR + "*"; + + private static final String DATABASE = UCHAR + "*"; + + private static final String WAIS_DOC = "wais://" + HOSTPORT + "/" + DATABASE + "/" + WTYPE + "/" + WPATH; + + private static final String WAIS_INDEX = "wais://" + HOSTPORT + "/" + DATABASE + "\\?" + SEARCH; + + private static final String WAIS_DATABASE = "wais://" + HOSTPORT + "/" + DATABASE; + + private static final String WAIS_URL = WAIS_DATABASE + "|" + WAIS_INDEX + "|" + WAIS_DOC; + + //MAILTO + private static final String ENCODED_822_ADDR = XCHAR + "+"; + + private static final String MAILTO_URL = "mailto:" + ENCODED_822_ADDR; + + //FILE + private static final String FILE_URL = "file://(?:" + HOST + "|localhost)?/" + FPATH ; + + //PROPERO + private static final String FIELD_VALUE = "[" + UCHAR + "\\x3F\\x3A\\x40\\x26]*"; //[UCHAR|?|:|@|&]* + + private static final String FIELD_NAME = "[" + UCHAR + "\\x3F\\x3A\\x40\\x26]*"; //[UCHAR|?|:|@|&]* + + private static final String FIELD_SPEC = ";" + FIELD_NAME + "=" + FIELD_VALUE; + + private static final String PSEGMENT = "[" + UCHAR + "\\x3F\\x3A\\x40\\x26\\x3D]*"; //[UCHAR|?|:|@|&|=]* + + private static final String PPATH = PSEGMENT + "(?:/" + PSEGMENT + ")*"; + + private static final String PROSPERO_URL = "prospero://" + HOSTPORT + "/" + PPATH + "(?:" + FIELD_SPEC + ")*"; + + //GENERIC + private static final String OTHER_URL = GENERIC_URL; + + private static final String URL = "((?:" + HTTP_URL + ")|(?:" + FTP_URL + ")|(?:" + NEWS_URL + ")|(?:" + NNTP_URL + ")|(?:" + TELNET_URL + ")|(?:" + GOPHER_URL + ")|(?:" + WAIS_URL + ")|(?:" + MAILTO_URL + ")|(?:" + FILE_URL + ")|(?:" + PROSPERO_URL + ")|(?:" + OTHER_URL + "))"; //URL Pattern + + //Expression Definitions + private static final String ATTRIBUTE_EXPRESSION = "^" + ATTRIBUTE_DESCRIPTION + ATTRIBUTE_SEPARATOR + FILL + SAFE_STRING + "{0,1}$"; //Regular Attribute + + private static final String BASE64_ATTRIBUTE_EXPRESSION = "^" + ATTRIBUTE_DESCRIPTION + ATTRIBUTE_SEPARATOR + BASE64_INDICATOR + FILL + BASE64_STRING + "$"; //Base 64 + + private static final String URL_ATTRIBUTE_EXPRESSION = "^" + ATTRIBUTE_DESCRIPTION + ATTRIBUTE_SEPARATOR + URL_INDICATOR + FILL + URL + "$"; //URL + + //Pattern Declarations + private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile(ATTRIBUTE_EXPRESSION); + + private static final Pattern BASE64_ATTRIBUTE_PATTERN = Pattern.compile(BASE64_ATTRIBUTE_EXPRESSION); + + private static final Pattern URL_ATTRIBUTE_PATTERN = Pattern.compile(URL_ATTRIBUTE_EXPRESSION); + + private boolean ordered = false; + + /** + * Default constructor. + */ + public DefaultAttributeValidationPolicy() { + + } + + /** + * Constructor for indicating whether or not attribute values should be ordered alphabetically. + * + * @param ordered value. + */ + public DefaultAttributeValidationPolicy(boolean ordered) { + this.ordered = ordered; + } + + /** + * Indicates whether or not the attribute values should be ordered alphabetically. + * + * @param ordered value. + */ + public void setOrdered(boolean ordered) { + this.ordered = ordered; + } + + /** + * Validates attribute contained in the buffer and returns an LdapAttribute. + *

+ * Ensures attributes meets one of three prescribed patterns for valid attributes: + *

    + *
  1. A standard attribute pattern of the form: ATTR_ID[;options]: VALUE
  2. + *
  3. A Base64 attribute pattern of the form: ATTR_ID[;options]:: BASE64_VALUE
  4. + *
  5. A url attribute pattern of the form: ATTR_ID[;options]:< URL_VALUE
  6. + *
+ *

+ * Upon success an LdapAttribute object is returned. + * + * @param buffer {@inheritDoc} + * @return {@inheritDoc} + * @throws InvalidAttributeFormatException if the attribute does not meet one of the three patterns above + * or the attribute cannot be parsed. + */ + public Attribute parse(String buffer) { + log.trace("Parsing --> [" + buffer + "]"); + + Matcher matcher = ATTRIBUTE_PATTERN.matcher(buffer); + if (matcher.matches()) { + //Is a regular attribute... + return parseStringAttribute(matcher); + } + + matcher = BASE64_ATTRIBUTE_PATTERN.matcher(buffer); + if (matcher.matches()) { + //Is a base64 attribute... + return parseBase64Attribute(matcher); + } + + matcher = URL_ATTRIBUTE_PATTERN.matcher(buffer); + if (matcher.matches()) { + //Is a URL attribute... + return parseUrlAttribute(matcher); + } + + //default: no match. + throw new InvalidAttributeFormatException("Not a valid attribute: [" + buffer + "]"); + } + + private LdapAttribute parseStringAttribute(Matcher matcher) { + String id = matcher.group(1); + String value = matcher.group(3); + List options = Arrays.asList((StringUtils.isEmpty(matcher.group(2)) ? new String[] {} : matcher.group(2).replaceFirst(";","").split(OPTION_SEPARATOR))); + + if (options.isEmpty()) { + return new LdapAttribute(id, value, ordered); + } else { + return new LdapAttribute(id, value, options, ordered); + } + } + + + private LdapAttribute parseBase64Attribute(Matcher matcher) { + try { + String id = matcher.group(1); + String value = matcher.group(3); + List options = Arrays.asList((StringUtils.isEmpty(matcher.group(2)) ? new String[] {} : matcher.group(2).replaceFirst(";","").split(OPTION_SEPARATOR))); + + if (options.isEmpty()) { + return new LdapAttribute(id, new BASE64Decoder().decodeBuffer(value), ordered); + } else { + return new LdapAttribute(id, new BASE64Decoder().decodeBuffer(value), options, ordered); + } + } catch (IOException e) { + throw new InvalidAttributeFormatException(e); + } + } + + + private LdapAttribute parseUrlAttribute(Matcher matcher) { + try { + String id = matcher.group(1); + String value = matcher.group(3); + List options = Arrays.asList((StringUtils.isEmpty(matcher.group(2)) ? new String[] {} : matcher.group(2).replaceFirst(";","").split(OPTION_SEPARATOR))); + + if (options.isEmpty()) { + return new LdapAttribute(id, new URI(value), ordered); + } else { + return new LdapAttribute(id, new URI(value), options, ordered); + } + } catch (URISyntaxException e) { + throw new InvalidAttributeFormatException(e); + } + } + +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/LineIdentifier.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/LineIdentifier.java new file mode 100644 index 00000000..71746616 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/LineIdentifier.java @@ -0,0 +1,69 @@ +/* + * Copyright 2005-2009 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.ldif.support; + +/** + * Enumeration declaring possible event types when parsing LDIF files. + * + * @author Keith Barlow + */ + +public enum LineIdentifier { + /** + * Every LDIF file may optionally start with a version identifier of the form 'version: 1'. + */ + VersionIdentifier, + + /** + * Signifies the start of a new record in the file has been encountered: a DN declaration. + */ + NewRecord, + + /** + * Signals the end of record has been reached. + */ + EndOfRecord, + + /** + * Signifies the event when a new attribute is encountered. + */ + Attribute, + + /** + * Indicates the current line parsed is a continuation of the previous line. + */ + Continuation, + + /** + * The current line is a comment and should be ignored. + */ + Comment, + + /** + * An LDAP changetype control was encountered. + */ + Control, + + /** + * Record being parsed is a 'changetype' record. + */ + ChangeType, + + /** + * Parsed line should be ignored - used to skip remaining lines in a 'changetype' record. + */ + Void +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/SeparatorPolicy.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/SeparatorPolicy.java new file mode 100644 index 00000000..402a0819 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/SeparatorPolicy.java @@ -0,0 +1,116 @@ +/* + * Copyright 2005-2009 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.ldif.support; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Policy object for enforcing LDIF record separation rules. Designed explicitly + * for use in LdifParser. This default separator policy should really not be + * required to be replaced but it is modular just in case. + *

+ * This class applies the separation policy prescribed in RFC2849 for LDIF files + * and identifies the line type from the input. + * + * @author Keith Barlow + * + */ +public class SeparatorPolicy { + + private static Log log = LogFactory.getLog(SeparatorPolicy.class); + + /* + * Line Identification Patterns. + */ + + private static final String VERSION_IDENTIFIER = "^version: [0-9]+(\\.[0-9]*){0,1}$"; + + private static final String CONTROL = "control:"; + + private static final String CHANGE_TYPE = "changetype:"; + + private static final String CONTINUATION = " "; + + private static final String COMMENT = "#"; + + private static final String NewRecord = "^dn:.*$"; + + private boolean record = false; + + private boolean skip = false; + + public SeparatorPolicy() { + + } + + /** + * Assess a read line. + *

+ * In LDIF, lines must adhere to a particular format. A line can only contain one attribute + * and its value. The value may span multiple lines. Continuation lines are marked by the presence + * of a single space in the 1st position. Non-continuation lines must start in the first position. + * + */ + public LineIdentifier assess(String line) { + log.trace("Assessing --> [" + line + "]"); + + if (record) { + if (StringUtils.isEmpty(line)) { + record = false; + skip = false; + return LineIdentifier.EndOfRecord; + + } else if (skip) { + return LineIdentifier.Void; + + } else { + if (line.startsWith(CONTROL)) { + skip = true; + return LineIdentifier.Control; + + } else if (line.startsWith(CHANGE_TYPE)) { + skip = true; + return LineIdentifier.ChangeType; + + } else if (line.startsWith(COMMENT)) { + return LineIdentifier.Comment; + + } else if (line.startsWith(CONTINUATION)) { + return LineIdentifier.Continuation; + + } else { + return LineIdentifier.Attribute; + + } + } + } else { + if (StringUtils.isNotEmpty(line) && line.matches(VERSION_IDENTIFIER) && !skip) { + //Version Identifiers are ignored by parser. + return LineIdentifier.VersionIdentifier; + + } else if (StringUtils.isNotEmpty(line) && line.matches(NewRecord)) { + record = true; + skip = false; + return LineIdentifier.NewRecord; + + } else { + return LineIdentifier.Void; + } + } + } +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/package.html b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/package.html new file mode 100644 index 00000000..37bb6b46 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/ldif/support/package.html @@ -0,0 +1,13 @@ + + + +Provides the necessary auxiliary classes utilized by the LDIFParser. +

+Notable classes in this package include: +

+ + + diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/BasicSchemaSpecification.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/BasicSchemaSpecification.java new file mode 100644 index 00000000..1f2bb2e6 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/BasicSchemaSpecification.java @@ -0,0 +1,66 @@ +package org.springframework.ldap.schema; + +import javax.naming.NamingException; + +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapAttributes; +import org.springframework.ldap.core.LdapRdn; + +import sun.misc.BASE64Encoder; + +/** + * BasicSchemaSpecification establishes a minimal set of requirements for object classes. + *

+ * This basic specification, which does not actually validate against any schema, deems objects + * valid as long as they meet the following criteria: + *

+ * + * @author Keith Barlow + * + */ +public class BasicSchemaSpecification implements Specification { + + /** + * Determines if the policy is satisfied by the supplied LdapAttributes object. + * + * @throws NamingException + */ + public boolean isSatisfiedBy(LdapAttributes record) throws NamingException { + if (record != null) { + + //DN is required. + DistinguishedName dn = record.getDN(); + if (dn != null) { + + //objectClass definition is required. + if (record.get("objectClass") != null) { + + //Naming attribute is required. + LdapRdn rdn = dn.getLdapRdn(dn.size() - 1); + if (record.get(rdn.getKey()) != null) { + Object object = record.get(rdn.getKey()).get(); + + if (object instanceof String) { + String value = (String) object; + if (rdn.getValue().equalsIgnoreCase(value)) { + return true; + } + } else if(object instanceof byte[]) { + BASE64Encoder encoder = new BASE64Encoder(); + String rdnValue = encoder.encode(rdn.getValue().getBytes()); + String attributeValue = encoder.encode((byte[]) object); + if (rdnValue.equals(attributeValue)) return true; + } + } + } + } + } + + return false; + } + +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/DefaultSchemaSpecification.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/DefaultSchemaSpecification.java new file mode 100644 index 00000000..debcae60 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/DefaultSchemaSpecification.java @@ -0,0 +1,42 @@ +/* + * Copyright 2005-2009 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.schema; + +import javax.naming.NamingException; + +import org.springframework.ldap.core.LdapAttributes; + +/** + * DefaultSchemaSpecification does not validate objects at all - it simply returns true. + *

+ * This specification is intended for cases where validation of the parsed entries is not + * required. + * + * @author Keith Barlow + * + */ +public class DefaultSchemaSpecification implements Specification { + + /** + * Determines if the policy is satisfied by the supplied LdapAttributes object. + * + * @throws NamingException + */ + public boolean isSatisfiedBy(LdapAttributes record) throws NamingException { + return true; + } + +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/Specification.java b/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/Specification.java new file mode 100644 index 00000000..4f4f3f17 --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/Specification.java @@ -0,0 +1,33 @@ +/* + * Copyright 2005-2009 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.schema; + +import javax.naming.NamingException; + +/** + * The specification interface is implemented to declare rules that + * a record must conform to. The motivation behind this class was + * to provide a mechanism to enable schema validations. + * + * @author Keith Barlow + * + * @param + */ +public interface Specification { + + boolean isSatisfiedBy(T record) throws NamingException; + +} diff --git a/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/package.html b/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/package.html new file mode 100644 index 00000000..8f29db9d --- /dev/null +++ b/ldif/ldif-core/src/main/java/org/springframework/ldap/schema/package.html @@ -0,0 +1,10 @@ + + + +This package is aimed at providing a mechanism to implement LDAP schemas. +

+Utilized by the LDIFParser to validate object composition post assembly, these +classes may also be referenced by other utilities where seen fit. + + + diff --git a/ldif/ldif-core/src/main/java/overview.html b/ldif/ldif-core/src/main/java/overview.html new file mode 100644 index 00000000..d5045446 --- /dev/null +++ b/ldif/ldif-core/src/main/java/overview.html @@ -0,0 +1,13 @@ + + + + +Insert title here + + +This document is the API specification for the Spring LDAP LDIF Parser and its associated utilities. +

+This series of packages provides an LDIF parser utility compliant with +"RFC 2849 : The LDAP Data Interchange Format (LDIF) - Technical Specification". + + \ No newline at end of file diff --git a/ldif/ldif-core/src/test/java/org/springframework/ldap/ldif/DefaultAttributeValidationPolicyTest.java b/ldif/ldif-core/src/test/java/org/springframework/ldap/ldif/DefaultAttributeValidationPolicyTest.java new file mode 100644 index 00000000..44c564ae --- /dev/null +++ b/ldif/ldif-core/src/test/java/org/springframework/ldap/ldif/DefaultAttributeValidationPolicyTest.java @@ -0,0 +1,155 @@ +package org.springframework.ldap.ldif; + +import static org.junit.Assert.*; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.Parameterized; +import org.springframework.ldap.core.LdapAttribute; +import org.springframework.ldap.ldif.support.DefaultAttributeValidationPolicy; + +import sun.misc.BASE64Decoder; + +/** + * Parses a preselected set of attributes to test the full spectrum of functionality + * expected of an attribute parser. Attributes are validated to ensure they conform to + * the requirements for attribute values prescribed in RFC2849. + * + * @author Keith Barlow + * + */ +@RunWith(Parameterized.class) +public class DefaultAttributeValidationPolicyTest { + + private static Log log = LogFactory.getLog(DefaultAttributeValidationPolicyTest.class); + + private static DefaultAttributeValidationPolicy policy = new DefaultAttributeValidationPolicy(); + + private static enum AttributeType { STRING, BASE64, URL } + + private String line; + private String id; + private String options; + private String value; + private AttributeType type; + + private List exceptions = Arrays.asList(new String[] { + "description: :A big sailing fan.", + "cn;lang-ja:: 5bCP56yg5Y6fIO.ODreODieODi+ODvA==", + "url:< http://java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28" + }); + + /** + * The data set to parse. + * @return + */ + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + //Format: line, id, options, value, type + + //String + { "cn: Keith Barlow", "cn", "", "Keith Barlow", AttributeType.STRING}, + { "sn: Jensen", "sn", "", "Jensen", AttributeType.STRING}, + { "cn: Barbara J Jensen", "cn", "", "Barbara J Jensen", AttributeType.STRING}, + { "telephonenumber: +1 408 555 1212", "telephonenumber", "", "+1 408 555 1212", AttributeType.STRING}, + { "description: A big sailing fan.", "description", "", "A big sailing fan.", AttributeType.STRING}, + { "title;lang-en;phonetic: Sales, Director", "title", ";lang-en;phonetic", "Sales, Director", AttributeType.STRING}, + { "mail: rogasawara@airius.co.jp", "mail", "", "rogasawara@airius.co.jp", AttributeType.STRING}, + { "description: A big sailing fan.", "description", "", "A big sailing fan.", AttributeType.STRING}, + { "description: :A big sailing fan.", "description", "", ":A big sailing fan.", AttributeType.STRING}, + + //Base64 + { "xml:: PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4=", "xml", "", "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4=", AttributeType.BASE64}, + { "ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2", "ou", ";lang-ja;phonetic", "44GI44GE44GO44KH44GG44G2", AttributeType.BASE64 }, + { "dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz", "dn", "", "dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz", AttributeType.BASE64 }, + { "cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==", "cn", ";lang-ja", "5bCP56yg5Y6fIOODreODieODi+ODvA==", AttributeType.BASE64 }, + { "cn;lang-ja:: 5bCP56yg5Y6fIO.ODreODieODi+ODvA==", "cn", ";lang-ja", "5bCP56yg5Y6fIO.ODreODieODi+ODvA==", AttributeType.BASE64 }, + + //Url + { "url:< http://www.oracle.com/", "url", "", "http://www.oracle.com/", AttributeType.URL}, + { "url:< http://java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html", "url", "", "http://java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html", AttributeType.URL}, + { "url:< ftp://kbarlow:test@ftp.is.co.za/rfc/rfc1808.txt", "url", "", "ftp://kbarlow:test@ftp.is.co.za/rfc/rfc1808.txt", AttributeType.URL}, + { "url;option:< ftp://ftp.is.co.za:2100/rfc/rfc1808.txt;type=a", "url", ";option", "ftp://ftp.is.co.za:2100/rfc/rfc1808.txt;type=a", AttributeType.URL}, + { "url:< telnet://kbarlow@melvyl.ucop.edu/", "url", "", "telnet://kbarlow@melvyl.ucop.edu/", AttributeType.URL}, + { "url;option1;option2:< telnet://kbarlow:test@melvyl.ucop.edu/", "url", ";option1;option2", "telnet://kbarlow:test@melvyl.ucop.edu/", AttributeType.URL}, + { "url:< gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles", "url", "", "gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles", AttributeType.URL}, + { "url:< file:///usr/local/directory/photos/fiona.jpg", "url", "", "file:///usr/local/directory/photos/fiona.jpg", AttributeType.URL}, + { "url:< mailto:java-net@java.sun.com", "url", "", "mailto:java-net@java.sun.com", AttributeType.URL}, + { "url:< news:comp.infosystems.www.servers.unix", "url", "", "news:comp.infosystems.www.servers.unix", AttributeType.URL}, + { "url:< prospero://host.dom:1525//pros/name;key=value", "url", "", "prospero://host.dom:1525//pros/name;key=value", AttributeType.URL}, + { "url:< nntp://news.cs.hut.fi/alt.html/239157", "url", "", "nntp://news.cs.hut.fi/alt.html/239157", AttributeType.URL}, + { "url:< wais://vega.lib.ncsu.edu/alawon.src?nren", "url", "", "wais://vega.lib.ncsu.edu/alawon.src?nren", AttributeType.URL}, + { "url:< http://java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28", "url", "", "http://java.sun.com/j2se/1.3/docs/guide/collections/designfaq.html#28", AttributeType.URL} + + }); + } + + /** + * DefaultAttributeValidationPolicyTest: Parameterized constructor. + * @param line The attribute to parse. + * @param id The ID portion of the attribute expected on successful parsing. + * @param options The Options expected on successful parsing. + * @param value The value expected from successful parsing. + * @param type The attribute type: one of enum AttributeType. + */ + public DefaultAttributeValidationPolicyTest(String line, String id, String options, String value, AttributeType type) { + this.line = line; + this.id = id; + this.options = options; + this.value = value; + this.type = type; + } + + /** + * The test case: parses passed in parameters and validates the outcome against the expected results. + */ + @Test + public void parseAttribute() { + try { + LdapAttribute attribute = (LdapAttribute) policy.parse(line); + + assertTrue("IDs do not match: [expected: " + attribute.getID() + ", obtained: " + id + "]", id.equalsIgnoreCase(attribute.getID())); + + String[] expected = StringUtils.isEmpty(options) ? new String[] {} : options.replaceFirst(";","").split(";"); + Arrays.sort(expected); + String[] obtained = attribute.getOptions().toArray(new String[] {}); + Arrays.sort(obtained); + assertArrayEquals("Options do not match: ", expected, obtained); + + switch(type) { + case STRING: + assertTrue("Value is not a string.", attribute.get() instanceof String); + assertEquals("Values do not match: ", value, (String) attribute.get()); + break; + + case BASE64: + byte[] bytes = new BASE64Decoder().decodeBuffer(value); + assertTrue("Value is not a byte[].", attribute.get() instanceof byte[]); + assertArrayEquals("Values do not match: ", bytes, (byte[]) attribute.get()); + break; + + case URL: + URI url = new URI(value); + assertTrue("Value is not a URL.", attribute.get() instanceof URI); + assertEquals("Values do not match: ", url, (URI) attribute.get()); + break; + } + + log.info("Success!"); + + } catch (Exception e) { + if (!exceptions.contains(line)) + fail("Exception thrown: " + e.getClass().getSimpleName() + " (message: " + e.getMessage() + ")"); + } + } +} diff --git a/ldif/ldif-core/src/test/java/org/springframework/ldap/ldif/LdifParserTest.java b/ldif/ldif-core/src/test/java/org/springframework/ldap/ldif/LdifParserTest.java new file mode 100644 index 00000000..283312b3 --- /dev/null +++ b/ldif/ldif-core/src/test/java/org/springframework/ldap/ldif/LdifParserTest.java @@ -0,0 +1,110 @@ +package org.springframework.ldap.ldif; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.ldap.core.LdapAttributes; +import org.springframework.ldap.ldif.parser.LdifParser; +import org.springframework.ldap.schema.BasicSchemaSpecification; + +/** + * Unit test for LdifParser. + * + * Test results in complete end to end test of all LdifParser functionality: + * 1.) Open a file + * 2.) Read lines and compose an attribute. + * 3.) Parse the attribute and create a LdapAttribute object. + * 4.) Repeat until end of record (Identify end of record). + * 5.) Return a valid LdapAttributes object. + * 6.) Close file upon completion. + * + * Provided test file is comprised of sample LDIFs from RFC2849 and exhausts the full range of + * the functionality prescribed by RFC2849 for the LDAP Data Interchange Format (LDIF). + * + * @author Keith Barlow + * + */ +public class LdifParserTest { + + private static Log log = LogFactory.getLog(LdifParserTest.class); + + private LdifParser parser; + + /** + * Default constructor: loads a preselected resource with sample LDIF entries. + * Each entry is parsed and checked for a DN and objectclass. Output is printed for visual verification + * of LDIF correctness. + */ + public LdifParserTest() { + parser = new LdifParser(new ClassPathResource("test.ldif")); + parser.setRecordSpecification(new BasicSchemaSpecification()); + } + + /** + * Setup: opens file. + */ + @Before + public void openLdif() { + try { + parser.open(); + } catch (IOException e) { + fail(e.getMessage()); + } + } + + /** + * Executes test: reads all records from LDIF file and validates an LdapAttributes object is successfully created. + */ + @Test + public void parseLdif() { + int count = 0; + + try { + LdapAttributes attributes; + + while (parser.hasMoreRecords()) { + try { + attributes = parser.getRecord(); + log.info("attributes:\n" + attributes); + if (attributes != null) { + assertTrue("A dn is required.", attributes.getDN() != null); + assertTrue("Object class is required.", attributes.get("objectclass") != null); + count++; + } + } catch (InvalidAttributeFormatException e) { + log.error(e); + if (count != 6) fail(); + } + + log.debug("hasMoreRecords: " + parser.hasMoreRecords()); + } + + log.info("record count: " + count); + //assertTrue("An incorrect number of records were parsed.", count == 8); + + log.info("Done!"); + + } catch (IOException e) { + fail(e.getMessage()); + } + } + + /** + * Cleanup: closes file. + */ + @After + public void closeLdif() { + try { + parser.close(); + } catch (IOException e) { + fail(e.getMessage()); + } + } +} diff --git a/ldif/ldif-core/src/test/resources/log4j.properties b/ldif/ldif-core/src/test/resources/log4j.properties new file mode 100644 index 00000000..1a6ed973 --- /dev/null +++ b/ldif/ldif-core/src/test/resources/log4j.properties @@ -0,0 +1,6 @@ +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.layout=org.apache.log4j.PatternLayout + +log4j.appender.console.layout.ConversionPattern=%d [%t] %-5p %c - %m%n + +log4j.logger.org.springframework.ldap.ldif=INFO, console \ No newline at end of file diff --git a/ldif/ldif-core/src/test/resources/test.ldif b/ldif/ldif-core/src/test/resources/test.ldif new file mode 100644 index 00000000..7ec2867d --- /dev/null +++ b/ldif/ldif-core/src/test/resources/test.ldif @@ -0,0 +1,235 @@ +version: 1 + +# +#Examples of LDAP Data Interchange Format +# + +# +#Example 1: An simple LDAP file with two entries +# + +dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Barbara Jensen +cn: Barbara J Jensen +cn: Babs Jensen +sn: Jensen +uid: bjensen +telephonenumber: +1 408 555 1212 +description: A big sailing fan. + +dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Bjorn Jensen +sn: Jensen +telephonenumber: +1 408 555 1212 + + +# +# Example 2: A file containing an entry with a folded attribute value +# + +dn:cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com +objectclass:top +objectclass:person +objectclass:organizationalPerson +cn:Barbara Jensen +cn:Barbara J Jensen +cn:Babs Jensen +sn:Jensen +uid:bjensen +telephonenumber:+1 408 555 1212 +description:Babs is a big sailing fan, and travels extensively in sea + rch of perfect sailing conditions. +title:Product Manager, Rod and Reel Division + +# +# Example 3: A file containing a base-64-encoded value +# + +dn: cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Gern Jensen +cn: Gern O Jensen +sn: Jensen +uid: gernj +telephonenumber: +1 408 555 1212 +description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl + IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG + VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg + b3V0IG1vcmUu + + +# +# Example 4: A file containing an entries with UTF-8-encoded attribute +# values, including language tags. Comments indicate the contents +# of UTF-8-encoded attributes and distinguished names. +# + +dn:: b3U95Za25qWt6YOoLG89QWlyaXVz +# dn:: ou=,o=Airius +objectclass: top +objectclass: organizationalUnit +ou:: 5Za25qWt6YOo +# ou:: +ou;lang-ja:: 5Za25qWt6YOo +# ou;lang-ja:: +ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2 +# ou;lang-ja:: +ou;lang-en: Sales +description: Japanese office + +dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz +# dn:: uid=,ou=,o=Airius +userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM= +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +uid: rogasawara +mail: rogasawara@airius.co.jp +givenname;lang-ja:: 44Ot44OJ44OL44O8 +# givenname;lang-ja:: +sn;lang-ja:: 5bCP56yg5Y6f +# sn;lang-ja:: +cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +# cn;lang-ja:: +title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw== +# title;lang-ja:: +preferredlanguage: ja +givenname:: 44Ot44OJ44OL44O8 +# givenname:: +sn:: 5bCP56yg5Y6f +# sn:: +cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +# cn:: +title:: 5Za25qWt6YOoIOmDqOmVtw== +# title:: +givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8 +# givenname;lang-ja;phonetic:: +# +sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ +# sn;lang-ja;phonetic:: +cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA== +# cn;lang-ja;phonetic:: +title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg== +# title;lang-ja;phonetic:: +# +givenname;lang-en: Rodney +sn;lang-en: Ogasawara +cn;lang-en: Rodney Ogasawara +title;lang-en: Sales, Director + + +# +# Example 5: An LDIF file containing an invalid attribute +# + +dn: cn=Harry Jacobs, ou=Product Development, dc=airius, dc=com +objectClass: top +objectClass: person +cn: Harry Jacobs +description: :A big sailing fan. + + +# +# Example 6: A file containing a reference to an external file +# + +dn: cn=Horatio Jensen, ou=Product Testing, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Horatio Jensen +cn: Horatio N Jensen +sn: Jensen +uid: hjensen +telephonenumber: +1 408 555 1212 +jpegphoto:< file:///usr/local/directory/photos/hjensen.jpg + + +# +# Example 7: A file containing a series of change records and comments +# + +# Add a new entry +dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com +changetype: add +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Fiona Jensen +sn: Jensen +uid: fiona +telephonenumber: +1 408 555 1212 +jpegphoto:< file:///usr/local/directory/photos/fiona.jpg + +# Delete an existing entry +dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com +changetype: delete + +# Modify an entry's relative distinguished name +dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com +changetype: modrdn +newrdn: cn=Paula Jensen +deleteoldrdn: 1 + +# Rename an entry and move all of its children to a new location in +# the directory tree (only implemented by LDAPv3 servers). +dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com +changetype: modrdn +newrdn: ou=Product Development Accountants +deleteoldrdn: 0 +newsuperior: ou=Accounting, dc=airius, dc=com + + +# Modify an entry: add an additional value to the postaladdress +# attribute, completely delete the description attribute, replace +# the telephonenumber attribute with two values, and delete a specific +# value from the facsimiletelephonenumber attribute +dn: cn=Paula Jensen, ou=Product Development, dc=airius, dc=com +changetype: modify +add: postaladdress +postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086 +- + +delete: description +- +replace: telephonenumber +telephonenumber: +1 408 555 1234 +telephonenumber: +1 408 555 5678 +- +delete: facsimiletelephonenumber +facsimiletelephonenumber: +1 408 555 9876 +- + +# Modify an entry: replace the postaladdress attribute with an empty +# set of values (which will cause the attribute to be removed), and +# delete the entire description attribute. Note that the first will +# always succeed, while the second will only succeed if at least +# one value for the description attribute is present. +dn: cn=Ingrid Jensen, ou=Product Support, dc=airius, dc=com +changetype: modify +replace: postaladdress +- +delete: description +- + + +# +# Example 8: An LDIF file containing a change record with a control +# + +# Delete an entry. The operation will attach the LDAPv3 +# Tree Delete Control defined in [9]. The criticality +# field is "true" and the controlValue field is +# absent, as required by [9]. +dn: ou=Product Development, dc=airius, dc=com +control: 1.2.840.113556.1.4.805 true +changetype: delete diff --git a/ldif/pom.xml b/ldif/pom.xml new file mode 100644 index 00000000..3b97f77b --- /dev/null +++ b/ldif/pom.xml @@ -0,0 +1,74 @@ + + + org.springframework.ldap + spring-ldap-parent + 1.3.1.CI-SNAPSHOT + + + 4.0.0 + org.springframework.ldap.ldif + spring-ldap-ldif + pom + Spring LDAP LDIF + This packages provides an LDIF Parser for use with the Spring LDAP suite of utilities. + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + org.apache.maven.plugins + maven-javadoc-plugin + + true + public + + http://java.sun.com/j2se/1.5.0/docs/api/ + http://static.springsource.org/spring/docs/2.5.x/api/ + http://static.springsource.org/spring-ldap/docs/1.3.x/apidocs/ + http://static.springsource.org/spring-batch/apidocs/ + + + + + + + ldif-core + ldif-batch + + + + commons-lang + commons-lang + compile + + + junit + junit + 4.4 + test + + + log4j + log4j + test + + + org.springframework + spring-beans + compile + + + org.springframework.ldap + spring-ldap + ${version} + all + compile + + + diff --git a/parent/tiger/pom.xml b/parent/tiger/pom.xml new file mode 100644 index 00000000..88a588e3 --- /dev/null +++ b/parent/tiger/pom.xml @@ -0,0 +1,25 @@ + + + + org.springframework.ldap + spring-ldap-parent + 1.3.1.CI-SNAPSHOT + + 4.0.0 + spring-ldap-parent-tiger + pom + Spring LDAP - Parent Tiger + + + + + maven-compiler-plugin + + 1.5 + 1.5 + + + + +