From 2d4a1851861ce33a861cb02b45ff1ab84e24e30b Mon Sep 17 00:00:00 2001 From: Ulrik Sandberg Date: Sun, 2 Aug 2009 19:30:55 +0000 Subject: [PATCH] Added LDIF parser files from Keith Barlow. --- .../ldif/InvalidAttributeFormatException.java | 40 ++ .../ldif/InvalidRecordFormatException.java | 40 ++ .../ldap/ldif/LdapAttribute.java | 212 +++++++++++ .../ldap/ldif/LdapAttributes.java | 194 ++++++++++ .../ldap/ldif/package-info.java | 7 + .../ldap/ldif/parser/LDIFParser.java | 320 ++++++++++++++++ .../ldap/ldif/parser/Parser.java | 62 +++ .../ldap/ldif/parser/package-info.java | 4 + .../support/AttributeValidationPolicy.java | 20 + .../DefaultAttributeValidationPolicy.java | 356 ++++++++++++++++++ .../ldap/ldif/support/LineIdentifier.java | 54 +++ .../ldap/ldif/support/SeparatorPolicy.java | 103 +++++ .../ldap/ldif/support/package-info.java | 10 + .../schema/DefaultSchemaSpecification.java | 66 ++++ .../ldap/schema/Specification.java | 18 + .../ldap/schema/package-info.java | 7 + .../DefaultAttributeValidationPolicyTest.java | 143 +++++++ .../ldap/ldif/LDIFParserTest.java | 100 +++++ .../ldap/ldif/package-info.java | 4 + core-tiger/src/test/resources/test.ldif | 225 +++++++++++ 20 files changed, 1985 insertions(+) create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/InvalidAttributeFormatException.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/InvalidRecordFormatException.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/LdapAttribute.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/LdapAttributes.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/package-info.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/parser/LDIFParser.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/parser/Parser.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/parser/package-info.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/support/AttributeValidationPolicy.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/support/DefaultAttributeValidationPolicy.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/support/LineIdentifier.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/support/SeparatorPolicy.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/ldif/support/package-info.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/schema/DefaultSchemaSpecification.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/schema/Specification.java create mode 100644 core-tiger/src/main/java/org/springframework/ldap/schema/package-info.java create mode 100644 core-tiger/src/test/java/org/springframework/ldap/ldif/DefaultAttributeValidationPolicyTest.java create mode 100644 core-tiger/src/test/java/org/springframework/ldap/ldif/LDIFParserTest.java create mode 100644 core-tiger/src/test/java/org/springframework/ldap/ldif/package-info.java create mode 100644 core-tiger/src/test/resources/test.ldif 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: + *

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

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

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

+ * + * @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 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}, + + //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 }, + + //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:< ftp://ftp.is.co.za:2100/rfc/rfc1808.txt;type=a", "url", "", "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:< telnet://kbarlow:test@melvyl.ucop.edu/", "url", "", "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} + + }); + } + + /** + * 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 = options.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) { + fail("Exception thrown: " + e.getClass().getSimpleName() + " (message: " + e.getMessage() + ")"); + } + } +} diff --git a/core-tiger/src/test/java/org/springframework/ldap/ldif/LDIFParserTest.java b/core-tiger/src/test/java/org/springframework/ldap/ldif/LDIFParserTest.java new file mode 100644 index 00000000..df84506f --- /dev/null +++ b/core-tiger/src/test/java/org/springframework/ldap/ldif/LDIFParserTest.java @@ -0,0 +1,100 @@ +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.ldif.LdapAttributes; +import org.springframework.ldap.ldif.parser.LDIFParser; + +/** + * 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")); + } + + /** + * 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() { + try { + LdapAttributes attributes; + int count = 0; + + while (parser.hasMoreRecords()) { + 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++; + } + + log.debug("hasMoreRecords: " + parser.hasMoreRecords()); + } + + log.info("record count: " + count); + 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/core-tiger/src/test/java/org/springframework/ldap/ldif/package-info.java b/core-tiger/src/test/java/org/springframework/ldap/ldif/package-info.java new file mode 100644 index 00000000..f0a0d81e --- /dev/null +++ b/core-tiger/src/test/java/org/springframework/ldap/ldif/package-info.java @@ -0,0 +1,4 @@ +/** + * This package provides unit classes to test the validity of other classes in this suite. + */ +package org.springframework.ldap.ldif; \ No newline at end of file diff --git a/core-tiger/src/test/resources/test.ldif b/core-tiger/src/test/resources/test.ldif new file mode 100644 index 00000000..3feb90d4 --- /dev/null +++ b/core-tiger/src/test/resources/test.ldif @@ -0,0 +1,225 @@ +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: 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 6: 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 7: 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