Object-Directory Mapping (ODM)

Object-relational mapping frameworks (such as Hibernate and JPA) offer developers the ability to use annotations to map relational database tables to Java objects. The Spring LDAP project offers a similar ability with respect to LDAP directories through a number of methods in LdapOperations:

  • <T> T findByDn(Name dn, Class<T> clazz)

  • <T> T findOne(LdapQuery query, Class<T> clazz)

  • <T> List<T> find(LdapQuery query, Class<T> clazz)

  • <T> List<T> findAll(Class<T> clazz)

  • <T> List<T> findAll(Name base, SearchControls searchControls, Class<T> clazz)

  • <T> List<T> findAll(Name base, Filter filter, SearchControls searchControls, Class<T> clazz)

  • void create(Object entry)

  • void update(Object entry)

  • void delete(Object entry)

Annotations

Entity classes managed with the object mapping methods are required to be annotated with annotations from the org.springframework.ldap.odm.annotations package. The available annotations are:

  • @Entry: Class level annotation indicating the objectClass definitions to which the entity maps. (required)

  • @Id: Indicates the entity DN. The field declaring this attribute must be a derivative of the javax.naming.Name class. (required)

  • @Attribute: Indicates the mapping of a directory attribute to the object class field.

  • @DnAttribute: Indicates the mapping of a DN attribute to the object class field.

  • @Transient: Indicates the field is not persistent and should be ignored by the OdmManager.

The @Entry and @Id annotations are required to be declared on managed classes. @Entry is used to specify which object classes the entity maps to and (optionally) the directory root of the LDAP entries represented by the class. All object classes for which fields are mapped are required to be declared. Note that, when creating new entries of the managed class, only the declared object classes are used.

In order for a directory entry to be considered a match to the managed entity, all object classes declared by the directory entry must be declared by the @Entry annotation. For example, assume that you have entries in your LDAP tree that have the following object classes: inetOrgPerson,organizationalPerson,person,top. If you are interested only in changing the attributes defined in the person object class, you can annotate your @Entry with @Entry(objectClasses = { "person", "top" }). However, if you want to manage attributes defined in the inetOrgPerson objectclass, you need to use the following: @Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }).

All entity fields are mapped by their field name to LDAP attributes. The remaining annotations — @Id, @Attribute, @Transient, and @DnAttribute — affect how that mapping occurs.

First, the @Id annotation maps the distinguished name of the entry to a field. The field must be an instance of javax.naming.Name.

Second, the @Attribute annotation maps entity fields to LDAP attributes. This is handy when the attribute name is different from the field name. To use @Attribute, you must declare the name of the attribute to which the field maps. Optionally, you can also guarantee and exact match by including the syntax OID of the LDAP attribute. Finally, @Attribute also provides the type declaration, which lets you indicate whether the attribute is regarded as binary- or string-based by the LDAP JNDI provider.

Third, the @Transient annotation indicates that the given entity field does not map to an LDAP attribute.

Finally, the @DnAttribute annotation additionally maps entity fields to components of an entry’s distinguished name.

Consider a class with the following annotation:

@DnAttribute(name="uid")
String uid;

and a DN like the following:

uid=carla,dc=springframework,dc=org

Then Spring LDAP will populate uid using uid=carla instead of looking for a uid attribute.

Only fields of type `String` can be annotated with `@DnAttribute`. Other types are not supported.

You can alternatively supply an index like so:

@DnAttribute(index=1)
String uid;

@DnAttribute(index=0)
String department;

which is handy for DNs that have multiple components:

uid=carla,department=engineering,dc=springframework,dc=org

Using an index also allows Spring LDAP to compute the DN for you when creating or locating an entity for update or deletion. For update scenarios, this also automatically takes care of moving entries in the tree if attributes that are part of the distinguished name have changed.

Note that while both attributes are present on `@DnAttribute`, if `index` is specified, then `name` is ignored.
Remember that all fields are mapped to LDAP attributes by default. @DnAttribute does not change this; in other words, fields annotated with @DnAttribute will also map to an LDAP attribute, unless you also annotate the field with @Transient.

Execution

When all components have been properly configured and annotated, the object mapping methods of LdapTemplate can be used as follows:

Example 1. Execution
@Entry(objectClasses = { "person", "top" }, base="ou=someOu")
public class Person {
   @Id
   private Name dn;

   @Attribute(name="cn")
   @DnAttribute(value="cn", index=1)
   private String fullName;

   // No @Attribute annotation means this will be bound to the LDAP attribute
   // with the same value
   private String description;

   @DnAttribute(value="ou", index=0)
   @Transient
   private String company;

   @Transient
   private String someUnmappedField;
   // ...more attributes below
}


public class OdmPersonRepo {
   @Autowired
   private LdapTemplate ldapTemplate;

   public Person create(Person person) {
      ldapTemplate.create(person);
      return person;
   }

   public Person findByUid(String uid) {
      return ldapTemplate.findOne(query().where("uid").is(uid), Person.class);
   }

   public void update(Person person) {
      ldapTemplate.update(person);
   }

   public void delete(Person person) {
      ldapTemplate.delete(person);
   }

   public List<Person> findAll() {
      return ldapTemplate.findAll(Person.class);
   }

   public List<Person> findByLastName(String lastName) {
      return ldapTemplate.find(query().where("sn").is(lastName), Person.class);
   }

   public Stream<Person> streamFindByLastName(String lastName) {
      return ldapTemplate.findStream(query().where("sn").is(lastName), Person.class);
   }
}

ODM and Distinguished Names as Attribute Values

Security groups in LDAP commonly contain a multi-value attribute, where each of the values is the distinguished name of a user in the system. The difficulties involved when handling these kinds of attributes are discussed in DirContextAdapter and Distinguished Names as Attribute Values.

ODM also has support for javax.naming.Name attribute values, making group modifications easy, as the following example shows:

Example 2. Example Group representation
@Entry(objectClasses = {"top", "groupOfUniqueNames"}, base = "cn=groups")
public class Group {

    @Id
    private Name dn;

    @Attribute(name="cn")
    @DnAttribute("cn")
    private String name;

    @Attribute(name="uniqueMember")
    private Set<Name> members;

    public Name getDn() {
        return dn;
    }

    public void setDn(Name dn) {
        this.dn = dn;
    }

    public Set<Name> getMembers() {
        return members;
    }

    public void setMembers(Set<Name> members) {
        this.members = members;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void addMember(Name member) {
        members.add(member);
    }

    public void removeMember(Name member) {
        members.remove(member);
    }
}

When you modify group members by using setMembers, addMember, and removeMember and then calling ldapTemplate.update(), attribute modifications are calculated by using distinguished name equality, meaning that the text formatting of distinguished names is disregarded when figuring out whether they are equal.