org.lsc.jndi.JndiServices.java Source code

Java tutorial

Introduction

Here is the source code for org.lsc.jndi.JndiServices.java

Source

/*
 ****************************************************************************
 * Ldap Synchronization Connector provides tools to synchronize
 * electronic identities from a list of data sources including
 * any database with a JDBC connector, another LDAP directory,
 * flat files...
 *
 *                  ==LICENSE NOTICE==
 *
 * Copyright (c) 2008 - 2011 LSC Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
    
 *  * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *   * Neither the name of the LSC Project nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *                  ==LICENSE NOTICE==
 *
 *               (c) 2008 - 2011 LSC Project
 *         Sebastien Bahloul <seb@lsc-project.org>
 *         Thomas Chemineau <thomas@lsc-project.org>
 *         Jonathan Clarke <jon@lsc-project.org>
 *         Remy-Christophe Schermesser <rcs@lsc-project.org>
 ****************************************************************************
 */
package org.lsc.jndi;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.ContextNotEmptyException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.ServiceUnavailableException;
import javax.naming.SizeLimitExceededException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import javax.naming.ldap.SortControl;
import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.codec.api.ControlFactory;
import org.apache.directory.api.ldap.codec.api.LdapApiService;
import org.apache.directory.api.ldap.codec.api.LdapApiServiceFactory;
import org.apache.directory.api.ldap.codec.controls.search.persistentSearch.PersistentSearchFactory;
import org.apache.directory.api.ldap.extras.controls.syncrepl_impl.SyncStateValueFactory;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.ldap.model.url.LdapUrl;
import org.lsc.Configuration;
import org.lsc.LscDatasets;
import org.lsc.configuration.LdapAuthenticationType;
import org.lsc.configuration.LdapConnectionType;
import org.lsc.configuration.LdapDerefAliasesType;
import org.lsc.configuration.LdapReferralType;
import org.lsc.configuration.LdapVersionType;
import org.lsc.exception.LscConfigurationException;
import org.lsc.exception.LscServiceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * General LDAP services wrapper.
 *
 * This class is designed to manage all the needed operations to the directory
 *
 * @author Sebastien Bahloul &lt;seb@lsc-project.org&gt;
 * @author Jonathan Clarke &lt;jon@lsc-project.org&gt;
 */
public final class JndiServices {

    protected static final String TLS_CONFIGURATION = "java.naming.tls";

    /** Default LDAP filter. */
    public static final String DEFAULT_FILTER = "objectClass=*";

    private static final Logger LOGGER = LoggerFactory.getLogger(JndiServices.class);

    /** the ldap ctx. */
    private LdapContext ctx;

    /** TLSResponse in case we use StartTLS */
    private StartTlsResponse tlsResponse;

    /** The context base dn. */
    private Dn contextDn;

    /** The instances cache. */
    private static Map<Properties, JndiServices> cache = new HashMap<Properties, JndiServices>();

    /** Number of results per page (through PagedResults extended control). */
    private int pageSize;

    private LdapUrl namingContext;

    /** Support for recursive deletion (default to false) */
    private boolean recursiveDelete;

    /** Attribute name to sort on. */
    private String sortedBy;

    /** Remember connection properties to reconnect */
    private Properties connProps;

    /**
     * Initiate the object and the connection according to the properties.
     *
     * @param connProps the connection properties to use to instantiate
     * connection
     * @throws NamingException thrown if a directory error is encountered
     * @throws IOException thrown if an error occurs negotiating StartTLS operation
     */
    private JndiServices(final Properties connProps) throws NamingException, IOException {
        this.connProps = connProps;
        initConnection();
    }

