Ldap4j is a java LDAP client, which can be used to query directory services. Its main goals are to be fully non-blocking, correct, and host environment and transport agnostic.
Also check the external issues file to help you choose an LDAP client implementation.
Ldap4j is an LDAP v3 client. It's fully non-blocking, and supports timeouts on all operations.
Ldap4j currently supports:
- all operations defined in LDAP v3, except removal of the TLS Layer,
- absolute true and false filters,
- all operational attributes,
- assertion control,
- attributes by object class,
- cancel operation,
- don't use copy control,
- fast bind operation,
- feature discovery,
- manage DSA IT control,
- matched values control,
- modify-increment operation,
- password modify operation,
- read entry controls,
- server side sorting control,
- simple paged results control,
- transactions,
- "Who am I?" operation.
Ldap4j supports TLS, through the standard StartTLS operation, and it also supports the non-standard LDAPS protocol. It supports an optional host name verification in both cases. Ldap4j supports TLS renegotiations.
Ldap4j supports separate executors for TLS handshake tasks.
A connection pool is provided to support traditional parallelism, and amortize the cost of TCP and TLS handshakes.
Ldap4j supports parallel operations using a single connection.
All operations are non-blocking, the client should never wait for parallel results by blocking the current thread.
All operations are subject to a timeout. All operations return a neutral result on a timeout, or raise an exception. The acquisitions and releases of system resources are not subject to timeouts.
Ldap4j is host environment agnostic, it can be used in a wide variety of environments with some glue logic. Glue has been written for:
- CompletableFutures
- Project Reactor
- ScheduledExecutorServices
- synchronous execution.
Ldap4j is also transport agnostic. Currently, it supports the following libraries:
- Apache MINA
- Java NIO
- Netty.
Various dependencies are neatly packaged into submodules. Choose according to your need.
<dependency>
<groupId>com.adaptiverecognition</groupId>
<artifactId>ldap4j-java</artifactId>
<version>1.2.2</version>
</dependency> <dependency>
<groupId>com.adaptiverecognition</groupId>
<artifactId>ldap4j-mina</artifactId>
<version>1.2.2</version>
</dependency> <dependency>
<groupId>com.adaptiverecognition</groupId>
<artifactId>ldap4j-netty</artifactId>
<version>1.2.2</version>
</dependency> <dependency>
<groupId>com.adaptiverecognition</groupId>
<artifactId>ldap4j-reactor-netty</artifactId>
<version>1.2.2</version>
</dependency>The samples subproject contains several short examples how ldap4j can be used. All samples do the simple task of:
- connecting to the public ldap server ldap.forumsys.com:389,
- authenticating itself to the server,
- and querying the users of the mathematicians group.
The result of executing one of the samples should look something like this:
ldap4j trampoline sample
connected
bound
mathematicians:
uid=euclid,dc=example,dc=com
uid=riemann,dc=example,dc=com
uid=euler,dc=example,dc=com
uid=gauss,dc=example,dc=com
uid=test,dc=example,dc=com
CompletableFutures can be used with the ldap4j client. This requires a thread pool.
// new thread pool
ScheduledExecutorService executor=Executors.newScheduledThreadPool(Context.defaultParallelism());
// connect
CompletableFuture<Void> future=FutureLdapConnection.factoryJavaAsync(
null, // use the global asynchronous channel group
executor,
Log.systemErr(),
Context.defaultParallelism(),
new InetSocketAddress("ldap.forumsys.com", 389),
10_000_000_000L, // timeout
TlsSettings.noTls()) // plain-text connection
.get()
.thenCompose((connection)->{
System.out.println("connected");
// authenticate
CompletableFuture<Void> rest=connection.writeRequestReadResponseChecked(
BindRequest.simple(
"cn=read-only-admin,dc=example,dc=com",
"password".toCharArray())
.controlsEmpty())
.thenCompose((ignore)->{
System.out.println("bound");
try {
// look up mathematicians
return connection.search(
new SearchRequest(
List.of("uniqueMember"), // attributes
"ou=mathematicians,dc=example,dc=com", // base object
DerefAliases.DEREF_ALWAYS,
Filter.parse("(objectClass=*)"),
Scope.WHOLE_SUBTREE,
100, // size limit
10, // time limit
false) // types only
.controlsEmpty());
}
catch (Throwable throwable) {
return CompletableFuture.failedFuture(throwable);
}
})
.thenCompose((searchResults)->{
System.out.println("mathematicians:");
searchResults.stream()
.map(ControlsMessage::message)
.filter(SearchResult::isEntry)
.map(SearchResult::asEntry)
.flatMap((entry)->entry.attributes().stream())
.filter((attribute)->"uniqueMember".equals(attribute.type().utf8()))
.flatMap((attribute)->attribute.values().stream())
.forEach(System.out::println);
return CompletableFuture.completedFuture(null);
});
// release resources, timeout only affects the LDAP and TLS shutdown sequences
return rest
.thenCompose((ignore)->connection.close())
.exceptionallyCompose((ignore)->connection.close());
});
//wait for the result in this thread
future.get(10_000_000_000L, TimeUnit.NANOSECONDS);Lava is the internal language of ldap4j. This is the most feature-rich way to use the client. Lava can be used reactive-style.
private static @NotNull Lava<Void> main() {
return Lava.supplier(()->{
System.out.println("ldap4j lava sample");
// create a connection, and guard the computation
return Closeable.withCloseable(
()->LdapConnection.factory(
// use the global asynchronous channel group
JavaAsyncChannelConnection.factory(null, Map.of()),
new InetSocketAddress("ldap.forumsys.com", 389),
TlsSettings.noTls()), // plain-text connection
(connection)->{
System.out.println("connected");
// authenticate
return connection.writeRequestReadResponseChecked(
BindRequest.simple(
"cn=read-only-admin,dc=example,dc=com",
"password".toCharArray())
.controlsEmpty())
.composeIgnoreResult(()->{
System.out.println("bound");
// look up mathematicians
return connection.search(
new SearchRequest(
List.of("uniqueMember"), // attributes
"ou=mathematicians,dc=example,dc=com", // base object
DerefAliases.DEREF_ALWAYS,
Filter.parse("(objectClass=*)"),
Scope.WHOLE_SUBTREE,
100, // size limit
10, // time limit
false) // types only
.controlsEmpty());
})
.compose((searchResults)->{
System.out.println("mathematicians:");
searchResults.stream()
.map(ControlsMessage::message)
.filter(SearchResult::isEntry)
.map(SearchResult::asEntry)
.flatMap((entry)->entry.attributes().stream())
.filter((attribute)->"uniqueMember".equals(attribute.type().utf8()))
.flatMap((attribute)->attribute.values().stream())
.forEach(System.out::println);
return Lava.VOID;
});
});
});
}
public static void main(String[] args) throws Throwable {
// new thread pool
ScheduledExecutorService executor=Executors.newScheduledThreadPool(Context.defaultParallelism());
try {
Context context=ThreadLocalScheduledExecutorContext.createDelayNanos(
10_000_000_000L, // timeout
executor,
Log.systemErr(),
Context.defaultParallelism());
// going to wait for the result in this thread
JoinCallback<Void> join=Callback.join(context);
// compute the result
context.get(join, main());
// wait for the result
join.joinEndNanos(context.endNanos());
}
finally {
executor.shutdown();
}
}Ldap4j can be used as a codec in a Netty pipeline, through the NettyLdapCodec class.
It handles Netty ByteBufs on the channel side,
and accepts Request objects from the application side, and returns Responses.
Check out the
NettyLdapCodecSample
for details.
Glue is provided to use ldap4j as a Reactor publisher. All asynchronous operations return a Mono object. The transport is hardcoded to Netty.
A pool is provided to amortize the cost of repeated TCP and TLS negotiations.
After starting the sample, the application can be reached here.
@Autowired
public EventLoopGroup eventLoopGroup;
@Autowired
public ReactorLdapPool pool;
public Mono<String> noPool() {
StringBuilder output=new StringBuilder();
output.append("<html><body>");
output.append("ldap4j reactor no-pool sample<br>");
// create a connection, and guard the computation
return ReactorLdapConnection.withConnection(
(evenLoopGroup)->Mono.empty(), // event loop group close
()->Mono.just(eventLoopGroup), // event loop group factory
(connection)->run(connection, output),
new InetSocketAddress("ldap.forumsys.com", 389),
10_000_000_000L, // timeout
TlsSettings.noTls()) // plaint-text connection
.flatMap((ignore)->{
output.append("</body></html>");
return Mono.just(output.toString());
});
}
@GetMapping(value = "/pool", produces = "text/html")
public Mono<String> pool() {
StringBuilder output=new StringBuilder();
output.append("<html><body>");
output.append("ldap4j reactor pool sample<br>");
// lease a connection, and guard the computation
return pool.lease((connection)->run(connection, output))
.flatMap((ignore)->{
output.append("</body></html>");
return Mono.just(output.toString());
});
}
private Mono<Object> run(ReactorLdapConnection connection, StringBuilder output) {
output.append("connected<br>");
// authenticate
return connection.writeRequestReadResponseChecked(
BindRequest.simple(
"cn=read-only-admin,dc=example,dc=com",
"password".toCharArray())
.controlsEmpty())
.flatMap((ignore)->{
output.append("bound<br>");
try {
// look up mathematicians
return connection.search(
new SearchRequest(
List.of("uniqueMember"), // attributes
"ou=mathematicians,dc=example,dc=com", // base object
DerefAliases.DEREF_ALWAYS,
Filter.parse("(objectClass=*)"),
Scope.WHOLE_SUBTREE,
100, // size limit
10, // time limit
false) // types only
.controlsEmpty());
}
catch (Throwable throwable) {
return Mono.error(throwable);
}
})
.flatMap((searchResults)->{
output.append("mathematicians:<br>");
searchResults.stream()
.map(ControlsMessage::message)
.filter(SearchResult::isEntry)
.map(SearchResult::asEntry)
.flatMap((entry)->entry.attributes().stream())
.filter((attribute)->"uniqueMember".equals(attribute.type().utf8()))
.flatMap((attribute)->attribute.values().stream())
.forEach((value)->{
output.append(value);
output.append("<br>");
});
return Mono.just(new Object());
});
}
@Bean
public EventLoopGroup evenLoopGroup() {
return new MultiThreadIoEventLoopGroup(4, NioIoHandler.newFactory());
}
@Bean
public ReactorLdapPool pool(@Autowired EventLoopGroup eventLoopGroup) {
return ReactorLdapPool.create(
eventLoopGroup,
(eventLoopGroup2)->Mono.empty(), // event loop group close
Log.slf4j(), // log to SLF4J
4, // pool size
new InetSocketAddress("ldap.forumsys.com", 389),
10_000_000_000L, // timeout
TlsSettings.noTls()); // plaint-text connection
}A trampoline is used to convert the asynchronous operations to synchronous ones. This can be used in simple command line or desktop applications. The transport is hardcoded to Java NIO polling.
// single timeout for all operations
long endNanos=System.nanoTime()+10_000_000_000L;
TrampolineLdapConnection connection=TrampolineLdapConnection.createJavaPoll(
endNanos,
Log.systemErr(), // log everything to the standard error
new InetSocketAddress("ldap.forumsys.com", 389),
TlsSettings.noTls()); // plain-text connection
// authenticate
connection.writeRequestReadResponseChecked(
endNanos,
BindRequest.simple(
"cn=read-only-admin,dc=example,dc=com",
"password".toCharArray())
.controlsEmpty());
// look up mathematicians
List<ControlsMessage<SearchResult>> searchResults=connection.search(
endNanos,
new SearchRequest(
List.of("uniqueMember"), // attributes
"ou=mathematicians,dc=example,dc=com", // base object
DerefAliases.DEREF_ALWAYS,
Filter.parse("(objectClass=*)"),
Scope.WHOLE_SUBTREE,
100, // size limit
10, // time limit
false) // types only
.controlsEmpty());
System.out.println("mathematicians:");
searchResults.stream()
.map(ControlsMessage::message)
.filter(SearchResult::isEntry)
.map(SearchResult::asEntry)
.flatMap((entry)->entry.attributes().stream())
.filter((attribute)->"uniqueMember".equals(attribute.type().utf8()))
.flatMap((attribute)->attribute.values().stream())
.forEach(System.out::println);
// release resources, timeout only affects the LDAP and TLS shutdown sequences
connection.close(endNanos);Operations can be issued concurrently on a single connection.
A MessageIdGenerator object is used to provide ids to new messages.
The default MessageIdGenerator cycles through the ids from 1 to 127.
This is acceptable when there are no parallel operations, or just some occasional abandon, or cancel.
While sending and receiving concurrent messages only
LdapConnection.writeMessage(ControlsMessage<M>, MessageIdGenerator)
and LdapConnection.readMessageCheckedParallel(Function<Integer, ParallelMessageReader<?, T>>)
can be used.
As usual, reads cannot be called concurrently with other reads, and writes cannot be called concurrently with other writes.
The caller must also generate and track the message ids used throughout the computation.
TrampolineParallelSample.java contains a simple example of a parallel search.
It connects to the public ldap server ldap.forumsys.com:389,
and queries all the objects 10 times concurrently.
The result of running that should look like this:
ldap4j trampoline parallel sample
connected
bound
result size: 22
parallel requests: 10
inversions: 0
There are 22 objects in the directory, and by inversions=0 we know that we received all the results
strictly ordered according to their respective request.
So we failed to detect any parallel execution of the parallel requests.
ControlsMessage<SearchRequest> searchRequest=new SearchRequest(
List.of("uniqueMember"), // attributes
"dc=example,dc=com", // base object
DerefAliases.DEREF_ALWAYS,
Filter.parse("(objectClass=*)"),
Scope.WHOLE_SUBTREE,
100, // size limit
10, // time limit
false) // types only
.controlsEmpty();
// count all the entries + done
int resultSize=connection.search(endNanos, searchRequest).size();
System.out.printf("result size: %,d%n", resultSize);
// start requests in parallel
int parallelRequests=10;
System.out.printf("parallel requests: %,d%n", parallelRequests);
for (int ii=0; parallelRequests>ii; ++ii) {
connection.writeMessage(
endNanos,
searchRequest,
MessageIdGenerator.constant(ii+1));
}
// read all result
int[] counts=new int[parallelRequests];
int inversions=0;
while (true) {
@NotNull Map<@NotNull Integer, ParallelMessageReader<?, @NotNull LdapMessage<SearchResult>>> readers
=new HashMap<>(parallelRequests);
for (int ii=parallelRequests-1; 0<=ii; --ii) {
if (resultSize!=counts[ii]) {
readers.put(ii+1, SearchResult.READER.parallel(Function::identity));
}
}
if (readers.isEmpty()) {
break;
}
@NotNull LdapMessage<SearchResult> searchResult
=connection.readMessageCheckedParallel(endNanos, readers::get);
int index=searchResult.messageId()-1;
++counts[index];
for (int ii=index-1; 0<=ii; --ii) {
inversions+=resultSize-counts[ii];
}
}
System.out.printf("inversions: %,d%n", inversions);Ldap4j can send and receive TLS renegotiation requests.
A TLS renegotiation can be started by methods LdapConnection.restartTlsHandshake()
and LdapConnection.restartTlsHandshake(@NotNull Consumer<@NotNull SSLEngine> consumer).
A renegotiation cannot be run parallel with any read nor write,
as it will produce output, and may consume some input.
The reception of renegotiation requests can be controlled by the creation time parameter explicitTlsRenegotiation.
With implicit tls renegotiation a renegotiation request will trigger a handshake, but no further action is needed. Subsequent reads and writes will complete the handshake.
With explicit tls renegotiation a renegotiation request will trigger a handshake,
and the read that received it will throw a TlsHandshakeRestartNeededException.
The handshake can be completed by calling one of the LdapConnection.restartTlsHandshake() methods.
Until the handshake completes all reads and writes will throw TlsHandshakeRestartNeededException.
As a consequence, anyone wishing to support TLS renegotiations must implement its own logic to correctly sequence reads, writes, renegotiations, and to retry reads after a renegotiation.
Ldap4j contains a command line client. Its main purpose is to facilitate field debugging.
It prints all options when it's started without any arguments.
./ldap4j.sh -plaintext ldap.forumsys.com \
bind-simple cn=read-only-admin,dc=example,dc=com argument password \
search -attribute uniqueMember ou=mathematicians,dc=example,dc=com '(objectClass=*)'
connecting to ldap.forumsys.com/54.80.223.88:389
connected
Password for cn=read-only-admin,dc=example,dc=com:
bind simple, cn=read-only-admin,dc=example,dc=com
bind successful
search
attributes: [uniqueMember]
base object: ou=mathematicians,dc=example,dc=com
deref. aliases: DEREF_ALWAYS
filter: (objectClass=*)
manage dsa it: false
scope: WHOLE_SUBTREE
size limit: 0 entries
time limit: 0 sec
types only: false
search entry
dn: ou=mathematicians,dc=example,dc=com
uniqueMember: PartialAttribute[type=uniqueMember, values=[uid=euclid,dc=example,dc=com, uid=riemann,dc=example,dc=com, uid=euler,dc=example,dc=com, uid=gauss,dc=example,dc=com, uid=test,dc=example,dc=com]]
search done
Ldap4j expects a resolved InetSocketAddress to connect to a server.
This conveniently sidesteps the question of how to obtain a resolved address.
Standard java libraries can only resolve addresses through a blocking API.
Java lacks the ability to get back exact error codes on I/O errors. To classify an exception ldap4j first checks the type of the exception, and when this fails, it checks the exception message. There's some properties files in ldap4j-java resources to list known types and message patterns:
Exceptions.connection.closed.properties,Exceptions.timeout.properties,Exceptions.unknown.host.properties.
Lava is the monadic library used to implement ldap4j. It abstracts java computations, and it carries around multiple objects:
- a clock to measure time,
- a suggested deadline for computations,
- an executor,
- a log,
- a timer, to wait for time to elapse.
There are three objects central to lava.
The Callback is the visitor of java computations.
A visitor can be used to pattern match.
This is not unlike a
CompletionHandler.
public interface Callback<T> {
void completed(T value);
void failed(@NotNull Throwable throwable);
}The Context groups together useful objects.
It also provides a few methods to uncouple the calling of
Callback.completed(), Callback.failed() and Lava.get()
from the current thread.
public interface Context extends Executor {
@NotNull Runnable awaitEndNanos(@NotNull Callback<Void> callback);
@NotNull Clock clock();
default <T> void complete(@NotNull Callback<T> callback, T value);
long endNanos();
default <T> void fail(@NotNull Callback<T> callback, @NotNull Throwable throwable);
default <T> void get(@NotNull Callback<T> callback, @NotNull Lava<T> supplier);
@NotNull Log log();
}Lava is the monad.
The creation of a lava object should do nothing most of the time,
and a new computation should be started for every call of Lava.get().
This class also provides some monadic constructors and compositions.
public interface Lava<T> {
static <E extends Throwable, T> @NotNull Lava<T> catchErrors(
@NotNull Function<@NotNull E, @NotNull Lava<T>> function,
@NotNull Supplier<@NotNull Lava<T>> supplier,
@NotNull Class<E> type);
static <T> @NotNull Lava<T> complete(T value);
default <U> @NotNull Lava<U> compose(@NotNull Function<T, @NotNull Lava<U>> function);
static <T> @NotNull Lava<T> fail(@NotNull Throwable throwable);
static <T> @NotNull Lava<T> finallyGet(
@NotNull Supplier<@NotNull Lava<Void>> finallyBlock,
@NotNull Supplier<@NotNull Lava<T>> tryBlock);
static <T, U> @NotNull Lava<@NotNull Pair<T, U>> forkJoin(
@NotNull Supplier<@NotNull Lava<T>> left,
@NotNull Supplier<@NotNull Lava<U>> right);
void get(@NotNull Callback<T> callback, @NotNull Context context) throws Throwable;
}Computing a lava monad requires a Context, which is mostly an
Executor.
A Context implementation for
ScheduledExecutorServices
is provided, it's called ScheduledExecutorContext.
ThreadLocalScheduledExecutorContext provides the same functionality as a ScheduledExecutorContext
and tries to exploit locality of reference and avoid context switches.
When the environment doesn't provide a scheduled executor, the MinHeap class can be used
to implement Context.awaitEndNanos().
A LavaEngine is and event-driven executor, which evaluates lava objects in a single thread, completely synchronously.
It uses a queue to delay tasks, and consumes tasks in a loop until the result is produced,
or there's no more tasks that can be run immediately.
A new computation can be started by JoinCallback<T> LdapEngine.get(Lava<T>).
Then LdapEngine.runAll() must be called repeatedly,
while the result is not yet produced, and sufficient time elapsed.
External events have to be delivered explicitly through helper classes, like EngineConnection,
or through starting new computations with LdapEngine.get()
A Trampoline evaluates lava objects in a single thread, completely synchronously.
It uses a queue to delay tasks, and consumes tasks in a loop until the result is produced.
It supports waits without busy-waiting, using Object.wait().
A new computation can be started by T Trampoline.contextEndNanos(long).get(boolean, boolean, Lava<T>).
The main difference of an LdapEngine and a Trampoline is in how they handle waits.
An LdapEngine is used for polling, and it won't block the current thread,
only running tasks that are immediately available.
A Trampoline will block the current thread
when the result is not yet produced and there's no tasks that can be run immediately.
Ldap4j is transport agnostic, at the lava level the network library can be chosen freely. Glue logic for multiple libraries are provided.
A MinaConnection requires an
IoProcessor.
EngineConnection is an event-driven connection.
It's backed up by a read and a write buffer in memory, which can be queried and updated synchronously.
A JavaAsyncChannelConnection uses a
AsynchronousSocketChannels.
If the given channel group is null, it will use the global JVM channel group.
A JavaChannelPollConnection uses a
SocketChannel.
This requires no external threads to work.
It polls repeatedly the underlying channel for the result, but uses
exponential backoff
to limit the time increase to a small linear factor,
and the number of unsuccessful polls to logarithmic in time.
This is intended for the same use cases as the trampoline, single threaded single user applications.
A NettyConnection requires an
EventLoopGroup,
and a matching
DuplexChannel
class.
In turn, an EventLoopGroup requires an
IoHandler
factory.
- NioIoHandler and NioSocketChannel are supported. These are available on all platforms.
- EpollIoHandler and EpollSocketChannel are supported. These are available on linuxes.
Ldap4j is licensed under the Apache License, Version 2.0.