org.apache.directory.studio.connection.core.io.api.DirectoryApiConnectionWrapper.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.directory.studio.connection.core.io.api.DirectoryApiConnectionWrapper.java

Source

/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *  
 *    http://www.apache.org/licenses/LICENSE-2.0
 *  
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License. 
 *  
 */
package org.apache.directory.studio.connection.core.io.api;

import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;

import javax.naming.ContextNotEmptyException;
import javax.naming.NameAlreadyBoundException;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.Control;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
import javax.security.auth.login.Configuration;

import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.codec.api.DefaultConfigurableBinaryAttributeDetector;
import org.apache.directory.api.ldap.model.cursor.SearchCursor;
import org.apache.directory.api.ldap.model.entry.AttributeUtils;
import org.apache.directory.api.ldap.model.entry.DefaultModification;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.ModificationOperation;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.FilterParser;
import org.apache.directory.api.ldap.model.message.AddRequest;
import org.apache.directory.api.ldap.model.message.AddRequestImpl;
import org.apache.directory.api.ldap.model.message.AddResponse;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.message.BindRequest;
import org.apache.directory.api.ldap.model.message.BindRequestImpl;
import org.apache.directory.api.ldap.model.message.BindResponse;
import org.apache.directory.api.ldap.model.message.DeleteRequest;
import org.apache.directory.api.ldap.model.message.DeleteRequestImpl;
import org.apache.directory.api.ldap.model.message.DeleteResponse;
import org.apache.directory.api.ldap.model.message.LdapResult;
import org.apache.directory.api.ldap.model.message.ModifyDnRequest;
import org.apache.directory.api.ldap.model.message.ModifyDnRequestImpl;
import org.apache.directory.api.ldap.model.message.ModifyDnResponse;
import org.apache.directory.api.ldap.model.message.ModifyRequest;
import org.apache.directory.api.ldap.model.message.ModifyRequestImpl;
import org.apache.directory.api.ldap.model.message.ModifyResponse;
import org.apache.directory.api.ldap.model.message.Referral;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.message.ResultResponse;
import org.apache.directory.api.ldap.model.message.SearchRequest;
import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.url.LdapUrl;
import org.apache.directory.ldap.client.api.LdapConnectionConfig;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.directory.ldap.client.api.SaslCramMd5Request;
import org.apache.directory.ldap.client.api.SaslDigestMd5Request;
import org.apache.directory.ldap.client.api.SaslGssApiRequest;
import org.apache.directory.ldap.client.api.SaslPlainRequest;
import org.apache.directory.ldap.client.api.exception.InvalidConnectionException;
import org.apache.directory.studio.common.core.jobs.StudioProgressMonitor;
import org.apache.directory.studio.connection.core.Connection;
import org.apache.directory.studio.connection.core.Connection.AliasDereferencingMethod;
import org.apache.directory.studio.connection.core.Connection.ReferralHandlingMethod;
import org.apache.directory.studio.connection.core.ConnectionCoreConstants;
import org.apache.directory.studio.connection.core.ConnectionCorePlugin;
import org.apache.directory.studio.connection.core.ConnectionParameter;
import org.apache.directory.studio.connection.core.ConnectionParameter.EncryptionMethod;
import org.apache.directory.studio.connection.core.IAuthHandler;
import org.apache.directory.studio.connection.core.ICredentials;
import org.apache.directory.studio.connection.core.IJndiLogger;
import org.apache.directory.studio.connection.core.Messages;
import org.apache.directory.studio.connection.core.Utils;
import org.apache.directory.studio.connection.core.io.ConnectionWrapper;
import org.apache.directory.studio.connection.core.io.ConnectionWrapperUtils;
import org.apache.directory.studio.connection.core.io.StudioNamingEnumeration;
import org.apache.directory.studio.connection.core.io.StudioTrustManager;
import org.apache.directory.studio.connection.core.io.jndi.CancelException;
import org.apache.directory.studio.connection.core.io.jndi.ReferralsInfo;
import org.eclipse.core.runtime.Preferences;
import org.eclipse.osgi.util.NLS;

