From 7c4c15e88a72b7b548ac54d11737d7cf3015b271 Mon Sep 17 00:00:00 2001 From: Ulrik Sandberg Date: Sun, 24 Jan 2010 00:58:28 +0000 Subject: [PATCH] Added Paul Harvey's Spring LDAP Object-Directory Mapper (ODM). --- odm/pom.xml | 173 +++++ .../ldap/odm/annotations/Attribute.java | 65 ++ .../ldap/odm/annotations/Entry.java | 25 + .../ldap/odm/annotations/Id.java | 23 + .../ldap/odm/annotations/Transient.java | 19 + .../ldap/odm/annotations/package-info.java | 9 + .../ldap/odm/core/OdmException.java | 20 + .../ldap/odm/core/OdmManager.java | 97 +++ .../ldap/odm/core/impl/AttributeMetaData.java | 226 +++++++ .../ldap/odm/core/impl/CaseIgnoreString.java | 32 + .../odm/core/impl/InvalidEntryException.java | 20 + .../ldap/odm/core/impl/MetaDataException.java | 20 + .../ldap/odm/core/impl/ObjectMetaData.java | 124 ++++ .../ldap/odm/core/impl/OdmManagerImpl.java | 474 +++++++++++++ .../core/impl/OdmManagerImplFactoryBean.java | 90 +++ .../core/impl/UnmanagedClassException.java | 21 + .../ldap/odm/core/impl/package-info.java | 9 + .../ldap/odm/core/package-info.java | 10 + .../ldap/odm/tools/AttributeSchema.java | 127 ++++ .../ldap/odm/tools/ObjectSchema.java | 91 +++ .../ldap/odm/tools/SchemaReader.java | 157 +++++ .../ldap/odm/tools/SchemaToJava.java | 442 +++++++++++++ .../ldap/odm/tools/SchemaViewer.java | 233 +++++++ .../ldap/odm/tools/SyntaxToJavaClass.java | 63 ++ .../ldap/odm/tools/package-info.java | 8 + .../typeconversion/ConverterException.java | 19 + .../odm/typeconversion/ConverterManager.java | 31 + .../odm/typeconversion/impl/Converter.java | 19 + .../impl/ConverterManagerFactoryBean.java | 193 ++++++ .../impl/ConverterManagerImpl.java | 161 +++++ .../impl/converters/FromStringConverter.java | 24 + .../impl/converters/ToStringConverter.java | 21 + .../impl/converters/package-info.java | 7 + .../odm/typeconversion/impl/package-info.java | 8 + .../ldap/odm/typeconversion/package-info.java | 10 + odm/src/main/resources/log4j.properties | 19 + .../ldap/odm/tools/binary-attributes.txt | 16 + .../ldap/odm/tools/oc-to-java.ftl | 163 +++++ .../ldap/odm/test/OrganizationalUnit.java | 137 ++++ .../springframework/ldap/odm/test/Person.java | 201 ++++++ .../ldap/odm/test/TestConverterManager.java | 207 ++++++ .../ldap/odm/test/TestLdap.java | 622 ++++++++++++++++++ .../odm/test/TestManagerConverterFactory.java | 87 +++ .../ldap/odm/test/TestSchemaToJava.java | 204 ++++++ .../ldap/odm/test/TestSchemaViewer.java | 125 ++++ .../ldap/odm/test/TestsWithJdepend.java | 27 + .../ldap/odm/test/UriConverter.java | 29 + .../odm/test/utils/CompilerInterface.java | 35 + .../ldap/odm/test/utils/ExecuteRunnable.java | 19 + .../ldap/odm/test/utils/GetFreePort.java | 24 + .../ldap/odm/test/utils/RunnableTest.java | 6 + .../test/resources/syntax-to-class-map.txt | 7 + odm/src/test/resources/testdata.ldif | 142 ++++ parent/pom.xml | 24 +- pom.xml | 7 + samples/simple-odm/pom.xml | 104 +++ .../ldap/odm/sample/SearchForPeople.java | 39 ++ .../ldap/odm/sample/SimplePerson.java | 218 ++++++ .../ldap/odm/sample/package-info.java | 7 + .../src/main/resources/log4j.properties | 19 + .../simple-odm/src/main/resources/spring.xml | 96 +++ .../odm/sample/test/TestSearchForPeople.java | 79 +++ .../src/test/resources/testdata.ldif | 116 ++++ src/assembly/all.xml | 1 + src/assembly/minimal.xml | 1 + src/assembly/sources.xml | 4 + src/assembly/with-dependencies.xml | 1 + 67 files changed, 5856 insertions(+), 1 deletion(-) create mode 100644 odm/pom.xml create mode 100755 odm/src/main/java/org/springframework/ldap/odm/annotations/Attribute.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/annotations/Entry.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/annotations/Id.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/annotations/Transient.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/annotations/package-info.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/OdmException.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/OdmManager.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/impl/AttributeMetaData.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/impl/CaseIgnoreString.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/impl/InvalidEntryException.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/impl/MetaDataException.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/impl/ObjectMetaData.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImpl.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImplFactoryBean.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/impl/UnmanagedClassException.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/impl/package-info.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/core/package-info.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/tools/AttributeSchema.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/tools/ObjectSchema.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/tools/SchemaReader.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/tools/SchemaToJava.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/tools/SchemaViewer.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/tools/SyntaxToJavaClass.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/tools/package-info.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/ConverterException.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/ConverterManager.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/Converter.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/ConverterManagerFactoryBean.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/ConverterManagerImpl.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/FromStringConverter.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/ToStringConverter.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/converters/package-info.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/impl/package-info.java create mode 100755 odm/src/main/java/org/springframework/ldap/odm/typeconversion/package-info.java create mode 100755 odm/src/main/resources/log4j.properties create mode 100755 odm/src/main/resources/org/springframework/ldap/odm/tools/binary-attributes.txt create mode 100755 odm/src/main/resources/org/springframework/ldap/odm/tools/oc-to-java.ftl create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/OrganizationalUnit.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/Person.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/TestConverterManager.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/TestLdap.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/TestManagerConverterFactory.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/TestSchemaToJava.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/TestSchemaViewer.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/TestsWithJdepend.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/UriConverter.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/utils/CompilerInterface.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/utils/ExecuteRunnable.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/utils/GetFreePort.java create mode 100755 odm/src/test/java/org/springframework/ldap/odm/test/utils/RunnableTest.java create mode 100755 odm/src/test/resources/syntax-to-class-map.txt create mode 100755 odm/src/test/resources/testdata.ldif create mode 100644 samples/simple-odm/pom.xml create mode 100755 samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SearchForPeople.java create mode 100755 samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SimplePerson.java create mode 100755 samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/package-info.java create mode 100755 samples/simple-odm/src/main/resources/log4j.properties create mode 100755 samples/simple-odm/src/main/resources/spring.xml create mode 100755 samples/simple-odm/src/test/java/org/springframework/ldap/odm/sample/test/TestSearchForPeople.java create mode 100755 samples/simple-odm/src/test/resources/testdata.ldif 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 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 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 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: + *

    + *
  1. Try to find and use a {@link Converter} registered for the + * fromClass, syntax and toClass and use it.
  2. + *
  3. If this fails, then if the toClass isAssignableFrom + * the fromClass then just assign it.
  4. + *
  5. If this fails try to find and use a {@link Converter} registered for the fromClass and + * the toClass ignoring the syntax.
  6. + *
  7. If this fails then throw a {@link org.springframework.ldap.odm.typeconversion.ConverterException}.
  8. + *
