/*******************************************************************************
* ADSync4J (https://github.com/zagyi/adsync4j)
*
* Copyright (c) 2013 Balazs Zagyvai
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Balazs Zagyvai
***************************************************************************** */
package org.adsync4j.unboundid;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.unboundid.ldap.sdk.*;
import org.adsync4j.api.LdapClientException;
import org.adsync4j.spi.LdapAttributeResolver;
import org.adsync4j.spi.LdapClient;
import org.slf4j.ext.XLogger;
import org.slf4j.ext.XLoggerFactory;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.UUID;
import static com.google.common.collect.Iterables.toArray;
import static org.adsync4j.impl.UUIDUtils.bytesToUUID;
/**
* This implementation of the {@link LdapClient} interface uses the UnboundID LDAP SDK to communicate with Active Directory.
* The LDAP connection used by this class ensures that all search operations are paged without any further effort from the
* client's side.
*/
public class UnboundIDLdapClient implements LdapClient<Attribute> {
private final static XLogger LOG = XLoggerFactory.getXLogger(UnboundIDLdapClient.class);
private final PagingUnboundIDConnectionFactory _connectionFactory;
private int _pageSize = DEFAULT_PAGE_SIZE;
private PagingLdapConnection _connection;
public UnboundIDLdapClient(PagingUnboundIDConnectionFactory connectionFactory) {
_connectionFactory = connectionFactory;
}
public void setPageSize(int pageSize) {
_pageSize = pageSize;
}
@Nonnull
@Override
public Attribute getRootDSEAttribute(String attribute) throws LdapClientException {
try {
RootDSE rootDSE = getConnection().getRootDSE();
LdapClientException.throwIfNull(rootDSE, "Root DSE not available.");
Attribute rootDSEAttribute = rootDSE.getAttribute(attribute);
LdapClientException.throwIfNull(rootDSEAttribute, "Could not retrieve attribute '%s' of the root DSE.", attribute);
LOG.debug("Successfully retrieved root DSE attribute: {}", rootDSEAttribute);
return rootDSEAttribute;
} catch (LDAPException e) {
throw new LdapClientException(e);
}
}
@Nonnull
@Override
public Attribute getEntryAttribute(String entryDN, String attributeName) throws LdapClientException {
try {
SearchResultEntry entry = getConnection().getEntry(entryDN, attributeName);
LdapClientException.throwIfNull(entry, "Missing entry '%s'", entryDN);
Attribute attribute = entry.getAttribute(attributeName);
LdapClientException.throwIfNull(attribute,
"Expected attribute '%s' on entry '%s' is missing.", entryDN, attributeName);
LOG.debug("Successfully retrieved attribute: {}", attribute);
return attribute;
} catch (LDAPException e) {
throw new LdapClientException(e);
}
}
@Nonnull
@Override
public Iterable<Attribute[]> search(
String searchBaseDN, String filter, List<String> attributes) throws LdapClientException
{
try {
SearchRequest searchRequest = new SearchRequest(
searchBaseDN,
SearchScope.SUB,
filter,
toArray(attributes, String.class));
Iterable<SearchResultEntry> searchResult = getConnection().search(searchRequest, _pageSize);
return resultEntriesToAttributeArrays(searchResult, attributes);
} catch (LDAPException e) {
throw new LdapClientException(e);
}
}
/**
* Transforms the provided series of search result entries into series of {@link Attribute} arrays that is guaranteed to
* contain attribute values in the same number and order as the second argument of attribute names (the attribute array may
* contain {@code null} values).
*
* @param searchResult A number of {@link SearchResultEntry} objects to transform.
* @param attributes Name of the attributes. Determines the number and order of attributes in the output.
* @return A series of {@link Attribute} arrays, each array representing one search result entry.
*/
private Iterable<Attribute[]> resultEntriesToAttributeArrays(
Iterable<SearchResultEntry> searchResult, final List<String> attributes)
{
return Iterables.transform(searchResult,
new Function<SearchResultEntry, Attribute[]>() {
@Override
public Attribute[] apply(SearchResultEntry resultEntry) {
return ensureAttributeOrder(resultEntry, attributes);
}
});
}
/**
* Extracts {@link Attribute}s from the provided {@link SearchResultEntry} and returns them in the same order as the
* attribute names are specified in the second argument. Some of the {@link Attribute} references may be null in the
* returned array.
*/
private Attribute[] ensureAttributeOrder(SearchResultEntry resultEntry, List<String> attributes) {
Attribute[] result = new Attribute[attributes.size()];
int i = 0;
for (String attributeName : attributes) {
Attribute attribute = resultEntry.getAttribute(attributeName);
result[i++] = attribute;
}
return result;
}
@Nonnull
@Override
public Iterable<UUID> searchDeleted(String rootDN, String filter) throws LdapClientException {
try {
SearchRequest searchRequest = new SearchRequest(rootDN, SearchScope.SUB, filter, OBJECT_GUID);
searchRequest.addControl(new Control(SHOW_DELETED_CONTROL_OID));
Iterable<SearchResultEntry> searchResult = getConnection().search(searchRequest, _pageSize);
return resultEntriesToUUIDs(searchResult);
} catch (LDAPException e) {
throw new LdapClientException(e);
}
}
/**
* Transforms the provided series of {@link SearchResultEntry} objects into a series of {@link UUID} objects. This method
* assumed that the first attribute of each entry is a 16-byte long byte array (the {@code objectGUID} attribute)
* representing the ID of the entry.
*/
private Iterable<UUID> resultEntriesToUUIDs(Iterable<SearchResultEntry> searchResult) {
return Iterables.transform(searchResult,
new Function<SearchResultEntry, UUID>() {
@Override
public UUID apply(SearchResultEntry resultEntry) {
Attribute objectGuidAttribute = resultEntry.getAttributes().iterator().next();
UUID uuid = bytesToUUID(objectGuidAttribute.getValueByteArray());
if (uuid == null) {
LOG.error("Deleted object's objectGUID is expected to be a UUID encoded in 16 bytes, " +
"but got: '{}'", objectGuidAttribute.getValue());
}
return uuid;
}
});
}
@Nonnull
@Override
public LdapAttributeResolver<Attribute> getAttributeResolver() {
return UnboundIdAttributeResolver.INSTANCE;
}
@Override
public void closeConnection() {
if (_connection != null) {
LOG.debug("Closing the LDAP connection.");
_connection.close();
}
}
/**
* Creates a new connection on the first invocation and caches it. Before returning the cached instance on subsequent
* invocations, it checks if the connection is open, an calls reconnect() in case it's not.
*/
private PagingLdapConnection getConnection() {
if (_connection == null) {
_connection = _connectionFactory.createConnection();
} else {
if (!_connection.isConnected()) {
try {
LOG.debug("Re-opening the LDAP connection.");
_connection.reconnect();
} catch (LDAPException e) {
// TODO: the 3rd synchronization operation within 1 second will end up here...
// because the sync operation always closes the connection, and the time check
// in reconnect() will cause the second reconnect() to fail with an exception
throw new LdapClientException(e);
}
}
}
return _connection;
}
}