/**
 * A ConnectionWrapper is a wrapper for a real directory connection implementation.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class DirectoryApiConnectionWrapper implements ConnectionWrapper {
    /** The search request number */
    private static int searchRequestNum = 0;

    /** The connection*/
    private Connection connection;

    /** The LDAP Connection Configuration */
    private LdapConnectionConfig ldapConnectionConfig;

    /** The LDAP Connection */
    private LdapNetworkConnection ldapConnection;

    /** The binary attribute detector */
    private DefaultConfigurableBinaryAttributeDetector binaryAttributeDetector;

    /** Indicates if the wrapper is connected */
    private boolean isConnected = false;

    /** The current job thread */
    private Thread jobThread;

    /** The bind principal */
    private String bindPrincipal;

    /** The bind password */
    private String bindPassword;

    /** The SASL PLAIN authzid */
    private String authzId;

    /**
     * Creates a new instance of JNDIConnectionContext.
     * 
     * @param connection the connection
     */
    public DirectoryApiConnectionWrapper(Connection connection) {
        this.connection = connection;
    }

    /**
     * {@inheritDoc}
     */
    public void connect(StudioProgressMonitor monitor) {
        ldapConnection = null;
        isConnected = false;
        jobThread = null;

        try {
            doConnect(monitor);
        } catch (Exception e) {
            disconnect();
            monitor.reportError(e);
        }
    }

    private void doConnect(final StudioProgressMonitor monitor) throws Exception {
        ldapConnection = null;
        isConnected = true;

        ldapConnectionConfig = new LdapConnectionConfig();
        ldapConnectionConfig.setLdapHost(connection.getHost());
        ldapConnectionConfig.setLdapPort(connection.getPort());

        long timeout = connection.getTimeout();

        if (timeout < 0) {
            timeout = 30000L;
        }

        ldapConnectionConfig.setTimeout(timeout);

        binaryAttributeDetector = new DefaultConfigurableBinaryAttributeDetector();
        ldapConnectionConfig.setBinaryAttributeDetector(binaryAttributeDetector);

        if ((connection.getEncryptionMethod() == EncryptionMethod.LDAPS)
                || (connection.getEncryptionMethod() == EncryptionMethod.START_TLS)) {
            ldapConnectionConfig.setUseSsl(connection.getEncryptionMethod() == EncryptionMethod.LDAPS);
            ldapConnectionConfig.setUseTls(connection.getEncryptionMethod() == EncryptionMethod.START_TLS);

            try {
                // get default trust managers (using JVM "cacerts" key store)
                TrustManagerFactory factory = TrustManagerFactory
                        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
                factory.init((KeyStore) null);
                TrustManager[] defaultTrustManagers = factory.getTrustManagers();

                // create wrappers around the trust managers
                StudioTrustManager[] trustManagers = new StudioTrustManager[defaultTrustManagers.length];

                for (int i = 0; i < defaultTrustManagers.length; i++) {
                    trustManagers[i] = new StudioTrustManager((X509TrustManager) defaultTrustManagers[i]);
                    trustManagers[i].setHost(connection.getHost());
                }

                ldapConnectionConfig.setTrustManagers(trustManagers);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // Set lower timeout for connecting
                    long oldTimeout = ldapConnectionConfig.getTimeout();
                    ldapConnectionConfig.setTimeout(Math.min(oldTimeout, 5000L));

                    // Connecting
                    ldapConnection = new LdapNetworkConnection(ldapConnectionConfig);
                    boolean connected = ldapConnection.connect();

                    if (!connected) {
                        throw new Exception(Messages.DirectoryApiConnectionWrapper_UnableToConnect);
                    }

                    // Set old timeout again
                    ldapConnectionConfig.setTimeout(oldTimeout);
                } catch (Exception e) {
                    exception = e;

                    try {
                        if (ldapConnection != null) {
                            ldapConnection.close();
                        }
                    } catch (Exception exception) {
                        // Nothing to do
                    } finally {
                        ldapConnection = null;
                        binaryAttributeDetector = null;
                    }
                }
            }
        };

        runAndMonitor(runnable, monitor);

        if (runnable.getException() != null) {
            throw runnable.getException();
        }
    }

    /**
     * {@inheritDoc}
     */
    public void disconnect() {
        if (jobThread != null) {
            Thread t = jobThread;
            jobThread = null;
            t.interrupt();
        }
        if (ldapConnection != null) {
            try {
                ldapConnection.close();
            } catch (Exception e) {
                // ignore
            }
            ldapConnection = null;
            binaryAttributeDetector = null;
        }
        isConnected = false;
    }

    /**
     * {@inheritDoc}
     */
    public void bind(StudioProgressMonitor monitor) {
        try {
            doBind(monitor);
        } catch (Exception e) {
            disconnect();
            monitor.reportError(e);
        }
    }

    private BindResponse bindSimple(String bindPrincipal, String bindPassword) throws LdapException {
        BindRequest bindRequest = new BindRequestImpl();
        bindRequest.setName(bindPrincipal);
        bindRequest.setCredentials(bindPassword);

        return ldapConnection.bind(bindRequest);
    }

    private BindResponse bindSaslPlain() throws LdapException {
        SaslPlainRequest saslPlainRequest = new SaslPlainRequest();
        saslPlainRequest.setUsername(bindPrincipal);
        saslPlainRequest.setCredentials(bindPassword);
        saslPlainRequest.setAuthorizationId(authzId);
        saslPlainRequest.setQualityOfProtection(connection.getConnectionParameter().getSaslQop());
        saslPlainRequest.setSecurityStrength(connection.getConnectionParameter().getSaslSecurityStrength());
        saslPlainRequest.setMutualAuthentication(connection.getConnectionParameter().isSaslMutualAuthentication());

        return ldapConnection.bindSaslPlain(bindPrincipal, bindPassword, authzId);
    }

    private void doBind(final StudioProgressMonitor monitor) throws Exception {
        if (ldapConnection != null && isConnected) {
            InnerRunnable runnable = new InnerRunnable() {
                public void run() {
                    try {
                        BindResponse bindResponse = null;

                        // No Authentication
                        if (connection.getConnectionParameter()
                                .getAuthMethod() == ConnectionParameter.AuthenticationMethod.NONE) {
                            BindRequest bindRequest = new BindRequestImpl();
                            bindResponse = ldapConnection.bind(bindRequest);
                        } else {
                            // Setup credentials
                            IAuthHandler authHandler = ConnectionCorePlugin.getDefault().getAuthHandler();
                            if (authHandler == null) {
                                Exception exception = new Exception(Messages.model__no_auth_handler);
                                monitor.setCanceled(true);
                                monitor.reportError(Messages.model__no_auth_handler, exception);
                                throw exception;
                            }
                            ICredentials credentials = authHandler
                                    .getCredentials(connection.getConnectionParameter());
                            if (credentials == null) {
                                Exception exception = new Exception();
                                monitor.setCanceled(true);
                                monitor.reportError(Messages.model__no_credentials, exception);
                                throw exception;
                            }
                            if (credentials.getBindPrincipal() == null || credentials.getBindPassword() == null) {
                                Exception exception = new Exception(Messages.model__no_credentials);
                                monitor.reportError(Messages.model__no_credentials, exception);
                                throw exception;
                            }
                            bindPrincipal = credentials.getBindPrincipal();
                            bindPassword = credentials.getBindPassword();

                            switch (connection.getConnectionParameter().getAuthMethod()) {
                            case SIMPLE:
                                // Simple Authentication
                                bindResponse = bindSimple(bindPrincipal, bindPassword);

                                break;

                            case SASL_PLAIN:
                                // SASL Plain authentication
                                bindResponse = bindSaslPlain();

                                break;

                            case SASL_CRAM_MD5:
                                // CRAM-MD5 Authentication
                                SaslCramMd5Request cramMd5Request = new SaslCramMd5Request();
                                cramMd5Request.setUsername(bindPrincipal);
                                cramMd5Request.setCredentials(bindPassword);
                                cramMd5Request
                                        .setQualityOfProtection(connection.getConnectionParameter().getSaslQop());
                                cramMd5Request.setSecurityStrength(
                                        connection.getConnectionParameter().getSaslSecurityStrength());
                                cramMd5Request.setMutualAuthentication(
                                        connection.getConnectionParameter().isSaslMutualAuthentication());

                                bindResponse = ldapConnection.bind(cramMd5Request);
                                break;

                            case SASL_DIGEST_MD5:
                                // DIGEST-MD5 Authentication
                                SaslDigestMd5Request digestMd5Request = new SaslDigestMd5Request();
                                digestMd5Request.setUsername(bindPrincipal);
                                digestMd5Request.setCredentials(bindPassword);
                                digestMd5Request.setRealmName(connection.getConnectionParameter().getSaslRealm());
                                digestMd5Request
                                        .setQualityOfProtection(connection.getConnectionParameter().getSaslQop());
                                digestMd5Request.setSecurityStrength(
                                        connection.getConnectionParameter().getSaslSecurityStrength());
                                digestMd5Request.setMutualAuthentication(
                                        connection.getConnectionParameter().isSaslMutualAuthentication());

                                bindResponse = ldapConnection.bind(digestMd5Request);
                                break;

                            case SASL_GSSAPI:
                                // GSSAPI Authentication
                                SaslGssApiRequest gssApiRequest = new SaslGssApiRequest();

                                Preferences preferences = ConnectionCorePlugin.getDefault().getPluginPreferences();
                                boolean useKrb5SystemProperties = preferences
                                        .getBoolean(ConnectionCoreConstants.PREFERENCE_USE_KRB5_SYSTEM_PROPERTIES);
                                String krb5LoginModule = preferences
                                        .getString(ConnectionCoreConstants.PREFERENCE_KRB5_LOGIN_MODULE);

                                if (!useKrb5SystemProperties) {
                                    gssApiRequest.setUsername(bindPrincipal);
                                    gssApiRequest.setCredentials(bindPassword);
                                    gssApiRequest.setQualityOfProtection(
                                            connection.getConnectionParameter().getSaslQop());
                                    gssApiRequest.setSecurityStrength(
                                            connection.getConnectionParameter().getSaslSecurityStrength());
                                    gssApiRequest.setMutualAuthentication(
                                            connection.getConnectionParameter().isSaslMutualAuthentication());
                                    gssApiRequest
                                            .setLoginModuleConfiguration(new InnerConfiguration(krb5LoginModule));

                                    switch (connection.getConnectionParameter().getKrb5Configuration()) {
                                    case FILE:
                                        gssApiRequest.setKrb5ConfFilePath(
                                                connection.getConnectionParameter().getKrb5ConfigurationFile());
                                        break;
                                    case MANUAL:
                                        gssApiRequest
                                                .setRealmName(connection.getConnectionParameter().getKrb5Realm());
                                        gssApiRequest
                                                .setKdcHost(connection.getConnectionParameter().getKrb5KdcHost());
                                        gssApiRequest
                                                .setKdcPort(connection.getConnectionParameter().getKrb5KdcPort());
                                        break;
                                    default:
                                        break;
                                    }
                                }

                                bindResponse = ldapConnection.bind(gssApiRequest);
                                break;
                            }
                        }

                        checkResponse(bindResponse);
                    } catch (Exception e) {
                        exception = e;
                    }
                }
            };

            runAndMonitor(runnable, monitor);

            if (runnable.getException() != null) {
                throw runnable.getException();
            }
        } else {
            throw new Exception(Messages.DirectoryApiConnectionWrapper_NoConnection);
        }
    }

    /***
     * {@inheritDoc}
     */
    public void unbind() {
        disconnect();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isConnected() {
        return (ldapConnection != null && ldapConnection.isConnected());
    }

    /**
     * {@inheritDoc}
     */
    public void setBinaryAttributes(Collection<String> binaryAttributes) {
        if (binaryAttributeDetector != null) {
            // Clear the initial list
            binaryAttributeDetector.setBinaryAttributes();

            // Add each binary attribute
            for (String binaryAttribute : binaryAttributes) {
                binaryAttributeDetector.addBinaryAttribute(binaryAttribute);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public StudioNamingEnumeration search(final String searchBase, final String filter,
            final SearchControls searchControls, final AliasDereferencingMethod aliasesDereferencingMethod,
            final ReferralHandlingMethod referralsHandlingMethod, final Control[] controls,
            final StudioProgressMonitor monitor, final ReferralsInfo referralsInfo) {
        final long requestNum = searchRequestNum++;

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // Preparing the search request
                    SearchRequest request = new SearchRequestImpl();
                    request.setBase(new Dn(searchBase));
                    ExprNode node = FilterParser.parse(filter, true);
                    request.setFilter(node);
                    request.setScope(convertSearchScope(searchControls));
                    if (searchControls.getReturningAttributes() != null) {
                        request.addAttributes(searchControls.getReturningAttributes());
                    }
                    request.addAllControls(convertControls(controls));
                    request.setSizeLimit(searchControls.getCountLimit());
                    request.setTimeLimit(searchControls.getTimeLimit());
                    request.setDerefAliases(convertAliasDerefMode(aliasesDereferencingMethod));

                    // Performing the search operation
                    SearchCursor cursor = ldapConnection.search(request);

                    // Returning the result of the search
                    namingEnumeration = new CursorStudioNamingEnumeration(connection, cursor, searchBase, filter,
                            searchControls, aliasesDereferencingMethod, referralsHandlingMethod, controls,
                            requestNum, monitor, referralsInfo);
                } catch (Exception e) {
                    exception = e;
                }

                NamingException ne = null;
                if (exception != null) {
                    ne = new NamingException(exception.getMessage());
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    if (namingEnumeration != null) {
                        logger.logSearchRequest(connection, searchBase, filter, searchControls,
                                aliasesDereferencingMethod, controls, requestNum, ne);
                    } else {
                        logger.logSearchRequest(connection, searchBase, filter, searchControls,
                                aliasesDereferencingMethod, controls, requestNum, ne);
                        logger.logSearchResultDone(connection, 0, requestNum, ne);
                    }
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (Exception e) {
            monitor.reportError(e);
            return null;
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
            return null;
        } else {
            return runnable.getResult();
        }
    }

    /**
     * Converts the search scope.
     *
     * @param searchControls
     *      the search controls
     * @return
     *      the associated search scope
     */
    private SearchScope convertSearchScope(SearchControls searchControls) {
        int scope = searchControls.getSearchScope();
        if (scope == SearchControls.OBJECT_SCOPE) {
            return SearchScope.OBJECT;
        } else if (scope == SearchControls.ONELEVEL_SCOPE) {
            return SearchScope.ONELEVEL;
        } else if (scope == SearchControls.SUBTREE_SCOPE) {
            return SearchScope.SUBTREE;
        } else {
            return SearchScope.SUBTREE;
        }
    }

    /**
     * Converts the controls.
     *
     * @param controls
     *      an array of controls
     * @return
     *      an array of converted controls
     */
    private org.apache.directory.api.ldap.model.message.Control[] convertControls(Control[] controls)
            throws Exception {
        if (controls != null) {
            org.apache.directory.api.ldap.model.message.Control[] returningControls = new org.apache.directory.api.ldap.model.message.Control[controls.length];

            for (int i = 0; i < controls.length; i++) {
                returningControls[i] = ldapConnection.getCodecService().fromJndiControl(controls[i]);
            }

            return returningControls;
        } else {
            return new org.apache.directory.api.ldap.model.message.Control[0];
        }
    }

    /**
     * Converts the Alias Dereferencing method.
     *
     * @param aliasesDereferencingMethod
     *      the Alias Dereferencing method.
     * @return
     *      the converted Alias Dereferencing method.
     */
    private AliasDerefMode convertAliasDerefMode(AliasDereferencingMethod aliasesDereferencingMethod) {
        switch (aliasesDereferencingMethod) {
        case ALWAYS:
            return AliasDerefMode.DEREF_ALWAYS;
        case FINDING:
            return AliasDerefMode.DEREF_FINDING_BASE_OBJ;
        case NEVER:
            return AliasDerefMode.NEVER_DEREF_ALIASES;
        case SEARCH:
            return AliasDerefMode.DEREF_IN_SEARCHING;
        default:
            return AliasDerefMode.DEREF_ALWAYS;
        }
    }

    /**
     * {@inheritDoc}
     */
    public void modifyEntry(final String dn, final ModificationItem[] modificationItems, final Control[] controls,
            final StudioProgressMonitor monitor, final ReferralsInfo referralsInfo) {
        if (connection.isReadOnly()) {
            monitor.reportError(
                    new Exception(NLS.bind(Messages.error__connection_is_readonly, connection.getName())));
            return;
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // Preparing the modify request
                    ModifyRequest request = new ModifyRequestImpl();
                    request.setName(new Dn(dn));
                    Modification[] modifications = convertModificationItems(modificationItems);
                    if (modifications != null) {
                        for (Modification modification : modifications) {
                            request.addModification(modification);
                        }
                    }
                    request.addAllControls(convertControls(controls));

                    // Performing the modify operation
                    ModifyResponse modifyResponse = ldapConnection.modify(request);

                    // Handle referral
                    Consumer<ReferralHandlingData> consumer = referralHandlingData -> referralHandlingData.connectionWrapper
                            .modifyEntry(referralHandlingData.referralDn, modificationItems, controls, monitor,
                                    referralHandlingData.newReferralsInfo);

                    if (checkAndHandleReferral(modifyResponse, monitor, referralsInfo, consumer)) {
                        return;
                    }

                    // Checking the response
                    checkResponse(modifyResponse);
                } catch (Exception e) {
                    exception = e;
                }

                NamingException ne = null;
                if (exception != null) {
                    ne = new NamingException(exception.getMessage());
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    logger.logChangetypeModify(connection, dn, modificationItems, controls, ne);
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (Exception e) {
            monitor.reportError(e);
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
        }
    }

    /**
     * Converts modification items.
     *
     * @param modificationItems
     *      an array of modification items
     * @return
     *      an array of converted modifications
     * @throws LdapInvalidAttributeValueException 
     */
    private Modification[] convertModificationItems(ModificationItem[] modificationItems)
            throws LdapInvalidAttributeValueException {
        if (modificationItems != null) {
            List<Modification> modifications = new ArrayList<>();

            for (ModificationItem modificationItem : modificationItems) {
                Modification modification = new DefaultModification();

                modification.setAttribute(AttributeUtils.toApiAttribute(modificationItem.getAttribute()));
                modification.setOperation(convertModificationOperation(modificationItem.getModificationOp()));
                modifications.add(modification);
            }

            return modifications.toArray(new Modification[0]);
        } else {
            return null;
        }
    }

    /**
     * Converts a modification operation.
     *
     * @param modificationOp
     *      a modification operation
     * @return
     *      the converted modification operation
     */
    private ModificationOperation convertModificationOperation(int modificationOp) {
        if (modificationOp == DirContext.ADD_ATTRIBUTE) {
            return ModificationOperation.ADD_ATTRIBUTE;
        } else if (modificationOp == DirContext.REPLACE_ATTRIBUTE) {
            return ModificationOperation.REPLACE_ATTRIBUTE;
        } else if (modificationOp == DirContext.REMOVE_ATTRIBUTE) {
            return ModificationOperation.REMOVE_ATTRIBUTE;
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public void renameEntry(final String oldDn, final String newDn, final boolean deleteOldRdn,
            final Control[] controls, final StudioProgressMonitor monitor, final ReferralsInfo referralsInfo) {
        if (connection.isReadOnly()) {
            monitor.reportError(
                    new Exception(NLS.bind(Messages.error__connection_is_readonly, connection.getName())));
            return;
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // Preparing the rename request
                    ModifyDnRequest request = new ModifyDnRequestImpl();
                    request.setName(new Dn(oldDn));
                    request.setDeleteOldRdn(deleteOldRdn);
                    Dn newName = new Dn(newDn);
                    request.setNewRdn(newName.getRdn());
                    request.setNewSuperior(newName.getParent());
                    request.addAllControls(convertControls(controls));

                    // Performing the rename operation
                    ModifyDnResponse modifyDnResponse = ldapConnection.modifyDn(request);

                    // Handle referral
                    Consumer<ReferralHandlingData> consumer = referralHandlingData -> referralHandlingData.connectionWrapper
                            .renameEntry(oldDn, newDn, deleteOldRdn, controls, monitor,
                                    referralHandlingData.newReferralsInfo);

                    if (checkAndHandleReferral(modifyDnResponse, monitor, referralsInfo, consumer)) {
                        return;
                    }

                    // Checking the response
                    checkResponse(modifyDnResponse);
                } catch (Exception e) {
                    exception = e;
                }

                NamingException ne = null;
                if (exception != null) {
                    ne = new NamingException(exception.getMessage());
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    logger.logChangetypeModDn(connection, oldDn, newDn, deleteOldRdn, controls, ne);
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (Exception e) {
            monitor.reportError(e);
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
        }
    }

    /**
     * {@inheritDoc}
     */
    public void createEntry(final String dn, final Attributes attributes, final Control[] controls,
            final StudioProgressMonitor monitor, final ReferralsInfo referralsInfo) {
        if (connection.isReadOnly()) {
            monitor.reportError(
                    new Exception(NLS.bind(Messages.error__connection_is_readonly, connection.getName())));
            return;
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // Preparing the add request
                    AddRequest request = new AddRequestImpl();
                    request.setEntryDn(new Dn(dn));
                    request.setEntry(AttributeUtils.toEntry(attributes, new Dn(dn)));
                    request.addAllControls(convertControls(controls));

                    // Performing the add operation
                    AddResponse addResponse = ldapConnection.add(request);

                    // Handle referral
                    Consumer<ReferralHandlingData> consumer = referralHandlingData -> referralHandlingData.connectionWrapper
                            .createEntry(referralHandlingData.referralDn, attributes, controls, monitor,
                                    referralHandlingData.newReferralsInfo);

                    if (checkAndHandleReferral(addResponse, monitor, referralsInfo, consumer)) {
                        return;
                    }

                    // Checking the response
                    checkResponse(addResponse);
                } catch (Exception e) {
                    exception = e;
                }

                NamingException ne = null;
                if (exception != null) {
                    ne = new NamingException(exception.getMessage());
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    logger.logChangetypeAdd(connection, dn, attributes, controls, ne);
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (Exception e) {
            monitor.reportError(e);
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
        }
    }

    /**
     * {@inheritDoc}
     */
    public void deleteEntry(final String dn, final Control[] controls, final StudioProgressMonitor monitor,
            final ReferralsInfo referralsInfo) {
        if (connection.isReadOnly()) {
            monitor.reportError(
                    new Exception(NLS.bind(Messages.error__connection_is_readonly, connection.getName())));
            return;
        }

        InnerRunnable runnable = new InnerRunnable() {
            public void run() {
                try {
                    // Preparing the delete request
                    DeleteRequest request = new DeleteRequestImpl();
                    request.setName(new Dn(dn));
                    request.addAllControls(convertControls(controls));

                    // Performing the delete operation
                    DeleteResponse deleteResponse = ldapConnection.delete(request);

                    // Handle referral
                    Consumer<ReferralHandlingData> consumer = referralHandlingData -> referralHandlingData.connectionWrapper
                            .deleteEntry(referralHandlingData.referralDn, controls, monitor,
                                    referralHandlingData.newReferralsInfo);

                    if (checkAndHandleReferral(deleteResponse, monitor, referralsInfo, consumer)) {
                        return;
                    }

                    // Checking the response
                    checkResponse(deleteResponse);
                } catch (Exception e) {
                    exception = e;
                }

                NamingException ne = null;
                if (exception != null) {
                    ne = new NamingException(exception.getMessage());
                }

                for (IJndiLogger logger : getJndiLoggers()) {
                    logger.logChangetypeDelete(connection, dn, controls, ne);
                }
            }
        };

        try {
            checkConnectionAndRunAndMonitor(runnable, monitor);
        } catch (Exception e) {
            monitor.reportError(e);
        }

        if (runnable.isCanceled()) {
            monitor.setCanceled(true);
        }
        if (runnable.getException() != null) {
            monitor.reportError(runnable.getException());
        }
    }

    /**
     * Inner runnable used in connection wrapper operations.
     *
     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
     */
    abstract class InnerRunnable implements Runnable {
        protected StudioNamingEnumeration namingEnumeration = null;
        protected Exception exception = null;
        protected boolean canceled = false;

        /**
         * Gets the exception.
         * 
         * @return the exception
         */
        public Exception getException() {
            return exception;
        }

        /**
         * Gets the result.
         * 
         * @return the result
         */
        public StudioNamingEnumeration getResult() {
            return namingEnumeration;
        }

        /**
         * Checks if is canceled.
         * 
         * @return true, if is canceled
         */
        public boolean isCanceled() {
            return canceled;
        }

        /**
         * Reset.
         */
        public void reset() {
            namingEnumeration = null;
            exception = null;
            canceled = false;
        }
    }

    private boolean checkAndHandleReferral(ResultResponse response, StudioProgressMonitor monitor,
            ReferralsInfo referralsInfo, Consumer<ReferralHandlingData> consumer)
            throws NamingException, LdapURLEncodingException {
        if (response == null) {
            return false;
        }

        LdapResult ldapResult = response.getLdapResult();
        if (ldapResult == null || !ResultCodeEnum.REFERRAL.equals(ldapResult.getResultCode())) {
            return false;
        }

        if (referralsInfo == null) {
            referralsInfo = new ReferralsInfo(true);
        }

        Referral referral = ldapResult.getReferral();
        referralsInfo.addReferral(referral);
        Referral nextReferral = referralsInfo.getNextReferral();

        Connection referralConnection = ConnectionWrapperUtils.getReferralConnection(nextReferral, monitor, this);
        if (referralConnection == null) {
            monitor.setCanceled(true);
            return true;
        }

        List<String> urls = new ArrayList<>(referral.getLdapUrls());
        String referralDn = new LdapUrl(urls.get(0)).getDn().getName();
        ReferralHandlingData referralHandlingData = new ReferralHandlingData(
                referralConnection.getConnectionWrapper(), referralDn, referralsInfo);
        consumer.accept(referralHandlingData);

        return true;
    }

    static class ReferralHandlingData {
        ConnectionWrapper connectionWrapper;
        String referralDn;
        ReferralsInfo newReferralsInfo;

        ReferralHandlingData(ConnectionWrapper connectionWrapper, String referralDn,
                ReferralsInfo newReferralsInfo) {
            this.connectionWrapper = connectionWrapper;
            this.referralDn = referralDn;
            this.newReferralsInfo = newReferralsInfo;
        }
    }

    private void checkConnectionAndRunAndMonitor(final InnerRunnable runnable, final StudioProgressMonitor monitor)
            throws Exception {
        // check connection
        if (!isConnected || ldapConnection == null) {
            doConnect(monitor);
            doBind(monitor);
        }
        if (ldapConnection == null) {
            throw new NamingException(Messages.DirectoryApiConnectionWrapper_NoConnection);
        }

        // loop for reconnection
        for (int i = 0; i <= 1; i++) {
            runAndMonitor(runnable, monitor);

            // check reconnection
            if ((i == 0) && (runnable.getException() instanceof InvalidConnectionException)) {
                doConnect(monitor);
                doBind(monitor);
                runnable.reset();
            } else {
                break;
            }
        }
    }

    private void runAndMonitor(final InnerRunnable runnable, final StudioProgressMonitor monitor)
            throws CancelException {
        if (!monitor.isCanceled()) {
            // monitor
            StudioProgressMonitor.CancelListener listener = event -> {
                if (monitor.isCanceled()) {
                    if (jobThread != null && jobThread.isAlive()) {
                        jobThread.interrupt();
                    }

                    if (ldapConnection != null) {
                        try {
                            ldapConnection.close();
                        } catch (Exception e) {
                        }

                        isConnected = false;
                        ldapConnection = null;
                    }

                    isConnected = false;
                }
            };

            monitor.addCancelListener(listener);
            jobThread = Thread.currentThread();

            // run
            try {
                runnable.run();
            } finally {
                monitor.removeCancelListener(listener);
                jobThread = null;
            }

            if (monitor.isCanceled()) {
                throw new CancelException();
            }
        }
    }

    private final class InnerConfiguration extends Configuration {
        private String krb5LoginModule;
        private AppConfigurationEntry[] configList = null;

        public InnerConfiguration(String krb5LoginModule) {
            this.krb5LoginModule = krb5LoginModule;
        }

        public AppConfigurationEntry[] getAppConfigurationEntry(String applicationName) {
            if (configList == null) {
                HashMap<String, Object> options = new HashMap<>();

                // TODO: this only works for Sun JVM
                options.put("refreshKrb5Config", "true"); //$NON-NLS-1$ //$NON-NLS-2$
                switch (connection.getConnectionParameter().getKrb5CredentialConfiguration()) {
                case USE_NATIVE:
                    options.put("useTicketCache", "true"); //$NON-NLS-1$ //$NON-NLS-2$
                    options.put("doNotPrompt", "true"); //$NON-NLS-1$ //$NON-NLS-2$
                    break;
                case OBTAIN_TGT:
                    options.put("doNotPrompt", "false"); //$NON-NLS-1$ //$NON-NLS-2$
                    break;
                }

                configList = new AppConfigurationEntry[1];
                configList[0] = new AppConfigurationEntry(krb5LoginModule, LoginModuleControlFlag.REQUIRED,
                        options);
            }
            return configList;
        }

        @Override
        public void refresh() {
        }
    }

    private List<IJndiLogger> getJndiLoggers() {
        return ConnectionCorePlugin.getDefault().getJndiLoggers();
    }

    /**
     * Checks the given response.
     *
     * @param response
     *      the response
     * @throws Exception
     *      if the LDAP result associated with the response is not a success
     */
    private void checkResponse(ResultResponse response) throws Exception {
        if (response != null) {
            LdapResult ldapResult = response.getLdapResult();
            if (ldapResult != null) {
                // NOT_ALLOWED_ON_NON_LEAF error (thrown when deleting an entry with children)
                if (ResultCodeEnum.NOT_ALLOWED_ON_NON_LEAF.equals(ldapResult.getResultCode())) {
                    throw new ContextNotEmptyException(ldapResult.getDiagnosticMessage());
                }
                // ENTRY_ALREADY_EXISTS error
                // (We need this conversion in the case where this error is thrown during an LDIF
                // import with the "Update existing entries" flag turned on)
                else if (ResultCodeEnum.ENTRY_ALREADY_EXISTS.equals(ldapResult.getResultCode())) {
                    throw new NameAlreadyBoundException(ldapResult.getDiagnosticMessage());
                }
                // Different from SUCCESS, we throw a generic exception
                else if (!ResultCodeEnum.SUCCESS.equals(ldapResult.getResultCode())) {
                    int code = ldapResult.getResultCode().getResultCode();
                    String message = ldapResult.getDiagnosticMessage();

                    // Checking if we got a message from the LDAP result
                    if (StringUtils.isEmpty(message)) {
                        // Assigning the generic result code description
                        message = Utils.getResultCodeDescription(code);
                    }

                    throw new Exception(NLS.bind("[LDAP: error code {0} - {1}]", new String[] //$NON-NLS-1$
                    { Integer.toString(code), message })); //$NON-NLS-1$
                }
            }
        }
    }
}