+ * 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
+ * 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
+ * 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
+ * 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
+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
+ * Usage
+ * 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
+ * Ensures attributes meets one of three prescribed patterns for valid attributes:
+ *
+ * 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
+ * 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 @@
+
+
+Notable classes in this package include:
+
+ * This basic specification, which does not actually validate against any schema, deems objects
+ * valid as long as they meet the following criteria:
+ *
+ * This specification is intended for cases where validation of the parsed entries is not
+ * required.
+ *
+ * @author Keith Barlow
+ *
+ */
+public class DefaultSchemaSpecification implements Specification
+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 @@
+
+
+
+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
+ * {@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.
+ *
+ * {@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.
+ *
+ *
+ *
+
+
+
+
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.
+ *
+ *
+ *
+ * @author Keith Barlow
+ *
+ */
+public class BasicSchemaSpecification implements Specification