    private void initConnection() throws NamingException, IOException {
        // log new connection with it's details
        logConnectingTo(connProps);

        /* should we negotiate TLS? */
        if (connProps.get(TLS_CONFIGURATION) != null && (Boolean) connProps.get(TLS_CONFIGURATION)) {
            /* if we're going to do TLS, we mustn't BIND before the STARTTLS operation
             * so we remove credentials from the properties to stop JNDI from binding */
            /* duplicate properties to avoid changing them (they are used as a cache key in getInstance() */
            Properties localConnProps = new Properties();
            localConnProps.putAll(connProps);
            String jndiContextAuthentication = localConnProps.getProperty(Context.SECURITY_AUTHENTICATION);
            String jndiContextPrincipal = localConnProps.getProperty(Context.SECURITY_PRINCIPAL);
            String jndiContextCredentials = localConnProps.getProperty(Context.SECURITY_CREDENTIALS);
            localConnProps.remove(Context.SECURITY_AUTHENTICATION);
            localConnProps.remove(Context.SECURITY_PRINCIPAL);
            localConnProps.remove(Context.SECURITY_CREDENTIALS);

            /* open the connection */
            ctx = new InitialLdapContext(localConnProps, null);

            /* initiate the STARTTLS extended operation */
            try {
                tlsResponse = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
                tlsResponse.negotiate();
            } catch (IOException e) {
                LOGGER.error("Error starting TLS encryption on connection to {}",
                        localConnProps.getProperty(Context.PROVIDER_URL));
                LOGGER.debug(e.toString(), e);
                throw e;
            } catch (NamingException e) {
                LOGGER.error("Error starting TLS encryption on connection to {}",
                        localConnProps.getProperty(Context.PROVIDER_URL));
                LOGGER.debug(e.toString(), e);
                throw e;
            }

            /* now we add the credentials back to the context, to BIND once TLS is started */
            ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, jndiContextAuthentication);
            ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, jndiContextPrincipal);
            ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, jndiContextCredentials);

        } else {
            /* don't start TLS, just connect normally (this can be on ldap:// or ldaps://) */
            ctx = new InitialLdapContext(connProps, null);
        }

        /* get LDAP naming context */
        try {
            namingContext = new LdapUrl((String) ctx.getEnvironment().get(Context.PROVIDER_URL));
        } catch (LdapURLEncodingException e) {
            LOGGER.error(e.toString());
            LOGGER.debug(e.toString(), e);
            throw new NamingException(e.getMessage());
        }

        /* handle options */
        contextDn = namingContext.getDn() != null ? namingContext.getDn() : null;

        String pageSizeStr = (String) ctx.getEnvironment().get("java.naming.ldap.pageSize");
        if (pageSizeStr != null) {
            pageSize = Integer.parseInt(pageSizeStr);
        } else {
            pageSize = -1;
        }

        sortedBy = (String) ctx.getEnvironment().get("java.naming.ldap.sortedBy");

        String recursiveDeleteStr = (String) ctx.getEnvironment().get("java.naming.recursivedelete");
        if (recursiveDeleteStr != null) {
            recursiveDelete = Boolean.parseBoolean(recursiveDeleteStr);
        } else {
            recursiveDelete = false;
        }

        /* Load SyncRepl response control */
        LdapApiService ldapApiService = LdapApiServiceFactory.getSingleton();
        ControlFactory<?> factory = new SyncStateValueFactory(ldapApiService);
        ldapApiService.registerControl(factory);
        /* Load Persistent Search response control */
        factory = new PersistentSearchFactory(ldapApiService);
        ldapApiService.registerControl(factory);
    }

    private void logConnectingTo(Properties connProps) {
        if (LOGGER.isInfoEnabled()) {
            StringBuilder sb = new StringBuilder();
            sb.append("Connecting to LDAP server ");
            sb.append(connProps.getProperty(Context.PROVIDER_URL));

            // log identity used to connect
            if (connProps.getProperty(Context.SECURITY_AUTHENTICATION) == null
                    || connProps.getProperty(Context.SECURITY_AUTHENTICATION).equals("none")) {
                sb.append(" anonymously");
            } else {
                sb.append(" as ");
                sb.append(connProps.getProperty(Context.SECURITY_PRINCIPAL));
            }

            // using TLS ?
            if (connProps.get(TLS_CONFIGURATION) != null && (Boolean) connProps.get(TLS_CONFIGURATION)) {
                sb.append(" with STARTTLS extended operation");
            }

            LOGGER.info(sb.toString());
        }
    }

    /**
     * Get the source directory connected service.
     * @return the source directory connected service
     */
    @Deprecated
    public static JndiServices getSrcInstance() {
        try {
            Properties srcProperties = Configuration.getSrcProperties();
            if (srcProperties != null && srcProperties.size() > 0) {
                return getInstance(srcProperties);
            }
            return null;
        } catch (Exception e) {
            LOGGER.error("Error opening the LDAP connection to the source!");
            throw new RuntimeException(e);
        }
    }

    /**
     * Get the target directory connected service.
     * @return the target directory connected service
     */
    @Deprecated
    public static JndiServices getDstInstance() {
        try {
            return getInstance(Configuration.getDstProperties());
        } catch (Exception e) {
            LOGGER.error("Error opening the LDAP connection to the destination!");
            throw new RuntimeException(e);
        }
    }

    public static JndiServices getInstance(final Properties props) throws NamingException, IOException {
        return getInstance(props, false);
    }

    /**
     * Instance getter. Manage a connections cache and return the good service
     * @param props the connection properties
     * @return the instance
     * @throws IOException
     * @throws NamingException
     */
    public static JndiServices getInstance(final Properties props, boolean forceNewConnection)
            throws NamingException, IOException {
        if (forceNewConnection) {
            return new JndiServices(props);
        } else {
            if (!cache.containsKey(props)) {
                cache.put(props, new JndiServices(props));
            }
            JndiServices instance = cache.get(props);
            if (instance.ctx == null) {
                instance.initConnection();
            }
            return instance;
        }
    }

    public static Properties getLdapProperties(LdapConnectionType connection) throws LscConfigurationException {
        Properties props = new Properties();
        props.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,
                (connection.getFactory() != null ? connection.getFactory() : "com.sun.jndi.ldap.LdapCtxFactory"));
        props.put(TLS_CONFIGURATION, connection.isTlsActivated());
        if (connection.getUsername() != null) {
            props.setProperty(DirContext.SECURITY_AUTHENTICATION, connection.getAuthentication().value());
            props.setProperty(DirContext.SECURITY_PRINCIPAL, connection.getUsername());
            if (connection.getAuthentication().equals(LdapAuthenticationType.GSSAPI)) {
                if (System.getProperty("java.security.krb5.conf") != null) {
                    throw new RuntimeException("Multiple Kerberos connections not supported (existing value: "
                            + System.getProperty("java.security.krb5.conf")
                            + "). Need to set another LSC instance or unset system property !");
                } else {
                    System.setProperty("java.security.krb5.conf",
                            new File(Configuration.getConfigurationDirectory(), "krb5.ini").getAbsolutePath());
                }
                if (System.getProperty("java.security.auth.login.config") != null) {
                    throw new RuntimeException("Multiple JAAS not supported (existing value: "
                            + System.getProperty("java.security.auth.login.config")
                            + "). Need to set another LSC instance or unset system property !");
                } else {
                    System.setProperty("java.security.auth.login.config",
                            new File(Configuration.getConfigurationDirectory(), "gsseg_jaas.conf")
                                    .getAbsolutePath());
                }
                props.setProperty("javax.security.sasl.server.authentication",
                        "" + connection.isSaslMutualAuthentication());
                //            props.put("java.naming.security.sasl.authorizationId", "dn:" + connection.getUsername());
                props.put("javax.security.auth.useSubjectCredsOnly", "true");
                props.put("com.sun.jndi.ldap.trace.ber", System.err); //debug trace
                props.setProperty("javax.security.sasl.qop", connection.getSaslQop().value());
                try {
                    LoginContext lc = new LoginContext(JndiServices.class.getName(),
                            new KerberosCallbackHandler(connection.getUsername(), connection.getPassword()));
                    lc.login();
                } catch (LoginException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            } else {
                props.setProperty(DirContext.SECURITY_CREDENTIALS, connection.getPassword());
            }
        } else {
            props.setProperty(DirContext.SECURITY_AUTHENTICATION, "none");
        }
        try {
            LdapUrl connectionUrl = new LdapUrl(connection.getUrl());
            if (connectionUrl.getHost() == null) {
                if (LOGGER.isDebugEnabled())
                    LOGGER.debug(
                            "Hostname is empty in LDAP URL, will try to lookup through the naming context ...");
                String domainExt = convertToDomainExtension(connectionUrl.getDn());
                if (domainExt != null) {
                    String hostname = lookupLdapSrvThroughDNS("_ldap._tcp." + domainExt);
                    if (hostname != null) {
                        connectionUrl.setHost(hostname.substring(0, hostname.indexOf(":")));
                        connectionUrl.setPort(Integer.parseInt(hostname.substring(hostname.indexOf(":") + 1)));
                        connection.setUrl(connectionUrl.toString());
                    }
                }
            }
        } catch (LdapURLEncodingException e) {
            throw new LscConfigurationException(e);
        }
        props.setProperty(DirContext.PROVIDER_URL, connection.getUrl());
        if (connection.getReferral() != null) {
            props.setProperty(DirContext.REFERRAL, connection.getReferral().value().toLowerCase());
        } else {
            props.setProperty(DirContext.REFERRAL, LdapReferralType.IGNORE.value().toLowerCase());
        }
        if (connection.getDerefAliases() != null) {
            props.setProperty("java.naming.ldap.derefAliases", getDerefJndiValue(connection.getDerefAliases()));
        } else {
            props.setProperty("java.naming.ldap.derefAliases", getDerefJndiValue(LdapDerefAliasesType.NEVER));
        }
        if (connection.getBinaryAttributes() != null) {
            props.setProperty("java.naming.ldap.attributes.binary",
                    StringUtils.join(connection.getBinaryAttributes().getString(), " "));
        }
        if (connection.getPageSize() != null) {
            props.setProperty("java.naming.ldap.pageSize", "" + connection.getPageSize());
        }
        if (connection.getSortedBy() != null) {
            props.setProperty("java.naming.ldap.sortedBy", connection.getSortedBy());
        }
        props.setProperty("java.naming.ldap.version",
                (connection.getVersion() == LdapVersionType.VERSION_2 ? "2" : "3"));
        if (connection.isRecursiveDelete() != null) {
            props.setProperty("java.naming.recursivedelete", Boolean.toString(connection.isRecursiveDelete()));
        }

        return props;
    }

    private static String lookupLdapSrvThroughDNS(String hostname) {
        Properties env = new Properties();
        env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
        env.put("java.naming.provider.url", "dns:");
        DirContext ctx;
        try {
            ctx = new InitialDirContext(env);
            if (ctx != null) {
                Attributes attrs = ctx.getAttributes(hostname, new String[] { "SRV" });
                String[] attributes = ((String) attrs.getAll().next().get()).split(" ");
                return attributes[3] + ":" + attributes[2];
            }
        } catch (NamingException e) {
        }
        return hostname + ":389";
    }

    private static String convertToDomainExtension(Dn dn) {
        String fqdn = "";
        List<Rdn> rdns = dn.getRdns();
        for (Rdn rdn : rdns) {
            if (!rdn.getAva().getType().equalsIgnoreCase("dc")) {
                return null;
            }
            if (fqdn.length() > 0) {
                fqdn = rdn.getNormValue().getString() + "." + fqdn;
            } else {
                fqdn = rdn.getNormValue().getString();
            }
        }
        return fqdn;
    }

    private static String getDerefJndiValue(LdapDerefAliasesType derefAliases) {
        switch (derefAliases) {
        case ALWAYS:
            return "always";
        case FIND:
            return "finding";
        case SEARCH:
            return "searching";
        case NEVER:
            return "never";
        }
        return "";
    }

    public static JndiServices getInstance(LdapConnectionType connection) {
        try {
            return getInstance(getLdapProperties(connection));
        } catch (Exception e) {
            LOGGER.error("Error opening the LDAP connection to the destination! (" + e.toString() + ")");
            throw new RuntimeException(e);
        }
    }

    /**
     * Search for an entry.
     *
     * This method is a simple LDAP search operation with SUBTREE search
     * control
     *
     * @param base
     *                the base of the search operation
     * @param filter
     *                the filter of the search operation
     * @return the entry or null if not found
     * @throws NamingException
     *                 thrown if something goes wrong
     */
    public SearchResult getEntry(final String base, final String filter) throws NamingException {
        SearchControls sc = new SearchControls();
        return getEntry(base, filter, sc);
    }

    /**
     * Search for an entry.
     *
     * This method is a simple LDAP search operation with SUBTREE search
     * control
     *
     * @param base the base of the search operation
     * @param filter  the filter of the search operation
     * @param sc the search controls
     * @return the entry or null if not found
     * @throws NamingException
     *                 thrown if something goes wrong
     */
    public SearchResult getEntry(final String base, final String filter, final SearchControls sc)
            throws NamingException {
        return getEntry(base, filter, sc, SearchControls.SUBTREE_SCOPE);
    }

    /**
     * Search for an entry.
     *
     * This method is a simple LDAP search operation with SUBTREE search
     * control
     *
     * @param base the base of the search operation
     * @param filter  the filter of the search operation
     * @param sc the search controls
     * @param scope the search scope to use
     * @return the entry or null if not found
     * @throws SizeLimitExceededException
     *                thrown if more than one entry is returned by the search
     * @throws NamingException
     *                 thrown if something goes wrong
     */
    public SearchResult getEntry(final String base, final String filter, final SearchControls sc, final int scope)
            throws NamingException {
        try {
            return doGetEntry(base, filter, sc, scope);
        } catch (NamingException nex) {
            if (nex instanceof CommunicationException || nex instanceof ServiceUnavailableException) {
                LOGGER.warn("Communication error, retrying: " + nex.getMessage());
                LOGGER.debug(nex.getMessage(), nex);
                try {
                    initConnection();
                } catch (IOException ioex) {
                    LOGGER.error("I/O error: " + ioex.getMessage());
                    LOGGER.debug(ioex.getMessage(), ioex);
                    // throw the initial communication exception
                    throw nex;
                }
                return doGetEntry(base, filter, sc, scope);
            } else {
                throw nex;
            }
        }
    }

    private SearchResult doGetEntry(final String base, final String filter, final SearchControls sc,
            final int scope) throws NamingException {
        //sanity checks
        String searchBase = base == null ? "" : base;
        String searchFilter = filter == null ? DEFAULT_FILTER : filter;

        NamingEnumeration<SearchResult> ne = null;
        try {
            sc.setSearchScope(scope);
            String rewrittenBase = null;
            if (contextDn != null && searchBase.toLowerCase().endsWith(contextDn.toString().toLowerCase())) {
                if (!searchBase.equalsIgnoreCase(contextDn.toString())) {
                    rewrittenBase = searchBase.substring(0,
                            searchBase.toLowerCase().lastIndexOf(contextDn.toString().toLowerCase()) - 1);
                } else {
                    rewrittenBase = "";
                }
            } else {
                rewrittenBase = searchBase;
            }
            ne = ctx.search(rewrittenBase, searchFilter, sc);

        } catch (NamingException nex) {
            LOGGER.error("Error while looking for {} in {}: {}", new Object[] { searchFilter, searchBase, nex });
            throw nex;
        }

        SearchResult sr = null;
        if (ne.hasMoreElements()) {
            sr = (SearchResult) ne.nextElement();
            if (ne.hasMoreElements()) {
                LOGGER.error("Too many entries returned (base: \"{}\", filter: \"{}\")", searchBase, searchFilter);
                throw new SizeLimitExceededException("Too many entries returned (base: \"" + searchBase
                        + "\", filter: \"" + searchFilter + "\")");
            } else {
                return sr;
            }
        } else {
            // try hasMore method to throw exceptions if there are any and we didn't get our entry
            ne.hasMore();
        }
        return sr;
    }

    /**
     * Check if the entry with the specified distinguish name exists (or
     * not).
     *
     * @param dn the entry's distinguish name
     * @param filter look at the dn according this filter
     * @return entry existence (or false if something goes wrong)
     */
    public boolean exists(final String dn, final String filter) {
        try {
            return (readEntry(dn, filter, true) != null);
        } catch (NamingException e) {
            LOGGER.error(e.toString());
            LOGGER.debug(e.toString(), e);
        }
        return false;
    }

    /**
     * Check if the entry with the specified distinguish name exists (or
     * not).
     *
     * @param dn the entry's distinguish name
     * @return entry existence (or false if something goes wrong)
     */
    public boolean exists(final String dn) {
        return exists(dn, DEFAULT_FILTER);
    }

    /**
     * Search for an entry.
     *
     * This method is a simple LDAP search operation with BASE search scope
     *
     * @param base
     *                the base of the search operation
     * @param allowError
     *                log error if not found or not
     * @return the entry or null if not found
     * @throws NamingException
     *                 thrown if something goes wrong
     */
    public SearchResult readEntry(final String base, final boolean allowError) throws NamingException {
        return readEntry(base, DEFAULT_FILTER, allowError);
    }

    public SearchResult readEntry(final String base, final String filter, final boolean allowError)
            throws NamingException {
        SearchControls sc = new SearchControls();
        return readEntry(base, filter, allowError, sc);
    }

    public String rewriteBase(final String base) {
        try {
            Dn lowerCasedContextDn = (contextDn == null) ? null : new Dn(contextDn.toString().toLowerCase());
            Dn lowerCasedBaseDn = new Dn(base.toLowerCase());
            if (!lowerCasedBaseDn.isDescendantOf(lowerCasedContextDn)) {
                return base;
            }

            if (lowerCasedBaseDn.equals(lowerCasedContextDn)) {
                return "";
            }

            Dn lowerCasedRelativeDn = lowerCasedBaseDn.getDescendantOf(lowerCasedContextDn);
            return base.substring(0, lowerCasedRelativeDn.toString().length());
        } catch (LdapInvalidDnException e) {
            throw new RuntimeException(e);
        }
    }

    public SearchResult readEntry(final String base, final String filter, final boolean allowError,
            final SearchControls sc) throws NamingException {
        try {
            return doReadEntry(base, filter, allowError, sc);
        } catch (NamingException nex) {
            if (nex instanceof CommunicationException || nex instanceof ServiceUnavailableException) {
                LOGGER.info("Communication error, retrying: " + nex.getMessage());
                LOGGER.debug(nex.getMessage(), nex);
                try {
                    initConnection();
                } catch (IOException ioex) {
                    LOGGER.error("I/O error: " + ioex.getMessage());
                    LOGGER.debug(ioex.getMessage(), ioex);
                    // throw the initial communication exception
                    throw nex;
                }
                return doReadEntry(base, filter, allowError, sc);
            } else {
                throw nex;
            }
        }
    }

    private SearchResult doReadEntry(final String base, final String filter, final boolean allowError,
            final SearchControls sc) throws NamingException {
        NamingEnumeration<SearchResult> ne = null;
        sc.setSearchScope(SearchControls.OBJECT_SCOPE);
        try {
            ne = ctx.search(rewriteBase(base), filter, sc);
        } catch (NamingException nex) {
            if (nex instanceof CommunicationException || nex instanceof ServiceUnavailableException) {
                throw nex;
            }
            if (!allowError) {
                LOGGER.error("Error while reading entry {}: {}", base, nex);
                LOGGER.debug(nex.toString(), nex);
            }
            return null;
        }

        SearchResult sr = null;
        if (ne.hasMore()) {
            sr = (SearchResult) ne.next();
            if (ne.hasMore()) {
                LOGGER.error("Too many entries returned (base: \"{}\")", base);
            } else {
                return sr;
            }
        }
        return sr;
    }

    /**
     * Search for a list of DN.
     *
     * This method is a simple LDAP search operation which is attended to
     * return a list of the entries DN
     *
     * @param base
     *                the base of the search operation
     * @param filter
     *                the filter of the search operation
     * @param scope
     *                the scope of the search operation
     * @return the dn of each entry that is returned by the directory
     * @throws NamingException
     *                 thrown if something goes wrong
     */
    public List<String> getDnList(final String base, final String filter, final int scope) throws NamingException {
        try {
            return doGetDnList(base, filter, scope);
        } catch (NamingException nex) {
            if (nex instanceof CommunicationException || nex instanceof ServiceUnavailableException) {
                LOGGER.warn("Communication error, retrying: " + nex.getMessage());
                LOGGER.debug(nex.getMessage(), nex);
                try {
                    initConnection();
                } catch (IOException ioex) {
                    LOGGER.error("I/O error: " + ioex.getMessage());
                    LOGGER.debug(ioex.getMessage(), ioex);
                    // throw the initial communication exception
                    throw nex;
                }
                return doGetDnList(base, filter, scope);
            } else {
                throw nex;
            }
        }
    }

    private List<String> doGetDnList(final String base, final String filter, final int scope)
            throws NamingException {
        NamingEnumeration<SearchResult> ne = null;
        List<String> iist = new ArrayList<String>();
        try {
            SearchControls sc = new SearchControls();
            sc.setDerefLinkFlag(false);
            sc.setReturningAttributes(new String[] { "1.1" });
            sc.setSearchScope(scope);
            sc.setReturningObjFlag(true);
            ne = ctx.search(base, filter, sc);

            String completedBaseDn = "";
            if (base.length() > 0) {
                completedBaseDn = "," + base;
            }
            while (ne.hasMoreElements()) {
                iist.add(((SearchResult) ne.next()).getName() + completedBaseDn);
            }
        } catch (NamingException e) {
            LOGGER.error(e.toString());
            LOGGER.debug(e.toString(), e);
            throw e;
        }
        return iist;
    }

    /**
     * Apply directory modifications.
     *
     * If no exception is thrown, modifications were done successfully
     *
     * @param jm modifications to apply
     * @return operation status
     * @throws CommunicationException If the connection to the directory is lost
     */
    public boolean apply(final JndiModifications jm) throws CommunicationException {
        try {
            return doApply(jm);
        } catch (CommunicationException cex) {
            LOGGER.warn("Communication error, retrying: " + cex.getMessage());
            LOGGER.debug(cex.getMessage(), cex);
            try {
                initConnection();
            } catch (IOException ioex) {
                LOGGER.error("I/O error: " + ioex.getMessage());
                LOGGER.debug(ioex.getMessage(), ioex);
                // throw the initial communication exception
                throw cex;
            } catch (NamingException nex) {
                LOGGER.error("Naming error: " + nex.getMessage());
                LOGGER.debug(nex.getMessage(), nex);
                // throw the initial communication exception
                throw cex;
            }
            return doApply(jm);
        }
    }

    private boolean doApply(final JndiModifications jm) throws CommunicationException {
        if (jm == null) {
            return true;
        }

        try {
            switch (jm.getOperation()) {

            case ADD_ENTRY:
                ctx.createSubcontext(new LdapName(rewriteBase(jm.getDistinguishName())),
                        getAttributes(jm.getModificationItems(), true));
                break;

            case DELETE_ENTRY:
                if (recursiveDelete) {
                    deleteChildrenRecursively(rewriteBase(jm.getDistinguishName()));
                } else {
                    ctx.destroySubcontext(new LdapName(rewriteBase(jm.getDistinguishName())));
                }
                break;

            case MODIFY_ENTRY:
                Object[] table = jm.getModificationItems().toArray();
                ModificationItem[] mis = new ModificationItem[table.length];
                System.arraycopy(table, 0, mis, 0, table.length);
                ctx.modifyAttributes(new LdapName(rewriteBase(jm.getDistinguishName())), mis);
                break;

            case MODRDN_ENTRY:
                //We do not display this warning if we do not apply the modification with the option modrdn = false
                LOGGER.warn(
                        "WARNING: updating the RDN of the entry will cancel other modifications! Relaunch synchronization to complete update.");
                ctx.rename(new LdapName(rewriteBase(jm.getDistinguishName())),
                        new LdapName(rewriteBase(jm.getNewDistinguishName())));
                break;

            default:
                LOGGER.error("Unable to identify the right modification type: {}", jm.getOperation());
                return false;
            }
            return true;

        } catch (ContextNotEmptyException e) {
            LOGGER.error(
                    "Object {} not deleted because it has children (LDAP error code 66 received). To delete this entry and it's subtree, set the dst.java.naming.recursivedelete property to true",
                    jm.getDistinguishName());
            return false;

        } catch (NamingException ne) {
            if (LOGGER.isErrorEnabled()) {
                StringBuilder errorMessage = new StringBuilder("Error while ");
                switch (jm.getOperation()) {
                case ADD_ENTRY:
                    errorMessage.append("adding");
                    break;
                case MODIFY_ENTRY:
                    errorMessage.append("modifying");
                    break;
                case MODRDN_ENTRY:
                    errorMessage.append("renaming");
                    break;
                case DELETE_ENTRY:
                    if (recursiveDelete) {
                        errorMessage.append("recursively ");
                    }
                    errorMessage.append("deleting");
                    break;
                }
                errorMessage.append(" entry ").append(jm.getDistinguishName());
                errorMessage.append(" in directory :").append(ne.toString());

                LOGGER.error(errorMessage.toString());
            }

            if (ne instanceof CommunicationException) {
                // we lost the connection to the source or destination, stop everything!
                throw (CommunicationException) ne;
            }
            if (ne instanceof ServiceUnavailableException) {
                // we lost the connection to the source or destination, stop everything!
                CommunicationException ce = new CommunicationException(ne.getExplanation());
                ce.setRootCause(ne);
                throw ce;
            }

            return false;
        }
    }

    /**
     * Delete children recursively
     * @param distinguishName the tree head to delete
     * @throws NamingException thrown if an error is encountered
     */
    private void deleteChildrenRecursively(String distinguishName) throws NamingException {
        try {
            doDeleteChildrenRecursively(distinguishName);
            return;
        } catch (NamingException nex) {
            if (nex instanceof CommunicationException || nex instanceof ServiceUnavailableException) {
                LOGGER.warn("Communication error, retrying: " + nex.getMessage());
                LOGGER.debug(nex.getMessage(), nex);
                try {
                    initConnection();
                } catch (IOException ioex) {
                    LOGGER.error("I/O error: " + ioex.getMessage());
                    LOGGER.debug(ioex.getMessage(), ioex);
                    // throw the initial communication exception
                    throw nex;
                }
                doDeleteChildrenRecursively(distinguishName);
                return;
            } else {
                throw nex;
            }
        }
    }

    private void doDeleteChildrenRecursively(String distinguishName) throws NamingException {
        SearchControls sc = new SearchControls();
        sc.setSearchScope(SearchControls.ONELEVEL_SCOPE);
        NamingEnumeration<SearchResult> ne = ctx.search(distinguishName, DEFAULT_FILTER, sc);
        while (ne.hasMore()) {
            SearchResult sr = (SearchResult) ne.next();
            String childrenDn = rewriteBase(sr.getName() + "," + distinguishName);
            deleteChildrenRecursively(childrenDn);
        }
        ctx.destroySubcontext(new LdapName(distinguishName));
    }

    /**
     * Return the modificationItems in the javax.naming.directory.Attributes
     * format.
     *
     * @param modificationItems
     *                the modification items list
     * @param forgetEmpty
     *                if specified, empty attributes will not be converted
     * @return the formatted attributes
     */
    private Attributes getAttributes(final List<ModificationItem> modificationItems, final boolean forgetEmpty) {
        Attributes attrs = new BasicAttributes();
        for (ModificationItem mi : modificationItems) {
            if (!(forgetEmpty && mi.getAttribute().size() == 0)) {
                attrs.put(mi.getAttribute());
            }
        }
        return attrs;
    }

    /**
     * Return the LDAP schema.
     *
     * @param attrsToReturn
     *                list of attribute names to return (or null for all
     *                'standard' attributes)
     * @return the map of name => attribute
     * @throws NamingException
     *                 thrown if something goes wrong (bad
     */
    @SuppressWarnings("unchecked")
    public Map<String, List<String>> getSchema(final String[] attrsToReturn) throws NamingException {
        Map<String, List<String>> attrsResult = new HashMap<String, List<String>>();

        // connect to directory
        Hashtable<String, String> props = (Hashtable<String, String>) ctx.getEnvironment();
        String baseUrl = (String) props.get(Context.PROVIDER_URL);
        baseUrl = baseUrl.substring(0, baseUrl.lastIndexOf('/'));
        props.put(Context.PROVIDER_URL, baseUrl);
        DirContext schemaCtx = new InitialLdapContext(props, null);

        // find schema entry
        SearchControls sc = new SearchControls();
        sc.setSearchScope(SearchControls.OBJECT_SCOPE);
        sc.setReturningAttributes(new String[] { "subschemaSubentry" });

        NamingEnumeration<SearchResult> schemaDnSR = schemaCtx.search("", "(objectclass=*)", sc);

        SearchResult sr = null;
        Attribute subschemaSubentry = null;
        String subschemaSubentryDN = null;

        if (schemaDnSR.hasMore()) {
            sr = schemaDnSR.next();
        }
        if (sr != null) {
            subschemaSubentry = sr.getAttributes().get("subschemaSubentry");
        }
        if (subschemaSubentry != null && subschemaSubentry.size() > 0) {
            subschemaSubentryDN = (String) subschemaSubentry.get();
        }

        if (subschemaSubentryDN != null) {
            // get schema attributes from subschemaSubentryDN
            Attributes schemaAttrs = schemaCtx.getAttributes(subschemaSubentryDN,
                    attrsToReturn != null ? attrsToReturn : new String[] { "*", "+" });

            if (schemaAttrs != null) {
                for (String attr : attrsToReturn) {
                    Attribute schemaAttr = schemaAttrs.get(attr);
                    if (schemaAttr != null) {
                        attrsResult.put(schemaAttr.getID(), (List<String>) Collections.list(schemaAttr.getAll()));
                    }
                }
            }
        }

        return attrsResult;
    }

    public List<String> sup(String dn, int level) throws NamingException {
        int ncLevel = (new LdapName(contextDn.toString())).size();

        LdapName lName = new LdapName(dn);
        List<String> cList = new ArrayList<String>();
        if (level > 0) {
            if (lName.size() > level) {
                for (int i = 0; i < level; i++) {
                    lName.remove(lName.size() - 1);
                }
                cList.add(lName.toString());
            }
        } else if (level == 0) {
            cList.add(lName.toString());
            int size = lName.size();
            for (int i = 0; i < size - 1 && i < size - ncLevel; i++) {
                lName.remove(lName.size() - 1);
                cList.add(lName.toString());
            }
        } else {
            return null;
        }
        return cList;
    }

    /**
     * Search for a list of attribute values
     *
     * This method is a simple LDAP search operation which is attended to
     * return a list of the attribute values in all returned entries
     *
     * @param base the base of the search operation
     * @param filter the filter of the search operation
     * @param scope the scope of the search operation
     * @param attrsNames table of attribute names to get
     * @return Map of DNs of all entries that are returned by the directory with an associated map of attribute names and values (never null)
     * @throws NamingException thrown if something goes wrong
     */
    public Map<String, LscDatasets> getAttrsList(final String base, final String filter, final int scope,
            final List<String> attrsNames) throws NamingException {
        try {
            return doGetAttrsList(base, filter, scope, attrsNames);
        } catch (NamingException nex) {
            if (nex instanceof CommunicationException || nex instanceof ServiceUnavailableException) {
                LOGGER.warn("Communication error, retrying: " + nex.getMessage());
                LOGGER.debug(nex.getMessage(), nex);
                try {
                    initConnection();
                } catch (IOException ioex) {
                    LOGGER.error("I/O error: " + ioex.getMessage());
                    LOGGER.debug(ioex.getMessage(), ioex);
                    // throw the initial communication exception
                    throw nex;
                }
                return doGetAttrsList(base, filter, scope, attrsNames);
            } else {
                throw nex;
            }
        }
    }

    /**
     * Retrieve a specific attribute from an object
     * 
     * @param objectDn
     * @param attribute
     * @return
     * @throws LscServiceException
     */
    public List<String> getAttributeValues(String objectDn, String attribute) throws LscServiceException {
        List<String> values = null;
        try {
            // Setup search
            SearchControls sc = new SearchControls();
            sc.setDerefLinkFlag(false);
            sc.setReturningAttributes(new String[] { attribute });
            sc.setSearchScope(SearchControls.OBJECT_SCOPE);
            sc.setReturningObjFlag(true);

            // Retrieve attribute values
            SearchResult res = getEntry(objectDn, "objectClass=*", sc, SearchControls.OBJECT_SCOPE);
            Attribute attr = res.getAttributes().get(attribute);
            if (attr != null) {
                values = new ArrayList<String>();
                NamingEnumeration<?> enu = attr.getAll();
                while (enu.hasMoreElements()) {
                    Object val = enu.next();
                    values.add(val.toString());
                }
            }
        } catch (NamingException e) {
            throw new LscServiceException(e);
        }
        return values;
    }

    public Map<String, LscDatasets> doGetAttrsList(final String base, final String filter, final int scope,
            final List<String> attrsNames) throws NamingException {

        // sanity checks
        String searchBase = base == null ? "" : rewriteBase(base);
        String searchFilter = filter == null ? DEFAULT_FILTER : filter;

        Map<String, LscDatasets> res = new LinkedHashMap<String, LscDatasets>();

        if (attrsNames == null || attrsNames.size() == 0) {
            LOGGER.error("No attribute names to read! Check configuration.");
            return res;
        }

        String[] attributes = new String[attrsNames.size()];
        attributes = attrsNames.toArray(attributes);

        SearchControls constraints = new SearchControls();
        constraints.setDerefLinkFlag(false);
        constraints.setReturningAttributes(attributes);
        constraints.setSearchScope(scope);
        constraints.setReturningObjFlag(true);

        try {
            boolean requestPagedResults = false;

            List<Control> extControls = new ArrayList<Control>();

            if (pageSize > 0) {
                requestPagedResults = true;
                LOGGER.debug("Using pagedResults control for {} entries at a time", pageSize);
            }

            if (requestPagedResults) {
                extControls.add(new PagedResultsControl(pageSize, Control.CRITICAL));
            }

            if (sortedBy != null) {
                extControls.add(new SortControl(sortedBy, Control.CRITICAL));
            }

            if (extControls.size() > 0) {
                ctx.setRequestControls(extControls.toArray(new Control[extControls.size()]));
            }

            byte[] pagedResultsResponse = null;
            do {
                NamingEnumeration<SearchResult> results = ctx.search(searchBase, searchFilter, constraints);

                if (results != null) {
                    Map<String, Object> attrsValues = null;
                    while (results.hasMoreElements()) {
                        attrsValues = new HashMap<String, Object>();

                        SearchResult ldapResult = (SearchResult) results.next();

                        // get the value for each attribute requested
                        for (String attributeName : attrsNames) {
                            Attribute attr = ldapResult.getAttributes().get(attributeName);
                            if (attr != null && attr.get() != null) {
                                attrsValues.put(attributeName, attr.get());
                            }
                        }

                        res.put(ldapResult.getNameInNamespace(), new LscDatasets(attrsValues));
                    }
                }

                Control[] respCtls = ctx.getResponseControls();
                if (respCtls != null) {
                    for (Control respCtl : respCtls) {
                        if (requestPagedResults && respCtl instanceof PagedResultsResponseControl) {
                            pagedResultsResponse = ((PagedResultsResponseControl) respCtl).getCookie();
                        }
                    }
                }

                if (requestPagedResults && pagedResultsResponse != null) {
                    ctx.setRequestControls(new Control[] {
                            new PagedResultsControl(pageSize, pagedResultsResponse, Control.CRITICAL) });
                }

            } while (pagedResultsResponse != null);

            // clear requestControls for future use of the JNDI context
            if (requestPagedResults) {
                ctx.setRequestControls(null);
            }
        } catch (CommunicationException e) {
            // Avoid handling the communication exception as a generic one
            throw e;
        } catch (ServiceUnavailableException e) {
            // Avoid handling the service unavailable exception as a generic one
            throw e;
        } catch (NamingException e) {
            // clear requestControls for future use of the JNDI context
            ctx.setRequestControls(null);
            LOGGER.error(e.toString());
            LOGGER.debug(e.toString(), e);

        } catch (IOException e) {
            // clear requestControls for future use of the JNDI context
            ctx.setRequestControls(null);
            LOGGER.error(e.toString());
            LOGGER.debug(e.toString(), e);
        }
        return res;
    }

    /**
     * @return the contextDn
     */
    public String getContextDn() {
        return contextDn.toString();
    }

    /**
     * Close connection before this object is deleted by the garbage collector.
     * @see java.lang.Object#finalize()
     */
    @Override
    protected void finalize() throws Throwable {
        // Close the TLS connection (revert back to the underlying LDAP association)
        if (tlsResponse != null) {
            tlsResponse.close();
        }

        // Close the connection to the LDAP server
        if (ctx != null) {
            ctx.close();
            ctx = null;
        }

        super.finalize();
    }

    /**
     * Get the JNDI context.
     * @return The LDAP context object in use by this class.
     */
    public LdapContext getContext() {
        return ctx;
    }

    public String completeDn(String dn) {
        if (!dn.toLowerCase().endsWith(contextDn.toString().toLowerCase())) {
            if (dn.length() > 0) {
                return dn + "," + contextDn.toString();
            } else {
                return contextDn.toString();
            }
        }
        return dn;
    }

    public static CallbackHandler getCallbackHandler(String user, String pass) {
        return new KerberosCallbackHandler(user, pass);
    }
}

class KerberosCallbackHandler implements CallbackHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(KerberosCallbackHandler.class);
    private String user;
    private String pass;

    public KerberosCallbackHandler(String user, String pass) {
        this.user = user;
        this.pass = pass;
    }

    public void handle(Callback[] cbs) throws IOException, UnsupportedCallbackException {
        for (Callback cb : cbs) {
            if (cb instanceof NameCallback) {
                ((NameCallback) cb).setName(user);
            } else if (cb instanceof PasswordCallback) {
                ((PasswordCallback) cb).setPassword(pass.toCharArray());
            } else {
                LOGGER.error("Unknown callback: " + cb.toString());
            }
        }
    }
}