How do you search and modify a directory with LdapTemplate — LdapQueryBuilder, AttributesMapper vs ContextMapper, and modifyAttributes?
answer
- query().where(...).is(...) fluent filter
- AttributesMapper = values only; ContextMapper = DN + rich getters
- modifyAttributes = REPLACE/ADD/REMOVE ModificationItems
- lookupContext -> mutate -> modifyAttributes(ctx) = auto diff
- rebind not atomic; base is relative
basics
~20 sBuild a filter with LdapQueryBuilder (query().where(...).is(...)) and pass a mapper: AttributesMapper turns raw Attributes into an object, ContextMapper turns a context (which also exposes the DN) into an object. To change an entry, call modifyAttributes with ModificationItems, or edit a DirContextOperations and pass it back.
solid answer
~30 sFor reads, `LdapQueryBuilder` (`import static ...query()`) builds a type-safe filter and search controls: `query().base("ou=people").where("objectclass").is("person").and("sn").like("Smi*")`. You pass a mapper to `ldapTemplate.search`: use **`AttributesMapper<T>`** when you only need attribute values, or **`ContextMapper<T>`** (receives `DirContextOperations`) when you also need the entry's **DN** or want the richer accessor API. For partial updates, `modifyAttributes(dn, ModificationItem[])` applies REPLACE/ADD/REMOVE operations attribute-by-attribute — the LDAP-idiomatic way, since it doesn't rewrite the whole entry. A cleaner alternative is `lookupContext(dn)` to get a `DirContextOperations`, mutate it with `setAttributeValue`/`addAttributeValue`/`removeAttributeValue`, then `modifyAttributes(ctx)` — Spring computes the minimal diff. `bind`/`unbind`/`rebind` create/delete/replace whole entries.
code
java · 27 linesimport static org.springframework.ldap.query.LdapQueryBuilder.query;
import org.springframework.ldap.query.SearchScope;
import org.springframework.ldap.core.*;
// SEARCH with ContextMapper (needs the DN)
List<Person> people = ldapTemplate.search(
query().base("ou=people").searchScope(SearchScope.SUBTREE)
.where("objectclass").is("person").and("sn").like("Smi*"),
(AbstractContextMapper<Person>) ctx -> {
Person p = new Person();
p.setDn(ctx.getDn()); // available on ContextMapper
p.setFullName(ctx.getStringAttribute("cn"));
p.setEmails(ctx.getStringAttributes("mail"));
return p;
});
// PARTIAL UPDATE via diff-tracking context (recommended)
Name dn = LdapNameBuilder.newInstance("uid=jdoe,ou=people").build();
DirContextOperations ctx = ldapTemplate.lookupContext(dn);
ctx.setAttributeValue("mail", "[email protected]");
ctx.addAttributeValue("description", "updated 2026");
ldapTemplate.modifyAttributes(ctx); // Spring emits minimal ModificationItems
// EXPLICIT modify with ModificationItem[]
ModificationItem[] mods = { new ModificationItem(
DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("title", "Engineer")) };
ldapTemplate.modifyAttributes(dn, mods);go deeper
Know that you build a filter and pass a mapper to search, and that modifyAttributes changes an entry.
Distinguish AttributesMapper vs ContextMapper, use LdapQueryBuilder, and apply REPLACE/ADD/REMOVE modifications.
Use lookupContext+modifyAttributes for minimal diffs, understand non-atomic rebind, relative base pitfalls, and injection-safe filters.
Establish patterns for safe queries and updates at scale, understand LDAP's lack of ACID transactions, and the compensating-transaction trade-offs.
**Building queries — `LdapQueryBuilder`.** Rather than concatenating raw RFC-4515 filter strings like `(&(objectclass=person)(sn=Smith))` (and risking injection), Spring LDAP offers a fluent builder in `org.springframework.ldap.query.LdapQueryBuilder`, usually imported statically as `query()`. Example: `query().base("ou=people").searchScope(SearchScope.SUBTREE).countLimit(100).where("objectclass").is("person").and("sn").like("Smi*")`. It escapes values, sets `base` (relative to the context source base), `searchScope` (`OBJECT`, `ONELEVEL`, `SUBTREE`), and limits. This produces an `LdapQuery` you hand to `search`. **Reading results — two mapper callbacks.** - **`AttributesMapper<T>`** (`org.springframework.ldap.core.AttributesMapper`) — its single method `mapFromAttributes(Attributes attributes)` receives the raw `javax.naming.directory.Attributes`. Use it when you only need attribute *values* and don't care about the DN. Reading a value: `(String) attributes.get("cn").get()`; a possibly-null attribute must be null-checked; multi-valued attributes require iterating a `NamingEnumeration`. - **`ContextMapper<T>`** (`org.springframework.ldap.core.ContextMapper`) — its method `mapFromContext(Object ctx)` receives a `DirContextOperations` (cast or use the typed `AbstractContextMapper<T>`). This exposes the entry's **DN** via `getDn()`/`getNameInNamespace()` and convenience getters like `getStringAttribute("cn")`, `getStringAttributes("mail")`, `getObjectAttribute(...)`. **Choose ContextMapper when you need the DN** or want the friendlier accessors; choose AttributesMapper for the simplest value-only mapping. (ODM's `find` methods are a higher-level alternative that skip writing a mapper entirely.) **Search variants.** `search(LdapQuery, Mapper)` is the modern form. Older overloads take a base, a raw filter string, `SearchControls`, and a mapper. `lookup(dn, mapper)` reads a single known entry. `findOne`/`find` are the ODM equivalents. **Writing — three granularities.** 1. **Whole-entry create/delete/replace:** `bind(dn, obj, attributes)` creates a new entry; `unbind(dn)` deletes it; `rebind(dn, obj, attributes)` deletes-then-creates (replaces) it. 2. **Attribute-level partial update:** `modifyAttributes(Name dn, ModificationItem[] mods)`. Each `ModificationItem` pairs an op — `DirContext.REPLACE_ATTRIBUTE`, `ADD_ATTRIBUTE`, `REMOVE_ATTRIBUTE` — with a `BasicAttribute`. This is the LDAP-idiomatic update: it changes only named attributes rather than rewriting the entry, which matters for concurrency and for server-maintained attributes. 3. **Diff-based partial update (recommended):** `DirContextOperations ctx = ldapTemplate.lookupContext(dn); ctx.setAttributeValue("mail", "[email protected]"); ctx.addAttributeValue("memberOf", ...); ldapTemplate.modifyAttributes(ctx);`. Spring tracks the original values and emits the minimal set of `ModificationItem`s automatically — the cleanest and least error-prone path. **Gotchas.** - LDAP has no cross-entry transactions; each modify is independent (Spring LDAP offers a compensating-transaction manager, but it's best-effort, not ACID). - `rebind` is *not* atomic — it unbinds then binds; a crash between can lose the entry. Prefer `modifyAttributes` for updates. - Reading an absent attribute: `attributes.get("x")` returns null — NPE if you `.get()` blindly. - Filter values from user input must go through the builder (or `LdapEncoder`/`Filter` API) to avoid LDAP injection. - Search `base` in `LdapQuery` is *relative* to the ContextSource `base`; double-including the base DN is a common bug returning zero results.
- When would you pick ContextMapper over AttributesMapper?When you need the entry's DN (ContextMapper's DirContextOperations exposes getDn()/getNameInNamespace()) or want the richer typed accessors like getStringAttributes; AttributesMapper only gives raw Attributes without the DN.
- Why prefer modifyAttributes(ctx) after lookupContext over rebind for an update?rebind unbinds then binds the whole entry (non-atomic, rewrites everything, can lose server-maintained attributes). lookupContext + modifyAttributes changes only the touched attributes and Spring computes the minimal diff.
- How do you avoid LDAP injection when a filter uses user input?Use LdapQueryBuilder (which escapes values) or the Filter/LdapEncoder API rather than concatenating raw filter strings.
saying these in an interview costs you the question
- Claiming AttributesMapper gives you the entry's DN (it doesn't — use ContextMapper)
- Using rebind for a simple attribute change and assuming it is atomic
- Concatenating user input into raw LDAP filter strings (injection)
- Duplicating the context-source base DN inside LdapQuery.base(), yielding zero results
- Calling attributes.get(name).get() without null-checking a possibly-absent attribute