Files
spring-ldap/modules/ROOT/pages/dirobjectfactory.adoc
2023-07-13 08:55:35 -06:00

402 lines
16 KiB
Plaintext

[[dirobjectfactory]]
= Simplifying Attribute Access and Manipulation with `DirContextAdapter`
A little-known -- and probably underestimated -- feature of the Java LDAP API is the ability to register a `DirObjectFactory` to automatically create objects from found LDAP entries.
Spring LDAP makes use of this feature to return https://docs.spring.io/spring-ldap/docs/current/apidocs/org/springframework/ldap/core/DirContextAdapter.html[`DirContextAdapter`] instances in certain search and lookup operations.
`DirContextAdapter` is a useful tool for working with LDAP attributes, particularly when adding or modifying data.
[[search-and-lookup-using-contextmapper]]
== Search and Lookup Using `ContextMapper`
Whenever an entry is found in the LDAP tree, its attributes and Distinguished Name (DN) are used by Spring LDAP to construct a `DirContextAdapter`.
This lets us use a https://docs.spring.io/spring-ldap/docs/current/apidocs/org/springframework/ldap/core/ContextMapper.html[`ContextMapper`] instead of an `AttributesMapper`
to transform found values, as follows:
.Searching using a ContextMapper
====
[source,java]
[subs="verbatim,quotes"]
----
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
**private static class PersonContextMapper implements ContextMapper {
public Object mapFromContext(Object ctx) {
DirContextAdapter context = (DirContextAdapter)ctx;
Person p = new Person();
p.setFullName(context.getStringAttribute("cn"));
p.setLastName(context.getStringAttribute("sn"));
p.setDescription(context.getStringAttribute("description"));
return p;
}
}**
public Person findByPrimaryKey(
String name, String company, String country) {
Name dn = buildDn(name, company, country);
return ldapClient.search().name(dn).toObject(**new PersonContextMapper()**);
}
}
----
====
As shown in the preceding example, we can retrieve the attribute values directly by name without having to go through the `Attributes` and `Attribute` classes.
This is particularly useful when working with multi-value attributes.
Extracting values from multi-value attributes normally requires looping through a `NamingEnumeration` of attribute values returned from the `Attributes` implementation.
`DirContextAdapter` does this for you
in the https://docs.spring.io/spring-ldap/docs/current/apidocs/org/springframework/ldap/core/DirContextAdapter.html#getStringAttributes(java.lang.String)[`getStringAttributes()`]
or https://docs.spring.io/spring-ldap/docs/current/apidocs/org/springframework/ldap/core/DirContextAdapter.html#getObjectAttributes(java.lang.String)[`getObjectAttributes()`] methods.
The following example uses the `getStringAttributes` method:
.Getting multi-value attribute values using `getStringAttributes()`
====
[source,java]
[subs="verbatim,quotes"]
----
private static class PersonContextMapper implements ContextMapper {
public Object mapFromContext(Object ctx) {
DirContextAdapter context = (DirContextAdapter)ctx;
Person p = new Person();
p.setFullName(context.getStringAttribute("cn"));
p.setLastName(context.getStringAttribute("sn"));
p.setDescription(context.getStringAttribute("description"));
// The roleNames property of Person is an String array
**p.setRoleNames(context.getStringAttributes("roleNames"));**
return p;
}
}
----
====
[[using-abstractcontextmapper]]
=== Using `AbstractContextMapper`
Spring LDAP provides an abstract base implementation of `ContextMapper`, called https://docs.spring.io/spring-ldap/docs/current/apidocs/org/springframework/ldap/core/support/AbstractContextMapper.html[`AbstractContextMapper`].
This implementation automatically takes care of the casting of the supplied `Object` parameter to `DirContexOperations`.
Using `AbstractContextMapper`, the `PersonContextMapper` shown earlier can thus be re-written as follows:
.Using an `AbstractContextMapper`
====
[source,java]
[subs="verbatim,quotes"]
----
private static class PersonContextMapper **extends AbstractContextMapper** {
public Object **doMapFromContext**(DirContextOperations ctx) {
Person p = new Person();
p.setFullName(ctx.getStringAttribute("cn"));
p.setLastName(ctx.getStringAttribute("sn"));
p.setDescription(ctx.getStringAttribute("description"));
return p;
}
}
----
====
[[adding-and-updating-data-by-using-dircontextadapter]]
== Adding and Updating Data by Using `DirContextAdapter`
`
While useful when extracting attribute values, `DirContextAdapter` is even more powerful for managing the details
involved in adding and updating data.
[[adding-data-by-using-dircontextadapter]]
=== Adding Data by Using `DirContextAdapter`
The following example uses `DirContextAdapter` to implement an improved implementation of the `create` repository method presented in xref:spring-ldap-basic-usage.adoc#basic-binding-data[Adding Data]:
.Binding using `DirContextAdapter`
====
[[example-binding-contextmapper]]
[source,java]
[subs="verbatim,quotes"]
----
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
public void create(Person p) {
Name dn = buildDn(p);
DirContextAdapter context = new DirContextAdapter(dn);
**context.setAttributeValues("objectclass", new String[] {"top", "person"});
context.setAttributeValue("cn", p.getFullname());
context.setAttributeValue("sn", p.getLastname());
context.setAttributeValue("description", p.getDescription());**
ldapClient.bind(dn).object(context).execute();
}
}
----
====
Note that we use the `DirContextAdapter` instance as the second parameter to bind, which should be a `Context`.
The third parameter is `null`, since we do not specify the attributes explicitly.
Also note the use of the `setAttributeValues()` method when setting the `objectclass` attribute values.
The `objectclass` attribute is multi-value. Similar to the troubles of extracting muti-value attribute data,
building multi-value attributes is tedious and verbose work. By using the `setAttributeValues()` method, you can have `DirContextAdapter` handle that work for you.
[[updating-data-by-using-dircontextadapter]]
=== Updating Data by Using `DirContextAdapter`
We previously saw that updating by using `modifyAttributes` is the recommended approach, but that doing so requires us to perform
the task of calculating attribute modifications and constructing `ModificationItem` arrays accordingly.
`DirContextAdapter` can do all of this for us, as follows:
.Updating using `DirContextAdapter`
====
[[modify-modifyAttributes]]
[source,java]
[subs="verbatim,quotes"]
----
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
public void update(Person p) {
Name dn = buildDn(p);
**DirContextOperations context = ldapClient.search().name(dn).toEntry();**
context.setAttributeValue("cn", p.getFullname());
context.setAttributeValue("sn", p.getLastname());
context.setAttributeValue("description", p.getDescription());
**ldapClient.modify(dn).attributes(context.getModificationItems()).execute();**
}
}
----
====
When calling `SearchSpec#toEntry`, the result is a `DirContextAdapter` instance by default.
While the `lookup` method returns an `Object`, `toEntry` automatically casts the return value to a `DirContextOperations`
(the interface that `DirContextAdapter` implements).
Notice that we have duplicate code in the `LdapTemplate#create` and `LdapTemplate#update` methods. This code maps from a domain object to a context. It can be extracted to a separate method, as follows:
.Adding and modifying using DirContextAdapter
====
[source,java]
[subs="verbatim,quotes"]
----
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapClient ldapClient;
...
public void create(Person p) {
Name dn = buildDn(p);
DirContextAdapter context = new DirContextAdapter(dn);
context.setAttributeValues("objectclass", new String[] {"top", "person"});
mapToContext(p, context);
ldapClient.bind(dn).object(context).execute();
}
public void update(Person p) {
Name dn = buildDn(p);
DirContextOperations context = ldapClient.search().name(dn).toEntry();
mapToContext(person, context);
ldapClient.modify(dn).attributes(context.getModificationItems()).execute();
}
protected void mapToContext (Person p, DirContextOperations context) {
context.setAttributeValue("cn", p.getFullName());
context.setAttributeValue("sn", p.getLastName());
context.setAttributeValue("description", p.getDescription());
}
}
----
====
[[dns-as-attribute-values]]
== `DirContextAdapter` and Distinguished Names as Attribute Values
When managing security groups in LDAP, it is common to have attribute values that represent
distinguished names. Since distinguished name equality differs from String equality (for example, whitespace and case differences
are ignored in distinguished name equality), calculating attribute modifications using string equality does not work as expected.
For instance, if a `member` attribute has a value of `cn=John Doe,ou=People` and we call `ctx.addAttributeValue("member", "CN=John Doe, OU=People")`,
the attribute is now considered to have two values, even though the strings actually represent the same
distinguished name.
As of Spring LDAP 2.0, supplying `javax.naming.Name` instances to the attribute modification methods makes `DirContextAdapter`
use distinguished name equality when calculating attribute modifications. If we modify the earlier example to be
`ctx.addAttributeValue("member", LdapUtils.newLdapName("CN=John Doe, OU=People"))`, it does *not* render a modification, as the following example shows:
.Group Membership Modification using DirContextAdapter
====
[source,java]
[subs="verbatim,quotes"]
----
public class GroupRepo implements BaseLdapNameAware {
private LdapClient ldapClient;
private LdapName baseLdapPath;
public void setLdapClient(LdapClient ldapClient) {
this.ldapClient = ldapClient;
}
public void setBaseLdapPath(LdapName baseLdapPath) {
this.setBaseLdapPath(baseLdapPath);
}
public void addMemberToGroup(String groupName, Person p) {
Name groupDn = buildGroupDn(groupName);
Name userDn = buildPersonDn(
person.getFullname(),
person.getCompany(),
person.getCountry());
DirContextOperation ctx = ldapClient.search().name(groupDn).toEntry();
ctx.addAttributeValue("member", userDn);
ldapClient.modify(groupDn).attributes(ctx.getModificationItems()).execute();
}
public void removeMemberFromGroup(String groupName, Person p) {
Name groupDn = buildGroupDn(String groupName);
Name userDn = buildPersonDn(
person.getFullname(),
person.getCompany(),
person.getCountry());
DirContextOperation ctx = ldapClient.search().name(groupDn).toEntry();
ctx.removeAttributeValue("member", userDn);
ldapClient.modify(groupDn).attributes(ctx.getModificationItems()).execute();
}
private Name buildGroupDn(String groupName) {
return LdapNameBuilder.newInstance("ou=Groups")
.add("cn", groupName).build();
}
private Name buildPersonDn(String fullname, String company, String country) {
return LdapNameBuilder.newInstance(baseLdapPath)
.add("c", country)
.add("ou", company)
.add("cn", fullname)
.build();
}
}
----
====
In the preceding example, we implement `BaseLdapNameAware` to get the base LDAP path as described in xref:configuration.adoc#base-context-configuration[Obtaining a Reference to the Base LDAP Path].
This is necessary because distinguished names as member attribute values must always be absolute from the directory root.
[[a-complete-personrepository-class]]
== A Complete `PersonRepository` Class
To illustrate the usefulness of Spring LDAP and `DirContextAdapter`, the following example shows a complete `Person` Repository implementation for LDAP:
====
[source,java]
[subs="verbatim,quotes"]
----
package com.example.repo;
import java.util.List;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.WhitespaceWildcardsFilter;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapClient ldapClient;
public void setLdapClient(LdapClient ldapClient) {
this.ldapClient = ldapClient;
}
public void create(Person person) {
DirContextAdapter context = new DirContextAdapter(buildDn(person));
mapToContext(person, context);
ldapClient.bind(context.getDn()).object(context).execute();
}
public void update(Person person) {
Name dn = buildDn(person);
DirContextOperations context = ldapClient.lookupContext(dn);
mapToContext(person, context);
ldapClient.modify(dn).attributes(context.getModificationItems()).execute();
}
public void delete(Person person) {
ldapClient.unbind(buildDn(person)).execute();
}
public Person findByPrimaryKey(String name, String company, String country) {
Name dn = buildDn(name, company, country);
return ldapClient.search().name(dn).toObject(getContextMapper());
}
public List<Person> findByName(String name) {
LdapQuery query = query()
.where("objectclass").is("person")
.and("cn").whitespaceWildcardsLike("name");
return ldapClient.search().query(query).toList(getContextMapper());
}
public List<Person> findAll() {
EqualsFilter filter = new EqualsFilter("objectclass", "person");
return ldapClient.search().query((query) -> query.filter(filter)).toList(getContextMapper());
}
protected ContextMapper getContextMapper() {
return new PersonContextMapper();
}
protected Name buildDn(Person person) {
return buildDn(person.getFullname(), person.getCompany(), person.getCountry());
}
protected Name buildDn(String fullname, String company, String country) {
return LdapNameBuilder.newInstance()
.add("c", country)
.add("ou", company)
.add("cn", fullname)
.build();
}
protected void mapToContext(Person person, DirContextOperations context) {
context.setAttributeValues("objectclass", new String[] {"top", "person"});
context.setAttributeValue("cn", person.getFullName());
context.setAttributeValue("sn", person.getLastName());
context.setAttributeValue("description", person.getDescription());
}
private static class PersonContextMapper extends AbstractContextMapper<Person> {
public Person doMapFromContext(DirContextOperations context) {
Person person = new Person();
person.setFullName(context.getStringAttribute("cn"));
person.setLastName(context.getStringAttribute("sn"));
person.setDescription(context.getStringAttribute("description"));
return person;
}
}
}
----
====
NOTE: In several cases, the Distinguished Name (DN) of an object is constructed by using properties of the object.
In the preceding example, the country, company and full name of the `Person` are used in the DN, which means that updating any of these properties actually requires moving the entry in the LDAP tree by using the `rename()` operation in addition to updating the `Attribute` values.
Since this is highly implementation-specific, this is something you need to keep track of yourself, either by disallowing the user to change these properties or performing the `rename()` operation in your `update()` method if needed.
Note that, by using xref:odm.adoc[Object-Directory Mapping (ODM)], the library can automatically handle this for you if you annotate your domain classes appropriately.