+ * + * @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/> + + "${listValue}"<#t/> + + + +<#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}; + + + + +<#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/> } + + <#else> + <#lt/> public Iterator get${attribute.name?cap_first}Iterator() { + <#lt/> return Collections.unmodifiableList(${attribute.name}).iterator(); + <#lt/> } + + + + + +<#macro equalsCode attributes> + <#list attributes as attribute> + <#lt/> append(${attribute.name}, other.${attribute.name}). + + + +<#macro hashCode attributes> + <#list attributes as attribute> + <#lt/> append(${attribute.name}). + + + +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}; + + +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}
  • + +*
+*/ +@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 schema.may as attribute> + <#lt/> append("${attribute.name}", ${attribute.name}). + + <#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 seeAlso=new ArrayList(); + + public Name getDn() { + return dn; + } + + public void setDn(Name dn) { + this.dn=dn; + } + + public Iterator getObjectClassIterator() { + return Collections.unmodifiableList(objectClass).iterator(); + } + + public void addCn(String cn) { + this.cn.add(cn); + } + + public void removeCn(String cn) { + this.cn.remove(cn); + } + + public Iterator getCnIterator() { + return cn.iterator(); + } + + public void addSn(String sn) { + this.sn.add(sn); + } + + public void removeSn(String sn) { + this.sn.remove(sn); + } + + public Iterator getSnIterator() { + return sn.iterator(); + } + + public void addDescription(String description) { + this.description.add(description); + } + + public void removeDescription(String description) { + this.description.remove(description); + } + + public Iterator getDescriptionIterator() { + return description.iterator(); + } + + public void addUserPassword(byte[] userPassword) { + this.userPassword.add(userPassword); + } + + public void removeUserPassword(byte[] userPassword) { + this.userPassword.remove(userPassword); + } + + public Iterator getUserPasswordIterator() { + return userPassword.iterator(); + } + + public void addTelephoneNumber(String telephoneNumber) { + this.telephoneNumber.add(telephoneNumber); + } + + public void removeTelephoneNumber(String telephoneNumber) { + this.telephoneNumber.remove(telephoneNumber); + } + + public Iterator getTelephoneNumberIterator() { + return telephoneNumber.iterator(); + } + + public void addSeeAlso(String seeAlso) { + this.seeAlso.add(seeAlso); + } + + public void removeSeeAlso(String seeAlso) { + this.seeAlso.remove(seeAlso); + } + + public Iterator getSeeAlsoIterator() { + return seeAlso.iterator(); + } + + @Override + public String toString() { + StringBuilder result=new StringBuilder(); + result.append(String.format("dn=%1$s", dn)); + result.append(String.format(" | objectClass=%1$s", objectClass)); + result.append(String.format(" | cn=%1$s", cn)); + result.append(String.format(" | sn=%1$s", sn)); + result.append(String.format(" | description=%1$s", description)); + result.append(String.format(" | userPassword=%1$s", userPassword)); + result.append(String.format(" | telephoneNumber=%1$s", telephoneNumber)); + result.append(String.format(" | seeAlso=%1$s", seeAlso)); + return result.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((dn == null) ? 0 : dn.hashCode()); + result = prime * result + ((objectClass == null) ? 0 : (new HashSet(objectClass)).hashCode()); + result = prime * result + ((cn == null) ? 0 : (new HashSet(cn)).hashCode()); + result = prime * result + ((sn == null) ? 0 : (new HashSet(sn)).hashCode()); + result = prime * result + ((description == null) ? 0 : (new HashSet(description)).hashCode()); + result = prime * result + ((userPassword == null) ? 0 : (new HashSet(userPassword)).hashCode()); + result = prime * result + ((telephoneNumber == null) ? 0 : (new HashSet(telephoneNumber)).hashCode()); + result = prime * result + ((seeAlso == null) ? 0 : (new HashSet(seeAlso)).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; + SimplePerson other = (SimplePerson) obj; + 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 (!(new HashSet(objectClass)).equals(new HashSet(other.objectClass))) + return false; + if (cn == null) { + if (other.cn != null) + return false; + } else if (!(new HashSet(cn)).equals(new HashSet(other.cn))) + return false; + if (sn == null) { + if (other.sn != null) + return false; + } else if (!(new HashSet(sn)).equals(new HashSet(other.sn))) + return false; + if (description == null) { + if (other.description != null) + return false; + } else if (!(new HashSet(description)).equals(new HashSet(other.description))) + return false; + if (userPassword == null) { + if (other.userPassword != null) + return false; + } else if (!(new HashSet(userPassword)).equals(new HashSet(other.userPassword))) + return false; + if (telephoneNumber == null) { + if (other.telephoneNumber != null) + return false; + } else if (!(new HashSet(telephoneNumber)).equals(new HashSet(other.telephoneNumber))) + return false; + if (seeAlso == null) { + if (other.seeAlso != null) + return false; + } else if (!(new HashSet(seeAlso)).equals(new HashSet(other.seeAlso))) + return false; + return true; + } +} diff --git a/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/package-info.java b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/package-info.java new file mode 100755 index 00000000..4c5cdbbc --- /dev/null +++ b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/package-info.java @@ -0,0 +1,7 @@ +/** + * A very simple example of the use of Spring ODM. + * + * @author Paul Harvey <paul.at.pauls-place.me.uk> + */ + +package org.springframework.ldap.odm.sample; \ No newline at end of file diff --git a/samples/simple-odm/src/main/resources/log4j.properties b/samples/simple-odm/src/main/resources/log4j.properties new file mode 100755 index 00000000..aa4032c5 --- /dev/null +++ b/samples/simple-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/samples/simple-odm/src/main/resources/spring.xml b/samples/simple-odm/src/main/resources/spring.xml new file mode 100755 index 00000000..60b69896 --- /dev/null +++ b/samples/simple-odm/src/main/resources/spring.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + java.lang.String + + + + + java.lang.Byte + java.lang.Short + java.lang.Integer + java.lang.Long + java.lang.Float + java.lang.Double + java.lang.Boolean + + + + + + + + java.lang.Byte + java.lang.Short + java.lang.Integer + java.lang.Long + java.lang.Float + java.lang.Double + java.lang.Boolean + + + + + java.lang.String + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.ldap.odm.sample.SimplePerson + + + + + diff --git a/samples/simple-odm/src/test/java/org/springframework/ldap/odm/sample/test/TestSearchForPeople.java b/samples/simple-odm/src/test/java/org/springframework/ldap/odm/sample/test/TestSearchForPeople.java new file mode 100755 index 00000000..cc754965 --- /dev/null +++ b/samples/simple-odm/src/test/java/org/springframework/ldap/odm/sample/test/TestSearchForPeople.java @@ -0,0 +1,79 @@ +package org.springframework.ldap.odm.sample.test; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +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.core.DistinguishedName; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.ldap.odm.sample.SearchForPeople; +import org.springframework.ldap.test.LdapTestUtils; + +public class TestSearchForPeople { + // Base DN for test data + private static final DistinguishedName baseName = new DistinguishedName("o=Whoniverse"); + + private static final String PRINCIPAL="uid=admin,ou=system"; + private static final String CREDENTIALS="secret"; + + // This port MUST be free on local host for these unit tests to function. + private static int PORT=10389; + + @BeforeClass + public static void setUpClass() throws Exception { + // Start an LDAP server and import test data + LdapTestUtils.startApacheDirectoryServer(PORT, baseName.toString(), "odm-test", PRINCIPAL, CREDENTIALS, null); + } + + @AfterClass + public static void tearDownClass() throws Exception { + LdapTestUtils.destroyApacheDirectoryServer(PRINCIPAL, CREDENTIALS); + } + + @Before + public void setUp() throws Exception { + // Bind to the directory + LdapContextSource contextSource = new LdapContextSource(); + contextSource.setUrl("ldap://127.0.0.1:" + PORT); + contextSource.setUserDn(""); + contextSource.setPassword(""); + contextSource.setPooled(false); + contextSource.afterPropertiesSet(); + + // Create the Sprint LDAP template + LdapTemplate template = new LdapTemplate(contextSource); + + // Clear out any old data - and load the test data + LdapTestUtils.cleanAndSetup(template.getContextSource(), baseName, new ClassPathResource("testdata.ldif")); + } + + @After + public void tearDown() { + } + + // Very simple test - mainly just to exercise the code and to + // ensure we get representative test coverage + @Test + public void runSample() throws Exception { + PrintStream originalOut=System.out; + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + System.setOut(new PrintStream(output)); + + SearchForPeople.main(null); + + assertEquals("dn=cn=Bramble Harvey,ou=Doctors,o=Whoniverse | objectClass=[person, top] | cn=[Bramble Harvey] | sn=[Harvey] | description=[Really not a Doctor] | userPassword=[] | telephoneNumber=[22] | seeAlso=[]", + output.toString().trim()); + } finally { + System.setOut(originalOut); + } + } +} diff --git a/samples/simple-odm/src/test/resources/testdata.ldif b/samples/simple-odm/src/test/resources/testdata.ldif new file mode 100755 index 00000000..c91e6aac --- /dev/null +++ b/samples/simple-odm/src/test/resources/testdata.ldif @@ -0,0 +1,116 @@ +dn: ou=Enemies,o=Whoniverse +objectClass: organizationalUnit +objectClass: top +ou: Blizzard + +dn: ou=Assistants,o=Whoniverse +objectClass: organizationalUnit +objectClass: top +ou: Blizzard + +dn: ou=Doctors,o=Whoniverse +objectClass: organizationalUnit +objectClass: top +ou: NCSoft + +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 + diff --git a/src/assembly/all.xml b/src/assembly/all.xml index 1ed89f55..2588fe2c 100644 --- a/src/assembly/all.xml +++ b/src/assembly/all.xml @@ -10,6 +10,7 @@ org.springframework.ldap:spring-ldap-core org.springframework.ldap:spring-ldap-core-tiger + org.springframework.ldap:spring-ldap-odm org.springframework.ldap:spring-ldap-test diff --git a/src/assembly/minimal.xml b/src/assembly/minimal.xml index 0d46b579..918953e9 100644 --- a/src/assembly/minimal.xml +++ b/src/assembly/minimal.xml @@ -13,6 +13,7 @@ org.springframework.ldap:spring-ldap-core org.springframework.ldap:spring-ldap-core-tiger + org.springframework.ldap:spring-ldap-odm org.springframework.ldap:spring-ldap-test diff --git a/src/assembly/sources.xml b/src/assembly/sources.xml index 81cceed8..a46d5fc4 100644 --- a/src/assembly/sources.xml +++ b/src/assembly/sources.xml @@ -14,6 +14,10 @@ core-tiger/src/main/java + + odm/src/main/java + + test-support/src/main/java diff --git a/src/assembly/with-dependencies.xml b/src/assembly/with-dependencies.xml index 0e7343ea..7d8b217e 100644 --- a/src/assembly/with-dependencies.xml +++ b/src/assembly/with-dependencies.xml @@ -13,6 +13,7 @@ org.springframework.ldap:spring-ldap-parent org.springframework.ldap:spring-ldap-core org.springframework.ldap:spring-ldap-core-tiger + org.springframework.ldap:spring-ldap-odm org.springframework.ldap:spring-ldap-test