diff --git a/core-tiger/src/main/java/org/springframework/ldap/ldif/InvalidAttributeFormatException.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/InvalidAttributeFormatException.java
new file mode 100644
index 00000000..fca3f970
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/InvalidAttributeFormatException.java
@@ -0,0 +1,40 @@
+/**
+ *
+ */
+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/core-tiger/src/main/java/org/springframework/ldap/ldif/InvalidRecordFormatException.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/InvalidRecordFormatException.java
new file mode 100644
index 00000000..d89b6664
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/InvalidRecordFormatException.java
@@ -0,0 +1,40 @@
+/**
+ *
+ */
+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/core-tiger/src/main/java/org/springframework/ldap/ldif/LdapAttribute.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/LdapAttribute.java
new file mode 100644
index 00000000..7b46a5c7
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/LdapAttribute.java
@@ -0,0 +1,212 @@
+/**
+ * Extends BasicAttribute adding support for options.
+ */
+package org.springframework.ldap.ldif;
+
+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 {@link java.lang.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 {@link java.lang.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 {@link java.lang.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 {@link java.lang.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 {@link java.lang.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 {@link java.lang.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 {@link java.lang.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 {@link java.lang.boolean} indicating result.
+ */
+ public boolean containsAll(Collection options) {
+ return this.options.containsAll(options);
+ }
+
+ /**
+ * Tests for the presence of options.
+ *
+ * @return {@link java.lang.boolean} indicating result.
+ */
+ public boolean hasOptions() {
+ return !options.isEmpty();
+ }
+
+ /**
+ * Removes an option from the the set.
+ *
+ * @param option {@link java.lang.String} option.
+ * @return {@link java.lang.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.util.String} options.
+ * @return {@link java.lang.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.util.String} options.
+ * @return {@link java.lang.boolean} indicating successful retention of options.
+ */
+ public boolean retainAllOptions(Collection options) {
+ return this.options.retainAll(options);
+ }
+}
diff --git a/core-tiger/src/main/java/org/springframework/ldap/ldif/LdapAttributes.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/LdapAttributes.java
new file mode 100644
index 00000000..f61417f5
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/LdapAttributes.java
@@ -0,0 +1,194 @@
+/**
+ * Extends BasicAttributes adding support for DNs.
+ */
+package org.springframework.ldap.ldif;
+
+import java.net.URI;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.BasicAttributes;
+
+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 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 {@link java.lang.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 {@link java.lang.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 {@link java.lang.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 {@link java.lang.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() {
+ try {
+ StringBuilder sb = new StringBuilder();
+
+ 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");
+ }
+ }
+ }
+
+ return sb.toString();
+
+ } catch (NamingException e) {
+ e.printStackTrace();
+ return "";
+ }
+ }
+}
diff --git a/core-tiger/src/main/java/org/springframework/ldap/ldif/package-info.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/package-info.java
new file mode 100644
index 00000000..77427fff
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * 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.
+ */
+package org.springframework.ldap.ldif;
\ No newline at end of file
diff --git a/core-tiger/src/main/java/org/springframework/ldap/ldif/parser/LDIFParser.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/parser/LDIFParser.java
new file mode 100644
index 00000000..14c6bfc7
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/parser/LDIFParser.java
@@ -0,0 +1,320 @@
+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.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.ldif.InvalidAttributeFormatException;
+import org.springframework.ldap.ldif.InvalidRecordFormatException;
+import org.springframework.ldap.ldif.LdapAttributes;
+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 LDIFPaser LDIFParser} provides the main interface for operation but requires three supporting classes to
+ * enable operation:
+ *
+ *
{@link SeparatorPolicy SeparatorPolicy} - establishes the mechanism by which lines are assembled into attributes.
+ *
{@link AttributeValidationPolicy AttributeValidationPolicy} - ensures that attributes are correctly structured prior to parsing.
+ *
{@link Specification Specification} - provides a mechanism by which object structure can be validated after assembly.
+ *
+ * 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.ldif.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.
+ *
+ * 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.
+ *
+ * @author Keith Barlow
+ *
+ */
+public class LDIFParser implements Parser {
+
+ 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 recordSpecification = new DefaultSchemaSpecification();
+
+ /**
+ * Default constructor.
+ */
+ public LDIFParser() {
+
+ }
+
+ /**
+ * 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.recordSpecification = specification;
+ }
+
+ public void setResource(Resource resource) {
+ this.resource = resource;
+ }
+
+ public void afterPropertiesSet() throws Exception {
+
+ }
+
+ public void open() throws IOException {
+ Assert.notNull(resource, "Resource must be set.");
+ Assert.isTrue(resource.exists(), resource.getDescription() + ": resource does not exist!");
+ Assert.isTrue(resource.isReadable(), "Resource is not readable.");
+
+ reader = new BufferedReader(new InputStreamReader(resource.getInputStream()));
+
+ }
+
+ 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()) return null;
+
+ LdapAttributes record = new LdapAttributes();
+ 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);
+
+ log.debug("satisfied: " + recordSpecification.isSatisfiedBy(record));
+
+ if (recordSpecification.isSatisfiedBy(record)) {
+ log.trace("Returning record.");
+ return record;
+
+ } else {
+ String dn;
+
+ try {
+ dn = (String) record.get("dn").get();
+ } catch (NamingException e) {
+ dn = "";
+ }
+
+ throw new InvalidRecordFormatException("Record [dn: " + dn + "] 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);
+ } catch (InvalidAttributeFormatException e) {
+ log.error(e);
+ record = null;
+ }
+ }
+
+}
diff --git a/core-tiger/src/main/java/org/springframework/ldap/ldif/parser/Parser.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/parser/Parser.java
new file mode 100644
index 00000000..d889816f
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/parser/Parser.java
@@ -0,0 +1,62 @@
+package org.springframework.ldap.ldif.parser;
+
+import java.io.IOException;
+
+import javax.naming.directory.Attributes;
+
+import org.springframework.beans.factory.InitializingBean;
+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 extends InitializingBean {
+
+ /**
+ * Sets the resource to parse.
+ *
+ * @param resource The resource to parse.
+ */
+ public void setResource(Resource resource);
+
+ /**
+ * 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;
+
+}
diff --git a/core-tiger/src/main/java/org/springframework/ldap/ldif/parser/package-info.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/parser/package-info.java
new file mode 100644
index 00000000..ec9ff03c
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/parser/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * This package contains the parser classes and interfaces.
+ */
+package org.springframework.ldap.ldif.parser;
\ No newline at end of file
diff --git a/core-tiger/src/main/java/org/springframework/ldap/ldif/support/AttributeValidationPolicy.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/AttributeValidationPolicy.java
new file mode 100644
index 00000000..71be142a
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/AttributeValidationPolicy.java
@@ -0,0 +1,20 @@
+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
+ * @return LdapAttribute representing the attribute parsed.
+ */
+ public Attribute parse(String buffer);
+
+}
diff --git a/core-tiger/src/main/java/org/springframework/ldap/ldif/support/DefaultAttributeValidationPolicy.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/DefaultAttributeValidationPolicy.java
new file mode 100644
index 00000000..cba01d7b
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/DefaultAttributeValidationPolicy.java
@@ -0,0 +1,356 @@
+/**
+ * Attribute validation policy
+ *
+ * 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.
+ *
+ */
+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.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.ldif.InvalidAttributeFormatException;
+import org.springframework.ldap.ldif.LdapAttribute;
+
+import sun.misc.BASE64Decoder;
+
+/**
+ * Ensures the buffer represents a valid attribute as defined by RFC2849.
+ *
+ * @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 {@link java.lang.boolean} value.
+ */
+ public DefaultAttributeValidationPolicy(boolean ordered) {
+ this.ordered = ordered;
+ }
+
+ /**
+ * Indicates whether or not the attribute values should be ordered alphabetically.
+ *
+ * @param ordered {@link java.lang.boolean} 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:
+ *
+ *
A standard attribute pattern of the form: ATTR_ID[;options]: VALUE
+ *
A Base64 attribute pattern of the form: ATTR_ID[;options]:: BASE64_VALUE
+ *
A url attribute pattern of the form: ATTR_ID[;options]:< URL_VALUE
+ *
+ *
+ * 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(matcher.group(2).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(matcher.group(2).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(matcher.group(2).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/core-tiger/src/main/java/org/springframework/ldap/ldif/support/LineIdentifier.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/LineIdentifier.java
new file mode 100644
index 00000000..d257b71f
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/LineIdentifier.java
@@ -0,0 +1,54 @@
+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/core-tiger/src/main/java/org/springframework/ldap/ldif/support/SeparatorPolicy.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/SeparatorPolicy.java
new file mode 100644
index 00000000..8e3c0693
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/SeparatorPolicy.java
@@ -0,0 +1,103 @@
+/**
+ * Policy object for enforcing LDIF record separation rules. Designed explicitly for use in LDIFParser.
+ */
+package org.springframework.ldap.ldif.support;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * 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/core-tiger/src/main/java/org/springframework/ldap/ldif/support/package-info.java b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/package-info.java
new file mode 100644
index 00000000..31a12325
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/ldif/support/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * Provides the necessary auxiliary classes utilized by the LDIFParser.
+ *
+ * Notable classes in this package include:
+ *
+ *
AttributeValidationPolicy - specifies the proper format valid attributes must adhere to.
+ *
SeparatorPolicy - translates lines read from the resource into attributes.
+ *
+ */
+package org.springframework.ldap.ldif.support;
\ No newline at end of file
diff --git a/core-tiger/src/main/java/org/springframework/ldap/schema/DefaultSchemaSpecification.java b/core-tiger/src/main/java/org/springframework/ldap/schema/DefaultSchemaSpecification.java
new file mode 100644
index 00000000..cdd94629
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/schema/DefaultSchemaSpecification.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.LdapRdn;
+import org.springframework.ldap.ldif.LdapAttributes;
+
+import sun.misc.BASE64Encoder;
+
+/**
+ * DefaultSchemaSpecification establishes a minimal set of requirements for object classes.
+ *
+ * The default specification, which does not validate against any schema, simply deems all
+ * objects valid as long as they have the following:
+ *
+ *
a non-null DN
+ *
a matching naming attribute class definition.
+ *
and an object
+ *
+ *
+ * @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 {
+ 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) record.get(rdn.getKey()).get();
+ 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/core-tiger/src/main/java/org/springframework/ldap/schema/Specification.java b/core-tiger/src/main/java/org/springframework/ldap/schema/Specification.java
new file mode 100644
index 00000000..504b6765
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/schema/Specification.java
@@ -0,0 +1,18 @@
+package org.springframework.ldap.schema;
+
+import javax.naming.NamingException;
+
+/**
+ * The specification interface is implemented to declare rules that
+ * a record must conform to. The motiviation 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/core-tiger/src/main/java/org/springframework/ldap/schema/package-info.java b/core-tiger/src/main/java/org/springframework/ldap/schema/package-info.java
new file mode 100644
index 00000000..fcb53f56
--- /dev/null
+++ b/core-tiger/src/main/java/org/springframework/ldap/schema/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * 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.
+ */
+package org.springframework.ldap.schema;
\ No newline at end of file
diff --git a/core-tiger/src/test/java/org/springframework/ldap/ldif/DefaultAttributeValidationPolicyTest.java b/core-tiger/src/test/java/org/springframework/ldap/ldif/DefaultAttributeValidationPolicyTest.java
new file mode 100644
index 00000000..b4677d05
--- /dev/null
+++ b/core-tiger/src/test/java/org/springframework/ldap/ldif/DefaultAttributeValidationPolicyTest.java
@@ -0,0 +1,143 @@
+package org.springframework.ldap.ldif;
+
+import static org.junit.Assert.*;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+
+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.ldif.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;
+
+ /**
+ * The data set to parse.
+ * @return
+ */
+ @Parameters
+ public static Collection