diff --git a/odm/pom.xml b/odm/pom.xml
new file mode 100644
index 00000000..62c79d25
--- /dev/null
+++ b/odm/pom.xml
@@ -0,0 +1,173 @@
+
+
+
+ org.springframework.ldap
+ spring-ldap-parent
+ 1.3.1.CI-SNAPSHOT
+
+ 4.0.0
+ org.springframework.ldap
+ spring-ldap-odm
+ jar
+ Spring LDAP ODM
+ Object Directory Mapping framework
+ http://springframework.org/ldap
+ 2009
+
+
+ Paul Harvey
+ paul@pauls-place.me.uk
+
+ Developer
+
+ 0
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ org.springframework.ldap.odm.tools.SchemaToJava
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 1.5
+ 1.5
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-report-plugin
+
+ always
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 2.4
+
+ ${java.version}
+
+
+
+
+
+
+
+
+
+
+ org.springframework
+ spring-context
+ runtime
+
+
+ org.springframework
+ spring-core
+ compile
+
+
+ org.freemarker
+ freemarker
+
+
+ org.springframework.ldap
+ spring-ldap-core
+ ${version}
+ compile
+
+
+ org.springframework.ldap
+ spring-ldap-core-tiger
+ ${version}
+ compile
+
+
+ commons-pool
+ commons-pool
+ jar
+ runtime
+
+
+ commons-cli
+ commons-cli
+ compile
+
+
+ org.springframework.ldap
+ spring-ldap-test
+ ${version}
+ test
+
+
+ log4j
+ log4j
+ 1.2.15
+ jar
+ runtime
+
+
+ jmxri
+ com.sun.jmx
+
+
+ jms
+ javax.jms
+
+
+ jmxtools
+ com.sun.jdmk
+
+
+
+
+ jdepend
+ jdepend
+ jar
+ test
+
+
+ junit
+ junit
+ 4.4
+ test
+
+
+ commons-logging
+ commons-logging
+
+
+ commons-codec
+ commons-codec
+ jar
+ test
+
+
+ commons-lang
+ commons-lang
+ jar
+ test
+
+
+
+
diff --git a/odm/src/main/java/org/springframework/ldap/odm/annotations/Attribute.java b/odm/src/main/java/org/springframework/ldap/odm/annotations/Attribute.java
new file mode 100755
index 00000000..a82e98f0
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/annotations/Attribute.java
@@ -0,0 +1,65 @@
+package org.springframework.ldap.odm.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * This annotation describes the mapping of a Java field to an LDAP attribute.
+ *
+ * The containing class must be annotated with {@link Entry}.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ * @see Entry
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Attribute {
+ /**
+ * The Type attribute indicates whether a field is regarded as binary based
+ * or string based by the LDAP JNDI provider.
+ */
+ enum Type {
+ /**
+ * A string field - returned by the JNDI LDAP provider as a {@link java.lang.String}.
+ */
+ STRING, /**
+ * A binary field - returned by the JNDI LDAP provider as a byte[].
+ */
+ BINARY
+ }
+
+ /**
+ * The LDAP attribute name that this field represents.
+ *
+ * Defaults to "" in which case the Java field name is used as the LDAP attribute name.
+ *
+ * @return The LDAP attribute name.
+ *
+ */
+ String name() default "";
+
+ /**
+ * Indicates whether this field is returned by the LDAP JNDI provider as a
+ * String (Type.STRING) or as a
+ * byte[] (Type.BINARY).
+ *
+ * @return Either Type.STRING to indicate a string attribute
+ * or Type.BINARY to indicate a binary attribute.
+ */
+ Type type() default Type.STRING;
+
+ /**
+ * The LDAP syntax of the attribute that this field represents.
+ *
+ * This optional value is typically used to affect the precision of conversion
+ * of values between LDAP and Java,
+ * see {@link org.springframework.ldap.odm.typeconversion.ConverterManager}
+ * and {@link org.springframework.ldap.odm.typeconversion.impl.ConverterManagerImpl}.
+ *
+ * @return The LDAP syntax of this attribute.
+ */
+ String syntax() default "";
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/annotations/Entry.java b/odm/src/main/java/org/springframework/ldap/odm/annotations/Entry.java
new file mode 100755
index 00000000..3de360bc
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/annotations/Entry.java
@@ -0,0 +1,25 @@
+package org.springframework.ldap.odm.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation marks a Java class to be persisted in an LDAP directory.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Entry {
+ /**
+ * A list of LDAP object classes that the annotated Java class represents.
+ *
+ * All fields will be persisted to LDAP unless annotated {@link Transient}.
+ *
+ * @return A list of LDAP classes which the annotated Java class represents.
+ */
+ String[] objectClasses();
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/annotations/Id.java b/odm/src/main/java/org/springframework/ldap/odm/annotations/Id.java
new file mode 100755
index 00000000..0688574f
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/annotations/Id.java
@@ -0,0 +1,23 @@
+package org.springframework.ldap.odm.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * This annotation marks a Java field as containing the Distinguished Name of an LDAP Entry.
+ *
+ * The marked field must be of type {@link javax.naming.Name} and must not
+ * be annotated {@link Attribute}.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ * @see Attribute
+ * @see javax.naming.Name
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Id {
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/annotations/Transient.java b/odm/src/main/java/org/springframework/ldap/odm/annotations/Transient.java
new file mode 100755
index 00000000..1342d2a8
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/annotations/Transient.java
@@ -0,0 +1,19 @@
+package org.springframework.ldap.odm.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation identifies a field in an {@link Entry} annotated class that
+ * should not be persisted to LDAP.
+ *
+ * @author Paul Harvey
+ *
+ * @see Entry
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Transient {
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/annotations/package-info.java b/odm/src/main/java/org/springframework/ldap/odm/annotations/package-info.java
new file mode 100755
index 00000000..fa3f6f8f
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/annotations/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Provides a set of annotations to describe the mapping of a Java class to an LDAP entry.
+ *
+ * These annotations are for use with {@link org.springframework.ldap.odm.core.OdmManager}.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+
+package org.springframework.ldap.odm.annotations;
\ No newline at end of file
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/OdmException.java b/odm/src/main/java/org/springframework/ldap/odm/core/OdmException.java
new file mode 100755
index 00000000..df76b15d
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/OdmException.java
@@ -0,0 +1,20 @@
+package org.springframework.ldap.odm.core;
+
+import org.springframework.ldap.NamingException;
+
+/**
+ * The root of the Spring LDAP ODM exception hierarchy.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ */
+@SuppressWarnings("serial")
+public class OdmException extends NamingException {
+ public OdmException(String message) {
+ super(message);
+ }
+
+ public OdmException(String message, Throwable e) {
+ super(message, e);
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/OdmManager.java b/odm/src/main/java/org/springframework/ldap/odm/core/OdmManager.java
new file mode 100755
index 00000000..ab2b512a
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/OdmManager.java
@@ -0,0 +1,97 @@
+package org.springframework.ldap.odm.core;
+
+import java.util.List;
+
+import javax.naming.Name;
+import javax.naming.directory.SearchControls;
+
+/**
+ * The OdmManager interface provides generic CRUD (create/read/update/delete)
+ * and searching operations against an LDAP directory.
+ *
+ * Each managed Java class must be appropriately annotated using
+ * {@link org.springframework.ldap.odm.annotations}.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ * @see org.springframework.ldap.odm.annotations.Entry
+ * @see org.springframework.ldap.odm.annotations.Attribute
+ * @see org.springframework.ldap.odm.annotations.Id
+ * @see org.springframework.ldap.odm.annotations.Transient
+ */
+public interface OdmManager {
+
+
+ /**
+ * Read a named entry from the LDAP directory.
+ *
+ * @param The Java type to return
+ * @param clazz The Java type to return
+ * @param dn The distinguished name of the entry to read from the LDAP directory.
+ * @return The entry as read from the directory
+ *
+ * @exception org.springframework.ldap.NamingException on error.
+ */
+ T read(Class clazz, Name dn);
+
+ /**
+ * Create the given entry in the LDAP directory.
+ *
+ * @param entry The entry to be create, it must not already exist in the directory.
+ *
+ * @exception org.springframework.ldap.NamingException on error.
+ */
+ void create(Object entry);
+
+ /**
+ * Update the given entry in the LDAP directory.
+ *
+ * @param entry The entry to update, it must already exist in the directory.
+ *
+ * @exception org.springframework.ldap.NamingException on error.
+ */
+ void update(Object entry);
+
+ /**
+ * Delete an entry from the LDAP directory.
+ *
+ * @param entry The entry to delete, it must already exist in the directory.
+ *
+ * @exception org.springframework.ldap.NamingException on error.
+ */
+ void delete(Object entry);
+
+ /**
+ * Find all entries in the LDAP directory of a given type.
+ *
+ * @param The Java type to return
+ * @param clazz The Java type to return
+ * @param base The root of the sub-tree at which to begin the search.
+ * @param searchControls The scope of the search.
+ * @return All entries that are of the type represented by the given
+ * Java class
+ *
+ * @exception org.springframework.ldap.NamingException on error.
+ */
+ List findAll(Class clazz, Name base, SearchControls searchControls);
+
+ /**
+ * Search for entries in the LDAP directory.
+ *
+ * Only those entries that both match the given search filter and
+ * are represented by the given Java class are returned
+ *
+ * @param The Java type to return
+ * @param clazz The Java type to return
+ * @param base The root of the sub-tree at which to begin the search.
+ * @param filter An LDAP search filter.
+ * @param searchControls The scope of the search.
+ * @return All matching entries.
+ *
+ * @exception org.springframework.ldap.NamingException on error.
+ *
+ * @see Sun's JNDI tutorial description of search filters.
+ * @see LDAP: String Representation of Search Filters RFC.
+ */
+ List search(Class clazz, Name base, String filter, SearchControls searchControls);
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/AttributeMetaData.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/AttributeMetaData.java
new file mode 100755
index 00000000..ccf032a3
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/AttributeMetaData.java
@@ -0,0 +1,226 @@
+package org.springframework.ldap.odm.core.impl;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Set;
+
+import javax.naming.Name;
+
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.Id;
+
+/*
+ * Extract attribute meta-data from the @Attribute annotation, the @Id annotation
+ * and via reflection.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+/* package */ final class AttributeMetaData {
+ private static final CaseIgnoreString OBJECT_CLASS_ATTRIBUTE_CI=new CaseIgnoreString("objectclass");
+
+ // Name of the LDAP attribute from the @Attribute annotation
+ private CaseIgnoreString name;
+
+ // Syntax of the LDAP attribute from the @Attribute annotation
+ private String syntax;
+
+ // Whether this attribute is binary from the @Attribute annotation
+ private boolean isBinary;
+
+ // The Java field corresponding to this meta-data
+ private final Field field;
+
+ // The Java class of the field corresponding to this meta data
+ // This is the actual scalar type meaning that if the field is
+ // List then the valueClass will be String
+ private Class> valueClass;
+
+ // Is this field annotated @Id
+ private boolean isId;
+
+ // Is this field multi-valued represented by a List
+ private boolean isList;
+
+ // Is this the objectClass attribute
+ private boolean isObjectClass;
+
+ // Extract information from the @Attribute annotation:
+ // syntax, isBinary, isObjectClass and name.
+ private boolean processAttributeAnnotation(Field field) {
+ // Default to no syntax specified
+ syntax = "";
+
+ // Default to a String based attribute
+ isBinary = false;
+
+ // Default name of attribute to the name of the field
+ name = new CaseIgnoreString(field.getName());
+
+ // We have not yet found the @Attribute annotation
+ boolean foundAnnotation=false;
+
+ // Grab the @Attribute annotation
+ Attribute attribute = field.getAnnotation(Attribute.class);
+
+ // Did we find the annotation?
+ if (attribute != null) {
+ // Pull attribute name, syntax and whether attribute is binary
+ // from the annotation
+ foundAnnotation=true;
+ String localAttributeName = attribute.name();
+ // Would be more efficient to use !isEmpty - but that then makes us Java 6 dependent
+ if (localAttributeName != null && localAttributeName.length()>0) {
+ name = new CaseIgnoreString(localAttributeName);
+ }
+ syntax = attribute.syntax();
+ isBinary = attribute.type() == Attribute.Type.BINARY;
+ }
+
+ isObjectClass=name.equals(OBJECT_CLASS_ATTRIBUTE_CI);
+
+ return foundAnnotation;
+ }
+
+ // Extract reflection information from the field:
+ // valueClass, isList
+ private void determineFieldType(Field field) {
+ // Determine the class of data stored in the field
+ Class> fieldType = field.getType();
+
+ // We support only lists for multi-valued attributes, as we must allow duplicate values
+ if (Set.class.isAssignableFrom(fieldType)) {
+ throw new MetaDataException(String.format("Only lists are allowed for multivlaued attributes, errpr in field %1$s in Entry class %2$s",
+ field, field.getDeclaringClass()));
+ }
+ isList = List.class.isAssignableFrom(fieldType);
+
+ valueClass=null;
+ if (!isList) {
+ // It's not a list so assume its single valued - so just take the field type
+ valueClass = fieldType;
+ } else {
+ // It's multi-valued - so we need to look at the signature in
+ // the class file to find the generic type - this is supported for class file
+ // format 49 and greater which corresponds to java 5 and later.
+ ParameterizedType paramType;
+ try {
+ paramType = (ParameterizedType)field.getGenericType();
+ } catch (ClassCastException e) {
+ throw new MetaDataException(String.format("Can't determine destination type for field %1$s in Entry class %2$s",
+ field, field.getDeclaringClass()), e);
+ }
+ Type[] actualParamArguments = paramType.getActualTypeArguments();
+ if (actualParamArguments.length == 1) {
+ if (actualParamArguments[0] instanceof Class) {
+ valueClass = (Class>)actualParamArguments[0];
+ } else {
+ if (actualParamArguments[0] instanceof GenericArrayType) {
+ // Deal with arrays
+ Type type=((GenericArrayType)actualParamArguments[0]).getGenericComponentType();
+ if (type instanceof Class) {
+ valueClass=Array.newInstance((Class>)type, 0).getClass();
+ }
+ }
+ }
+ }
+ }
+
+ // Check we have been able to determine the value class
+ if (valueClass==null) {
+ throw new MetaDataException(String.format("Can't determine destination type for field %1$s in class %2$s",
+ field, field.getDeclaringClass()));
+ }
+ }
+
+ // Extract information from the @Id annotation:
+ // isId
+ private boolean processIdAnnotation(Field field, Class> fieldType) {
+ // Are we dealing with the Id field?
+ isId=field.getAnnotation(Id.class)!=null;
+
+ if (isId) {
+ // It must be of type Name or a subclass of that of
+ if (!Name.class.isAssignableFrom(fieldType)) {
+ throw new MetaDataException(
+ String.format("The id field must be of type javax.naming.Name or a subclass that of in Entry class %1$s",
+ field.getDeclaringClass()));
+ }
+ }
+
+ return isId;
+ }
+
+ // Extract meta-data from the given field
+ public AttributeMetaData(Field field) {
+ this.field=field;
+
+ // Reflection data
+ determineFieldType(field);
+
+ // Data from the @Attribute annotation
+ boolean foundAttributeAnnotation=processAttributeAnnotation(field);
+
+ // Data from the @Id annotation
+ boolean foundIdAnnoation=processIdAnnotation(field, valueClass);
+
+ // Check that the field has not been annotated with both @Attribute and with @Id
+ if (foundAttributeAnnotation && foundIdAnnoation) {
+ throw new MetaDataException(
+ String.format("You may not specifiy an %1$s annoation and an %2$s annotation on the same field, error in field %3$s in Entry class %4$s",
+ Id.class, Attribute.class, field.getName(), field.getDeclaringClass()));
+ }
+
+ // If this is the objectclass attribute then it must be of type List
+ if (isObjectClass() && (!isList() || valueClass!=String.class)) {
+ throw new MetaDataException(String.format("The type of the objectclass attribute must be List in classs %1$s",
+ field.getDeclaringClass()));
+ }
+ }
+
+ public String getSyntax() {
+ return syntax;
+ }
+
+ public boolean isBinary() {
+ return isBinary;
+ }
+
+ public Field getField() {
+ return field;
+ }
+
+ public CaseIgnoreString getName() {
+ return name;
+ }
+
+ public boolean isList() {
+ return isList;
+ }
+
+ public boolean isId() {
+ return isId;
+ }
+
+ public boolean isObjectClass() {
+ return isObjectClass;
+ }
+
+ public Class> getValueClass() {
+ return valueClass;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return String.format("name=%1$s | field=%2$s | valueClass=%3$s | syntax=%4$s| isBinary=%5$s | isId=%6$s | isList=%7$s | isObjectClass=%8$s",
+ getName(), getField(), getValueClass().getName(), getSyntax(), isBinary(), isId(), isList(), isObjectClass());
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/CaseIgnoreString.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/CaseIgnoreString.java
new file mode 100755
index 00000000..70a04d1a
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/CaseIgnoreString.java
@@ -0,0 +1,32 @@
+package org.springframework.ldap.odm.core.impl;
+
+// A case independent String wrapper.
+/* package */ final class CaseIgnoreString implements Comparable {
+ private final String string;
+ private final int hashCode;
+
+ public CaseIgnoreString(String string) {
+ if (string == null)
+ throw new NullPointerException();
+ this.string = string;
+ hashCode = string.toUpperCase().hashCode();
+ }
+
+ public boolean equals(Object other) {
+ return other instanceof CaseIgnoreString &&
+ ((CaseIgnoreString)other).string.equalsIgnoreCase(string);
+ }
+
+ public int hashCode() {
+ return hashCode;
+ }
+
+ public int compareTo(CaseIgnoreString other) {
+ CaseIgnoreString cis = (CaseIgnoreString)other;
+ return String.CASE_INSENSITIVE_ORDER.compare(string, cis.string);
+ }
+
+ public String toString() {
+ return string;
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/InvalidEntryException.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/InvalidEntryException.java
new file mode 100755
index 00000000..f6fe3a43
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/InvalidEntryException.java
@@ -0,0 +1,20 @@
+package org.springframework.ldap.odm.core.impl;
+
+import org.springframework.ldap.odm.core.OdmException;
+
+/**
+ * Thrown to indicate that an instance is not suitable for persisting in the LDAP directory.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ */
+@SuppressWarnings("serial")
+public class InvalidEntryException extends OdmException {
+ public InvalidEntryException(String message) {
+ super(message);
+ }
+
+ public InvalidEntryException(String message, Throwable reason) {
+ super(message, reason);
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/MetaDataException.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/MetaDataException.java
new file mode 100755
index 00000000..3edecb6d
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/MetaDataException.java
@@ -0,0 +1,20 @@
+package org.springframework.ldap.odm.core.impl;
+
+import org.springframework.ldap.odm.core.OdmException;
+
+/**
+ * Thrown to indicate an error in the annotated meta-data.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ */
+@SuppressWarnings("serial")
+public class MetaDataException extends OdmException {
+ public MetaDataException(String message) {
+ super(message);
+ }
+
+ public MetaDataException(String message, Throwable reason) {
+ super(message, reason);
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/ObjectMetaData.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/ObjectMetaData.java
new file mode 100755
index 00000000..6c0f889d
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/ObjectMetaData.java
@@ -0,0 +1,124 @@
+package org.springframework.ldap.odm.core.impl;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+import org.springframework.ldap.odm.annotations.Transient;
+
+/*
+ * An internal class to process the meta-data and reflection data for an entry.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+/* package */ final class ObjectMetaData implements Iterable {
+ private static final Log LOG = LogFactory.getLog(ObjectMetaData.class);
+
+ private AttributeMetaData idAttribute;
+
+ private Map fieldToAttribute = new HashMap();
+
+ private Set objectClasses = new HashSet();
+
+ public Set getObjectClasses() {
+ return objectClasses;
+ }
+
+ public AttributeMetaData getIdAttribute() {
+ return idAttribute;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Iterable#iterator()
+ */
+ public Iterator iterator() {
+ return fieldToAttribute.keySet().iterator();
+ }
+
+ public AttributeMetaData getAttribute(Field field) {
+ return fieldToAttribute.get(field);
+ }
+
+ public ObjectMetaData(Class> clazz) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Extracting metadata from %1$s", clazz));
+ }
+
+ // Get object class metadata - the @Entity annotation
+ Entry entity = (Entry)clazz.getAnnotation(Entry.class);
+ if (entity != null) {
+ // Default objectclass name to the class name unless it's specified
+ // in @Entity(name={objectclass1, objectclass2});
+ String[] localObjectClasses = entity.objectClasses();
+ if (localObjectClasses != null && localObjectClasses.length > 0 && localObjectClasses[0].length() > 0) {
+ for (String localObjectClass:localObjectClasses) {
+ objectClasses.add(new CaseIgnoreString(localObjectClass));
+ }
+ } else {
+ objectClasses.add(new CaseIgnoreString(clazz.getSimpleName()));
+ }
+ } else {
+ throw new MetaDataException(String.format("Class %1$s must have a class level %2$s annotation", clazz,
+ Entry.class));
+ }
+
+ // Check the class is final
+ if (!Modifier.isFinal(clazz.getModifiers())) {
+ LOG.warn(String.format("The Entry class %1$s should be declared final", clazz.getSimpleName()));
+ }
+
+ // Get field meta-data - the @Attribute annotation
+ Field[] fields = clazz.getDeclaredFields();
+ for (Field field : fields) {
+ // So we can write to private fields
+ field.setAccessible(true);
+
+ // Skip transient and synthetic fields
+ if (field.getAnnotation(Transient.class) != null || field.isSynthetic()) {
+ continue;
+ }
+
+ AttributeMetaData currentAttributeMetaData=new AttributeMetaData(field);
+ if (currentAttributeMetaData.isId()) {
+ if (idAttribute!=null) {
+ // There can be only one id field
+ throw new MetaDataException(
+ String.format("You man have only one field with the %1$s annotation in class %2$s", Id.class, clazz));
+ }
+ idAttribute=currentAttributeMetaData;
+ }
+ fieldToAttribute.put(field, currentAttributeMetaData);
+ }
+
+ if (idAttribute == null) {
+ throw new MetaDataException(
+ String.format("All Entry classes must define a field with the %1$s annotation, error in class %2$s", Id.class,
+ clazz));
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Extracted metadata from %1$s as %2$s", clazz, this));
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return String.format("objectsClasses=%1$s | idField=%2$s | attributes=%3$s",
+ objectClasses, idAttribute.getName(), fieldToAttribute);
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImpl.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImpl.java
new file mode 100755
index 00000000..59c38af8
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImpl.java
@@ -0,0 +1,474 @@
+package org.springframework.ldap.odm.core.impl;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.Name;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchControls;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.core.DirContextAdapter;
+import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapOperations;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.core.simple.ParameterizedContextMapper;
+import org.springframework.ldap.filter.AndFilter;
+import org.springframework.ldap.filter.EqualsFilter;
+import org.springframework.ldap.odm.core.OdmException;
+import org.springframework.ldap.odm.core.OdmManager;
+import org.springframework.ldap.odm.typeconversion.ConverterManager;
+
+/**
+ * An implementation of {@link org.springframework.ldap.odm.core.OdmManager} which
+ * uses {@link org.springframework.ldap.odm.typeconversion.ConverterManager} to
+ * convert between Java and LDAP representations of attribute values.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ */
+public final class OdmManagerImpl implements OdmManager {
+ private static final Log LOG = LogFactory.getLog(OdmManagerImpl.class);
+
+ // The link to the LDAP directory
+ private final LdapOperations ldapTemplate;
+
+ // The converter manager to use to translate values between LDAP and Java
+ private final ConverterManager converterManager;
+
+ private static String OBJECT_CLASS_ATTRIBUTE="objectclass";
+ private static CaseIgnoreString OBJECT_CLASS_ATTRIBUTE_CI=new CaseIgnoreString(OBJECT_CLASS_ATTRIBUTE);
+
+ private static final class EntityData {
+ private final ObjectMetaData metaData;
+ private final String ocFilter;
+
+ private EntityData(ObjectMetaData metaData, String ocFilter) {
+ this.metaData=metaData;
+ this.ocFilter=ocFilter;
+ }
+ }
+
+ // A map of managed classes to to meta data about those classes
+ private final Map, EntityData> metaDataMap=new HashMap, EntityData>();
+
+ public OdmManagerImpl(ConverterManager converterManager,
+ ContextSource contextSource,
+ Set> managedClasses) {
+
+ this.converterManager=converterManager;
+ this.ldapTemplate=new LdapTemplate(contextSource);
+
+ if (managedClasses!=null) {
+ for (Class> managedClass: managedClasses) {
+ addManagedClass(managedClass);
+ }
+ }
+ }
+
+ public OdmManagerImpl(ConverterManager converterManager,
+ ContextSource contextSource) {
+ this(converterManager, contextSource, null);
+ }
+
+ private EntityData getEntityData(Class> managedClass) {
+ EntityData result=metaDataMap.get(managedClass);
+ if (result==null) {
+ throw new UnmanagedClassException(String.format("The %1$s class is not managed by this OdmManager", managedClass));
+ }
+ return result;
+ }
+
+ /**
+ * Adds an {@link org.springframework.ldap.odm.annotations} annotated class to the set
+ * managed by this OdmManager.
+ *
+ * @param managedClass The class to add to the managed set.
+ */
+ public void addManagedClass(Class> managedClass) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Adding class %1$s to managed set", managedClass));
+ }
+
+ // Extract the meta-data from the class
+ ObjectMetaData metaData=new ObjectMetaData(managedClass);
+
+ // Check we can construct the target type - it must have a zero argument public constructor
+ try {
+ managedClass.getConstructor();
+ } catch (NoSuchMethodException e) {
+ throw new InvalidEntryException(String.format(
+ "The class %1$s must have a zero argument constructor to be an Entry", managedClass));
+ }
+
+ // Check we have all of the necessary converters for the class
+ for (Field field : metaData) {
+ AttributeMetaData attributeInfo = metaData.getAttribute(field);
+ if (!attributeInfo.isId() && !(attributeInfo.isObjectClass())) {
+ Class> jndiClass = (attributeInfo.isBinary()) ? byte[].class : String.class;
+ Class> javaClass = attributeInfo.getValueClass();
+ if (!converterManager.canConvert(jndiClass, attributeInfo.getSyntax(), javaClass)) {
+ throw new InvalidEntryException(String.format(
+ "Missing converter from %1$s to %2$s, this is needed for field %3$s on Entry %4$s",
+ jndiClass, javaClass, field.getName(), managedClass));
+ }
+ if (!converterManager.canConvert(javaClass, attributeInfo.getSyntax(), jndiClass)) {
+ throw new InvalidEntryException(String.format(
+ "Missing converter from %1$s to %2$s, this is needed for field %3$s on Entry %4$s",
+ javaClass, jndiClass, field.getName(), managedClass));
+ }
+ }
+ }
+
+ // Filter so we only read the object classes supported by the managedClass
+ AndFilter ocFilter = new AndFilter();
+ for (CaseIgnoreString oc : metaData.getObjectClasses()) {
+ ocFilter.and(new EqualsFilter(OBJECT_CLASS_ATTRIBUTE, oc.toString()));
+ }
+
+ metaDataMap.put(managedClass, new EntityData(metaData, ocFilter.encode()));
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.odm.core.OdmManager#create(java.lang.Object)
+ */
+ public T read(Class clazz, Name dn) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Reading Entry at - %s$1", dn));
+ }
+
+ getEntityData(clazz);
+
+ T result = clazz.cast(ldapTemplate.lookup(dn, new GenericContextMapper(clazz)));
+ if (result==null) {
+ throw new OdmException(String.format("Entry %1$s has excess object classes", dn));
+ }
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Found entry - %s$1", result));
+ }
+
+ return result;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.odm.core.OdmManager#create(java.lang.Object)
+ */
+ public void create(Object entry) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Creating entry - %s$1", entry));
+ }
+
+ DirContextAdapter context = new DirContextAdapter(getId(entry));
+ mapToContext(entry, context);
+ ldapTemplate.bind(context);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.odm.core.OdmManager#update(java.lang.Object, boolean)
+ */
+ public void update(Object entry) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Updating entry - %s$1", entry));
+ }
+
+ DirContextAdapter context = new DirContextAdapter(getId(entry));
+ mapToContext(entry, context);
+ ldapTemplate.rebind(context);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.odm.core.OdmManager#delete(javax.naming.Name)
+ */
+ public void delete(Object entry) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Deleting %s$1", entry));
+ }
+
+ // Just to check that this is a managed class
+ getEntityData(entry.getClass());
+ ldapTemplate.unbind(getId(entry));
+ }
+
+ private Name getId(Object entry) {
+ try {
+ return (Name)getEntityData(entry.getClass()).metaData.getIdAttribute().getField().get(entry);
+ } catch (Exception e) {
+ throw new InvalidEntryException(String.format("Can't get Id field from Entry %1$s", entry),
+ e);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.ldap.odm.core.OdmManager#search(java.lang.Class, javax.naming.Name, java.lang.String, javax.naming.directory.SearchControls)
+ */
+ public List search(Class managedClass, Name base, String filter, SearchControls scope) {
+ EntityData entityData=getEntityData(managedClass);
+
+ // Add a filter so we only read the object class we can deal with
+ String finalFilter = entityData.ocFilter;
+ if (filter != null && filter.length() != 0) {
+ StringBuilder fixedFilter = new StringBuilder();
+ fixedFilter.append("(&(").append(filter).append(")").append(entityData.ocFilter).append(")");
+ finalFilter = fixedFilter.toString();
+ }
+
+ // Search from the root if we are not told where to search from
+ Name localBase = base;
+ if (base == null || base.size() == 0) {
+ localBase = DistinguishedName.EMPTY_PATH;
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Searching - base=%1$s, finalFilter=%2$s, scope=%3$s", base, finalFilter, scope));
+ }
+
+ @SuppressWarnings("unchecked")
+ List result = ldapTemplate.search(localBase, finalFilter, scope, new GenericContextMapper(managedClass));
+ result.remove(null);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Found %1$s Entries - %2$s", result.size(), result));
+ }
+
+ return result;
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.ldap.odm.core.OdmManager#findAll(javax.naming.Name, javax.naming.directory.SearchControls)
+ */
+ public List findAll(Class managedClass, Name base, SearchControls scope) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Searching for all Entries with objectClass=%1$s, with base=%2$s, scope=%3$s",
+ getEntityData(managedClass).metaData.getObjectClasses(), base, scope));
+ }
+
+ return search(managedClass, base, null, scope);
+ }
+
+ /**
+ * Used to convert from Java representation of an Ldap Entry when writing to
+ * the Ldap directory
+ *
+ * @param entry - The entry to convert.
+ * @param context - The LDAP context to store the converted entry
+ * @throws javax.naming.NamingEnumeration on error.
+ */
+ private void mapToContext(Object entry, DirContextOperations context) {
+ ObjectMetaData metaData=getEntityData(entry.getClass()).metaData;
+
+ // Object classes are set from the metadata obtained from the @Entity annotation
+ int numOcs=metaData.getObjectClasses().size();
+ CaseIgnoreString[] metaDataObjectClasses=metaData.getObjectClasses().toArray(new CaseIgnoreString[numOcs]);
+
+ String[] stringOcs=new String[numOcs];
+ for (int ocIndex=0; ocIndex targetClass = (attributeInfo.isBinary()) ? byte[].class : String.class;
+ // Multi valued?
+ if (!attributeInfo.isList()) {
+ // Single valued - get the value of the field
+ Object fieldValue = field.get(entry);
+ // Ignore null field values
+ if (fieldValue != null) {
+ // Convert the field value to the required type and write it into the JNDI context
+ context.setAttributeValue(attributeInfo.getName().toString(), converterManager.convert(fieldValue,
+ attributeInfo.getSyntax(), targetClass));
+ }
+ } else { // Multi-valued
+ // We need to build up a list of of the values
+ List attributeValues = new ArrayList();
+ // Get the list of values
+ Collection> fieldValues = (Collection>)field.get(entry);
+ // Ignore null lists
+ if (fieldValues != null) {
+ for (final Object o : fieldValues) {
+ // Ignore null values
+ if (o != null) {
+ attributeValues.add((String)converterManager.convert(o, attributeInfo.getSyntax(),
+ targetClass));
+ }
+ }
+ context.setAttributeValues(attributeInfo.getName().toString(), attributeValues.toArray());
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new InvalidEntryException(String.format("Can't set attribute %1$s", attributeInfo.getName()),
+ e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Used to convert from the JNDI LDAP representation of an Entry to the Java representation when reading from LDAP
+ */
+ private class GenericContextMapper implements ParameterizedContextMapper {
+ private final Class managedClass;
+
+ private GenericContextMapper(Class managedClass) {
+ this.managedClass=managedClass;
+ }
+
+ // Called by Spring LDAP to do the conversion
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.springframework.ldap.core.simple.ParameterizedContextMapper#mapFromContext(java.lang.Object)
+ */
+ public T mapFromContext(Object object) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Converting to Java Entry class %1$s from %2$s", managedClass, object));
+ }
+
+ // The Java representation of the LDAP entry
+ T result = null;
+
+ // This is guaranteed by Spring LDAP to be a DirContextOperations
+ DirContextOperations context = (DirContextOperations)object;
+
+ ObjectMetaData metaData=getEntityData(managedClass).metaData;
+
+ try {
+ // The result class must have a zero argument constructor
+ result = managedClass.newInstance();
+
+ // Build a map of JNDI attribute names to values
+ Map attributeValueMap = new HashMap();
+ // Get a NamingEnumeration to loop through the JNDI attributes in the entry
+ Attributes attributes = context.getAttributes();
+ NamingEnumeration extends Attribute> attributesEnumeration = attributes.getAll();
+ // Loop through all of the JNDI attributes
+ while (attributesEnumeration.hasMoreElements()) {
+ Attribute currentAttribute = (Attribute)attributesEnumeration.nextElement();
+ // Add the current attribute to the map keyed on the lowercased (case indep) id of the attribute
+ attributeValueMap.put(new CaseIgnoreString(currentAttribute.getID()), currentAttribute);
+ }
+
+ // Now loop through all the fields in the Java representation populating it with values from the
+ // attributeValueMap
+ for (Field field : metaData) {
+ // Get the current field
+ AttributeMetaData attributeInfo = metaData.getAttribute(field);
+ // We deal with the Id field specially
+ if (!attributeInfo.isId()) {
+ // Not the ID - but is is multi valued?
+ if (!attributeInfo.isList()) {
+ // No - its single valued, grab the JNDI attribute that corresponds to the metadata on the
+ // current field
+ Attribute attribute = attributeValueMap.get(attributeInfo.getName());
+ // There is no guarantee that this attribute is present in the directory - so ignore nulls
+ if (attribute != null) {
+ // Grab the JNDI value
+ Object value = attribute.get();
+ // Check the value is not null
+ if (value != null) {
+ // Convert the JNDI value to its Java representation - this will throw if the
+ // conversion fails
+ Object convertedValue = converterManager.convert(value, attributeInfo.getSyntax(),
+ attributeInfo.getValueClass());
+ // Set it in the Java version
+ field.set(result, convertedValue);
+ }
+ }
+ } else { // We are dealing with a multi valued attribute
+ // We need to build up a list of values
+ List fieldValues = new ArrayList();
+ // Grab the attribute from the JNDI representation
+ Attribute currentAttribute = attributeValueMap.get(attributeInfo.getName());
+ // There is no guarantee that this attribute is present in the directory - so ignore nulls
+ if (currentAttribute != null) {
+ // Loop through the values of the JNDI attribute
+ NamingEnumeration> valuesEmumeration = currentAttribute.getAll();
+ while (valuesEmumeration.hasMore()) {
+ // Get the current value
+ Object value = valuesEmumeration.nextElement();
+ // Check the value is not null
+ if (value != null) {
+ // Convert the value to its Java representation and add it to our working list
+ fieldValues.add(converterManager.convert(value, attributeInfo.getSyntax(),
+ attributeInfo.getValueClass()));
+ }
+ }
+ }
+ // Now we need to set the List in to a Java object
+ field.set(result, fieldValues);
+ }
+ } else { // The id field
+ field.set(result, converterManager.convert(context.getDn(), attributeInfo.getSyntax(),
+ attributeInfo.getValueClass()));
+ }
+ }
+
+ // If this is the objectclass attribute then check that values correspond to the metadata we have
+ // for the Java representation
+ Attribute ocAttribute = attributeValueMap.get(OBJECT_CLASS_ATTRIBUTE_CI);
+ if (ocAttribute != null) {
+ // Get all object class values from the JNDI attribute
+ Set objectClassesFromJndi = new HashSet();
+ NamingEnumeration> objectClassesFromJndiEnum = ocAttribute.getAll();
+ while (objectClassesFromJndiEnum.hasMoreElements()) {
+ objectClassesFromJndi.add(new CaseIgnoreString((String)objectClassesFromJndiEnum.nextElement()));
+ }
+ // OK - checks its the same as the meta-data we have
+ if (!objectClassesFromJndi.equals(metaData.getObjectClasses())) {
+ // The items found has classes in addition to those searched for - so ditch it
+ return null;
+ }
+
+ } else {
+ throw new InvalidEntryException(String.format("No object classes were returned for class %1$s",
+ managedClass.getName()));
+ }
+
+ } catch (NamingException ne) {
+ throw new InvalidEntryException(String.format("Problem creating %1$s from LDAP Entry %2$s",
+ managedClass, object), ne);
+ } catch (IllegalAccessException iae) {
+ throw new InvalidEntryException(String.format(
+ "Could not create an instance of %1$s could not access field", managedClass.getName()), iae);
+ } catch (InstantiationException ie) {
+ throw new InvalidEntryException(String.format("Could not instantiate %1$s", managedClass), ie);
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Converted object - %1$s", result));
+ }
+
+ return result;
+ }
+ }
+
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImplFactoryBean.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImplFactoryBean.java
new file mode 100755
index 00000000..808978da
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImplFactoryBean.java
@@ -0,0 +1,90 @@
+package org.springframework.ldap.odm.core.impl;
+
+import java.util.Set;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.FactoryBeanNotInitializedException;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.odm.typeconversion.ConverterManager;
+
+/**
+ * A Spring Factory bean which creates {@link OdmManagerImpl} instances.
+ *
+ * Typical configuration would appear as follows:
+ *
+ * <bean id="odmManager" class="org.springframework.ldap.odm.core.impl.OdmManagerImplFactoryBean">
+ * <property name="converterManager" ref="converterManager" />
+ * <property name="contextSource" ref="contextSource" />
+ * <property name="managedClasses">
+ * <set>
+ * <value>org.myorg.myldapentries.Person</value>
+ * <value>org.myorg.myldapentries.OrganizationalUnit</value>
+ * </set>
+ * </property>
+ * </bean>
+ *
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public final class OdmManagerImplFactoryBean implements FactoryBean {
+ private ContextSource contextSource=null;
+ private Set> managedClasses=null;
+ private ConverterManager converterManager=null;
+
+ /**
+ * Set the ContextSource to use to interact with the LDAP directory.
+ * @param contextSource The ContextSource to use.
+ */
+ public void setContextSource(ContextSource contextSource) {
+ this.contextSource=contextSource;
+ }
+
+ /**
+ * Set the list of {@link org.springframework.ldap.odm.annotations}
+ * annotated classes the OdmManager will process.
+ * @param managedClasses The list of classes to manage.
+ */
+ public void setManagedClasses(Set> managedClasses) {
+ this.managedClasses=managedClasses;
+ }
+
+ /**
+ * Set the ConverterManager to use to convert between LDAP
+ * and Java representations of attributes.
+ * @param converterManager The ConverterManager to use.
+ */
+ public void setConverterManager(ConverterManager converterManager) {
+ this.converterManager=converterManager;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.beans.factory.FactoryBean#getObject()
+ */
+ public Object getObject() throws Exception {
+ if (contextSource==null) {
+ throw new FactoryBeanNotInitializedException("contextSource property has not been set");
+ }
+ if (managedClasses==null) {
+ throw new FactoryBeanNotInitializedException("managedClasses property has not been set");
+ }
+ if (converterManager==null) {
+ throw new FactoryBeanNotInitializedException("converterManager property has not been set");
+ }
+
+ return new OdmManagerImpl(converterManager, contextSource, managedClasses);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.beans.factory.FactoryBean#getObjectType()
+ */
+ public Class> getObjectType() {
+ return OdmManagerImpl.class;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.beans.factory.FactoryBean#isSingleton()
+ */
+ public boolean isSingleton() {
+ return true;
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/UnmanagedClassException.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/UnmanagedClassException.java
new file mode 100755
index 00000000..0a4ad4ef
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/UnmanagedClassException.java
@@ -0,0 +1,21 @@
+package org.springframework.ldap.odm.core.impl;
+
+import org.springframework.ldap.odm.core.OdmException;
+
+/**
+ * Thrown when an OdmManager method is called with a class
+ * which is not being managed by the OdmManager.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ */
+@SuppressWarnings("serial")
+public class UnmanagedClassException extends OdmException {
+ public UnmanagedClassException(String message, Throwable reason) {
+ super(message, reason);
+ }
+
+ public UnmanagedClassException(String message) {
+ super(message);
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/package-info.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/package-info.java
new file mode 100755
index 00000000..c58a484f
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Provides a single public class which implements {@link org.springframework.ldap.odm.core.OdmManager}.
+ *
+ * The OdmManager implementation works in conjunction with {@link org.springframework.ldap.odm.typeconversion} to provide
+ * conversion between the representation of attributes in LDAP and in Java.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+package org.springframework.ldap.odm.core.impl;
\ No newline at end of file
diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/package-info.java b/odm/src/main/java/org/springframework/ldap/odm/core/package-info.java
new file mode 100755
index 00000000..39c75036
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/core/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * Provides an OdmManager interface for interaction with an LDAP directory.
+ *
+ * Implementations of this interface are intended to be used in conjunction with classes
+ * annotated with {@link org.springframework.ldap.odm.annotations}.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+
+package org.springframework.ldap.odm.core;
\ No newline at end of file
diff --git a/odm/src/main/java/org/springframework/ldap/odm/tools/AttributeSchema.java b/odm/src/main/java/org/springframework/ldap/odm/tools/AttributeSchema.java
new file mode 100755
index 00000000..12230155
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/tools/AttributeSchema.java
@@ -0,0 +1,127 @@
+package org.springframework.ldap.odm.tools;
+
+/**
+ * Simple value class to hold the schema of an attribute.
+ *
+ * It is only public to allow Freemarker access.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public final class AttributeSchema {
+
+ private final String name;
+
+ private final String syntax;
+
+ private final boolean isMultiValued;
+
+ private final boolean isPrimitive;
+
+ private final String scalarType;
+
+ private final boolean isBinary;
+
+ private final boolean isArray;
+
+ public AttributeSchema(final String name, final String syntax, final boolean isMultiValued,
+ final boolean isPrimitive, final boolean isBinary, final boolean isArray, final String scalarType) {
+ this.name = name;
+ this.syntax = syntax;
+ this.isMultiValued = isMultiValued;
+ this.isPrimitive = isPrimitive;
+ this.scalarType = scalarType;
+ this.isBinary = isBinary;
+ this.isArray = isArray;
+ }
+
+ public boolean getIsArray() {
+ return isArray;
+ }
+
+ public boolean getIsBinary() {
+ return isBinary;
+ }
+
+ public boolean getIsPrimitive() {
+ return isPrimitive;
+ }
+
+ public String getScalarType() {
+ return scalarType;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getSyntax() {
+ return syntax;
+ }
+
+ public boolean getIsMultiValued() {
+ return isMultiValued;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+
+ return String.format(
+ "{ name=%1$s, syntax=%2$s, isMultiValued=%3$s, isPrimitive=%4$s, isBinary=%5$s, isArray=%6$s, scalarType=%7$s }",
+ name, syntax, isMultiValued, isPrimitive, isBinary, isArray, scalarType);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (isArray ? 1231 : 1237);
+ result = prime * result + (isBinary ? 1231 : 1237);
+ result = prime * result + (isMultiValued ? 1231 : 1237);
+ result = prime * result + (isPrimitive ? 1231 : 1237);
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((scalarType == null) ? 0 : scalarType.hashCode());
+ result = prime * result + ((syntax == null) ? 0 : syntax.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ AttributeSchema other = (AttributeSchema) obj;
+ if (isArray != other.isArray)
+ return false;
+ if (isBinary != other.isBinary)
+ return false;
+ if (isMultiValued != other.isMultiValued)
+ return false;
+ if (isPrimitive != other.isPrimitive)
+ return false;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ if (scalarType == null) {
+ if (other.scalarType != null)
+ return false;
+ } else if (!scalarType.equals(other.scalarType))
+ return false;
+ if (syntax == null) {
+ if (other.syntax != null)
+ return false;
+ } else if (!syntax.equals(other.syntax))
+ return false;
+ return true;
+ }
+
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/tools/ObjectSchema.java b/odm/src/main/java/org/springframework/ldap/odm/tools/ObjectSchema.java
new file mode 100755
index 00000000..e281f4ed
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/tools/ObjectSchema.java
@@ -0,0 +1,91 @@
+package org.springframework.ldap.odm.tools;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Simple value class to hold the schema of an object class
+ *
+ * It is public only to allow Freemarker access.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public final class ObjectSchema {
+ private final Set must = new HashSet();
+
+ private final Set may = new HashSet();
+
+ private final Set objectClass = new HashSet();
+
+ public void addMust(AttributeSchema must) {
+ this.must.add(must);
+ }
+
+ public void addMay(AttributeSchema may) {
+ this.may.add(may);
+ }
+
+ public void addObjectClass(String objectClass) {
+ this.objectClass.add(objectClass);
+ }
+
+ public Set getMust() {
+ return Collections.unmodifiableSet(must);
+ }
+
+ public Set getMay() {
+ return Collections.unmodifiableSet(may);
+ }
+
+ public Set getObjectClass() {
+ return Collections.unmodifiableSet(objectClass);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return String.format("objectClass=%1$s | must=%2$s | may=%3$s", objectClass, must, may);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((may == null) ? 0 : may.hashCode());
+ result = prime * result + ((must == null) ? 0 : must.hashCode());
+ result = prime * result + ((objectClass == null) ? 0 : objectClass.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ObjectSchema other = (ObjectSchema) obj;
+ if (may == null) {
+ if (other.may != null)
+ return false;
+ } else if (!may.equals(other.may))
+ return false;
+ if (must == null) {
+ if (other.must != null)
+ return false;
+ } else if (!must.equals(other.must))
+ return false;
+ if (objectClass == null) {
+ if (other.objectClass != null)
+ return false;
+ } else if (!objectClass.equals(other.objectClass))
+ return false;
+ return true;
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/tools/SchemaReader.java b/odm/src/main/java/org/springframework/ldap/odm/tools/SchemaReader.java
new file mode 100755
index 00000000..b45e8bef
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/tools/SchemaReader.java
@@ -0,0 +1,157 @@
+package org.springframework.ldap.odm.tools;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+
+import org.springframework.ldap.odm.tools.SyntaxToJavaClass.ClassInfo;
+
+// Processes LDAP Schema
+/* package */ final class SchemaReader {
+ private final DirContext schemaContext;
+
+ private final SyntaxToJavaClass syntaxToJavaClass;
+
+ private final Set binarySet;
+
+ public SchemaReader(DirContext schemaContext, SyntaxToJavaClass syntaxToJavaClass, Set binarySet) {
+ this.schemaContext = schemaContext;
+ this.syntaxToJavaClass = syntaxToJavaClass;
+ this.binarySet = binarySet;
+ }
+
+ // Get the object schema for the given object classes
+ public ObjectSchema getObjectSchema(Set objectClasses)
+ throws NamingException, ClassNotFoundException {
+
+ ObjectSchema result = new ObjectSchema();
+ createObjectClass(objectClasses, schemaContext, result);
+ return result;
+ }
+
+ private enum SchemaAttributeType {
+ SUP, MUST, MAY, UNKNOWN
+ }
+
+ private SchemaAttributeType getSchemaAttributeType(String type) {
+ SchemaAttributeType result = SchemaAttributeType.UNKNOWN;
+
+ if (type.equals("SUP")) {
+ result = SchemaAttributeType.SUP;
+ } else {
+ if (type.equals("MUST")) {
+ result = SchemaAttributeType.MUST;
+ } else {
+ if (type.equals("MAY")) {
+ result = SchemaAttributeType.MAY;
+ }
+ }
+ }
+ return result;
+ }
+
+ private AttributeSchema createAttributeSchema(String name, DirContext schemaContext)
+ throws NamingException, ClassNotFoundException {
+
+ // Get the schema definition
+ Attributes attributeSchema = schemaContext.getAttributes("AttributeDefinition/" + name);
+
+ // Get the syntax - ditching any trailing length value that { and whatever follows it
+ String syntax = ((String)attributeSchema.get("SYNTAX").get()).split("\\{")[0];
+
+ // Is it binary?
+ boolean isBinary=binarySet.contains(syntax);
+
+ // Use it to look up the required Java class
+ ClassInfo classInfo = syntaxToJavaClass.getClassInfo(syntax);
+
+ // Now we can set the java class
+ String javaClassName = null;
+ boolean isPrimitive = false;
+ boolean isArray = false;
+
+ if (classInfo!=null) {
+ javaClassName=classInfo.getClassName();
+ Class> javaClass=Class.forName(classInfo.getFullClassName());
+ javaClassName=javaClass.getSimpleName();
+ isPrimitive=javaClass.isPrimitive();
+ isArray=javaClass.isArray();
+ } else {
+ if (isBinary) {
+ javaClassName="byte[]";
+ isPrimitive=false;
+ isArray=true;
+ } else {
+ javaClassName="String";
+ isPrimitive=false;
+ isArray=false;
+ }
+ }
+
+ return new AttributeSchema(name, syntax,
+ attributeSchema.get("SINGLE-VALUE") == null,
+ isPrimitive, isBinary, isArray, javaClassName);
+ }
+
+ // Recursively extract schema from the directory and process it
+ private void createObjectClass(Set objectClasses, DirContext schemaContext, ObjectSchema schema)
+ throws NamingException, ClassNotFoundException {
+
+ // Super classes
+ Set supList = new HashSet();
+
+ // For each of the given object classes
+ for (String objectClass : objectClasses) {
+ // Add to set of included object classes
+ schema.addObjectClass(objectClass);
+
+ // Grab the LDAP schema of the object class
+ Attributes attributes = schemaContext.getAttributes("ClassDefinition/" + objectClass);
+ NamingEnumeration extends Attribute> valuesEnumeration = attributes.getAll();
+
+ // Loop through each of the attributes
+ while (valuesEnumeration.hasMoreElements()) {
+ Attribute currentAttribute = valuesEnumeration.nextElement();
+
+ // Get the attribute name and lower case it (as this is all case indep)
+ String currentId = currentAttribute.getID().toUpperCase();
+
+ // Is this a MUST, MAY or SUP attribute
+ SchemaAttributeType type = getSchemaAttributeType(currentId);
+
+ // Loop through all the values
+ NamingEnumeration> currentValues = currentAttribute.getAll();
+ while (currentValues.hasMoreElements()) {
+ String currentValue = (String)currentValues.nextElement();
+ switch (type) {
+ case SUP:
+ // Its a super class
+ String lowerCased=currentValue.toLowerCase();
+ if (!schema.getObjectClass().contains(lowerCased)) {
+ supList.add(lowerCased);
+ }
+ break;
+ case MUST:
+ // Add must attribute
+ schema.addMust(createAttributeSchema(currentValue, schemaContext));
+ break;
+ case MAY:
+ // Add may attribute
+ schema.addMay(createAttributeSchema(currentValue, schemaContext));
+ break;
+ default:
+ // Nothing to do
+ }
+ }
+ }
+
+ // Recurse for super classes
+ createObjectClass(supList, schemaContext, schema);
+ }
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/tools/SchemaToJava.java b/odm/src/main/java/org/springframework/ldap/odm/tools/SchemaToJava.java
new file mode 100755
index 00000000..7b3fd241
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/tools/SchemaToJava.java
@@ -0,0 +1,442 @@
+package org.springframework.ldap.odm.tools;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.cli.PosixParser;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+/**
+ * This tool creates a Java class representation of a set of LDAP object classes for use
+ * with {@link org.springframework.ldap.odm.core.OdmManager}.
+ *
+ * The schema of a named list of object classes is read from an LDAP directory and used
+ * to generate a representative Java class. The Java class is automatically annotated with
+ * {@link org.springframework.ldap.odm.annotations} for use with
+ * {@link org.springframework.ldap.odm.core.OdmManager}.
+ *
+ * The mapping of LDAP attributes to their Java representations may be configured by supplying the
+ * -s flag or the equivalent --syntaxmap flag whose argument is
+ * the name of a file with the following structure:
+ *
+ * # List of attribute syntax to java class mappings
+ *
+ * # Syntax Java class
+ * # ------ ----------
+ *
+ * 1.3.6.1.4.1.1466.115.121.1.50, java.lang.Integer
+ * 1.3.6.1.4.1.1466.115.121.1.40, some.other.Class
+ *
+ *
+ * Syntaxes not included in this map will be represented as {@link java.lang.String} if they are returned as Strings by the
+ * JNDI LDAP provider and will be represented as byte[] if they are returned by the provider as byte[].
+ *
+ * Command line flags are as follows:
+ *
+ *
+ * -c,--class <class name> Name of the Java class to create. Mandatory.
+ * -s,--syntaxmap <map file> Configuration file of LDAP syntaxes to Java classes mappings. Optional.
+ * -h,--help Print this help message then exit.
+ * -k,--package <package name> Package to create the Java class in. Mandatory.
+ * -l,--url <ldap url> Ldap url of the directory service to bind to. Defaults to ldap://127.0.0.1:389. Optional.
+ * -o,--objectclasses <LDAP object class lists> Comma separated list of LDAP object classes. Mandatory.
+ * -u,--username <dn> DN to bind with. Defaults to "". Optional.
+ * -p,--password <password> Password to bind with. Defaults to "". Optional.
+ * -t,--outputdir <output directory> Base output directory, defaults to ".". Optional.
+ *
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ */
+public final class SchemaToJava {
+ private static Log LOG = LogFactory.getLog(SchemaToJava.class);
+
+ // Name of the FreeMarker template used to generate the Java code.
+ private static String TEMPLATE_FILE = "oc-to-java.ftl";
+
+ // Name of file containing the list of attributes syntaxes to
+ // returned as byte[] by the JNDI LDAP provider.
+ private static String BINARY_FILE = "binary-attributes.txt";
+
+ // Class to use a base for loading resources
+ private static final Class> loaderClass=SchemaToJava.class;
+
+ // Default LDAP Url to bind with
+ private static final String DEFAULT_URL="ldap://127.0.0.1:389";
+
+ // Command line flags
+ private enum Flag {
+ URL("l", "url"),
+ USERNAME("u", "username"),
+ PASSWORD("p", "password"),
+ OBJECTCLASS("o", "objectclasses"),
+ CLASS("c", "class"),
+ PACKAGE("k", "package"),
+ SYNTAX_MAP("s", "syntaxmap"),
+ OUTPUT_DIR("t", "outputdir"),
+ HELP("h", "help");
+
+ private String shortName;
+
+ private String longName;
+
+ private Flag(String shortName, String longName) {
+ this.shortName = shortName;
+ this.longName = longName;
+ }
+
+ public String getShort() {
+ return shortName;
+ }
+
+ public String getLong() {
+ return longName;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("short=%1$s, long=%2$s", shortName, longName);
+ }
+ }
+
+ private static final Options options = new Options();
+ static {
+ options.addOption(Flag.URL.getShort(), Flag.URL.getLong(), true, "Ldap url (defaults to "+DEFAULT_URL+")");
+ options.addOption(Flag.USERNAME.getShort(), Flag.USERNAME.getLong(), true, "DN to bind with (defaults to \"\"");
+ options.addOption(Flag.PASSWORD.getShort(), Flag.PASSWORD.getLong(), true, "Password to bind with (defaults to \"\"");
+ options.addOption(Flag.OBJECTCLASS.getShort(), Flag.OBJECTCLASS.getLong(), true, "Comma separated list of object classes");
+ options.addOption(Flag.CLASS.getShort(), Flag.CLASS.getLong(), true, "Name of the Java class to create");
+ options.addOption(Flag.PACKAGE.getShort(), Flag.PACKAGE.getLong(), true, "Package to create the Java class in");
+ options.addOption(Flag.SYNTAX_MAP.getShort(), Flag.SYNTAX_MAP.getLong(), true, "Syntax map file (optional)");
+ options.addOption(Flag.OUTPUT_DIR.getShort(), Flag.OUTPUT_DIR.getLong(), true, "Base output directory (defaults to .)");
+ options.addOption(Flag.HELP.getShort(), Flag.HELP.getLong(), false, "Print this help message");
+ }
+
+ // Read list of LDAP syntaxes that are returned as byte[]
+ private static Set readBinarySet(File binarySetFile)
+ throws IOException {
+
+ Set result = new HashSet();
+
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new FileReader(binarySetFile));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String trimmed = line.trim();
+ if (trimmed.length() > 0) {
+ if (trimmed.charAt(0) != '#') {
+ String[] parts = trimmed.split("\\s");
+ if (parts.length > 0) {
+ result.add(parts[0]);
+ }
+ }
+ }
+ }
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+
+ return result;
+ }
+
+ // Read mappings of LDAP syntaxes to Java classes.
+ private static Map readSyntaxMap(File syntaxMapFile)
+ throws IOException {
+
+ Map result = new HashMap();
+
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new FileReader(syntaxMapFile));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String trimmed = line.trim();
+ if (trimmed.length() > 0) {
+ if (trimmed.charAt(0) != '#') {
+ String[] parts = trimmed.split(",");
+ if (parts.length != 2) {
+ throw new IOException(String.format("Failed to parse line \"%1$s\"",
+ trimmed));
+ }
+ String partOne = parts[0].trim();
+ String partTwo = parts[1].trim();
+ if (partOne.length() == 0 || partTwo.length() == 0) {
+ throw new IOException(String.format("Failed to parse line \"%1$s\"",
+ trimmed));
+ }
+ result.put(partOne, partTwo);
+ }
+ }
+ }
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+
+ return result;
+ }
+
+ // Bind to the directory, read and process the schema
+ private static ObjectSchema readSchema(String url, String user, String pass,
+ SyntaxToJavaClass syntaxToJavaClass, Set binarySet, Set objectClasses)
+ throws NamingException, ClassNotFoundException {
+
+ // Set up environment
+ Hashtable env = new Hashtable();
+ env.put(Context.PROVIDER_URL, url);
+ env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+ if (user != null) {
+ env.put(Context.SECURITY_PRINCIPAL, user);
+ }
+ if (pass != null) {
+ env.put(Context.SECURITY_CREDENTIALS, pass);
+ }
+
+ DirContext context = new InitialDirContext(env);
+ DirContext schemaContext = context.getSchema("");
+ SchemaReader reader = new SchemaReader(schemaContext, syntaxToJavaClass, binarySet);
+ ObjectSchema schema = reader.getObjectSchema(objectClasses);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Schema - %1$s", schema.toString()));
+ }
+
+ return schema;
+ }
+
+ // Create the Java
+ private static void createCode(String packageName,
+ String className, ObjectSchema schema, Set imports, File outputFile)
+ throws IOException, TemplateException {
+
+ Configuration freeMarkerConfiguration = new Configuration();
+
+ freeMarkerConfiguration.setClassForTemplateLoading(loaderClass, "");
+ freeMarkerConfiguration.setObjectWrapper(new DefaultObjectWrapper());
+
+ // Build the model for FreeMarker
+ Map model = new HashMap();
+ model.put("package", packageName);
+ model.put("class", className);
+ model.put("schema", schema);
+ model.put("imports", imports);
+
+ // Have FreeMarker process the model with the template
+ Template template = freeMarkerConfiguration.getTemplate(TEMPLATE_FILE);
+
+ if (LOG.isDebugEnabled()) {
+ Writer out = new OutputStreamWriter(System.out);
+ template.process(model, out);
+ out.flush();
+ }
+
+ LOG.debug(String.format("Writing java to: %1$s", outputFile.getAbsolutePath()));
+
+ FileOutputStream outputStream=new FileOutputStream(outputFile);
+ Writer out = new OutputStreamWriter(outputStream);
+ template.process(model, out);
+ out.flush();
+ out.close();
+ }
+
+ // Create the output file for the generated code along with all intervening directories
+ private static File makeOutputFile(String outputDir, String packageName, String className)
+ throws IOException {
+
+ // Convert the package name to a path
+ Pattern pattern=Pattern.compile("\\.");
+ Matcher matcher=pattern.matcher(packageName);
+ String sepToUse=File.separator;
+ if (sepToUse.equals("\\")) {
+ sepToUse="\\\\";
+ }
+
+ // Try to create the necessary directories
+ String directoryPath=outputDir+File.separator+matcher.replaceAll(sepToUse);
+ File directory=new File(directoryPath);
+ File outputFile=new File(directory, className+".java");
+
+ LOG.debug(String.format("Attempting to create output file at %1$s", outputFile.getAbsolutePath()));
+
+ try {
+ directory.mkdirs();
+ outputFile.createNewFile();
+ } catch (SecurityException se) {
+ throw new IOException(String.format("Can't write to output file %1$s", outputFile.getAbsoluteFile()));
+ } catch (IOException ioe) {
+ throw new IOException(String.format("Can't write to output file %1$s", outputFile.getAbsoluteFile()));
+ }
+
+ return outputFile;
+ }
+
+ private static Set parseObjectClassesFlag(String objectClassesFlag) {
+ Set objectClasses = new HashSet();
+
+ for (String objectClassFlag : objectClassesFlag.split(",")) {
+ if (objectClassFlag.length() > 0) {
+ objectClasses.add(objectClassFlag.toLowerCase().trim());
+ }
+ }
+
+ return objectClasses;
+ }
+
+ private static void error(String message) {
+ System.err.println(String.format("%1$s: %2$s", SchemaToJava.class.getSimpleName(), message));
+ System.exit(1);
+ }
+
+ public static void main(String[] argv) {
+ CommandLineParser parser = new PosixParser();
+ CommandLine cmd = null;
+
+ // Parse out the command line options
+ try {
+ cmd = parser.parse(options, argv);
+ } catch (ParseException e) {
+ error(e.toString());
+ }
+
+ // If the help flag is specified ignore other flags, print a usage message and exit
+ if (cmd.hasOption(Flag.HELP.getShort())) {
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.printHelp(120, SchemaToJava.class.getSimpleName(), null, options, null, true);
+ System.exit(0);
+ }
+
+ // Class name flag
+ String className = cmd.getOptionValue(Flag.CLASS.getShort());
+ if (className == null) {
+ error("You must specify the name of a Java class to create");
+ }
+
+ // Package name flag
+ String packageName = cmd.getOptionValue(Flag.PACKAGE.getShort());
+ if (packageName == null) {
+ error("You must specifiy a package name");
+ }
+
+ // Output base directory
+ String outputDir = cmd.getOptionValue(Flag.OUTPUT_DIR.getShort(), ".");
+ File outputFile = null;
+ try {
+ outputFile = makeOutputFile(outputDir, packageName, className);
+ } catch (IOException e) {
+ error(e.toString());
+ }
+
+ // Get the flags we need to bind to the directory
+ String url = cmd.getOptionValue(Flag.URL.getShort(), DEFAULT_URL);
+ String user = cmd.getOptionValue(Flag.USERNAME.getShort());
+ String pass = cmd.getOptionValue(Flag.PASSWORD.getShort());
+
+ // Parse out object classes
+ String objectClassesFlag = cmd.getOptionValue(Flag.OBJECTCLASS.getShort());
+ if (objectClassesFlag==null) {
+ error("You must specificy a package name");
+ }
+ Set objectClasses = parseObjectClassesFlag(objectClassesFlag);
+ if (objectClasses.size()==0) {
+ error("You must specificy a package name");
+ }
+
+ // Look for the optional syntax to Java class mapping file
+ String syntaxMapFileName = cmd.getOptionValue(Flag.SYNTAX_MAP.getShort(), null);
+ SyntaxToJavaClass syntaxToJavaClass=new SyntaxToJavaClass(new HashMap());
+ if (syntaxMapFileName!=null) {
+ File syntaxMapFile=new File(syntaxMapFileName);
+ if (syntaxMapFile.canRead()) {
+ try {
+ syntaxToJavaClass = new SyntaxToJavaClass(readSyntaxMap(syntaxMapFile));
+ } catch (IOException e) {
+ error(String.format("Error reading syntax map file %1$s - %2$s",
+ syntaxMapFile.getAbsolutePath(), e.toString()));
+ }
+ } else {
+ error(String.format("Cannot read syntax map file %s$1",
+ syntaxMapFile.getAbsolutePath()));
+ }
+ }
+
+ // Read binary mapping file
+ URL binarySetUrl=loaderClass.getResource(BINARY_FILE);
+ if (binarySetUrl==null) {
+ error(String.format("Can't locatate binary mappings file %1$s", BINARY_FILE));
+ }
+ File binarySetFile=new File(binarySetUrl.getFile());
+ if (!binarySetFile.canRead()) {
+ error(String.format("Can't read from binary mappings file %1$s", BINARY_FILE));
+ }
+ Set binarySet = null;
+ try {
+ binarySet = readBinarySet(binarySetFile);
+ } catch (IOException e) {
+ error(String.format("Error reading binary set file %1$s - %2$s", binarySetFile.getAbsolutePath(), e));
+ }
+
+ // Read schema from the directory
+ ObjectSchema schema=null;
+ try {
+ schema=readSchema(url, user, pass, syntaxToJavaClass, binarySet, objectClasses);
+ } catch (NamingException ne) {
+ error(String.format("Error processing schema - %1$s", ne));
+ } catch (ClassNotFoundException cnfe) {
+ error(String.format("Error processing schema - %1$s", cnfe));
+ }
+
+ // Work out what imports we need
+ Set imports = new HashSet();
+ for (AttributeSchema attributeSchema : schema.getMay()) {
+ SyntaxToJavaClass.ClassInfo classInfo = syntaxToJavaClass.getClassInfo(attributeSchema.getSyntax());
+ if (classInfo != null) {
+ String classPackageName = classInfo.getPackageName();
+ if (classPackageName != null && classPackageName.length() > 0) {
+ imports.add(classInfo);
+ }
+ }
+ }
+
+ // Create the Java code
+ try {
+ createCode(packageName, className, schema, imports, outputFile);
+ } catch (TemplateException te) {
+ error(String.format("Error generating code - %1$s", te.toString()));
+ } catch (IOException ioe) {
+ error(String.format("Error generatign code - %1$s", ioe.toString()));
+ }
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/tools/SchemaViewer.java b/odm/src/main/java/org/springframework/ldap/odm/tools/SchemaViewer.java
new file mode 100755
index 00000000..30c03ec6
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/tools/SchemaViewer.java
@@ -0,0 +1,233 @@
+package org.springframework.ldap.odm.tools;
+
+import java.io.PrintStream;
+import java.util.Hashtable;
+
+import javax.naming.AuthenticationException;
+import javax.naming.CommunicationException;
+import javax.naming.Context;
+import javax.naming.NameClassPair;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.cli.PosixParser;
+
+/**
+ * A simple utility to list LDAP directory schema.
+ *
+ * SchemaViewer takes the following flags:
+ *
+ * -h,--help< Print this help message
+ * -l,--url <arg> Ldap url of directory to bind to (defaults to ldap://127.0.0.1:389)
+ * -u,--username <arg> DN to bind with (defaults to "")
+ * -p,--password <arg> Password to bind with (defaults to "")
+ * -o,--objectclass <arg> Object class name or ? for all. Print object class schema
+ * -a,--attribute <arg> Attribute name or ? for all. Print attribute schema
+ * -s,--syntax <arg> Syntax or ? for all. Print syntax
+ *
+ *
+ * Only one of -a, -o and -s should be specified.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ *
+ */
+public final class SchemaViewer {
+ private static final String DEFAULT_URL="ldap://127.0.0.1:389";
+
+ private enum Flag {
+ URL("l", "url"),
+ USERNAME("u", "username"),
+ PASSWORD("p", "password"),
+ OBJECTCLASS("o", "objectclass"),
+ ATTRIBUTE("a", "attribute"),
+ SYNTAX("s", "syntax"),
+ HELP("h", "help"),
+ ERROR("e", "error");
+
+ private String shortName;
+
+ private String longName;
+
+ private Flag(String shortName, String longName) {
+ this.shortName = shortName;
+ this.longName = longName;
+ }
+
+ public String getShort() {
+ return shortName;
+ }
+
+ public String getLong() {
+ return longName;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("short=%1$s, long=%2$s", shortName, longName);
+ }
+ }
+
+ private enum SchemaContext {
+ OBJECTCLASS("ClassDefinition"), ATTRIBUTE("AttributeDefinition"), SYNTAX("SyntaxDefinition");
+
+ private String value;
+
+ private SchemaContext(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("value=%1$s", value);
+ }
+ }
+
+ private static final Options options = new Options();
+ static {
+ options.addOption(Flag.URL.getShort(), Flag.URL.getLong(), true, "Ldap url (defaults to "+DEFAULT_URL+")");
+ options.addOption(Flag.USERNAME.getShort(), Flag.USERNAME.getLong(), true, "DN to bind with (defaults to \"\")");
+ options.addOption(Flag.PASSWORD.getShort(), Flag.PASSWORD.getLong(), true, "Password to bind with defaults to \"\")");
+ options.addOption(Flag.OBJECTCLASS.getShort(), Flag.OBJECTCLASS.getLong(), true,
+ "Object class name or ? for all. Print object class schema");
+ options.addOption(Flag.ATTRIBUTE.getShort(), Flag.ATTRIBUTE.getLong(), true,
+ "Attribute name or ? for all. Print attribute schema");
+ options.addOption(Flag.SYNTAX.getShort(), Flag.SYNTAX.getLong(), true,
+ "Syntax OID or ? for all. Print attribute syntax");
+ options.addOption(Flag.HELP.getShort(), Flag.HELP.getLong(), false, "Print this help message");
+ options.addOption(Flag.ERROR.getShort(), Flag.ERROR.getLong(), false, "Send output to standard error");
+ }
+
+ private static void printAttrs(Attributes attrs) throws NamingException {
+ NamingEnumeration extends Attribute> attrsEnum = attrs.getAll();
+ while (attrsEnum.hasMore()) {
+ Attribute currentAttr = attrsEnum.next();
+ outstream.print(String.format("%1$s:", currentAttr.getID()));
+ NamingEnumeration> valuesEnum = currentAttr.getAll();
+ while (valuesEnum.hasMoreElements()) {
+ outstream.print(String.format("%1$s ", valuesEnum.nextElement().toString()));
+ }
+ outstream.println();
+ }
+ }
+
+ private static void printObject(String contextName, String schemaName, DirContext schemaContext)
+ throws NameNotFoundException, NamingException {
+
+ DirContext oContext = (DirContext)schemaContext.lookup(contextName + "/" + schemaName);
+
+ outstream.println("NAME:" + schemaName);
+ printAttrs(oContext.getAttributes(""));
+ }
+
+ private static void printSchema(String contextName, DirContext schemaContext) throws NameNotFoundException,
+ NamingException {
+
+ outstream.println();
+
+ NamingEnumeration schemaList = schemaContext.list(contextName);
+
+ while (schemaList.hasMore()) {
+ NameClassPair ncp = schemaList.nextElement();
+
+ printObject(contextName, ncp.getName(), schemaContext);
+ outstream.println();
+ }
+
+ outstream.println();
+ }
+
+ private static void print(String optionValue, String contextName, DirContext schemaContext)
+ throws NameNotFoundException, NamingException {
+
+ if (optionValue.equals(WILDCARD)) {
+ printSchema(contextName, schemaContext);
+ } else {
+ printObject(contextName, optionValue, schemaContext);
+ }
+ }
+
+ private static PrintStream outstream=System.out;
+ private static String WILDCARD = "?";
+
+ public static void main(String[] argv) {
+ CommandLineParser parser = new PosixParser();
+ CommandLine cmd = null;
+
+ try {
+ cmd = parser.parse(options, argv);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ System.exit(1);
+ }
+
+ if (cmd.hasOption(Flag.HELP.getShort())) {
+ HelpFormatter formatter = new HelpFormatter();
+
+ formatter.printHelp(120, SchemaViewer.class.getSimpleName(), null, options, null, true);
+ System.exit(0);
+ }
+
+ if (cmd.hasOption(Flag.ERROR.getShort())) {
+ outstream=System.err;
+ }
+
+ String url = cmd.getOptionValue(Flag.URL.getShort(), DEFAULT_URL);
+ String user = cmd.getOptionValue(Flag.USERNAME.getShort(), "");
+ String pass = cmd.getOptionValue(Flag.PASSWORD.getShort(), "");
+
+ Hashtable env = new Hashtable();
+ env.put(Context.PROVIDER_URL, url);
+ env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+ if (user != null) {
+ env.put(Context.SECURITY_PRINCIPAL, user);
+ }
+ if (pass != null) {
+ env.put(Context.SECURITY_CREDENTIALS, pass);
+ if (user == null) {
+ System.err.println("You must specify a user if you specify a password");
+ System.exit(1);
+ }
+ }
+
+ try {
+ DirContext context = new InitialDirContext(env);
+ DirContext schemaContext = context.getSchema("");
+
+ if (cmd.hasOption(Flag.OBJECTCLASS.getShort())) {
+ print(cmd.getOptionValue(Flag.OBJECTCLASS.getShort()), SchemaContext.OBJECTCLASS.getValue(),
+ schemaContext);
+ }
+
+ if (cmd.hasOption(Flag.ATTRIBUTE.getShort())) {
+ print(cmd.getOptionValue(Flag.ATTRIBUTE.getShort()), SchemaContext.ATTRIBUTE.getValue(), schemaContext);
+ }
+
+ if (cmd.hasOption(Flag.SYNTAX.getShort())) {
+ print(cmd.getOptionValue(Flag.SYNTAX.getShort()), SchemaContext.SYNTAX.getValue(), schemaContext);
+ }
+
+ } catch (AuthenticationException e) {
+ System.err.println(String.format("Failed to bind to ldap server at %1$s", url));
+ } catch (CommunicationException e) {
+ System.err.println(String.format("Failed to contact ldap server at %1$s", url));
+ } catch (NameNotFoundException e) {
+ System.err.println(String.format("Can't find object %1$s", e.getMessage()));
+ } catch (NamingException e) {
+ System.err.println(e.toString());
+ }
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/tools/SyntaxToJavaClass.java b/odm/src/main/java/org/springframework/ldap/odm/tools/SyntaxToJavaClass.java
new file mode 100755
index 00000000..5631cfc4
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/tools/SyntaxToJavaClass.java
@@ -0,0 +1,63 @@
+package org.springframework.ldap.odm.tools;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * A map from an LDAP syntax to the Java class used to represent it.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+/* package */ final class SyntaxToJavaClass {
+ public final static class ClassInfo {
+ private final String className;
+
+ private final String packageName;
+
+ private ClassInfo(String className, String packageName) {
+ this.className = className;
+ this.packageName = packageName;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public String getFullClassName() {
+ StringBuilder result=new StringBuilder();
+ if (packageName!=null) {
+ result.append(packageName).append(".").append(className);
+ } else {
+ result.append(className);
+ }
+ return result.toString();
+ }
+ }
+
+ private final Map mapSyntaxToClassInfo = new HashMap();
+
+ public SyntaxToJavaClass(Map mapSyntaxToClass) {
+ for (Entry syntaxAndClass : mapSyntaxToClass.entrySet()) {
+ String fullClassName = syntaxAndClass.getValue().trim();
+ String packageName = null;
+ String className = null;
+ int lastDotIndex = fullClassName.lastIndexOf('.');
+ if (lastDotIndex != -1) {
+ className = fullClassName.substring(lastDotIndex + 1);
+ packageName = fullClassName.substring(0, lastDotIndex);
+ } else {
+ className = fullClassName;
+ }
+ mapSyntaxToClassInfo.put(syntaxAndClass.getKey(), new ClassInfo(className, packageName));
+ }
+ }
+
+ public ClassInfo getClassInfo(String syntax) {
+ return mapSyntaxToClassInfo.get(syntax);
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/tools/package-info.java b/odm/src/main/java/org/springframework/ldap/odm/tools/package-info.java
new file mode 100755
index 00000000..66aad68e
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/tools/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * Provides a tool to create a Java class representation of a set of LDAP object classes
+ * and a simple tool to view LDAP schema.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+
+package org.springframework.ldap.odm.tools;
\ No newline at end of file
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/ConverterException.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/ConverterException.java
new file mode 100755
index 00000000..9c0dfb70
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/ConverterException.java
@@ -0,0 +1,19 @@
+package org.springframework.ldap.odm.typeconversion;
+
+import org.springframework.ldap.NamingException;
+
+/**
+ * Thrown by the conversion framework to indicate an error condition - typically a failed type conversion.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+@SuppressWarnings("serial")
+public final class ConverterException extends NamingException {
+ public ConverterException(final String message) {
+ super(message);
+ }
+
+ public ConverterException(final String message, final Throwable e) {
+ super(message, e);
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/ConverterManager.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/ConverterManager.java
new file mode 100755
index 00000000..01a52d5f
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/ConverterManager.java
@@ -0,0 +1,31 @@
+package org.springframework.ldap.odm.typeconversion;
+
+/**
+ * A simple interface to be implemented to provide type conversion functionality.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public interface ConverterManager {
+ /**
+ * Determine whether this converter manager is able to carry out a specified conversion.
+ *
+ * @param fromClass Convert from the fromClass.
+ * @param syntax Using the LDAP syntax (may be null).
+ * @param toClass To the toClass.
+ * @return True if the conversion is supported, false otherwise.
+ */
+ boolean canConvert(Class> fromClass, String syntax, Class> toClass);
+
+ /**
+ * Convert a given source object with an optional LDAP syntax to an instance of a given class.
+ *
+ * @param The class to convert to.
+ * @param source The object to convert.
+ * @param syntax The LDAP syntax to use (may be null).
+ * @param toClass The class to convert to.
+ * @return The converted object.
+ *
+ * @throws ConverterException If the conversion can not be successfully completed.
+ */
+ T convert(Object source, String syntax, Class toClass);
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/Converter.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/Converter.java
new file mode 100755
index 00000000..6941d41a
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/Converter.java
@@ -0,0 +1,19 @@
+package org.springframework.ldap.odm.typeconversion.impl;
+
+/**
+ * Interface specifying the conversion between two classes
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public interface Converter {
+ /**
+ * Attempt to convert a given object to a named class.
+ *
+ * @param The class to convert to.
+ * @param source The object to convert.
+ * @param toClass The class to convert to.
+ * @return The converted class or null if the conversion was not possible.
+ * @throws Exception Any exception may be throw by a Converter on error.
+ */
+ T convert(Object source, Class toClass) throws Exception;
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/ConverterManagerFactoryBean.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/ConverterManagerFactoryBean.java
new file mode 100755
index 00000000..6763375d
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/ConverterManagerFactoryBean.java
@@ -0,0 +1,193 @@
+package org.springframework.ldap.odm.typeconversion.impl;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.FactoryBeanNotInitializedException;
+
+/**
+ * A utility class to allow {@link ConverterManagerImpl} instances to be easily configured via spring.xml
+ *
+ * The following shows a typical simple example which creates two {@link Converter} instances:
+ *
+ * fromStringConverter
+ * toStringConverter
+ *
+ * Configured in an {@link ConverterManagerImpl} to:
+ *
+ * Use fromStringConverter to convert from String to Byte, Short,
+ * Integer, Long, Float, Double, Boolean
+ * Use toStringConverter to convert from Byte, Short,
+ * Integer, Long, Float, Double, Boolean to String
+ *
+ *
+ * <bean id="converterManager" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean">
+ * <property name="converterConfig">
+ * <set>
+ * <bean class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
+ * <property name="fromClasses">
+ * <set>
+ * <value>java.lang.String</value>
+ * </set>
+ * </property>
+ * <property name="toClasses">
+ * <set>
+ * <value>java.lang.Byte</value>
+ * <value>java.lang.Short</value>
+ * <value>java.lang.Integer</value>
+ * <value>java.lang.Long</value>
+ * <value>java.lang.Float</value>
+ * <value>java.lang.Double</value>
+ * <value>java.lang.Boolean</value>
+ * </set>
+ * </property>
+ * <property name="converter" ref="fromStringConverter" />
+ * </bean>
+ * <bean class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
+ * <property name="fromClasses">
+ * <set>
+ * <value>java.lang.Byte</value>
+ * <value>java.lang.Short</value>
+ * <value>java.lang.Integer</value>
+ * <value>java.lang.Long</value>
+ * <value>java.lang.Float</value>
+ * <value>java.lang.Double</value>
+ * <value>java.lang.Boolean</value>
+ * </set>
+ * </property>
+ * <property name="toClasses">
+ * <set>
+ * <value>java.lang.String</value>
+ * </set>
+ * </property>
+ * <property name="converter" ref="toStringConverter" />
+ * </bean>
+ * </set>
+ * </property>
+ * </bean>
+ *
+ * {@link ConverterConfig} has a second constructor which takes an additional parameter to allow
+ * an LDAP syntax to be defined.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public final class ConverterManagerFactoryBean implements FactoryBean {
+ private static Log LOG = LogFactory.getLog(ConverterManagerFactoryBean.class);
+
+ /**
+ * Configuration information for a single Converter instance.
+ */
+ public final static class ConverterConfig {
+ // The set of classes the Converter will convert from.
+ private Set> fromClasses = new HashSet>();
+
+ // The (optional) LDAP syntax.
+ private String syntax=null;
+
+ // The set of classes the Converter will convert to.
+ private Set> toClasses = new HashSet>();
+
+ // The Converter to use.
+ private Converter converter=null;
+
+ public ConverterConfig() {
+ }
+
+ /**
+ * @param fromClasses Comma separated list of classes the {@link Converter} should can convert from.
+ */
+ public void setFromClasses(Set> fromClasses) {
+ this.fromClasses=fromClasses;
+ }
+
+ /**
+ * @param toClasses Comma separated list of classes the {@link Converter} can convert to.
+ */
+ public void setToClasses(Set> toClasses) {
+ this.toClasses=toClasses;
+
+ }
+
+ /**
+ * @param syntax An LDAP syntax supported by the {@link Converter}.
+ */
+ public void setSyntax(String syntax) {
+ this.syntax=syntax;
+ }
+
+ /**
+ * @param converter The {@link Converter} to use.
+ */
+ public void setConverter(Converter converter) {
+ this.converter=converter;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("fromClasses=%1$s, syntax=%2$s, toClasses=%3$s, converter=%4$s",
+ fromClasses, syntax, toClasses, converter);
+ }
+ }
+
+ private Set converterConfigList=null;
+
+
+ /**
+ * @param converterConfigList
+ */
+ public void setConverterConfig(Set converterConfigList) {
+ this.converterConfigList=converterConfigList;
+ }
+
+ /**
+ * Creates a ConverterManagerImpl populating it with Converter instances from the converterConfigList property.
+ *
+ * @return The newly created {@link org.springframework.ldap.odm.typeconversion.ConverterManager}.
+ * @throws ClassNotFoundException Thrown if any of the classes to be converted to or from cannot be found.
+ *
+ * @see org.springframework.beans.factory.FactoryBean#getObject()
+ */
+ public Object getObject() throws Exception {
+ if (converterConfigList==null) {
+ throw new FactoryBeanNotInitializedException("converterConfigList has not been set");
+ }
+
+ ConverterManagerImpl result = new ConverterManagerImpl();
+ for (ConverterConfig converterConfig : converterConfigList) {
+ if (converterConfig.fromClasses==null ||
+ converterConfig.toClasses==null ||
+ converterConfig.converter==null) {
+
+ throw new FactoryBeanNotInitializedException(
+ String.format("All of fromClasses, toClasses and converter must be specified in bean %1$s",
+ converterConfig.toString()));
+ }
+ for (Class> fromClass : converterConfig.fromClasses) {
+ for (Class> toClass : converterConfig.toClasses) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Adding converter from %1$s to %2$s", fromClass, toClass));
+ }
+ result.addConverter(fromClass, converterConfig.syntax, toClass, converterConfig.converter);
+ }
+ }
+ }
+ return result;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.beans.factory.FactoryBean#getObjectType()
+ */
+ public Class> getObjectType() {
+ return ConverterManagerImpl.class;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.beans.factory.FactoryBean#isSingleton()
+ */
+ public boolean isSingleton() {
+ return true;
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/ConverterManagerImpl.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/ConverterManagerImpl.java
new file mode 100755
index 00000000..2ce4adb5
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/ConverterManagerImpl.java
@@ -0,0 +1,161 @@
+package org.springframework.ldap.odm.typeconversion.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.ldap.odm.typeconversion.ConverterException;
+import org.springframework.ldap.odm.typeconversion.ConverterManager;
+
+/**
+ * An implementation of {@link org.springframework.ldap.odm.typeconversion.ConverterManager}.
+ *
+ * The algorithm used is to:
+ *
+ * Try to find and use a {@link Converter} registered for the
+ * fromClass, syntax and toClass and use it.
+ * If this fails, then if the toClass isAssignableFrom
+ * the fromClass then just assign it.
+ * If this fails try to find and use a {@link Converter} registered for the fromClass and
+ * the toClass ignoring the syntax.
+ * If this fails then throw a {@link org.springframework.ldap.odm.typeconversion.ConverterException}.
+ *
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public final class ConverterManagerImpl implements ConverterManager {
+ /**
+ * Separator used to form keys into the converters Map.
+ */
+ private static final String KEY_SEP = ":";
+
+ /**
+ * Map of keys created via makeConverterKey to Converter instances.
+ */
+ private final Map converters = new HashMap();
+
+ /**
+ * Make a key into the converters map - the keys is formed from the fromClass, syntax and toClass
+ *
+ * @param fromClass The class to convert from.
+ * @param syntax The LDAP syntax.
+ * @param toClass The class to convert to.
+ * @return key
+ */
+ private String makeConverterKey(Class> fromClass, String syntax, Class> toClass) {
+ StringBuilder key = new StringBuilder();
+ if (syntax==null) {
+ syntax="";
+ }
+ key.append(fromClass.getName()).append(KEY_SEP).append(syntax).append(KEY_SEP).append(toClass.getName());
+ return key.toString();
+ }
+
+ /**
+ * Create an empty ConverterManagerImpl
+ */
+ public ConverterManagerImpl() {
+ }
+
+ /**
+ * Used to help in the process of dealing with primitive types by mapping them to
+ * their equivalent boxed class.
+ */
+ private static Map, Class>> primitiveTypeMap = new HashMap, Class>>();
+ static {
+ primitiveTypeMap.put(Byte.TYPE, Byte.class);
+ primitiveTypeMap.put(Short.TYPE, Short.class);
+ primitiveTypeMap.put(Integer.TYPE, Integer.class);
+ primitiveTypeMap.put(Long.TYPE, Long.class);
+ primitiveTypeMap.put(Float.TYPE, Float.class);
+ primitiveTypeMap.put(Double.TYPE, Double.class);
+ primitiveTypeMap.put(Boolean.TYPE, Boolean.class);
+ primitiveTypeMap.put(Character.TYPE, Character.class);
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.ldap.odm.typeconversion.ConverterManager#canConvert(java.lang.Class, java.lang.String, java.lang.Class)
+ */
+ public boolean canConvert(Class> fromClass, String syntax, Class> toClass) {
+ Class> fixedToClass = toClass;
+ if (toClass.isPrimitive()) {
+ fixedToClass = primitiveTypeMap.get(toClass);
+ }
+ Class> fixedFromClass = fromClass;
+ if (fromClass.isPrimitive()) {
+ fixedFromClass = primitiveTypeMap.get(fromClass);
+ }
+ return fixedToClass.isAssignableFrom(fixedFromClass) ||
+ (converters.get(makeConverterKey(fixedFromClass, syntax, fixedToClass)) != null) ||
+ (converters.get(makeConverterKey(fixedFromClass, null, fixedToClass)) != null);
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.ldap.odm.typeconversion.ConverterManager#convert(java.lang.Object, java.lang.String, java.lang.Class)
+ */
+ @SuppressWarnings("unchecked")
+ public T convert(Object source, String syntax, Class toClass) {
+ Object result = null;
+
+ // What are we converting form
+ Class> fromClass = source.getClass();
+
+ // Deal with primitives
+ Class> targetClass = toClass;
+ if (toClass.isPrimitive()) {
+ targetClass = primitiveTypeMap.get(toClass);
+ }
+
+ // Try to convert with any syntax we have been given
+ Converter syntaxConverter = converters.get(makeConverterKey(fromClass, syntax, targetClass));
+ if (syntaxConverter != null) {
+ try {
+ result = syntaxConverter.convert(source, targetClass);
+ } catch (Exception e) {
+ // Ignore as we may still be able to convert successfully
+ }
+ }
+
+ // Do we actually need to do any conversion?
+ if (result == null && targetClass.isAssignableFrom(fromClass)) {
+ result = source;
+ }
+
+ // If we were given a syntax and we failed to convert drop back to any mapping
+ // that will work from class -> to class
+ if (result == null && syntax != null) {
+ Converter nullSyntaxConverter = converters.get(makeConverterKey(fromClass, null, targetClass));
+ if (nullSyntaxConverter != null) {
+ try {
+ result = nullSyntaxConverter.convert(source, targetClass);
+ } catch (Exception e) {
+ // Handled at the end of the method
+ }
+ }
+ }
+
+ if (result == null) {
+ throw new ConverterException(String.format(
+ "Cannot convert %1$s of class %2$s via syntax %3$s to class %4$s", source, source.getClass(),
+ syntax, toClass));
+ }
+
+ // We cannot do the safe thing of doing a .cast as we need to rely on auto-unboxing to deal with primitives!
+ return (T)result;
+ }
+
+ /**
+ * Add a {@link Converter} to this ConverterManager.
+ *
+ * @param fromClass The class the Converter should be used to convert from.
+ * @param syntax The LDAP syntax that the Converter should be used for.
+ * @param toClass The class the Converter should be used to convert to.
+ * @param converter The Converter to add.
+ */
+ public void addConverter(Class> fromClass, String syntax, Class> toClass, Converter converter) {
+ converters.put(makeConverterKey(fromClass, syntax, toClass), converter);
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/FromStringConverter.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/FromStringConverter.java
new file mode 100755
index 00000000..d5497977
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/FromStringConverter.java
@@ -0,0 +1,24 @@
+package org.springframework.ldap.odm.typeconversion.impl.converters;
+
+import java.lang.reflect.Constructor;
+
+import org.springframework.ldap.odm.typeconversion.impl.Converter;
+
+/**
+ * A Converter from a {@link java.lang.String} to any class which has a single argument
+ * public constructor taking a {@link java.lang.String}.
+ *
+ * This should only be used as a fall-back converter, as a last attempt.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public final class FromStringConverter implements Converter {
+
+ /* (non-Javadoc)
+ * @see org.springframework.ldap.odm.typeconversion.impl.Converter#convert(java.lang.Object, java.lang.Class)
+ */
+ public T convert(Object source, Class toClass) throws Exception {
+ Constructor constructor = toClass.getConstructor(java.lang.String.class);
+ return constructor.newInstance(source);
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/ToStringConverter.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/ToStringConverter.java
new file mode 100755
index 00000000..1382ad15
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/ToStringConverter.java
@@ -0,0 +1,21 @@
+package org.springframework.ldap.odm.typeconversion.impl.converters;
+
+import org.springframework.ldap.odm.typeconversion.impl.Converter;
+
+
+/**
+ * A Converter from any class to a {@link java.lang.String} via the toString method.
+ *
+ * This should only be used as a fall-back converter, as a last attempt.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public final class ToStringConverter implements Converter {
+
+ /* (non-Javadoc)
+ * @see org.springframework.ldap.odm.typeconversion.impl.Converter#convert(java.lang.Object, java.lang.Class)
+ */
+ public T convert(Object source, Class toClass) {
+ return toClass.cast(source.toString());
+ }
+}
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/package-info.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/package-info.java
new file mode 100755
index 00000000..937bc5cc
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Provides some basic implementations of the {@link org.springframework.ldap.odm.typeconversion.impl.Converter} interface.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+
+package org.springframework.ldap.odm.typeconversion.impl.converters;
\ No newline at end of file
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/package-info.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/package-info.java
new file mode 100755
index 00000000..7ab5faa0
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * Provides an implementation of the {@link org.springframework.ldap.odm.typeconversion.ConverterManager} interface.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+
+package org.springframework.ldap.odm.typeconversion.impl;
+
diff --git a/odm/src/main/java/org/springframework/ldap/odm/typeconversion/package-info.java b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/package-info.java
new file mode 100755
index 00000000..b310ba0e
--- /dev/null
+++ b/odm/src/main/java/org/springframework/ldap/odm/typeconversion/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * Provides an interface to be implemented to create a type conversion framework.
+ *
+ * This is used to convert between the LDAP and Java representations of attributes.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+
+package org.springframework.ldap.odm.typeconversion;
+
diff --git a/odm/src/main/resources/log4j.properties b/odm/src/main/resources/log4j.properties
new file mode 100755
index 00000000..aa4032c5
--- /dev/null
+++ b/odm/src/main/resources/log4j.properties
@@ -0,0 +1,19 @@
+log4j.rootLogger=error, stdout
+log4j.logger.org.springframework.ldap.odm=error
+log4j.logger.org.springframework=error
+
+# Messages to the console
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+
+log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
+
+# Message to a log file
+log4j.appender.log=org.apache.log4j.RollingFileAppender
+log4j.appender.log.File=jndi.log
+
+log4j.appender.log.MaxFileSize=100KB
+log4j.appender.log.MaxBackupIndex=1
+
+log4j.appender.log.layout=org.apache.log4j.PatternLayout
+log4j.appender.log.layout.ConversionPattern=%p %t %c - %m%n
diff --git a/odm/src/main/resources/org/springframework/ldap/odm/tools/binary-attributes.txt b/odm/src/main/resources/org/springframework/ldap/odm/tools/binary-attributes.txt
new file mode 100755
index 00000000..1e5e3ba4
--- /dev/null
+++ b/odm/src/main/resources/org/springframework/ldap/odm/tools/binary-attributes.txt
@@ -0,0 +1,16 @@
+# List of syntaxes that will be returned by the JNDI provider as byte[], taken from
+# http://java.sun.com/products/jndi/tutorial/ldap/misc/attrs.html
+# See all http://www.ietf.org/rfc/rfc2256.txt for attribute names with ;binary suffix
+
+# Syntax Used by Attributes
+# ------ ------------------
+
+1.3.6.1.4.1.1466.115.121.1.23 photo, personalSignature
+1.3.6.1.4.1.1466.115.121.1.4 audio
+1.3.6.1.4.1.1466.115.121.1.28 jpegPhoto
+1.3.6.1.4.1.1466.115.121.1.40 javaSerializedData
+1.3.6.1.4.1.1466.115.121.1.40 userPassword
+1.3.6.1.4.1.1466.115.121.1.8 userCertificate, cACertificate
+1.3.6.1.4.1.1466.115.121.1.9 authorityRevocationList, certificateRevocationList
+1.3.6.1.4.1.1466.115.121.1.10 crossCertificatePair
+1.3.6.1.4.1.1466.115.121.1.6 x500UniqueIdentifier
diff --git a/odm/src/main/resources/org/springframework/ldap/odm/tools/oc-to-java.ftl b/odm/src/main/resources/org/springframework/ldap/odm/tools/oc-to-java.ftl
new file mode 100755
index 00000000..bd645886
--- /dev/null
+++ b/odm/src/main/resources/org/springframework/ldap/odm/tools/oc-to-java.ftl
@@ -0,0 +1,163 @@
+<#ftl strip_whitespace="true">
+
+<#macro commaSeparatedList strings>
+ <#local first=true/>
+ <#list strings as listValue>
+ <#if !first>
+ , <#t/>
+ <#else>
+ <#local first=false/>
+ #if>
+ "${listValue}"<#t/>
+ #list>
+#macro>
+
+<#macro doAttribute attributes>
+ <#list attributes as attribute>
+ <#local binary=attribute.isBinary?string(", type=Type.BINARY", "")>
+
+ <#lt/> @Attribute(name="${attribute.name}", syntax="${attribute.syntax}"${binary})
+ <#if attribute.isMultiValued>
+ <#lt/> private List<${attribute.scalarType}> ${attribute.name}=new ArrayList<${attribute.scalarType}>();
+ <#else>
+ <#lt/> private ${attribute.scalarType} ${attribute.name};
+ #if>
+ #list>
+#macro>
+
+<#macro getSet attributes>
+ <#list attributes as attribute>
+ <#if attribute.name!="objectClass">
+ <#if attribute.isMultiValued>
+ <#lt/> public void add${attribute.name?cap_first}(${attribute.scalarType} ${attribute.name}) {
+ <#lt/> this.${attribute.name}.add(${attribute.name});
+ <#lt/> }
+
+ <#lt/> public void remove${attribute.name?cap_first}(${attribute.scalarType} ${attribute.name}) {
+ <#lt/> this.${attribute.name}.remove(${attribute.name});
+ <#lt/> }
+
+ <#lt/> public Iterator<${attribute.scalarType}> get${attribute.name?cap_first}Iterator() {
+ <#lt/> return ${attribute.name}.iterator();
+ <#lt/> }
+
+ <#else>
+ <#lt/> public ${attribute.scalarType} get${attribute.name?cap_first}() {
+ <#lt/> return ${attribute.name};
+ <#lt/> }
+
+ <#lt/> public void set${attribute.name?cap_first}(${attribute.scalarType} ${attribute.name}) {
+ <#lt/> this.${attribute.name}=${attribute.name};
+ <#lt/> }
+ #if>
+ <#else>
+ <#lt/> public Iterator get${attribute.name?cap_first}Iterator() {
+ <#lt/> return Collections.unmodifiableList(${attribute.name}).iterator();
+ <#lt/> }
+
+ #if>
+ #list>
+#macro>
+
+<#macro equalsCode attributes>
+ <#list attributes as attribute>
+ <#lt/> append(${attribute.name}, other.${attribute.name}).
+ #list>
+#macro>
+
+<#macro hashCode attributes>
+ <#list attributes as attribute>
+ <#lt/> append(${attribute.name}).
+ #list>
+#macro>
+
+package ${package};
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.HashSet;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import javax.naming.Name;
+<#list imports as import>
+import ${import.packageName}.${import.className};
+#list>
+
+import static org.springframework.ldap.odm.annotations.Attribute.*;
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+
+/**
+* Generated by Spring LDAP ODM to represent the LDAP object classes:
+*
+<#list schema.objectClass as objectClass>
+* ${objectClass}
+#list>
+*
+*/
+@Entry(objectClasses={<@commaSeparatedList schema.objectClass/>})
+public final class ${class} {
+
+ @Id
+ private Name dn;
+<@doAttribute schema.must/>
+<@doAttribute schema.may/>
+ public Name getDn() {
+ return dn;
+ }
+
+ public void setDn(Name dn) {
+ this.dn=dn;
+ }
+
+<@getSet schema.must/>
+<@getSet schema.may/>
+ <#lt/> @Override
+ <#lt/> public String toString() {
+ <#lt/>
+ <#lt/> return new ToStringBuilder(this).
+ <#lt/> append("dn", dn).
+ <#list schema.must as attribute>
+ <#lt/> append("${attribute.name}", ${attribute.name}).
+ #list>
+ <#list schema.may as attribute>
+ <#lt/> append("${attribute.name}", ${attribute.name}).
+ #list>
+ <#lt/> toString();
+ <#lt/> }
+
+ <#lt/> @Override
+ <#lt/> public int hashCode() {
+ <#lt> return new HashCodeBuilder().
+ <@hashCode schema.must/>
+ <@hashCode schema.may/>
+ <#lt/> toHashCode();
+ <#lt/> }
+
+ <#lt/> @Override
+ <#lt/> public boolean equals(Object obj) {
+ <#lt/> if (this == obj)
+ <#lt/> return true;
+ <#lt/> if (obj == null)
+ <#lt/> return false;
+ <#lt/> if (getClass() != obj.getClass())
+ <#lt/> return false;
+ <#lt/>
+ <#lt/> ${class} other = (${class}) obj;
+ <#lt/>
+ <#lt/> return new EqualsBuilder().
+ <#lt/> append(dn, other.dn).
+ <@equalsCode schema.must/>
+ <@equalsCode schema.may/>
+ <#lt/> isEquals();
+ <#lt/> }
+}
+
+
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/OrganizationalUnit.java b/odm/src/test/java/org/springframework/ldap/odm/test/OrganizationalUnit.java
new file mode 100755
index 00000000..200ee73f
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/OrganizationalUnit.java
@@ -0,0 +1,137 @@
+package org.springframework.ldap.odm.test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+import javax.naming.Name;
+
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+
+/**
+ * Automatically generated to represent the LDAP object classes
+ * "organizationalunit", "top".
+ */
+@Entry(objectClasses = { "organizationalunit", "top" })
+public final class OrganizationalUnit {
+
+ @Id
+ private Name dn;
+
+ @Attribute(name = "objectClass", syntax = "1.3.6.1.4.1.1466.115.121.1.38")
+ private List objectClass = new ArrayList();
+
+ @Attribute(name = "ou", syntax = "1.3.6.1.4.1.1466.115.121.1.15")
+ private String ou;
+
+ @Attribute(name = "street", syntax = "1.3.6.1.4.1.1466.115.121.1.15")
+ private String street;
+
+ @Attribute(name = "description", syntax = "1.3.6.1.4.1.1466.115.121.1.15")
+ private String description;
+
+ public OrganizationalUnit() {
+ }
+
+ public OrganizationalUnit(Name dn, String street, String description) {
+ this.dn = dn;
+ this.street = street;
+ this.description = description;
+
+ objectClass.add("top");
+ objectClass.add("organizationalUnit");
+
+
+ int size = dn.size();
+ if (size > 1) {
+ ou = dn.get(size - 1).split("=")[1];
+ } else {
+ ou = "";
+ }
+
+ }
+
+ public Name getDn() {
+ return dn;
+ }
+
+ public void setDn(Name dn) {
+ this.dn = dn;
+ }
+
+ public List getObjectClasses() {
+ return Collections.unmodifiableList(objectClass);
+ }
+
+ public String getOu() {
+ return ou;
+ }
+
+ public String getStreet() {
+ return street;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("objectClasses=%1$s | dn=%2$s | ou=%3$s | street=%4$s | description=%5$s", objectClass,
+ dn, ou, street, description);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((description == null) ? 0 : description.hashCode());
+ result = prime * result + ((dn == null) ? 0 : dn.hashCode());
+ result = prime * result + ((objectClass == null) ? 0 : new HashSet(objectClass).hashCode());
+ result = prime * result + ((ou == null) ? 0 : ou.hashCode());
+ result = prime * result + ((street == null) ? 0 : street.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ OrganizationalUnit other = (OrganizationalUnit) obj;
+ if (description == null) {
+ if (other.description != null)
+ return false;
+ } else if (!description.equals(other.description))
+ return false;
+ if (dn == null) {
+ if (other.dn != null)
+ return false;
+ } else if (!dn.equals(other.dn))
+ return false;
+ if (objectClass == null) {
+ if (other.objectClass != null)
+ return false;
+ } else
+ if (objectClass.size()!=other.objectClass.size() ||
+ !(new HashSet(objectClass)).equals(new HashSet(other.objectClass)))
+ return false;
+ if (ou == null) {
+ if (other.ou != null)
+ return false;
+ } else if (!ou.equals(other.ou))
+ return false;
+ if (street == null) {
+ if (other.street != null)
+ return false;
+ } else if (!street.equals(other.street))
+ return false;
+ return true;
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/Person.java b/odm/src/test/java/org/springframework/ldap/odm/test/Person.java
new file mode 100755
index 00000000..ab56de9b
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/Person.java
@@ -0,0 +1,201 @@
+package org.springframework.ldap.odm.test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+import javax.naming.Name;
+
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+import org.springframework.ldap.odm.annotations.Transient;
+import org.springframework.ldap.odm.annotations.Attribute.Type;
+
+// Simple LDAP entry for testing
+@Entry(objectClasses = { "inetorgperson", "organizationalperson", "person", "top" })
+public final class Person {
+ public Person() {
+ }
+
+ public Person(Name dn, String surname, List desc, int telephoneNumber, byte[] jpegPhoto) {
+ this.dn = dn;
+ this.surname = surname;
+ this.desc = desc;
+ this.telephoneNumber = telephoneNumber;
+ this.jpegPhoto = jpegPhoto;
+ objectClasses = new ArrayList();
+ objectClasses.add("top");
+ objectClasses.add("person");
+ objectClasses.add("organizationalPerson");
+ objectClasses.add("inetOrgPerson");
+ int size = dn.size();
+ if (size > 1) {
+ cn = dn.get(size - 1).split("=")[1];
+ } else {
+ cn = "";
+ }
+ }
+
+ @Transient
+ private String someRandomField = null;
+
+ @Transient
+ private List someRandomList = new ArrayList();
+
+ @Attribute(name = "objectClass")
+ private List objectClasses;
+
+ @Id
+ private Name dn;
+
+ // No annotation on purpose!
+ private String cn;
+
+ @Attribute(name = "sn")
+ private String surname;
+
+ // Everything should be sets and in search operations also as results can be in any order
+ @Attribute(name = "description")
+ private List desc;
+
+ @Attribute
+ private int telephoneNumber;
+
+ @Attribute(type = Type.BINARY)
+ byte[] jpegPhoto;
+
+ public Name getDn() {
+ return dn;
+ }
+
+ public void setDn(Name dn) {
+ this.dn = dn;
+ }
+
+ public String getCn() {
+ return cn;
+ }
+
+ public void setCn(String cn) {
+ this.cn = cn;
+ }
+
+ public String getSurname() {
+ return surname;
+ }
+
+ public void setSurname(String surname) {
+ this.surname = surname;
+ }
+
+ public List getDesc() {
+ return desc;
+ }
+
+ public void setDesc(List desc) {
+ this.desc = desc;
+ }
+
+ public int getTelephoneNumber() {
+ return telephoneNumber;
+ }
+
+ public void setTelephoneNumber(int telephoneNumber) {
+ this.telephoneNumber = telephoneNumber;
+ }
+
+ public byte[] getJpegPhoto() {
+ return jpegPhoto;
+ }
+
+ public void setJpegPhoto(byte[] jpegPhoto) {
+ this.jpegPhoto = jpegPhoto;
+ }
+
+ public List getObjectClasses() {
+ return objectClasses;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder jpegString=new StringBuilder();
+ if (jpegPhoto!=null) {
+ for (byte b:jpegPhoto) {
+ jpegString.append(Byte.toString(b));
+ }
+ }
+
+ return String.format(
+ "objectClasses=%1$s | dn=%2$s | cn=%3$s | sn=%4$s | desc=%5$s | telephoneNumber=%6$s | jpegPhoto=%7$s",
+ objectClasses, dn, cn, surname, desc, telephoneNumber, jpegString);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((cn == null) ? 0 : cn.hashCode());
+ result = prime * result + ((desc == null) ? 0 : new HashSet(desc).hashCode());
+ result = prime * result + ((dn == null) ? 0 : dn.hashCode());
+ result = prime * result + Arrays.hashCode(jpegPhoto);
+ result = prime * result + ((objectClasses == null) ? 0 : new HashSet(objectClasses).hashCode());
+ result = prime * result + ((someRandomField == null) ? 0 : someRandomField.hashCode());
+ result = prime * result + ((someRandomList == null) ? 0 : someRandomList.hashCode());
+ result = prime * result + ((surname == null) ? 0 : surname.hashCode());
+ result = prime * result + telephoneNumber;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Person other = (Person) obj;
+ if (cn == null) {
+ if (other.cn != null)
+ return false;
+ } else if (!cn.equals(other.cn))
+ return false;
+ if (desc == null) {
+ if (other.desc != null)
+ return false;
+ } else if (desc.size()!=other.desc.size() || !(new HashSet(desc)).equals(new HashSet(other.desc)))
+ return false;
+ if (dn == null) {
+ if (other.dn != null)
+ return false;
+ } else if (!dn.equals(other.dn))
+ return false;
+ if (!Arrays.equals(jpegPhoto, other.jpegPhoto))
+ return false;
+ if (objectClasses == null) {
+ if (other.objectClasses != null)
+ return false;
+ } else if (desc.size()!=other.desc.size() || !(new HashSet(objectClasses)).equals(new HashSet(other.objectClasses)))
+ return false;
+ if (someRandomField == null) {
+ if (other.someRandomField != null)
+ return false;
+ } else if (!someRandomField.equals(other.someRandomField))
+ return false;
+ if (someRandomList == null) {
+ if (other.someRandomList != null)
+ return false;
+ } else if (!someRandomList.equals(other.someRandomList))
+ return false;
+ if (surname == null) {
+ if (other.surname != null)
+ return false;
+ } else if (!surname.equals(other.surname))
+ return false;
+ if (telephoneNumber != other.telephoneNumber)
+ return false;
+ return true;
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/TestConverterManager.java b/odm/src/test/java/org/springframework/ldap/odm/test/TestConverterManager.java
new file mode 100755
index 00000000..633a2691
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/TestConverterManager.java
@@ -0,0 +1,207 @@
+package org.springframework.ldap.odm.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.URI;
+import java.util.BitSet;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.ldap.odm.test.utils.ExecuteRunnable;
+import org.springframework.ldap.odm.test.utils.RunnableTest;
+import org.springframework.ldap.odm.typeconversion.ConverterException;
+import org.springframework.ldap.odm.typeconversion.impl.Converter;
+import org.springframework.ldap.odm.typeconversion.impl.ConverterManagerImpl;
+import org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter;
+import org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter;
+
+public final class TestConverterManager {
+ private ConverterManagerImpl converterManager;
+
+ @Before
+ public void setUp() {
+ converterManager = new ConverterManagerImpl();
+
+ Converter ptc = new FromStringConverter();
+ converterManager.addConverter(String.class, "", Byte.class, ptc);
+ converterManager.addConverter(String.class, "", Short.class, ptc);
+ converterManager.addConverter(String.class, "", Integer.class, ptc);
+ converterManager.addConverter(String.class, "", Long.class, ptc);
+ converterManager.addConverter(String.class, "", Double.class, ptc);
+ converterManager.addConverter(String.class, "", Float.class, ptc);
+ converterManager.addConverter(String.class, "", Boolean.class, ptc);
+
+ Converter tsc = new ToStringConverter();
+ converterManager.addConverter(Byte.class, "", String.class, tsc);
+ converterManager.addConverter(Short.class, "", String.class, tsc);
+ converterManager.addConverter(Integer.class, "", String.class, tsc);
+ converterManager.addConverter(Long.class, "", String.class, tsc);
+ converterManager.addConverter(Double.class, "", String.class, tsc);
+ converterManager.addConverter(Float.class, "", String.class, tsc);
+ converterManager.addConverter(Boolean.class, "", String.class, tsc);
+
+ Converter uric = new UriConverter();
+ converterManager.addConverter(URI.class, "", String.class, uric);
+ converterManager.addConverter(String.class, "", URI.class, uric);
+ }
+
+ @After
+ public void tearDown() {
+ converterManager = null;
+ }
+
+ private static class ConverterTestData {
+ public final Class destClass;
+
+ public final Object sourceData;
+
+ public final T expectedValue;
+
+ public final String syntax;
+
+ public ConverterTestData(Object sourceData, Class destClass, T expectedValue) {
+ this(sourceData, "", destClass, expectedValue);
+ }
+
+ public ConverterTestData(Object sourceData, String syntax, Class destClass, T expectedValue) {
+ this.destClass = destClass;
+ this.sourceData = sourceData;
+ this.expectedValue = expectedValue;
+ this.syntax = syntax;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("sourceData=%1$s | syntax=%2$s | destClass=%3$s | expectedValue=%4$s", sourceData,
+ syntax, destClass, expectedValue);
+ }
+ }
+
+ // Class to Class conversion without any syntaxes
+ @Test
+ public void basicTypeConverion() throws Exception {
+ final ConverterTestData>[] primitiveTypeTests = new ConverterTestData>[] {
+ new ConverterTestData("33", Byte.class, Byte.valueOf((byte)33)),
+ new ConverterTestData("-88", Byte.class, Byte.valueOf((byte)-88)),
+ new ConverterTestData("666", Short.class, Short.valueOf((short)666)),
+ new ConverterTestData("-123", Short.class, Short.valueOf((short)-123)),
+ new ConverterTestData("123", Integer.class, Integer.valueOf(123)),
+ new ConverterTestData("-500", Integer.class, Integer.valueOf(-500)),
+ new ConverterTestData("123456", Long.class, Long.valueOf(123456)),
+ new ConverterTestData("-654321", Long.class, Long.valueOf(-654321)),
+ new ConverterTestData("2", Double.class, Double.valueOf(2)),
+ new ConverterTestData("-0.4", Double.class, Double.valueOf(-0.4)),
+ new ConverterTestData("666", Float.class, Float.valueOf(666)),
+ new ConverterTestData("-0.75", Float.class, Float.valueOf(-0.75F)),
+ new ConverterTestData("false", Boolean.class, Boolean.FALSE),
+ new ConverterTestData("TRUE", Boolean.class, Boolean.TRUE),
+ new ConverterTestData("This is a string", String.class, "This is a string"),
+ new ConverterTestData("This is another String", String.class, "This is another String"),
+ new ConverterTestData((byte)66, String.class, "66"),
+ new ConverterTestData((int)1234, String.class, "1234"),
+ new ConverterTestData((int)-9876, String.class, "-9876"),
+ new ConverterTestData("http://google.com/", URI.class, new URI("http://google.com/")),
+ new ConverterTestData("http://apache.org/index.html", URI.class, new URI(
+ "http://apache.org/index.html")),
+ new ConverterTestData(new URI("http://google.com/"), String.class, "http://google.com/"),
+ new ConverterTestData(new URI("http://apache.org/index.html"), String.class,
+ "http://apache.org/index.html") };
+
+ new ExecuteRunnable>().runTests(new RunnableTest>() {
+ public void runTest(ConverterTestData> testData) {
+ assertEquals(testData.expectedValue, converterManager.convert(testData.sourceData, "",
+ testData.destClass));
+ }
+ }, primitiveTypeTests);
+ }
+
+ private static class SquaredConverter implements Converter {
+ public T convert(Object source, Class toClass) throws Exception {
+ Integer intSource = null;
+
+ if (source.getClass() == String.class) {
+ intSource = new Integer((String)source);
+ }
+ else {
+ if (source.getClass() == Integer.class) {
+ intSource = (Integer)source;
+ }
+ }
+
+ Integer result = null;
+ if (intSource != null) {
+ result = intSource * intSource;
+ }
+
+ return toClass.cast(result);
+ }
+ }
+
+ private static class CubedConverter implements Converter {
+ public T convert(Object source, Class toClass) throws Exception {
+ Integer intSource = null;
+
+ if (source.getClass() == String.class) {
+ intSource = new Integer((String)source);
+ }
+ else {
+ if (source.getClass() == Integer.class) {
+ intSource = (Integer)source;
+ }
+ }
+
+ Integer result = null;
+ if (intSource != null) {
+ result = intSource * intSource * intSource;
+ }
+
+ return toClass.cast(result);
+ }
+ }
+
+ // Tests using syntaxes for "finer grained" mapping
+ @Test
+ public void syntaxBasedConversion() throws Exception {
+ Converter squaredConverter = new SquaredConverter();
+ converterManager.addConverter(String.class, "1", Integer.class, squaredConverter);
+ converterManager.addConverter(Integer.class, "1", Integer.class, squaredConverter);
+ Converter cubedConverter = new CubedConverter();
+ converterManager.addConverter(String.class, "2", Integer.class, cubedConverter);
+ converterManager.addConverter(Integer.class, "3", Integer.class, cubedConverter);
+
+ final ConverterTestData>[] syntaxTests = new ConverterTestData>[] {
+ new ConverterTestData("3", "", Integer.class, Integer.valueOf(3)),
+ new ConverterTestData("4", "", Integer.class, Integer.valueOf(4)),
+ new ConverterTestData(5, "", Integer.class, Integer.valueOf(5)),
+ new ConverterTestData(6, "", Integer.class, Integer.valueOf(6)),
+ new ConverterTestData("3", "1", Integer.class, Integer.valueOf(9)),
+ new ConverterTestData("4", "1", Integer.class, Integer.valueOf(16)),
+ new ConverterTestData(5, "1", Integer.class, Integer.valueOf(25)),
+ new ConverterTestData(6, "1", Integer.class, Integer.valueOf(36)),
+ new ConverterTestData("3", "2", Integer.class, Integer.valueOf(27)),
+ new ConverterTestData("4", "2", Integer.class, Integer.valueOf(64)),
+ new ConverterTestData(5, "3", Integer.class, Integer.valueOf(125)),
+ new ConverterTestData(6, "3", Integer.class, Integer.valueOf(216)), };
+
+ new ExecuteRunnable>().runTests(new RunnableTest>() {
+ public void runTest(ConverterTestData> testData) {
+ assertEquals(testData.expectedValue, converterManager.convert(testData.sourceData, testData.syntax,
+ testData.destClass));
+ }
+ }, syntaxTests);
+
+ }
+
+ // No converter for classes
+ @Test(expected = ConverterException.class)
+ public void noClassConverter() throws Exception {
+ converterManager.convert(BitSet.class, "", Integer.class);
+ }
+
+ // Invalid syntax so converter fails
+ @Test(expected = ConverterException.class)
+ public void invalidSyntax() throws Exception {
+ converterManager.convert(String.class, "not a uri", URI.class);
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/TestLdap.java b/odm/src/test/java/org/springframework/ldap/odm/test/TestLdap.java
new file mode 100755
index 00000000..66b2c4f5
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/TestLdap.java
@@ -0,0 +1,622 @@
+package org.springframework.ldap.odm.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.naming.Name;
+import javax.naming.directory.SearchControls;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.cli.PosixParser;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.ldap.NameNotFoundException;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.support.LdapContextSource;
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+import org.springframework.ldap.odm.core.OdmException;
+import org.springframework.ldap.odm.core.OdmManager;
+import org.springframework.ldap.odm.core.impl.InvalidEntryException;
+import org.springframework.ldap.odm.core.impl.MetaDataException;
+import org.springframework.ldap.odm.core.impl.OdmManagerImpl;
+import org.springframework.ldap.odm.core.impl.UnmanagedClassException;
+import org.springframework.ldap.odm.test.utils.ExecuteRunnable;
+import org.springframework.ldap.odm.test.utils.GetFreePort;
+import org.springframework.ldap.odm.test.utils.RunnableTest;
+import org.springframework.ldap.odm.typeconversion.impl.Converter;
+import org.springframework.ldap.odm.typeconversion.impl.ConverterManagerImpl;
+import org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter;
+import org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter;
+import org.springframework.ldap.test.LdapTestUtils;
+
+// Tests all OdmManager functions
+public final class TestLdap {
+ private static final Log LOG = LogFactory.getLog(TestLdap.class);
+
+ // Base DN for test data
+ private static final DistinguishedName baseName = new DistinguishedName("o=Whoniverse");
+
+ // This port MUST be free on local host for these unit tests to function.
+ private static int port;
+
+ // Maximum number of objects to return in testing
+ private static final long COUNT_LIMIT=20;
+
+ // Maximum time to wait for results in testing (ms)
+ private static final int TIME_LIMIT=60000;
+
+ private static SearchControls searchControls=
+ new SearchControls(SearchControls.SUBTREE_SCOPE,
+ COUNT_LIMIT,
+ TIME_LIMIT,
+ null,
+ true,
+ false);
+
+ private ConverterManagerImpl converterManager;
+
+ private ContextSource contextSource;
+
+ private OdmManager odmManager;
+
+ // Base 64 encoded jpeg photo used to test binary reading and writing
+ private static byte[] photo;
+ static {
+ try {
+ String photoString="/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkS"+
+ "Ew8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRg"+
+ "yIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wA"+
+ "ARCAAnABoDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAA"+
+ "gEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcY"+
+ "GRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipK"+
+ "TlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8v"+
+ "P09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFB"+
+ "AQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygp"+
+ "KjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJm"+
+ "aoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9"+
+ "oADAMBAAIRAxEAPwDx2z0mK6gV/tSo2SCpUHGD9Qa2vDvgGfX7+eI3ot4EOEmEefMOOcZPQdOvW"+
+ "s3VLGzsWiihUvM0ayONxATPIHXk4wfxFdd4L1aw0rw3eS3V3GHWRswkgugwCpVSckEk9OhB9azu"+
+ "2ro6lCKnyyZwmvaHcaBrV1plxKkkluw+dRkMCAQfbgjiqi27sob1GfuVc1q9n1XVL3UdqxJI/wD"+
+ "q/MXIXGAMd+B2HWoUZ1jUbZeABwKqzMJWT0Ld3cfaJ5ZFYtnCg+oUBf6V2XhvwVpcvhxfFXibVF"+
+ "tNK3lEgjJ3yMGK7SRzkkHhRnHORXJQ6exVXB2r1K9Cfx7V0UWpeD7S3WJ9N1TUPLYssV7cBYkZg"+
+ "AxAU4zwOcZ4HoKtppWQ4tSk5SNPx14o8M3/AITXT/D2kRRxB1jE4RIyuOc7ME84PzcHr2PPmX2y"+
+ "YcZX8q37fQxPpqzsH8xvuqAeeuO/0H3e9Zn2dxwbK5JHXCH/AOJqNOhdSM1ZzVjRNwRbjGRxxzn"+
+ "FdRouijV9Pi1C6toEMqn7MsK7SqBiCxOSSSVI5J4x6miiliW1FMzpbszreUw3H2VcuscpGT3UE9"+
+ "Rn0HStE+IrZSQbEuRxvU4De4yc0UVFKCnuerms2vZryP/Z";
+
+ byte[] photoBytes=photoString.getBytes("US-ASCII");
+ photo=Base64.decodeBase64(photoBytes);
+ } catch (IOException e) {
+ throw new RuntimeException("Problem decoding photo", e);
+ }
+ }
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ // Added because the close down of Apache DS on Linux does
+ // not seem to free up its port.
+ port=GetFreePort.getFreePort();
+
+ // Start an LDAP server and import test data
+ LdapTestUtils.startApacheDirectoryServer(port, baseName.toString(), "odm-test", "", "", null);
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws Exception {
+ LdapTestUtils.destroyApacheDirectoryServer("", "");
+ }
+
+ private static ContextSource getContextSource(String url, String username, String password) throws Exception {
+ LdapContextSource contextSource = new LdapContextSource();
+ contextSource.setUrl(url);
+ contextSource.setUserDn(username);
+ contextSource.setPassword(password);
+ contextSource.setPooled(false);
+ contextSource.afterPropertiesSet();
+
+ return contextSource;
+ }
+
+ // Pulled out of setup method to allow it to be called with parameters from main as
+ // an integration test
+ public void setUp(String url, String username, String password) throws Exception {
+ // Create some basic converters and a converter manager
+ converterManager = new ConverterManagerImpl();
+
+ Converter ptc = new FromStringConverter();
+ converterManager.addConverter(String.class, "", Byte.class, ptc);
+ converterManager.addConverter(String.class, "", Short.class, ptc);
+ converterManager.addConverter(String.class, "", Integer.class, ptc);
+ converterManager.addConverter(String.class, "", Long.class, ptc);
+ converterManager.addConverter(String.class, "", Double.class, ptc);
+ converterManager.addConverter(String.class, "", Float.class, ptc);
+ converterManager.addConverter(String.class, "", Boolean.class, ptc);
+
+ Converter tsc = new ToStringConverter();
+ converterManager.addConverter(Byte.class, "", String.class, tsc);
+ converterManager.addConverter(Short.class, "", String.class, tsc);
+ converterManager.addConverter(Integer.class, "", String.class, tsc);
+ converterManager.addConverter(Long.class, "", String.class, tsc);
+ converterManager.addConverter(Double.class, "", String.class, tsc);
+ converterManager.addConverter(Float.class, "", String.class, tsc);
+ converterManager.addConverter(Boolean.class, "", String.class, tsc);
+
+ // Bind to the directory
+ contextSource = getContextSource(url, username, password);
+
+ // Clear out any old data - and load the test data
+ LdapTestUtils.cleanAndSetup(contextSource, baseName, new ClassPathResource("testdata.ldif"));
+
+ // Create our OdmManager
+ Set> managedClasses=new HashSet>();
+ managedClasses.add(Person.class);
+ managedClasses.add(OrganizationalUnit.class);
+ odmManager = new OdmManagerImpl(converterManager, contextSource, managedClasses);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ setUp("ldap://127.0.0.1:" + port, "", "");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ LdapTestUtils.clearSubContexts(contextSource, baseName);
+
+ odmManager=null;
+ contextSource=null;
+ converterManager=null;
+ }
+
+ private enum PersonName {
+ WILLIAM(0), PATRICK(1), JON(2), TOM(3), PETER(4), DAVROS(5), DALEKS(6), MASTER(7);
+
+ private int index;
+
+ private PersonName(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+ }
+
+ private Person[] personTestData=new Person[] {
+ new Person(new DistinguishedName("cn=William Hartnell,ou=Doctors,o=Whoniverse"), "Hartnell", Arrays
+ .asList(new String[] { "First Doctor", "Grumpy" }), 1, null),
+ new Person(new DistinguishedName("cn=Patrick Troughton,ou=Doctors,o=Whoniverse"), "Troughton", Arrays
+ .asList(new String[] { "Second Doctor", "Clown" }), 2, null),
+ new Person(new DistinguishedName("cn=Jon Pertwee,ou=Doctors,o=Whoniverse"), "Pertwee", Arrays
+ .asList(new String[] { "Third Doctor", "Dandy" }), 3, null),
+ new Person(new DistinguishedName("cn=Tom Baker,ou=Doctors,o=Whoniverse"), "Baker", Arrays
+ .asList(new String[] { "Fourth Doctor", "The one and only!" }), 4, null),
+ new Person(new DistinguishedName("cn=Peter Davison,ou=Doctors,o=Whoniverse"), "Davison", Arrays
+ .asList(new String[] { "Fifth Doctor" }), 5, null),
+ new Person(new DistinguishedName("cn=Davros,ou=Enemies,o=Whoniverse"), "Unknown", Arrays
+ .asList(new String[] { "Creator of the Daleks", "Kaled head scientist" }), 0, null),
+ new Person(new DistinguishedName("cn=Daleks,ou=Enemies,o=Whoniverse"), "NA", Arrays
+ .asList(new String[] { "The Doctor's greatest foe" }), 0, null),
+ new Person(new DistinguishedName("cn=Master,ou=Enemies,o=Whoniverse"), "Unknown", Arrays
+ .asList(new String[] { "An evil Time Lord" }), 0, photo), };
+
+
+ // Read various entries from the sample data set and check they are what we'd expect.
+ @Test
+ public void read() throws Exception {
+ new ExecuteRunnable().runTests(new RunnableTest() {
+ public void runTest(Person testData) {
+ Name dn = testData.getDn();
+ LOG.debug(String.format("reading - %1$s", dn));
+ Person personEntry = odmManager.read(Person.class, dn);
+ LOG.debug(String.format("read - %1$s", personEntry));
+ assertEquals(testData, personEntry);
+ }
+ }, personTestData);
+ }
+
+ private static class SearchTestData {
+ private String search;
+
+ private SearchControls searchScope;
+
+ private Person[] people;
+
+ public SearchTestData(String search, SearchControls searchScope, Person[] people) {
+ this.search = search;
+ this.searchScope = searchScope;
+ this.people = people;
+ }
+ }
+
+ private SearchTestData[] searchTestData = {
+ new SearchTestData("(sn=Unknown)",
+ searchControls,
+ new Person[] {
+ personTestData[PersonName.DAVROS.getIndex()],
+ personTestData[PersonName.MASTER.getIndex()] }),
+ new SearchTestData("(description=*Doctor)",
+ searchControls,
+ new Person[] {
+ personTestData[PersonName.WILLIAM.getIndex()],
+ personTestData[PersonName.PATRICK.getIndex()],
+ personTestData[PersonName.JON.getIndex()],
+ personTestData[PersonName.TOM.getIndex()],
+ personTestData[PersonName.PETER.getIndex()] }),
+ };
+
+ // Carry out various searches against the test data set and check the results are what we'd expect.
+ @Test
+ public void search() throws Exception {
+ new ExecuteRunnable().runTests(new RunnableTest() {
+ public void runTest(SearchTestData testData) {
+ String search = testData.search;
+ LOG.debug(String.format("searching - %1$s", search));
+ List results = odmManager.search(Person.class, baseName, testData.search, testData.searchScope);
+ LOG.debug(String.format("found - %1$s", results));
+ assertEquals(new HashSet(Arrays.asList(testData.people)), new HashSet(results));
+ }
+ }, searchTestData);
+ }
+
+ private enum OrganizationalName {
+ ENEMIES(0), ASSISTANTS(1), DOCTORS(2);
+
+ private int index;
+
+ private OrganizationalName(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+ }
+
+ private static OrganizationalUnit ouTestData[]=new OrganizationalUnit[] {
+ new OrganizationalUnit(new DistinguishedName("ou=Enemies,o=Whoniverse"), "Acacia Avenue", "The bad guys"),
+ new OrganizationalUnit(new DistinguishedName("ou=Assistants,o=Whoniverse"), "Somewhere in space", "The plucky helpers"),
+ new OrganizationalUnit(new DistinguishedName("ou=Doctors,o=Whoniverse"), "Somewhere in time", "Our hero"),
+ };
+
+ // Check everything works OK with a second managed class
+ @Test
+ public void testSecondOc() {
+ LOG.debug("Reading all organizatinalUnits");
+ List allOus=odmManager.findAll(OrganizationalUnit.class, baseName, searchControls);
+ LOG.debug(String.format("Found - %1$s", allOus));
+ assertEquals(new HashSet(Arrays.asList(ouTestData)), new HashSet(allOus));
+
+ OrganizationalUnit testOu=ouTestData[OrganizationalName.ASSISTANTS.getIndex()];
+ LOG.debug(String.format("Reading - %1$s", testOu.getDn()));
+ OrganizationalUnit ou=odmManager.read(OrganizationalUnit.class, testOu.getDn());
+ LOG.debug(String.format("Found - %1$s", ou));
+ assertEquals(testOu, ou);
+ }
+
+ // Find all entries managed by the OdmManager in the test data set and check they are what we expect.
+ @Test
+ public void findAll() throws Exception {
+ LOG.debug("finding all people");
+ List allPeople = odmManager.findAll(Person.class, baseName, searchControls);
+ LOG.debug(String.format("found %1$s", allPeople));
+ assertEquals(new HashSet(Arrays.asList(personTestData)), new HashSet(allPeople));
+ }
+
+ private Person[] createTestData = {
+ new Person(new DistinguishedName("cn=Colin Baker,ou=Doctors,o=Whoniverse"), "Baker", Arrays
+ .asList(new String[] { "Sixth Doctor" }), 6, null),
+ new Person(new DistinguishedName("cn=Sylvester McCoy,ou=Doctors,o=Whoniverse"), "McCoy", Arrays
+ .asList(new String[] { "Seventh Doctor" }), 7, null),
+ new Person(new DistinguishedName("cn=Paul McGann,ou=Doctors,o=Whoniverse"), "McGann", Arrays
+ .asList(new String[] { "Eigth Doctor" }), 8, photo), };
+
+ // Create some entries, read them back and check they are what we'd expect.
+ @Test
+ public void create() throws Exception {
+ for (Person person : createTestData) {
+ LOG.debug(String.format("creating - %1$s", person));
+ odmManager.create(person);
+ }
+ LOG.debug("Created all, reading back");
+ new ExecuteRunnable().runTests(new RunnableTest() {
+ public void runTest(Person testData) {
+ Name dn = testData.getDn();
+ LOG.debug(String.format("reading - %1$s", dn));
+ Person personEntry = odmManager.read(Person.class, dn);
+ LOG.debug(String.format("read - %1$s", personEntry));
+ assertEquals(testData, personEntry);
+ }
+ }, createTestData);
+ }
+
+ // Update an entry from the test data set, read it back and check it is what we'd expect.
+ @Test
+ public void update() throws Exception {
+ Person william = personTestData[PersonName.WILLIAM.getIndex()];
+ william.setTelephoneNumber(666);
+ william.setSurname("Harvey");
+ odmManager.update(william);
+ Person readWilliam = odmManager.read(Person.class, william.getDn());
+ assertEquals(william, readWilliam);
+ }
+
+ private Person[] deleteData = {
+ personTestData[PersonName.JON.getIndex()],
+ personTestData[PersonName.TOM.getIndex()], personTestData[PersonName.DAVROS.getIndex()], };
+
+ private Person[] whatsLeft = {
+ personTestData[PersonName.WILLIAM.getIndex()],
+ personTestData[PersonName.PATRICK.getIndex()], personTestData[PersonName.PETER.getIndex()],
+ personTestData[PersonName.DALEKS.getIndex()], personTestData[PersonName.MASTER.getIndex()], };
+
+ // Delete a some entries from the the test data set and check what's left is what we'd expect
+ @Test
+ public void delete() throws Exception {
+ for (Person toDelete : deleteData) {
+ LOG.debug(String.format("deleting - %1$s", toDelete.getDn()));
+ odmManager.delete(toDelete);
+ }
+
+ List allPeople = odmManager.findAll(Person.class, baseName, searchControls);
+ assertEquals(new HashSet(Arrays.asList(whatsLeft)), new HashSet(allPeople));
+ }
+
+ // Trying to read a non-existant entry should be flagged as an error
+ @Test(expected = NameNotFoundException.class)
+ public void readNonExistant() throws Exception {
+ odmManager.read(Person.class, new DistinguishedName("cn=Hili Harvey,ou=Doctors,o=Whoniverse"));
+ }
+
+ // Read an entry with classes in addition to those supported by the Entry
+ @Test(expected = OdmException.class)
+ public void readAdditionalObjectClasses() throws Exception {
+ odmManager.read(Person.class, new DistinguishedName("cn=Paul Harvey,ou=Doctors,o=Whoniverse"));
+ }
+
+ private final static class NoEntry {
+ @SuppressWarnings("unused")
+ @Id
+ Name id;
+ }
+
+ // Every class to be managed must be annotated @Entry
+ @Test(expected = MetaDataException.class)
+ public void noEntryAnnotation() {
+ ((OdmManagerImpl)odmManager).addManagedClass(NoEntry.class);
+ }
+
+ @Entry(objectClasses="test")
+ private final static class NoId {
+ }
+
+ // There must be a field with the @Id annotation
+ @Test(expected = MetaDataException.class)
+ public void noId() {
+ ((OdmManagerImpl)odmManager).addManagedClass(NoId.class);
+ }
+
+ @Entry(objectClasses="test")
+ private final static class TwoIds {
+ @SuppressWarnings("unused")
+ @Id
+ private Name firstId;
+
+ @SuppressWarnings("unused")
+ @Id
+ private Name secondId;
+
+ @SuppressWarnings("unused")
+ public TwoIds() {
+ }
+ }
+
+ // Only one field may be annotated @Id
+ @Test(expected = MetaDataException.class)
+ public void twoIds() {
+ ((OdmManagerImpl)odmManager).addManagedClass(TwoIds.class);
+ }
+
+ @Entry(objectClasses="test")
+ public final static class NoConstructor {
+ @SuppressWarnings("unused")
+ @Id
+ private Name id;
+
+ public NoConstructor(String aValue) {
+ }
+ }
+
+ // All Entry annotated classes must have a zero argument public constructor
+ @Test(expected = InvalidEntryException.class)
+ public void noConstructor() {
+ ((OdmManagerImpl)odmManager).addManagedClass(NoConstructor.class);
+ }
+
+ @Entry(objectClasses="test")
+ public final static class AttributeOnId {
+ @SuppressWarnings("unused")
+ @Id
+ @Attribute
+ private Name id;
+ }
+
+ // It is illegal put put both the Id and the Attribute annotation on the same field
+ @Test(expected = MetaDataException.class)
+ public void attributeOnId() {
+ ((OdmManagerImpl)odmManager).addManagedClass(AttributeOnId.class);
+ }
+
+ @Entry(objectClasses="test")
+ public final static class IdIsNotAName {
+ @SuppressWarnings("unused")
+ @Id
+ private String id;
+ }
+
+ // The field annotation with @Id must be of type javax.naming.Name
+ @Test(expected = MetaDataException.class)
+ public void idIsNotAName() {
+ ((OdmManagerImpl)odmManager).addManagedClass(IdIsNotAName.class);
+ }
+
+ @Entry(objectClasses="test")
+ public final static class MissingConverter {
+ @SuppressWarnings("unused")
+ @Id
+ private Name id;
+
+ @SuppressWarnings("unused")
+ private BufferedImage image;
+ }
+
+ // The OdmManager should flag any missing converters when it is instantiated
+ @Test(expected = InvalidEntryException.class)
+ public void missingConverter() {
+ ((OdmManagerImpl)odmManager).addManagedClass(MissingConverter.class);
+ }
+
+ @Entry(objectClasses="test")
+ public final static class WrongClassForOc {
+ @SuppressWarnings("unused")
+ @Id
+ private Name id;
+
+ @SuppressWarnings("unused")
+ @Attribute(name="objectClass")
+ private int ocs;
+ }
+
+ // The OdmManager should flag if the objectClass attribute is not of the appropriate type
+ @Test(expected = MetaDataException.class)
+ public void wrongClassForOc() {
+ ((OdmManagerImpl)odmManager).addManagedClass(WrongClassForOc.class);
+ }
+
+ // The OdmManager should flag any attempt to use a "unmanaged" class
+ @Test(expected = UnmanagedClassException.class)
+ public void unManagedClass() {
+ ((OdmManagerImpl)odmManager).read(Integer.class, baseName);
+ }
+
+ private enum Flag {
+ URL("l", "url"),
+ USERNAME("u", "username"),
+ PASSWORD("p", "password"),
+ HELP("h", "help");
+
+ private String shortName;
+ private String longName;
+
+ private Flag(String shortName, String longName) {
+ this.shortName = shortName;
+ this.longName = longName;
+ }
+
+ public String getShort() {
+ return shortName;
+ }
+
+ public String getLong() {
+ return longName;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("short=%1$s, long=%2$s", shortName, longName);
+ }
+ }
+
+ private static final String DEFAULT_LDAP_URL="ldap://localhost:389";
+ private static final String DEFAULT_USERNAME="";
+ private static final String DEFAULT_PASSWORD="";
+
+ private static final Options options = new Options();
+ static {
+ options.addOption(Flag.URL.getShort(), Flag.URL.getLong(), true, "Ldap url to bind to, defaults to "+DEFAULT_LDAP_URL);
+ options.addOption(Flag.USERNAME.getShort(), Flag.USERNAME.getLong(), true, "DN to bind with, defaults to "+DEFAULT_USERNAME);
+ options.addOption(Flag.PASSWORD.getShort(), Flag.PASSWORD.getLong(), true, "Password to bind with defaults to "+DEFAULT_PASSWORD);
+ options.addOption(Flag.HELP.getShort(), Flag.HELP.getLong(), false, "Print this help message");
+ }
+
+
+ private static void runLdapTestCases(String url, String username, String password, String[] testCases) throws Exception {
+
+ for (String testCase:testCases) {
+ LOG.debug(String.format("Starting ldap test case %1$s", testCase));
+
+ // Set up
+ TestLdap testLdap=new TestLdap();
+ testLdap.setUp(url, username, password);
+
+ // Run the test
+ Method testMethod=testLdap.getClass().getMethod(testCase);
+ testMethod.invoke(testLdap);
+
+ // Tear down
+ testLdap.tearDown();
+
+ LOG.debug(String.format("Test case %1$s completed", testCase));
+ }
+ }
+
+ /*
+ * Run unit tests as an integration test against an external LDAP server.
+ *
+ * Three flags are required:
+ *
+ * -l ldap url of target server
+ * -u dn to bind with
+ * -p password to bind with
+ *
+ * The organisation o=Whoniverse must already exists and the bound user must have
+ * write permission.
+ *
+ */
+ public static void main(String[] argv) throws Exception {
+ CommandLineParser parser = new PosixParser();
+ CommandLine cmd = null;
+
+ try {
+ cmd = parser.parse(options, argv);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ System.exit(1);
+ }
+
+ if (cmd.hasOption(Flag.HELP.getShort())) {
+ HelpFormatter formatter = new HelpFormatter();
+
+ formatter.printHelp(120, TestLdap.class.getSimpleName(), null, options, null, true);
+ System.exit(0);
+ }
+
+ String url=cmd.getOptionValue(Flag.URL.getShort(), DEFAULT_LDAP_URL);
+ String username=cmd.getOptionValue(Flag.USERNAME.getShort(), DEFAULT_USERNAME);
+ String password=cmd.getOptionValue(Flag.PASSWORD.getShort(), DEFAULT_PASSWORD);
+
+ // Run all the tests
+ runLdapTestCases(url, username, password, new String[] { "create", "delete", "findAll", "read", "search", "update", "testSecondOc" } );
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/TestManagerConverterFactory.java b/odm/src/test/java/org/springframework/ldap/odm/test/TestManagerConverterFactory.java
new file mode 100755
index 00000000..e375d96f
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/TestManagerConverterFactory.java
@@ -0,0 +1,87 @@
+package org.springframework.ldap.odm.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+import org.springframework.ldap.odm.test.utils.ExecuteRunnable;
+import org.springframework.ldap.odm.test.utils.RunnableTest;
+import org.springframework.ldap.odm.typeconversion.ConverterManager;
+import org.springframework.ldap.odm.typeconversion.impl.Converter;
+import org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean;
+
+public class TestManagerConverterFactory {
+ private static class NullConverter implements Converter {
+ public T convert(Object source, Class toClass) throws Exception {
+ return null;
+ }
+ }
+ private static final Converter nullConverter=new NullConverter();
+
+ private static class ConverterConfigTestData {
+ private Class>[] fromClasses;
+ private String syntax;
+ private Class>[] toClasses;
+
+ private ConverterConfigTestData(Class>[] fromClasses, String syntax, Class>[] toClasses) {
+ this.fromClasses=fromClasses;
+ this.syntax=syntax;
+ this.toClasses=toClasses;
+ }
+ }
+
+ private static ConverterConfigTestData[] converterConfigTestData=new ConverterConfigTestData[] {
+ new ConverterConfigTestData(new Class>[] { String.class }, "", new Class>[] { Integer.class }),
+ new ConverterConfigTestData(new Class>[] { Byte.class, java.lang.Integer.class }, "", new Class>[] { String.class, Long.class }),
+ new ConverterConfigTestData(new Class>[] { String.class }, "123", new Class>[] { java.net.URI.class }),
+ };
+
+ private static class ConverterTestData {
+ private final Class> fromClass;
+ private final String syntax;
+ private final Class> toClass;
+ private final boolean canConvert;
+
+ private ConverterTestData(Class> fromClass, String syntax, Class> toClass, boolean canConvert) {
+ this.fromClass=fromClass;
+ this.syntax=syntax;
+ this.toClass=toClass;
+ this.canConvert=canConvert;
+ }
+ }
+
+ private ConverterTestData[] converterTestData=new ConverterTestData[] {
+ new ConverterTestData(java.lang.String.class, "", java.lang.Integer.class, true),
+ new ConverterTestData(java.lang.Byte.class, "", java.lang.Long.class, true),
+ new ConverterTestData(java.lang.Integer.class, "444", java.lang.String.class, true),
+ new ConverterTestData(java.lang.String.class, "123", java.net.URI.class, true),
+ new ConverterTestData(java.lang.String.class, "123", java.lang.Byte.class, false),
+ new ConverterTestData(java.lang.Byte.class, "", java.lang.Integer.class, false)
+ };
+
+ @Test
+ public void testConverterFactory() throws Exception {
+ ConverterManagerFactoryBean converterManagerFactory=new ConverterManagerFactoryBean();
+ Set configList=new HashSet();
+ for (ConverterConfigTestData config:converterConfigTestData) {
+ ConverterManagerFactoryBean.ConverterConfig converterConfig=new ConverterManagerFactoryBean.ConverterConfig();
+ converterConfig.setFromClasses(new HashSet>(Arrays.asList(config.fromClasses)));
+ converterConfig.setSyntax(config.syntax);
+ converterConfig.setToClasses(new HashSet>(Arrays.asList(config.toClasses)));
+ converterConfig.setConverter(nullConverter);
+ configList.add(converterConfig);
+ }
+ converterManagerFactory.setConverterConfig(configList);
+ final ConverterManager converterManager=(ConverterManager)converterManagerFactory.getObject();
+
+ new ExecuteRunnable().runTests(new RunnableTest() {
+ public void runTest(ConverterTestData testData) {
+ assertEquals(testData.canConvert,
+ converterManager.canConvert(testData.fromClass, testData.syntax, testData.toClass));
+ }
+ }, converterTestData);
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/TestSchemaToJava.java b/odm/src/test/java/org/springframework/ldap/odm/test/TestSchemaToJava.java
new file mode 100755
index 00000000..c625e86c
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/TestSchemaToJava.java
@@ -0,0 +1,204 @@
+package org.springframework.ldap.odm.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Iterator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.support.LdapContextSource;
+import org.springframework.ldap.odm.core.impl.OdmManagerImpl;
+import org.springframework.ldap.odm.test.utils.CompilerInterface;
+import org.springframework.ldap.odm.test.utils.GetFreePort;
+import org.springframework.ldap.odm.tools.SchemaToJava;
+import org.springframework.ldap.odm.typeconversion.impl.Converter;
+import org.springframework.ldap.odm.typeconversion.impl.ConverterManagerImpl;
+import org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter;
+import org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter;
+import org.springframework.ldap.test.LdapTestUtils;
+
+// Tests the generation of entry Java classes from LDAP schema
+public final class TestSchemaToJava {
+ private static final Log LOG = LogFactory.getLog(TestLdap.class);
+
+ private static final DistinguishedName baseName = new DistinguishedName("o=Whoniverse");
+
+ private static final String tempDir=System.getProperty("java.io.tmpdir");
+
+ // These unit tests require this port to free on localhost
+ private static int port;
+
+ private ConverterManagerImpl converterManager;
+
+ private LdapContextSource contextSource;
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ // Added because the close down of Apache DS on Linux does
+ // not seem to free up its port.
+ port=GetFreePort.getFreePort();
+
+ // Start an in process LDAP server
+ LdapTestUtils.startApacheDirectoryServer(port, baseName.toString(), "odm-test", "", "", null);
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws Exception {
+ // Stop the in process LDAP server
+ LdapTestUtils.destroyApacheDirectoryServer("", "");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ // Create some basic converters and a converter manager
+ converterManager = new ConverterManagerImpl();
+
+ Converter ptc = new FromStringConverter();
+ converterManager.addConverter(String.class, "", Byte.class, ptc);
+ converterManager.addConverter(String.class, "", Short.class, ptc);
+ converterManager.addConverter(String.class, "", Integer.class, ptc);
+ converterManager.addConverter(String.class, "", Long.class, ptc);
+ converterManager.addConverter(String.class, "", Double.class, ptc);
+ converterManager.addConverter(String.class, "", Float.class, ptc);
+ converterManager.addConverter(String.class, "", Boolean.class, ptc);
+
+ Converter tsc = new ToStringConverter();
+ converterManager.addConverter(Byte.class, "", String.class, tsc);
+ converterManager.addConverter(Short.class, "", String.class, tsc);
+ converterManager.addConverter(Integer.class, "", String.class, tsc);
+ converterManager.addConverter(Long.class, "", String.class, tsc);
+ converterManager.addConverter(Double.class, "", String.class, tsc);
+ converterManager.addConverter(Float.class, "", String.class, tsc);
+ converterManager.addConverter(Boolean.class, "", String.class, tsc);
+
+ // Bind to the directory
+ contextSource = new LdapContextSource();
+ contextSource.setUrl("ldap://127.0.0.1:" + port);
+ contextSource.setUserDn("");
+ contextSource.setPassword("");
+ contextSource.setPooled(false);
+ contextSource.afterPropertiesSet();
+
+ // Clear out any old data - and load the test data
+ LdapTestUtils.cleanAndSetup(contextSource, baseName, new ClassPathResource("testdata.ldif"));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ LdapTestUtils.destroyApacheDirectoryServer("", "");
+
+ contextSource=null;
+ converterManager=null;
+ }
+
+ // Figure out the path of the created Java file
+ private static String calculateOutputDirectory(String outputDir, String packageName) {
+ // Convert the package name to a path
+ Pattern pattern=Pattern.compile("\\.");
+ Matcher matcher=pattern.matcher(packageName);
+ String sepToUse=File.separator;
+ if (sepToUse.equals("\\")) {
+ sepToUse="\\\\";
+ }
+
+ return outputDir+File.separator+matcher.replaceAll(sepToUse);
+ }
+
+ // Due of the nature of the code under test this unit test is a little unusual:
+ //
+ // 1) Generate an entry class corresponding to objects classes
+ // "inetorgperson, organizationalperson, person, top"
+ // using the SchemaToJavaTool
+ // 2) Compile the generated code
+ // 3) Create an OdmManager to managing the newly created
+ // entry class.
+ // 4) Use this OdmManager to read an entry from LDAP and check the results.
+ //
+ @Test
+ @Ignore
+ public void generate() throws Exception {
+ final String className="Person";
+ final String packageName="org.springframework.ldap.odm.testclasses";
+
+ // Add classes dir to class path - needed for compilation
+ System.setProperty("java.class.path",
+ System.getProperty("java.class.path")+File.pathSeparator+"target/classes");
+
+ String[] flags=new String[] {
+ "--url", "ldap://127.0.0.1:"+port,
+ "--objectclasses", "inetorgperson",
+ "--syntaxmap", "target/test-classes/syntax-to-class-map.txt",
+ "--class", className,
+ "--package", packageName,
+ "--outputdir", tempDir };
+
+ // Generate the code using SchemaToJava
+ SchemaToJava.main(flags);
+
+ // Java 5 - we'll use the Java 6 Compiler API once we can drop support for Java 5.
+ String javaDir = calculateOutputDirectory(tempDir, packageName);
+
+ CompilerInterface.compile(javaDir, className+".java");
+ // Java 5
+
+ // OK it compiles so lets load our new class
+ URL[] urls = new URL[] { new File(tempDir).toURI().toURL() };
+ URLClassLoader ucl = new URLClassLoader(urls, getClass().getClassLoader());
+ Class> clazz = ucl.loadClass(packageName+"."+className);
+
+ // Create our OdmManager using our new class
+ OdmManagerImpl odmManager = new OdmManagerImpl(converterManager, contextSource);
+ odmManager.addManagedClass(clazz);
+
+ // And try reading from the directory using it
+ DistinguishedName testDn=new DistinguishedName(baseName);
+ testDn.addAll(new DistinguishedName("cn=William Hartnell,ou=Doctors"));
+ Object fromDirectory=odmManager.read(clazz, testDn);
+
+ LOG.debug(String.format("Read - %1$s", fromDirectory));
+
+ // Check some returned values
+ Method getDnMethod=clazz.getMethod("getDn");
+ Object dn=getDnMethod.invoke(fromDirectory);
+ assertEquals(testDn, dn);
+
+ Method getCnIteratorMethod=clazz.getMethod("getCnIterator");
+ @SuppressWarnings("unchecked")
+ Iterator cnIterator=(Iterator)getCnIteratorMethod.invoke(fromDirectory);
+ int cnCount=0;
+ while (cnIterator.hasNext()) {
+ cnCount++;
+ assertEquals("William Hartnell", cnIterator.next());
+ }
+ assertEquals(1, cnCount);
+
+ Method telephoneNumberIteratorMethod=clazz.getMethod("getTelephoneNumberIterator");
+ @SuppressWarnings("unchecked")
+ Iterator telephoneNumberIterator=(Iterator)telephoneNumberIteratorMethod.invoke(fromDirectory);
+ int telephoneNumberCount=0;
+ while (telephoneNumberIterator.hasNext()) {
+ telephoneNumberCount++;
+ assertEquals(Integer.valueOf(1), telephoneNumberIterator.next());
+ }
+ assertEquals(1, telephoneNumberCount);
+
+ // Reread and check whether equals and hashCode are at least sane
+ Object fromDirectory2=odmManager.read(clazz, testDn);
+ assertEquals(fromDirectory, fromDirectory2);
+ assertEquals(fromDirectory.hashCode(), fromDirectory2.hashCode());
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/TestSchemaViewer.java b/odm/src/test/java/org/springframework/ldap/odm/test/TestSchemaViewer.java
new file mode 100755
index 00000000..6bc26524
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/TestSchemaViewer.java
@@ -0,0 +1,125 @@
+package org.springframework.ldap.odm.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.odm.test.utils.ExecuteRunnable;
+import org.springframework.ldap.odm.test.utils.GetFreePort;
+import org.springframework.ldap.odm.test.utils.RunnableTest;
+import org.springframework.ldap.odm.tools.SchemaViewer;
+import org.springframework.ldap.test.LdapTestUtils;
+
+public final class TestSchemaViewer {
+ // Base DN for test data
+ private static final DistinguishedName baseName = new DistinguishedName("o=Whoniverse");
+
+ private static final String PRINCIPAL="";
+ private static final String CREDENTIALS="";
+ private static final String lineSeparator = System.getProperty ("line.separator");
+
+ private static int port;
+
+ private static String[] commonFlags;
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ // Added because the close down of Apache DS on Linux does
+ // not seem to free up its port.
+ port=GetFreePort.getFreePort();
+
+ commonFlags=new String[] {
+ "--url", "ldap://127.0.0.1:"+port,
+ "--username", "",
+ "--password", "",
+ "--error"};
+
+ // Start an in process LDAP server
+ LdapTestUtils.startApacheDirectoryServer(port, baseName.toString(), "odm-test", "", "", null);
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws Exception {
+ LdapTestUtils.destroyApacheDirectoryServer(PRINCIPAL, CREDENTIALS);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ private static String runSchemaViewer(String... flags) {
+ String result=null;
+ PrintStream originalOut=System.out;
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ try {
+ System.setErr(new PrintStream(output));
+
+ List commandFlags=
+ new ArrayList(Arrays.asList(commonFlags));
+ commandFlags.addAll(Arrays.asList(flags));
+
+ SchemaViewer.main(commandFlags.toArray(new String[0]));
+
+ // Turn end of lines into | for portability
+ result=output.toString().trim().replace(lineSeparator, "|");
+
+ } finally {
+ System.setErr(originalOut);
+ }
+
+ return result;
+ }
+
+ private static class TestData {
+ private final String flag;
+ private final String value;
+ private final String result;
+
+ public TestData(String flag, String value, String result) {
+ this.flag=flag;
+ this.value=value;
+ this.result=result;
+ }
+ }
+
+ // This makes the test dependent on the order in which the data is returned - it is invalid to assume that this will not change
+ private static TestData[] viewerTestData=new TestData[] {
+ new TestData("-o", "top",
+ "NAME:top|MUST:objectClass |NAME:top |NUMERICOID:2.5.6.0 |DESC:top of the superclass chain |ABSTRACT:true"),
+ new TestData("-o", "country",
+ "NAME:country|MUST:c |SUP:top |NAME:country |STRUCTURAL:true |NUMERICOID:2.5.6.2 |DESC:RFC2256: a country |MAY:searchGuide description"),
+ new TestData("-a", "sn",
+ "NAME:sn|SUP:name |SYNTAX:1.3.6.1.4.1.1466.115.121.1.15 |NAME:sn surname |EQUALITY:caseIgnoreMatch |SUBSTR:caseIgnoreSubstringsMatch |USAGE:userApplications |NUMERICOID:2.5.4.4 |DESC:RFC2256: last (family) name(s) for which the entity is known by"),
+ new TestData("-a", "jpegPhoto",
+ "NAME:jpegPhoto|SYNTAX:1.3.6.1.4.1.1466.115.121.1.28 |NAME:jpegPhoto |USAGE:userApplications |NUMERICOID:0.9.2342.19200300.100.1.60 |DESC:RFC2798: a JPEG image"),
+ new TestData("-s", "jpeg",
+ "NAME:jpeg|NAME:JPEG |NUMERICOID:1.3.6.1.4.1.1466.115.121.1.28"),
+ new TestData("-s", "OID",
+ "NAME:OID|NAME:OID |NUMERICOID:1.3.6.1.4.1.1466.115.121.1.38"),
+ };
+
+ // Very simple test - mainly just to exercise the code and to
+ // ensure we get representative test coverage
+ @Test
+ public void testSchemaViewer() throws Exception {
+ new ExecuteRunnable().runTests(new RunnableTest() {
+ public void runTest(TestData testData) {
+ String result=runSchemaViewer(testData.flag, testData.value);
+ assertEquals(testData.result, result);
+ }
+ }, viewerTestData);
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/TestsWithJdepend.java b/odm/src/test/java/org/springframework/ldap/odm/test/TestsWithJdepend.java
new file mode 100755
index 00000000..82920860
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/TestsWithJdepend.java
@@ -0,0 +1,27 @@
+package org.springframework.ldap.odm.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import jdepend.framework.JDepend;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestsWithJdepend {
+ private JDepend jdepend;
+
+ @Before
+ public void setUp() throws IOException {
+ jdepend = new JDepend();
+ jdepend.addDirectory("target/classes");
+ }
+
+ @Test
+ public void testAllPackages() {
+ jdepend.analyze();
+ assertEquals(false, jdepend.containsCycles());
+ }
+
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/UriConverter.java b/odm/src/test/java/org/springframework/ldap/odm/test/UriConverter.java
new file mode 100755
index 00000000..2922989a
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/UriConverter.java
@@ -0,0 +1,29 @@
+package org.springframework.ldap.odm.test;
+
+import java.net.URI;
+
+import org.springframework.ldap.odm.typeconversion.impl.Converter;
+
+/**
+ * A bi-directional converter between {@link java.net.URI} and {@link java.lang.String}.
+ *
+ * @author Paul Harvey <paul.at.pauls-place.me.uk>
+ */
+public class UriConverter implements Converter {
+
+ /* (non-Javadoc)
+ * @see org.springframework.ldap.odm.typeconversion.impl.Converter#convert(java.lang.Object, java.lang.Class)
+ */
+ public T convert(Object source, Class toClass) throws Exception {
+ T result = null;
+ if (String.class.isAssignableFrom(source.getClass()) && toClass == URI.class) {
+ result = toClass.cast(new URI((String)source));
+ } else {
+ if (URI.class.isAssignableFrom(source.getClass()) && toClass == String.class) {
+ result = toClass.cast(source.toString());
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/utils/CompilerInterface.java b/odm/src/test/java/org/springframework/ldap/odm/test/utils/CompilerInterface.java
new file mode 100755
index 00000000..e4cb6f21
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/utils/CompilerInterface.java
@@ -0,0 +1,35 @@
+package org.springframework.ldap.odm.test.utils;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class CompilerInterface {
+ // Compile the given file - when we can drop Java 5 we'll use the Java 6 compiler API
+ public static void compile(String directory, String file) throws Exception {
+
+ ProcessBuilder pb = new ProcessBuilder(
+ new String[] { "javac",
+ "-cp", "."+File.pathSeparatorChar+"target"+File.separatorChar+"classes"+
+ File.pathSeparatorChar+System.getProperty("java.class.path"),
+ directory+File.separatorChar+file });
+
+ pb.redirectErrorStream(true);
+ Process proc = pb.start();
+ InputStream is = proc.getInputStream();
+ InputStreamReader isr = new InputStreamReader(is);
+
+ char[] buf = new char[1024];
+ int count;
+ StringBuilder builder = new StringBuilder();
+ while ((count = isr.read(buf)) > 0) {
+ builder.append(buf, 0, count);
+ }
+
+ boolean ok = proc.waitFor() == 0;
+
+ if (!ok) {
+ throw new RuntimeException(builder.toString());
+ }
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/utils/ExecuteRunnable.java b/odm/src/test/java/org/springframework/ldap/odm/test/utils/ExecuteRunnable.java
new file mode 100755
index 00000000..68e182be
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/utils/ExecuteRunnable.java
@@ -0,0 +1,19 @@
+package org.springframework.ldap.odm.test.utils;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+// Simple utility class to run a given test over a set of test data
+public final class ExecuteRunnable {
+
+ public void runTests(RunnableTest runnableTest, U[] testData) throws Exception {
+ StackTraceElement ste = Thread.currentThread().getStackTrace()[2];
+ Log LOG = LogFactory.getLog(ste.getClassName());
+ for (U testDatum : testData) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(String.format("Running test with data %1$s", testDatum));
+ }
+ runnableTest.runTest(testDatum);
+ }
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/utils/GetFreePort.java b/odm/src/test/java/org/springframework/ldap/odm/test/utils/GetFreePort.java
new file mode 100755
index 00000000..b53b6124
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/utils/GetFreePort.java
@@ -0,0 +1,24 @@
+package org.springframework.ldap.odm.test.utils;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+// Added because the close down of the embedded Apache DS used
+// for unit testing does not seem to free up its port.
+public class GetFreePort {
+ private static Log LOG=LogFactory.getLog(GetFreePort.class);
+
+ public static int getFreePort()
+ throws IOException {
+ ServerSocket server = new ServerSocket(0);
+ int port = server.getLocalPort();
+ server.close();
+
+ LOG.debug(String.format("Port number: %1$s", port));
+
+ return port;
+ }
+}
diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/utils/RunnableTest.java b/odm/src/test/java/org/springframework/ldap/odm/test/utils/RunnableTest.java
new file mode 100755
index 00000000..3e7c6130
--- /dev/null
+++ b/odm/src/test/java/org/springframework/ldap/odm/test/utils/RunnableTest.java
@@ -0,0 +1,6 @@
+package org.springframework.ldap.odm.test.utils;
+
+// Interface to implement for tests to be run by ExecuteRunnable
+public interface RunnableTest {
+ void runTest(T testData) throws Exception;
+}
diff --git a/odm/src/test/resources/syntax-to-class-map.txt b/odm/src/test/resources/syntax-to-class-map.txt
new file mode 100755
index 00000000..98a7d860
--- /dev/null
+++ b/odm/src/test/resources/syntax-to-class-map.txt
@@ -0,0 +1,7 @@
+# List of attribute syntax to java class mappings
+
+# Syntax Java class
+# ------ ----------
+
+1.3.6.1.4.1.1466.115.121.1.50, java.lang.Integer
+1.3.6.1.4.1.1466.115.121.1.40, [B
diff --git a/odm/src/test/resources/testdata.ldif b/odm/src/test/resources/testdata.ldif
new file mode 100755
index 00000000..14bb89ed
--- /dev/null
+++ b/odm/src/test/resources/testdata.ldif
@@ -0,0 +1,142 @@
+dn: ou=Enemies,o=Whoniverse
+objectClass: organizationalUnit
+objectClass: top
+ou: Enemies
+street: Acacia Avenue
+description: The bad guys
+
+dn: ou=Assistants,o=Whoniverse
+objectClass: organizationalUnit
+objectClass: top
+ou: Assistants
+street: Somewhere in space
+description: The plucky helpers
+
+dn: ou=Doctors,o=Whoniverse
+objectClass: organizationalUnit
+objectClass: top
+ou: Doctors
+street: Somewhere in time
+description: Our hero
+
+dn: cn=William Hartnell,ou=Doctors,o=Whoniverse
+objectClass: person
+objectClass: inetorgperson
+objectclass: organizationalperson
+objectClass: top
+cn: William Hartnell
+description: First Doctor
+description: Grumpy
+sn: Hartnell
+telephonenumber: 1
+
+dn: cn=Patrick Troughton,ou=Doctors,o=Whoniverse
+objectClass: person
+objectClass: inetorgperson
+objectclass: organizationalperson
+objectClass: top
+cn: Patrick Troughton
+description: Second Doctor
+description:Clown
+sn: Troughton
+telephonenumber: 2
+
+dn: cn=Jon Pertwee,ou=Doctors,o=Whoniverse
+objectClass: person
+objectClass: inetorgperson
+objectclass: organizationalperson
+objectClass: top
+cn: Jon Pertwee
+description: Third Doctor
+description: Dandy
+sn: Pertwee
+telephonenumber: 3
+
+dn: cn=Tom Baker,ou=Doctors,o=Whoniverse
+objectClass: person
+objectClass: inetorgperson
+objectclass: organizationalperson
+objectClass: top
+cn: Tom Baker
+description: Fourth Doctor
+description: The one and only!
+sn: Baker
+telephonenumber: 4
+
+dn: cn=Peter Davison,ou=Doctors,o=Whoniverse
+objectClass: person
+objectClass: inetorgperson
+objectclass: organizationalperson
+objectClass: top
+cn: Peter Davison
+description: Fifth Doctor
+sn: Davison
+telephonenumber: 5
+
+dn: cn=Paul Harvey,ou=Doctors,o=Whoniverse
+objectClass: person
+objectClass: inetorgperson
+objectclass: organizationalperson
+objectClass: top
+objectClass: userSecurityInformation
+cn: Paul Harvey
+description: Not a Doctor
+sn: Harvey
+telephonenumber: 11
+
+dn: cn=Bramble Harvey,ou=Doctors,o=Whoniverse
+objectClass: person
+objectClass: top
+cn: Bramble
+description: Really not a Doctor
+sn: Harvey
+telephonenumber: 22
+
+dn: cn=Davros,ou=Enemies,o=Whoniverse
+objectClass: person
+objectClass: inetorgperson
+objectclass: organizationalperson
+objectClass: top
+cn: Davros
+description: Creator of the Daleks
+description: Kaled head scientist
+sn: Unknown
+
+dn: cn=Daleks,ou=Enemies,o=Whoniverse
+objectClass: person
+objectClass: inetorgperson
+objectclass: organizationalperson
+objectClass: top
+cn: Daleks
+description: The Doctor's greatest foe
+sn: NA
+
+dn: cn=Master,ou=Enemies,o=Whoniverse
+objectClass: person
+objectClass: inetorgperson
+objectclass: organizationalperson
+objectClass: top
+cn: Master
+description: An evil Time Lord
+sn: Unknown
+jpegphoto:: /9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkS
+ Ew8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRg
+ yIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wA
+ ARCAAnABoDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAA
+ gEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcY
+ GRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipK
+ TlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8v
+ P09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFB
+ AQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygp
+ KjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJm
+ aoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9
+ oADAMBAAIRAxEAPwDx2z0mK6gV/tSo2SCpUHGD9Qa2vDvgGfX7+eI3ot4EOEmEefMOOcZPQdOvW
+ s3VLGzsWiihUvM0ayONxATPIHXk4wfxFdd4L1aw0rw3eS3V3GHWRswkgugwCpVSckEk9OhB9azu
+ 2ro6lCKnyyZwmvaHcaBrV1plxKkkluw+dRkMCAQfbgjiqi27sob1GfuVc1q9n1XVL3UdqxJI/wD
+ q/MXIXGAMd+B2HWoUZ1jUbZeABwKqzMJWT0Ld3cfaJ5ZFYtnCg+oUBf6V2XhvwVpcvhxfFXibVF
+ tNK3lEgjJ3yMGK7SRzkkHhRnHORXJQ6exVXB2r1K9Cfx7V0UWpeD7S3WJ9N1TUPLYssV7cBYkZg
+ AxAU4zwOcZ4HoKtppWQ4tSk5SNPx14o8M3/AITXT/D2kRRxB1jE4RIyuOc7ME84PzcHr2PPmX2y
+ YcZX8q37fQxPpqzsH8xvuqAeeuO/0H3e9Zn2dxwbK5JHXCH/AOJqNOhdSM1ZzVjRNwRbjGRxxzn
+ FdRouijV9Pi1C6toEMqn7MsK7SqBiCxOSSSVI5J4x6miiliW1FMzpbszreUw3H2VcuscpGT3UE9
+ Rn0HStE+IrZSQbEuRxvU4De4yc0UVFKCnuerms2vZryP/Z
+
diff --git a/parent/pom.xml b/parent/pom.xml
index 8ab58a10..f0c5f2f7 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -395,13 +395,28 @@
commons-lang
commons-lang
- 2.1
+ 2.4
commons-pool
commons-pool
1.3
+
+ commons-cli
+ commons-cli
+ 1.2
+
+
+ commons-codec
+ commons-codec
+ 1.3
+
+
+ org.freemarker
+ freemarker
+ 2.3.9
+
commons-logging
@@ -433,6 +448,13 @@
2.0.1
test
+
+ jdepend
+ jdepend
+ 2.9.1
+ jar
+ test
+
diff --git a/pom.xml b/pom.xml
index dbda7ff3..808a0788 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,6 +64,12 @@
Eric Dalquist
+
+ Keith Barlow
+
+
+ Paul Harvey
+
The Spring LDAP Framework
@@ -109,6 +115,7 @@
parent
core
core-tiger
+ odm
test-support
test
diff --git a/samples/simple-odm/pom.xml b/samples/simple-odm/pom.xml
new file mode 100644
index 00000000..4c372bb7
--- /dev/null
+++ b/samples/simple-odm/pom.xml
@@ -0,0 +1,104 @@
+
+
+
+
+ org.springframework.ldap
+ spring-ldap-parent
+ 1.3.1.CI-SNAPSHOT
+
+ 4.0.0
+ org.springframework.ldap
+ spring-ldap-odm-sample
+ Simple Spring LDAP ODM Sample
+ A very simple example of using the the Spring LDAP ODM
+
+
+ Paul Harvey
+ paul@pauls-place.me.uk
+
+ Developer
+
+ 0
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ org.springframework.ldap.odm.sample.SearchForPeople
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 1.5
+ 1.5
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-report-plugin
+
+ always
+
+
+
+
+
+
+ log4j
+ log4j
+ runtime
+
+
+ org.springframework.ldap
+ spring-ldap-odm
+ ${version}
+
+
+ junit
+ junit
+ jar
+ 4.4
+ test
+
+
+ org.springframework.ldap
+ spring-ldap-test
+ ${version}
+ test
+
+
+ org.springframework
+ spring-core
+
+
+ org.springframework
+ spring-context
+
+
+ commons-logging
+ commons-logging
+
+
+ org.springframework.ldap
+ spring-ldap-core
+ ${version}
+
+
+ commons-lang
+ commons-lang
+ jar
+
+
+
diff --git a/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SearchForPeople.java b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SearchForPeople.java
new file mode 100755
index 00000000..fb88825d
--- /dev/null
+++ b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SearchForPeople.java
@@ -0,0 +1,39 @@
+package org.springframework.ldap.odm.sample;
+
+import java.util.List;
+
+import javax.naming.directory.SearchControls;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.odm.core.OdmManager;
+
+// A very simple example - just showing how little code you actually need to write
+// when using Spring LDAP ODM
+public class SearchForPeople {
+ private static final SearchControls searchControls =
+ new SearchControls(SearchControls.SUBTREE_SCOPE, 100, 10000,
+ null, true, false);
+
+ private static final DistinguishedName baseDn = new DistinguishedName("o=Whoniverse");
+
+ private static void print(List personList) {
+ for (SimplePerson person : personList) {
+ System.out.println(person);
+ }
+ }
+
+ public static void main(String[] argv) {
+ ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "spring.xml" });
+
+ // Grab the OdmManager wired by Spring
+ OdmManager odmManager = (OdmManager)context.getBean("odmManager");
+
+ // Find people with a surname of Harvey
+ List searchResults = odmManager.search(SimplePerson.class, baseDn, "sn=Harvey", searchControls);
+
+ // Print the results
+ print(searchResults);
+ }
+}
diff --git a/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SimplePerson.java b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SimplePerson.java
new file mode 100755
index 00000000..91ac26fb
--- /dev/null
+++ b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SimplePerson.java
@@ -0,0 +1,218 @@
+package org.springframework.ldap.odm.sample;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.HashSet;
+import java.util.ArrayList;
+
+import javax.naming.Name;
+
+import static org.springframework.ldap.odm.annotations.Attribute.*;
+
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+
+/**
+* Automatically generated to represent the LDAP object classes
+* "person", "top".
+*/
+@Entry(objectClasses={"person", "top"})
+public final class SimplePerson {
+
+ @Id
+ private Name dn;
+
+ @Attribute(name="objectClass", syntax="1.3.6.1.4.1.1466.115.121.1.38")
+ private List objectClass=new ArrayList();
+
+
+ @Attribute(name="cn", syntax="1.3.6.1.4.1.1466.115.121.1.15")
+ private List cn=new ArrayList();
+
+
+ @Attribute(name="sn", syntax="1.3.6.1.4.1.1466.115.121.1.15")
+ private List sn=new ArrayList();
+
+
+ @Attribute(name="description", syntax="1.3.6.1.4.1.1466.115.121.1.15")
+ private List description=new ArrayList();
+
+
+ @Attribute(name="userPassword", syntax="1.3.6.1.4.1.1466.115.121.1.40", type=Type.BINARY)
+ private List userPassword=new ArrayList();
+
+
+ @Attribute(name="telephoneNumber", syntax="1.3.6.1.4.1.1466.115.121.1.50")
+ private List telephoneNumber=new ArrayList();
+
+
+ @Attribute(name="seeAlso", syntax="1.3.6.1.4.1.1466.115.121.1.12")
+ private List