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.
Currently, the client only supports query operations.
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 the following operations:
- bind,
- fast bind,
- search (with manage DSA IT control),
- start TLS,
- unbind.
Ldap4j supports TLS, with optional host name verification, and it supports the non-standard LDAPS protocol.
Ldap4j doesn't support parallel operations yet. A connection pool is provided to alleviate the need for parallel operations.
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.0.0</version>
</dependency>
<dependency>
<groupId>com.adaptiverecognition</groupId>
<artifactId>ldap4j-mina</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.adaptiverecognition</groupId>
<artifactId>ldap4j-netty</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.adaptiverecognition</groupId>
<artifactId>ldap4j-reactor-netty</artifactId>
<version>1.0.0</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(8);
// connect
CompletableFuture<Void> future=FutureLdapConnection.factoryJavaAsync(
null, // use the global asynchronous channel group
executor,
Log.systemErr(),
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.bindSimple(
"cn=read-only-admin,dc=example,dc=com", "password".toCharArray())
.thenCompose((ignore)->{
System.out.println("bound");
try {
// look up mathematicians
return connection.search(
false,
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
}
catch (Throwable throwable) {
return CompletableFuture.failedFuture(throwable);
}
})
.thenCompose((searchResults)->{
System.out.println("mathematicians:");
searchResults.get(0)
.asEntry()
.attributes()
.get("uniqueMember")
.values()
.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.bindSimple(
"cn=read-only-admin,dc=example,dc=com", "password".toCharArray())
.composeIgnoreResult(()->{
System.out.println("bound");
// look up mathematicians
return connection.search(
false, // manage DSA IT
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
})
.compose((searchResults)->{
System.out.println("mathematicians:");
searchResults.get(0)
.asEntry()
.attributes()
.get("uniqueMember")
.values()
.forEach(System.out::println);
return Lava.VOID;
});
});
});
}
public static void main(String[] args) throws Throwable {
// new thread pool
ScheduledExecutorService executor=Executors.newScheduledThreadPool(8);
try {
ScheduledExecutorContext context=ScheduledExecutorContext.createDelayNanos(
10_000_000_000L, // timeout
executor,
Log.systemErr());
// 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();
}
}
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.bindSimple(
"cn=read-only-admin,dc=example,dc=com",
"password".toCharArray())
.flatMap((ignore)->{
output.append("bound<br>");
try {
// look up mathematicians
return connection.search(
false, // manage DSA IT
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
}
catch (Throwable throwable) {
return Mono.error(throwable);
}
})
.flatMap((searchResults)->{
output.append("mathematicians:<br>");
searchResults.get(0)
.asEntry()
.attributes()
.get("uniqueMember")
.values()
.forEach((value)->{
output.append(value);
output.append("<br>");
});
return Mono.just(new Object());
});
}
@Bean
public EventLoopGroup evenLoopGroup() {
return new NioEventLoopGroup(4);
}
@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.bindSimple("cn=read-only-admin,dc=example,dc=com", endNanos, "password".toCharArray());
// look up mathematicians
List<SearchResult> searchResults=connection.search(
endNanos,
false, // manage DSA IT
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
System.out.println("mathematicians:");
searchResults.get(0)
.asEntry()
.attributes()
.get("uniqueMember")
.values()
.forEach(System.out::println);
// release resources, timeout only affects the LDAP and TLS shutdown sequences
connection.close(endNanos);
Ldap4j contains a command line client. Its main purpose is to facilitate field debugging.
It prints all options when it runs without any arguments.
./ldap4j.sh -plaintext ldap.forumsys.com \
bind-simple cn=read-only-admin,dc=example,dc=com console \
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, 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
.
When the environment doesn't provide a scheduled executor, the MinHeap
class can be used
to implement Context.awaitEndNanos()
.
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().
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.
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.
- NioEventLoopGroup and NioSocketChannel are supported. These are available on all platforms.
- EpollEventLoopGroup and EpollSocketChannel are supported. These are available on linuxes.
- KQueueEventLoopGroup and KQueueSocketChannel are untested. These are available on macs.
Ldap4j is licensed under the Apache License, Version 2.0.