com.evolveum.polygon.connector.ldap.AbstractLdapConnector.java Source code

Java tutorial

Introduction

Here is the source code for com.evolveum.polygon.connector.ldap.AbstractLdapConnector.java

Source

/*
 * Copyright (c) 2015-2016 Evolveum
 *
 * Licensed 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 com.evolveum.polygon.connector.ldap;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.ArrayUtils;
import org.apache.directory.api.ldap.extras.controls.permissiveModify.PermissiveModify;
import org.apache.directory.api.ldap.extras.controls.permissiveModify.PermissiveModifyImpl;
import org.apache.directory.api.ldap.extras.controls.vlv.VirtualListViewRequest;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException;
import org.apache.directory.api.ldap.model.cursor.SearchCursor;
import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.DefaultModification;
import org.apache.directory.api.ldap.model.entry.Entry;
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.entry.Value;
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.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
import org.apache.directory.api.ldap.model.filter.EqualityNode;
import org.apache.directory.api.ldap.model.filter.ExprNode;
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.LdapResult;
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.Response;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
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.SearchResultEntry;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.message.controls.PagedResults;
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.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.LdapSyntax;
import org.apache.directory.api.ldap.model.schema.MatchingRule;
import org.apache.directory.api.ldap.model.schema.MutableAttributeType;
import org.apache.directory.api.ldap.model.schema.Normalizer;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.ldap.model.url.LdapUrl;
import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager;
import org.apache.directory.ldap.client.api.DefaultSchemaLoader;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.directory.ldap.client.api.exception.InvalidConnectionException;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.ConfigurationException;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.exceptions.ConnectorIOException;
import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException;
import org.identityconnectors.framework.common.exceptions.UnknownUidException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.identityconnectors.framework.common.objects.Name;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.PredefinedAttributes;
import org.identityconnectors.framework.common.objects.QualifiedUid;
import org.identityconnectors.framework.common.objects.ResultsHandler;
import org.identityconnectors.framework.common.objects.Schema;
import org.identityconnectors.framework.common.objects.SearchResult;
import org.identityconnectors.framework.common.objects.SyncResultsHandler;
import org.identityconnectors.framework.common.objects.SyncToken;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.framework.common.objects.filter.ContainsAllValuesFilter;
import org.identityconnectors.framework.common.objects.filter.EqualsFilter;
import org.identityconnectors.framework.common.objects.filter.Filter;
import org.identityconnectors.framework.common.objects.filter.FilterTranslator;
import org.identityconnectors.framework.common.objects.filter.OrFilter;
import org.identityconnectors.framework.spi.Configuration;
import org.identityconnectors.framework.spi.PoolableConnector;
import org.identityconnectors.framework.spi.SearchResultsHandler;
import org.identityconnectors.framework.spi.operations.CreateOp;
import org.identityconnectors.framework.spi.operations.DeleteOp;
import org.identityconnectors.framework.spi.operations.SchemaOp;
import org.identityconnectors.framework.spi.operations.SearchOp;
import org.identityconnectors.framework.spi.operations.SyncOp;
import org.identityconnectors.framework.spi.operations.TestOp;
import org.identityconnectors.framework.spi.operations.UpdateAttributeValuesOp;

import com.evolveum.polygon.common.SchemaUtil;
import com.evolveum.polygon.connector.ldap.schema.GuardedStringValue;
import com.evolveum.polygon.connector.ldap.schema.LdapFilterTranslator;
import com.evolveum.polygon.connector.ldap.schema.AbstractSchemaTranslator;
import com.evolveum.polygon.connector.ldap.schema.ScopedFilter;
import com.evolveum.polygon.connector.ldap.search.DefaultSearchStrategy;
import com.evolveum.polygon.connector.ldap.search.SearchStrategy;
import com.evolveum.polygon.connector.ldap.search.SimplePagedResultsSearchStrategy;
import com.evolveum.polygon.connector.ldap.search.VlvSearchStrategy;
import com.evolveum.polygon.connector.ldap.sync.AdDirSyncStrategy;
import com.evolveum.polygon.connector.ldap.sync.ModifyTimestampSyncStrategy;
import com.evolveum.polygon.connector.ldap.sync.SunChangelogSyncStrategy;
import com.evolveum.polygon.connector.ldap.sync.SyncStrategy;

public abstract class AbstractLdapConnector<C extends AbstractLdapConfiguration> implements PoolableConnector,
        TestOp, SchemaOp, SearchOp<Filter>, CreateOp, DeleteOp, UpdateAttributeValuesOp, SyncOp {

    private static final Log LOG = Log.getLog(AbstractLdapConnector.class);

    private C configuration;
    private ConnectionManager<C> connectionManager;
    private SchemaManager schemaManager = null;
    private AbstractSchemaTranslator<C> schemaTranslator = null;
    private SyncStrategy<C> syncStrategy = null;
    private Boolean usePermissiveModify = null;

    public AbstractLdapConnector() {
        super();
        LOG.info("Creating {0} connector instance {1}", this.getClass().getSimpleName(), this);
    }

    @Override
    public C getConfiguration() {
        return configuration;
    }

    protected ConnectionManager<C> getConnectionManager() {
        return connectionManager;
    }

    @Override
    public void init(Configuration configuration) {
        LOG.info("Initializing {0} connector instance {1}", this.getClass().getSimpleName(), this);
        this.configuration = (C) configuration;
        this.configuration.recompute();
        connectionManager = new ConnectionManager<>(this.configuration);
        connectionManager.connect();
        if (LOG.isOk()) {
            LOG.ok("Servers:\n{0}", connectionManager.dumpServers());
        }
    }

    @Override
    public void test() {
        LOG.info("Test {0} connector instance {1}", this.getClass().getSimpleName(), this);
        cleanupBeforeTest();
        connectionManager.connect();
        if (configuration.isEnableExtraTests()) {
            extraTests();
        }
        reconnectAfterTest();
        checkAlive();
        additionalConnectionTests();
        try {
            LOG.ok("Fetching root DSE");
            Entry rootDse = connectionManager.getDefaultConnection().getRootDse();
            LOG.ok("Root DSE: {0}", rootDse);
        } catch (LdapException e) {
            throw processLdapException(null, e);
        }
    }

    protected void cleanupBeforeTest() {
        try {
            LOG.ok("Closing connections ... to reopen them again");
            connectionManager.close();
        } catch (IOException e) {
            throw new ConnectorIOException(e.getMessage(), e);
        }
        schemaManager = null;
        schemaTranslator = null;
    }

    protected void reconnectAfterTest() {

    }

    protected void additionalConnectionTests() {

    }

    protected void extraTests() {

        analyzeAttrDef("dc");

        analyzeDn("CN=foo bar,OU=people,DC=EXamPLE,dc=CoM");
        analyzeDn(configuration.getBaseContext());
        analyzeDn(configuration.getBindDn());

        testAncestor("dc=example,dc=com", "uid=foo,ou=people,dc=example,dc=com", true);
        testAncestor("uid=foo,ou=people,dc=example,dc=com", "dc=example,dc=com", false);
        testAncestor("dc=example,dc=com", "dc=example,dc=com", true);
        testAncestor("dc=example,dc=com", "CN=foo bar,OU=people,DC=example,DC=com", true);
        // TODO: This fails for LDAP servers (MID-3477)
        testAncestor("dc=example,dc=com", "CN=foo bar,OU=people,DC=EXamPLE,DC=COM", true);
        testAncestor("DC=example,DC=com", "cn=foo bar,ou=people,dc=example,dc=com", true);
        testAncestor("DC=exAMple,DC=com", "CN=foo bar,OU=people,DC=EXamPLE,dc=COM", true);
        testAncestor("DC=badEXAMPLE,DC=com", "CN=foo bar,OU=people,DC=EXamPLE,dc=COM", false);
        testAncestor("DC=badexample,DC=com", "CN=foo bar,OU=people,DC=example,dc=com", false);
        testAncestor("dc=badexample,dc=com", "cn=foo bar,ou=people,dc=example,dc=com", false);
    }

    private void analyzeAttrDef(String attrName) {
        AttributeType attributeType = getSchemaManager().getAttributeType(attrName);
        LOG.ok("Definition of LDAP attribute {0}: {1}", attrName, attributeType);
        if (attributeType != null) {
            MatchingRule equality = attributeType.getEquality();
            LOG.ok("Equality matching rule {0}", equality);
            if (equality != null) {
                Normalizer normalizer = equality.getNormalizer();
                LOG.ok("Equality normalizer ({0}): {1}", normalizer == null ? null : normalizer.getClass(),
                        normalizer);
                if (normalizer != null) {
                    String in = " tHiS is REALLY stRAngE  ";
                    try {
                        LOG.ok("Normalized ''{0}'' -> ''{1}''", in, normalizer.normalize(in));
                    } catch (LdapException e) {
                        LOG.error("Normalized error (input: '" + in + "': " + e.getMessage(), e);
                    }
                }
            }

            LdapSyntax syntax = attributeType.getSyntax();
            LOG.ok("Syntax {0}", syntax);
            if (syntax != null) {
                LOG.ok("Syntax checker {0}", syntax.getSyntaxChecker());
            }
        }
    }

    private void analyzeDn(String stringDn) {
        if (stringDn == null) {
            return;
        }
        Dn dn = asDn(getSchemaManager(), stringDn);
        LOG.ok("Parsed DN {0}: {1}", stringDn, dn);
        List<Rdn> rdns = dn.getRdns();
        LOG.ok("Parsed RDNs: {0}", rdns);
        Rdn lastRdn = rdns.get(rdns.size() - 1);
        LOG.ok("Last RDN: {0}", lastRdn);
        LOG.ok("Last RDN AVA: {0}", lastRdn.getAva());
        LOG.ok("Last RDN AVA name: {0}", lastRdn.getAva().getName());
        LOG.ok("Last RDN AVA norm name: {0}", lastRdn.getAva().getNormName());
        LOG.ok("Last RDN AVA type: {0}", lastRdn.getAva().getType());
        LOG.ok("Last RDN AVA attributeType: {0}", lastRdn.getAva().getAttributeType());
        LOG.ok("Last RDN norm value: {0}", lastRdn.getNormValue());
    }

    protected void testAncestor(String upper, String lower, boolean expectedMatch) {
        Dn upperDn = asDn(upper);
        Dn lowerDn = asDn(lower);
        boolean ancestorOf = LdapUtil.isAncestorOf(upperDn, lowerDn, getSchemaTranslator());
        if (ancestorOf && !expectedMatch) {
            String msg = "Dn '" + upper + "' is wrongly evaluated as ancestor of '" + lower
                    + "' (it should NOT be).";
            LOG.error("Extra test: {0}", msg);
            throw new ConnectorException(msg);
        }
        if (!ancestorOf && expectedMatch) {
            String msg = "Dn '" + upper + "' is NOT evaluated as ancestor of '" + lower + "' (but it should be).";
            LOG.error("Extra test: {0}", msg);
            throw new ConnectorException(msg);
        }
        if (LOG.isOk()) {
            String msg;
            if (ancestorOf) {
                msg = "Dn '" + upper + "' is correctly evaluated as ancestor of '" + lower + "'";
            } else {
                msg = "Dn '" + upper + "' is correctly evaluated NOT yo be ancestor of '" + lower + "'";
            }
            LOG.ok("Extra test: {0}", msg);
        }
    }

    private Dn asDn(String stringDn) {
        try {
            return new Dn(stringDn);
        } catch (LdapInvalidDnException e) {
            throw new ConnectorException("Cannot parse '" + stringDn + " as DN: " + e.getMessage(), e);
        }
    }

    private Dn asDn(SchemaManager schemaManager, String stringDn) {
        try {
            return new Dn(schemaManager, stringDn);
        } catch (LdapInvalidDnException e) {
            throw new ConnectorException("Cannot parse '" + stringDn + " as DN: " + e.getMessage(), e);
        }
    }

    protected SchemaManager getSchemaManager() {
        if (schemaManager == null) {
            try {
                boolean schemaQuirksMode = configuration.isSchemaQuirksMode();
                LOG.ok("Loading schema (quirksMode={0})", schemaQuirksMode);
                DefaultSchemaLoader schemaLoader = new DefaultSchemaLoader(connectionManager.getDefaultConnection(),
                        schemaQuirksMode);
                DefaultSchemaManager defSchemaManager = new DefaultSchemaManager(schemaLoader);
                try {
                    if (schemaQuirksMode) {
                        defSchemaManager.setRelaxed();
                        defSchemaManager.loadAllEnabledRelaxed();
                    } else {
                        defSchemaManager.loadAllEnabled();
                    }
                } catch (Exception e) {
                    throw new ConnectorIOException(e.getMessage(), e);
                }
                if (!defSchemaManager.getErrors().isEmpty()) {
                    if (schemaQuirksMode) {
                        LOG.ok("There are {0} schema errors, but we are in quirks mode so we are ignoring them",
                                defSchemaManager.getErrors().size());
                        if (isLogSchemaErrors()) {
                            for (Throwable error : defSchemaManager.getErrors()) {
                                LOG.ok("Schema error (ignored): {0}: {1}", error.getClass().getName(),
                                        error.getMessage());
                            }
                        }
                    } else {
                        throw new ConnectorIOException("Errors loading schema " + defSchemaManager.getErrors());
                    }
                }
                schemaManager = defSchemaManager;
                //             connection.setSchemaManager(defSchemaManager);
                //             connection.loadSchema(defSchemaManager);
            } catch (LdapException e) {
                throw new ConnectorIOException(e.getMessage(), e);
            } catch (Exception e) {
                // Brutal. We cannot really do anything smarter here.
                throw new ConnectorException(e.getMessage(), e);
            }

            try {
                LOG.info("Schema loaded, {0} schemas, {1} object classes, {2} errors",
                        schemaManager.getAllSchemas().size(), schemaManager.getObjectClassRegistry().size(),
                        schemaManager.getErrors().size());
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
            patchSchemaManager(schemaManager);
        }
        return schemaManager;
    }

    protected void patchSchemaManager(SchemaManager schemaManager) {
        // Nothing to do here. But useful in subclasses.
    }

    protected boolean isLogSchemaErrors() {
        return true;
    }

    protected AbstractSchemaTranslator<C> getSchemaTranslator() {
        if (schemaTranslator == null) {
            schemaTranslator = createSchemaTranslator();
            connectionManager.setSchemaTranslator(schemaTranslator);
        }
        return schemaTranslator;
    }

    protected abstract AbstractSchemaTranslator<C> createSchemaTranslator();

    @Override
    public Schema schema() {
        if (!connectionManager.isConnected()) {
            return null;
        }
        // always fetch fresh schema when this method is called
        schemaManager = null;
        schemaTranslator = null;
        try {
            return getSchemaTranslator().translateSchema(connectionManager);
        } catch (InvalidConnectionException e) {
            // The connection might have been disconnected. Try to reconnect.
            connectionManager.connect();
            try {
                return getSchemaTranslator().translateSchema(connectionManager);
            } catch (InvalidConnectionException e1) {
                throw new ConnectorException("Reconnect error: " + e.getMessage(), e);
            }
        }
    }

    private void prepareIcfSchema() {
        try {
            getSchemaTranslator().prepareIcfSchema(connectionManager);
        } catch (InvalidConnectionException e) {
            // The connection might have been disconnected. Try to reconnect.
            connectionManager.connect();
            try {
                getSchemaTranslator().prepareIcfSchema(connectionManager);
            } catch (InvalidConnectionException e1) {
                throw new ConnectorException("Reconnect error: " + e.getMessage(), e);
            }
        }
    }

    protected boolean isUsePermissiveModify() throws LdapException {
        if (usePermissiveModify == null) {
            switch (configuration.getUsePermissiveModify()) {
            case AbstractLdapConfiguration.USE_PERMISSIVE_MODIFY_ALWAYS:
                usePermissiveModify = true;
                break;
            case AbstractLdapConfiguration.USE_PERMISSIVE_MODIFY_NEVER:
                usePermissiveModify = false;
                break;
            case AbstractLdapConfiguration.USE_PERMISSIVE_MODIFY_AUTO:
                usePermissiveModify = connectionManager.getDefaultConnection()
                        .isControlSupported(PermissiveModify.OID);
                break;
            default:
                throw new ConfigurationException(
                        "Unknown usePermissiveModify value " + configuration.getUsePermissiveModify());
            }
        }
        return usePermissiveModify;
    }

    @Override
    public FilterTranslator<Filter> createFilterTranslator(ObjectClass objectClass, OperationOptions options) {
        // Just return dummy filter translator that does not translate anything. We need better control over the
        // filter translation than what the framework can provide.
        return new FilterTranslator<Filter>() {
            @Override
            public List<Filter> translate(Filter filter) {
                List<Filter> list = new ArrayList<Filter>(1);
                list.add(filter);
                return list;
            }
        };
    }

    @Override
    public void executeQuery(ObjectClass objectClass, Filter icfFilter, ResultsHandler handler,
            OperationOptions options) {
        prepareIcfSchema();
        org.apache.directory.api.ldap.model.schema.ObjectClass ldapObjectClass = getSchemaTranslator()
                .toLdapObjectClass(objectClass);

        SearchStrategy<C> searchStrategy;
        if (isEqualsFilter(icfFilter, Name.NAME)) {
            // Search by __NAME__, which means DN. This translated to a base search.
            searchStrategy = searchByDn(schemaTranslator.toDn(((EqualsFilter) icfFilter).getAttribute()),
                    objectClass, ldapObjectClass, handler, options);

        } else if (isEqualsFilter(icfFilter, Uid.NAME)) {
            // Search by __UID__. Special case for performance.
            searchStrategy = searchByUid((Uid) ((EqualsFilter) icfFilter).getAttribute(), objectClass,
                    ldapObjectClass, handler, options);

        } else if (isSecondaryIdentifierOrFilter(icfFilter)) {
            // Very special case. Search by DN or other secondary identifier value. It is used by IDMs to get object by 
            // This is not supported by LDAP. But it can be quite common. Therefore we want to support it as a special
            // case by executing two searches.

            searchStrategy = searchBySecondaryIdenfiers(icfFilter, objectClass, ldapObjectClass, handler, options);

        } else {

            searchStrategy = searchUsual(icfFilter, objectClass, ldapObjectClass, handler, options);

        }

        if (handler instanceof SearchResultsHandler) {
            if (searchStrategy == null) {
                // We have found nothing
                SearchResult searchResult = new SearchResult(null, 0, true);
                ((SearchResultsHandler) handler).handleResult(searchResult);
            } else {
                String cookie = searchStrategy.getPagedResultsCookie();
                int remainingResults = searchStrategy.getRemainingPagedResults();
                boolean completeResultSet = searchStrategy.isCompleteResultSet();
                SearchResult searchResult = new SearchResult(cookie, remainingResults, completeResultSet);
                ((SearchResultsHandler) handler).handleResult(searchResult);
            }
        } else {
            LOG.warn("Result handler is NOT SearchResultsHandler, it is {0}", handler.getClass());
        }

    }

    private boolean isEqualsFilter(Filter icfFilter, String icfAttrname) {
        return icfFilter != null && (icfFilter instanceof EqualsFilter)
                && icfAttrname.equals(((EqualsFilter) icfFilter).getName());
    }

    private boolean isSecondaryIdentifierOrFilter(Filter icfFilter) {
        if (icfFilter == null) {
            return false;
        }
        if (!(icfFilter instanceof OrFilter)) {
            return false;
        }
        Filter leftSubfilter = ((OrFilter) icfFilter).getLeft();
        Filter rightSubfilter = ((OrFilter) icfFilter).getRight();
        if (isEqualsFilter(leftSubfilter, Name.NAME) && ((rightSubfilter instanceof EqualsFilter)
                || (rightSubfilter instanceof ContainsAllValuesFilter))) {
            return true;
        }
        if (isEqualsFilter(rightSubfilter, Name.NAME) && ((leftSubfilter instanceof EqualsFilter)
                || (leftSubfilter instanceof ContainsAllValuesFilter))) {
            return true;
        }
        return false;
    }

    protected SearchStrategy<C> searchByDn(Dn dn, ObjectClass objectClass,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapObjectClass, ResultsHandler handler,
            OperationOptions options) {
        // This translated to a base search.
        // We know that this can return at most one object. Therefore always use simple search.
        SearchStrategy<C> searchStrategy = getDefaultSearchStrategy(objectClass, ldapObjectClass, handler, options);
        String[] attributesToGet = getAttributesToGet(ldapObjectClass, options);
        try {

            searchStrategy.search(dn, null, SearchScope.OBJECT, attributesToGet);

        } catch (UnknownUidException e) {
            // This is not really an error. This means that the object does not exist. But in this
            // case we are supposed to return nothing. We are NOT supposed to throw an error.
            // So our job id done. We already returned nothing. And we will just ignore the
            // exception.
            return searchStrategy;

        } catch (LdapException e) {
            throw processLdapException("Error searching for DN '" + dn + "'", e);

        }
        return searchStrategy;
    }

    /**
     * Returns a complete object based on ICF UID.
     * 
     * This is different from resolveDn() method in that it returns a complete object.
     * The resolveDn() method is supposed to be optimized to only return DN.
     */
    protected SearchStrategy<C> searchByUid(Uid uid, ObjectClass objectClass,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapObjectClass, ResultsHandler handler,
            OperationOptions options) {
        String uidValue = SchemaUtil.getSingleStringNonBlankValue(uid);
        if (LdapUtil.isDnAttribute(configuration.getUidAttribute())) {
            return searchByDn(schemaTranslator.toDn(uidValue), objectClass, ldapObjectClass, handler, options);
        } else {
            // We know that this can return at most one object. Therefore always use simple search.
            SearchStrategy<C> searchStrategy = getDefaultSearchStrategy(objectClass, ldapObjectClass, handler,
                    options);
            String[] attributesToGet = getAttributesToGet(ldapObjectClass, options);
            SearchScope scope = getScope(options);
            ExprNode filterNode = LdapUtil.createUidSearchFilter(uidValue, ldapObjectClass, getSchemaTranslator());
            Dn baseDn = getBaseDn(options);
            checkBaseDnPresent(baseDn);
            try {
                searchStrategy.search(baseDn, filterNode, scope, attributesToGet);
            } catch (LdapException e) {
                throw processLdapException("Error searching for UID '" + uidValue + "'", e);
            }

            return searchStrategy;
        }
    }

    private SearchStrategy<C> searchBySecondaryIdenfiers(Filter icfFilter, ObjectClass objectClass,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapObjectClass, final ResultsHandler handler,
            OperationOptions options) {
        // This translated to a base search.
        // We know that this can return at most one object. Therefore always use simple search.

        Filter leftSubfilter = ((OrFilter) icfFilter).getLeft();
        Filter rightSubfilter = ((OrFilter) icfFilter).getRight();
        EqualsFilter dnSubfilter;
        Filter otherSubfilter;
        if ((leftSubfilter instanceof EqualsFilter) && Name.NAME.equals(((EqualsFilter) leftSubfilter).getName())) {
            dnSubfilter = (EqualsFilter) leftSubfilter;
            otherSubfilter = rightSubfilter;
        } else {
            dnSubfilter = (EqualsFilter) rightSubfilter;
            otherSubfilter = leftSubfilter;
        }

        final String[] mutableFirstUid = new String[1];
        ResultsHandler innerHandler = new ResultsHandler() {
            @Override
            public boolean handle(ConnectorObject connectorObject) {
                if (mutableFirstUid[0] == null) {
                    mutableFirstUid[0] = connectorObject.getUid().getUidValue();
                } else {
                    if (connectorObject.getUid().getUidValue().equals(mutableFirstUid[0])) {
                        // We have already returned this object, skip it.
                        return true;
                    }
                }
                return handler.handle(connectorObject);
            }
        };

        // Search by DN first. This is supposed to be more efficient.

        Dn dn = schemaTranslator.toDn(dnSubfilter.getAttribute());
        try {
            searchByDn(dn, objectClass, ldapObjectClass, innerHandler, options);
        } catch (UnknownUidException e) {
            // No problem. The Dn is not here. Just no on.
            LOG.ok("The DN \"{0}\" not found: {1} (this is OK)", dn, e.getMessage());
        }

        // Search by the other attribute now

        // We know that this can return at most one object. Therefore always use simple search.
        SearchStrategy<C> searchStrategy = getDefaultSearchStrategy(objectClass, ldapObjectClass, innerHandler,
                options);
        LdapFilterTranslator filterTranslator = new LdapFilterTranslator(getSchemaTranslator(), ldapObjectClass);
        ScopedFilter scopedFilter = filterTranslator.translate(otherSubfilter, ldapObjectClass);
        ExprNode filterNode = scopedFilter.getFilter();
        String[] attributesToGet = getAttributesToGet(ldapObjectClass, options);
        SearchScope scope = getScope(options);
        Dn baseDn = getBaseDn(options);
        checkBaseDnPresent(baseDn);
        try {
            searchStrategy.search(baseDn, filterNode, scope, attributesToGet);
        } catch (LdapException e) {
            throw processLdapException("Error searching in " + baseDn, e);
        }

        return searchStrategy;

    }

    private void checkBaseDnPresent(Dn baseDn) {
        if (baseDn == null) {
            throw new ConfigurationException(
                    "No base DN present. Are you sure you have set up the base context in connector configuration?");
        }
    }

    private SearchStrategy<C> searchUsual(Filter icfFilter, ObjectClass objectClass,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapObjectClass, ResultsHandler handler,
            OperationOptions options) {
        Dn baseDn = getBaseDn(options);
        LdapFilterTranslator filterTranslator = createLdapFilterTranslator(ldapObjectClass);
        ScopedFilter scopedFilter = filterTranslator.translate(icfFilter, ldapObjectClass);
        ExprNode filterNode = scopedFilter.getFilter();
        String[] attributesToGet = getAttributesToGet(ldapObjectClass, options);

        SearchStrategy<C> searchStrategy;
        if (scopedFilter.getBaseDn() != null) {

            // The filter was limited by a ICF filter clause for __NAME__
            // so we look at exactly one object here
            searchStrategy = getDefaultSearchStrategy(objectClass, ldapObjectClass, handler, options);
            try {
                searchStrategy.search(scopedFilter.getBaseDn(), filterNode, SearchScope.OBJECT, attributesToGet);
            } catch (LdapException e) {
                throw processLdapException("Error searching for " + scopedFilter.getBaseDn(), e);
            }

        } else {

            // This is the real (usual) search
            searchStrategy = chooseSearchStrategy(objectClass, ldapObjectClass, handler, options);
            SearchScope scope = getScope(options);
            checkBaseDnPresent(baseDn);
            try {
                searchStrategy.search(baseDn, filterNode, scope, attributesToGet);
            } catch (LdapException e) {
                throw processLdapException("Error searching in " + baseDn, e);
            }

        }

        return searchStrategy;
    }

    protected LdapFilterTranslator createLdapFilterTranslator(
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapObjectClass) {
        return new LdapFilterTranslator(getSchemaTranslator(), ldapObjectClass);
    }

    private Dn getBaseDn(OperationOptions options) {
        if (options != null && options.getContainer() != null) {
            QualifiedUid containerQUid = options.getContainer();
            // HACK WARNING: this is a hack to overcome bad framework design.
            // Even though this has to be Uid, we interpret it as a DN.
            // The framework uses UID to identify everything. This is naive.
            // Strictly following the framework contract would mean to always
            // do two LDAP searches instead of one in this case.
            // So we deviate from the contract here. It is naughty, but it
            // is efficient.
            return getSchemaTranslator().toDn(containerQUid.getUid());
        } else {
            return getSchemaTranslator().toDn(configuration.getBaseContext());
        }
    }

    private SearchScope getScope(OperationOptions options) {
        if (options == null || options.getScope() == null) {
            return SearchScope.SUBTREE;
        }
        return SearchScope.getSearchScope(SearchScope.getSearchScope(options.getScope()));
    }

    protected String[] getAttributesToGet(org.apache.directory.api.ldap.model.schema.ObjectClass ldapObjectClass,
            OperationOptions options) {
        return LdapUtil.getAttributesToGet(ldapObjectClass, options, getSchemaTranslator());
    }

    protected SearchStrategy<C> chooseSearchStrategy(ObjectClass objectClass,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapObjectClass, ResultsHandler handler,
            OperationOptions options) {
        AbstractSchemaTranslator<C> schemaTranslator = getSchemaTranslator();
        String pagingStrategy = configuration.getPagingStrategy();
        if (pagingStrategy == null) {
            pagingStrategy = LdapConfiguration.PAGING_STRATEGY_AUTO;
        }

        if (options != null && options.getAllowPartialResults() != null && options.getAllowPartialResults()
                && options.getPagedResultsOffset() == null && options.getPagedResultsCookie() == null
                && options.getPageSize() == null) {
            // Search that allow partial results, no need for paging. Regardless of the configured strategy.
            return getDefaultSearchStrategy(objectClass, ldapObjectClass, handler, options);
        }

        if (LdapConfiguration.PAGING_STRATEGY_NONE.equals(pagingStrategy)) {
            // This may fail on a sizeLimit. But this is what has been configured so we are going to do it anyway.
            LOG.ok("Selecting default search strategy because strategy setting is set to {0}", pagingStrategy);
            return getDefaultSearchStrategy(objectClass, ldapObjectClass, handler, options);

        } else if (LdapConfiguration.PAGING_STRATEGY_SPR.equals(pagingStrategy)) {
            if (supportsControl(PagedResults.OID)) {
                LOG.ok("Selecting SimplePaged search strategy because strategy setting is set to {0}",
                        pagingStrategy);
                return new SimplePagedResultsSearchStrategy<>(connectionManager, configuration, schemaTranslator,
                        objectClass, ldapObjectClass, handler, options);
            } else {
                throw new ConfigurationException("Configured paging strategy " + pagingStrategy
                        + ", but the server does not support PagedResultsControl.");
            }

        } else if (LdapConfiguration.PAGING_STRATEGY_VLV.equals(pagingStrategy)) {
            if (supportsControl(VirtualListViewRequest.OID)) {
                LOG.ok("Selecting VLV search strategy because strategy setting is set to {0}", pagingStrategy);
                return new VlvSearchStrategy<>(connectionManager, configuration, getSchemaTranslator(), objectClass,
                        ldapObjectClass, handler, options);
            } else {
                throw new ConfigurationException(
                        "Configured paging strategy " + pagingStrategy + ", but the server does not support VLV.");
            }

        } else if (LdapConfiguration.PAGING_STRATEGY_AUTO.equals(pagingStrategy)) {
            if (options.getPagedResultsOffset() != null) {
                // Always prefer VLV even if the offset is 1. We expect that the client will use paging and subsequent
                // queries will come with offset other than 1. The server may use a slightly different sorting for VLV and other
                // paging mechanisms. Bu we want consisten results. Therefore in this case prefer VLV even if it might be less efficient.
                if (supportsControl(VirtualListViewRequest.OID)) {
                    LOG.ok("Selecting VLV search strategy because strategy setting is set to {0} and the request specifies an offset",
                            pagingStrategy);
                    return new VlvSearchStrategy<>(connectionManager, configuration, getSchemaTranslator(),
                            objectClass, ldapObjectClass, handler, options);
                } else {
                    throw new UnsupportedOperationException(
                            "Requested search from offset (" + options.getPagedResultsOffset()
                                    + "), but the server does not support VLV. Unable to execute the search.");
                }
            } else {
                if (supportsControl(PagedResults.OID)) {
                    // SPR is usually a better choice if no offset is specified. Less overhead on the server.
                    LOG.ok("Selecting SimplePaged search strategy because strategy setting is set to {0} and the request does not specify an offset",
                            pagingStrategy);
                    return new SimplePagedResultsSearchStrategy<>(connectionManager, configuration,
                            schemaTranslator, objectClass, ldapObjectClass, handler, options);
                } else if (supportsControl(VirtualListViewRequest.OID)) {
                    return new VlvSearchStrategy<>(connectionManager, configuration, getSchemaTranslator(),
                            objectClass, ldapObjectClass, handler, options);
                } else {
                    throw new UnsupportedOperationException(
                            "Requested paged search, but the server does not support VLV or PagedResultsControl. Unable to execute the search.");
                }
            }
        }

        return getDefaultSearchStrategy(objectClass, ldapObjectClass, handler, options);
    }

    private boolean supportsControl(String oid) {
        try {
            return connectionManager.getDefaultConnection().getSupportedControls().contains(oid);
        } catch (LdapException e) {
            throw new ConnectorIOException("Cannot fetch list of supported controls: " + e.getMessage(), e);
        }
    }

    protected SearchStrategy<C> getDefaultSearchStrategy(ObjectClass objectClass,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapObjectClass, ResultsHandler handler,
            OperationOptions options) {
        return new DefaultSearchStrategy<>(connectionManager, configuration, getSchemaTranslator(), objectClass,
                ldapObjectClass, handler, options);
    }

    @Override
    public Uid create(ObjectClass icfObjectClass, Set<Attribute> createAttributes, OperationOptions options) {

        String dnStringFromName = null;
        for (Attribute icfAttr : createAttributes) {
            if (icfAttr.is(Name.NAME)) {
                dnStringFromName = SchemaUtil.getSingleStringNonBlankValue(icfAttr);
            }
        }
        if (dnStringFromName == null) {
            throw new InvalidAttributeValueException("Missing NAME attribute");
        }

        AbstractSchemaTranslator<C> shcemaTranslator = getSchemaTranslator();
        org.apache.directory.api.ldap.model.schema.ObjectClass ldapStructuralObjectClass = shcemaTranslator
                .toLdapObjectClass(icfObjectClass);

        List<org.apache.directory.api.ldap.model.schema.ObjectClass> ldapAuxiliaryObjectClasses = new ArrayList<>();
        for (Attribute icfAttr : createAttributes) {
            if (icfAttr.is(PredefinedAttributes.AUXILIARY_OBJECT_CLASS_NAME)) {
                for (Object val : icfAttr.getValue()) {
                    ldapAuxiliaryObjectClasses
                            .add(getSchemaTranslator().toLdapObjectClass(new ObjectClass((String) val)));
                }
            }
        }

        String[] ldapObjectClassNames = new String[ldapAuxiliaryObjectClasses.size() + 1];
        ldapObjectClassNames[0] = ldapStructuralObjectClass.getName();
        for (int i = 0; i < ldapAuxiliaryObjectClasses.size(); i++) {
            ldapObjectClassNames[i + 1] = ldapAuxiliaryObjectClasses.get(i).getName();
        }
        Entry entry;
        try {
            entry = new DefaultEntry(dnStringFromName);
        } catch (LdapInvalidDnException e) {
            throw new InvalidAttributeValueException("Wrong DN '" + dnStringFromName + "': " + e.getMessage(), e);
        }
        entry.put("objectClass", ldapObjectClassNames);

        for (Attribute icfAttr : createAttributes) {
            if (icfAttr.is(Name.NAME)) {
                continue;
            }
            if (icfAttr.is(PredefinedAttributes.AUXILIARY_OBJECT_CLASS_NAME)) {
                continue;
            }
            AttributeType ldapAttrType = shcemaTranslator.toLdapAttribute(ldapStructuralObjectClass,
                    icfAttr.getName());
            List<Value<Object>> ldapValues = shcemaTranslator.toLdapValues(ldapAttrType, icfAttr.getValue());
            // Do NOT set attributeType here. The attributeType may not match the type of the value.
            entry.put(ldapAttrType.getName(), ldapValues.toArray(new Value[ldapValues.size()]));
            // no simple way how to check if he attribute was added. It may end up with ERR_04451. So let's just
            // hope that it worked well. It should - unless there is a connector bug.
        }

        preCreate(ldapStructuralObjectClass, entry);

        if (LOG.isOk()) {
            LOG.ok("Adding entry: {0}", entry);
        }

        processEntryBeforeCreate(entry);

        AddRequest addRequest = new AddRequestImpl();
        addRequest.setEntry(entry);

        Dn entryDn = addRequest.getEntryDn();
        LdapNetworkConnection connection = connectionManager.getConnection(entryDn);

        OperationLog.logOperationReq(connection, "Add REQ Entry:\n{0}", entry);

        AddResponse addResponse;
        try {

            addResponse = connection.add(addRequest);

        } catch (LdapException e) {
            OperationLog.logOperationErr(connection, "Add ERROR {0}: {1}", dnStringFromName, e.getMessage(), e);
            throw processLdapException("Error adding LDAP entry " + dnStringFromName, e);
        }

        OperationLog.logOperationRes(connection, "Add RES {0}: {1}", dnStringFromName, addResponse.getLdapResult());

        if (addResponse.getLdapResult().getResultCode() != ResultCodeEnum.SUCCESS) {
            throw processCreateResult(dnStringFromName, addResponse);
        }

        String uidAttributeName = configuration.getUidAttribute();
        if (LdapUtil.isDnAttribute(uidAttributeName)) {
            return new Uid(dnStringFromName);
        }

        Uid uid = null;
        for (Attribute icfAttr : createAttributes) {
            if (icfAttr.is(uidAttributeName)) {
                uid = new Uid(SchemaUtil.getSingleStringNonBlankValue(icfAttr));
            }
        }
        if (uid != null) {
            return uid;
        }

        // read the entry back and return UID

        Entry entryRead = searchSingleEntry(connectionManager, entry.getDn(), LdapUtil.createAllSearchFilter(),
                SearchScope.OBJECT, new String[] { uidAttributeName }, "re-reading entry to get UID");
        org.apache.directory.api.ldap.model.entry.Attribute uidLdapAttribute = entryRead.get(uidAttributeName);
        if (uidLdapAttribute == null) {
            throw new InvalidAttributeValueException(
                    "No value for UID attribute " + uidAttributeName + " in object " + dnStringFromName);
        }
        if (uidLdapAttribute.size() == 0) {
            throw new InvalidAttributeValueException(
                    "No value for UID attribute " + uidAttributeName + " in object " + dnStringFromName);
        } else if (uidLdapAttribute.size() > 1) {
            throw new InvalidAttributeValueException("More than one value (" + uidLdapAttribute.size()
                    + ") for UID attribute " + uidAttributeName + " in object " + dnStringFromName);
        }
        Value<?> uidLdapAttributeValue = uidLdapAttribute.get();
        AttributeType uidLdapAttributeType = getSchemaManager().getAttributeType(uidAttributeName);
        uid = new Uid(getSchemaTranslator().toIcfIdentifierValue(uidLdapAttributeValue, uidAttributeName,
                uidLdapAttributeType));

        return uid;
    }

    protected RuntimeException processCreateResult(String dn, AddResponse addResponse) {
        return processLdapResult("Error adding LDAP entry " + dn, addResponse.getLdapResult());
    }

    protected void preCreate(org.apache.directory.api.ldap.model.schema.ObjectClass ldapStructuralObjectClass,
            Entry entry) {
        // Nothing to do here. Hooks for subclasses.
    }

    @Override
    public Uid update(ObjectClass objectClass, Uid uid, Set<Attribute> replaceAttributes,
            OperationOptions options) {

        Dn newDn = null;
        for (Attribute icfAttr : replaceAttributes) {
            if (icfAttr.is(Name.NAME)) {
                // This is rename. Which means change of DN. This is a special operation

                newDn = getSchemaTranslator().toDn(icfAttr);
                ldapRename(objectClass, uid, newDn, options);

                // Do NOT return here. There may still be other (non-name) attributes to update
            }
        }

        return ldapUpdate(objectClass, uid, newDn, replaceAttributes, options,
                ModificationOperation.REPLACE_ATTRIBUTE);
    }

    private void ldapRename(ObjectClass objectClass, Uid uid, Dn newDn, OperationOptions options) {
        Dn oldDn;

        if (getConfiguration().isUseUnsafeNameHint() && uid.getNameHint() != null) {
            String dnHintString = uid.getNameHintValue();
            oldDn = getSchemaTranslator().toDn(dnHintString);
            LOG.ok("Using (unsafe) DN from the name hint: {0} for rename", oldDn);
            try {

                ldapRenameAttempt(oldDn, newDn);
                return;

            } catch (Throwable e) {
                LOG.warn(
                        "Attempt to delete object with DN failed (DN taked from the name hint). The operation will continue with next attempt. Error: {0}",
                        e.getMessage(), e);
            }
        }

        oldDn = resolveDn(objectClass, uid, options);
        LOG.ok("Resolved DN: {0}", oldDn);

        ldapRenameAttempt(oldDn, newDn);
    }

    private void ldapRenameAttempt(Dn oldDn, Dn newDn) {
        if (oldDn.equals(newDn)) {
            // nothing to rename, just ignore
        } else {
            LdapNetworkConnection connection = connectionManager.getConnection(oldDn);
            try {
                OperationLog.logOperationReq(connection, "MoveAndRename REQ {0} -> {1}", oldDn, newDn);
                // Make sure that DNs are passed in as (user-provided) strings. Otherwise the Directory API
                // will convert it do OID=value notation. And some LDAP servers (such as OpenDJ) does not handle
                // that well.
                connection.moveAndRename(oldDn.getName(), newDn.getName());
                OperationLog.logOperationRes(connection, "MoveAndRename RES OK {0} -> {1}", oldDn, newDn);
            } catch (LdapException e) {
                OperationLog.logOperationErr(connection, "MoveAndRename ERROR {0} -> {1}: {2}", oldDn, newDn,
                        e.getMessage(), e);
                throw processLdapException("Rename/move of LDAP entry from " + oldDn + " to " + newDn + " failed",
                        e);
            }
        }
    }

    @Override
    public Uid addAttributeValues(ObjectClass objectClass, Uid uid, Set<Attribute> valuesToAdd,
            OperationOptions options) {

        for (Attribute icfAttr : valuesToAdd) {
            if (icfAttr.is(Name.NAME)) {
                throw new InvalidAttributeValueException("Cannot add value of attribute " + Name.NAME);
            }
        }

        return ldapUpdate(objectClass, uid, null, valuesToAdd, options, ModificationOperation.ADD_ATTRIBUTE);
    }

    @Override
    public Uid removeAttributeValues(ObjectClass objectClass, Uid uid, Set<Attribute> valuesToRemove,
            OperationOptions options) {

        for (Attribute icfAttr : valuesToRemove) {
            if (icfAttr.is(Name.NAME)) {
                throw new InvalidAttributeValueException("Cannot remove value of attribute " + Name.NAME);
            }
        }

        return ldapUpdate(objectClass, uid, null, valuesToRemove, options, ModificationOperation.REMOVE_ATTRIBUTE);
    }

    private Uid ldapUpdate(ObjectClass icfObjectClass, Uid uid, Dn newDn, Set<Attribute> values,
            OperationOptions options, ModificationOperation modOp) {

        org.apache.directory.api.ldap.model.schema.ObjectClass ldapStructuralObjectClass = getSchemaTranslator()
                .toLdapObjectClass(icfObjectClass);

        Dn dn = newDn;
        if (dn == null) {

            if (getConfiguration().isUseUnsafeNameHint() && uid.getNameHint() != null) {
                String dnHintString = uid.getNameHintValue();
                dn = getSchemaTranslator().toDn(dnHintString);
                LOG.ok("Using (unsafe) DN from the name hint: {0} for update", dn);
                try {

                    return ldapUpdateAttempt(icfObjectClass, uid, dn, values, options, modOp,
                            ldapStructuralObjectClass);

                } catch (Throwable e) {
                    LOG.warn(
                            "Attempt to delete object with DN failed (DN taked from the name hint). The operation will continue with next attempt. Error: {0}",
                            e.getMessage(), e);
                }
            }

            dn = resolveDn(icfObjectClass, uid, options);
            LOG.ok("Resolved DN: {0}", dn);
        }

        return ldapUpdateAttempt(icfObjectClass, uid, dn, values, options, modOp, ldapStructuralObjectClass);

    }

    private Uid ldapUpdateAttempt(ObjectClass icfObjectClass, Uid uid, Dn dn, Set<Attribute> values,
            OperationOptions options, ModificationOperation modOp,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapStructuralObjectClass) {

        List<Modification> modifications = new ArrayList<Modification>(values.size());
        for (Attribute icfAttr : values) {
            if (icfAttr.is(Name.NAME)) {
                continue;
            }
            if (icfAttr.is(PredefinedAttributes.AUXILIARY_OBJECT_CLASS_NAME)) {
                if (modOp == ModificationOperation.REPLACE_ATTRIBUTE) {
                    // We need to keep structural object class
                    String[] stringValues = new String[icfAttr.getValue().size() + 1];
                    stringValues[0] = ldapStructuralObjectClass.getName();
                    int i = 1;
                    for (Object val : icfAttr.getValue()) {
                        stringValues[i] = (String) val;
                        i++;
                    }
                    modifications
                            .add(new DefaultModification(modOp, SchemaConstants.OBJECT_CLASS_AT, stringValues));
                } else {
                    String[] stringValues = new String[icfAttr.getValue().size()];
                    int i = 0;
                    for (Object val : icfAttr.getValue()) {
                        stringValues[i] = (String) val;
                        i++;
                    }
                    modifications
                            .add(new DefaultModification(modOp, SchemaConstants.OBJECT_CLASS_AT, stringValues));
                }
            } else {
                addAttributeModification(dn, modifications, ldapStructuralObjectClass, icfObjectClass, icfAttr,
                        modOp);
            }
        }

        if (modifications.isEmpty()) {
            LOG.ok("Skipping modify({0}) operation as there are no modifications to execute", modOp);
        } else {

            modify(dn, modifications);

            postUpdate(icfObjectClass, uid, values, options, modOp, dn, ldapStructuralObjectClass, modifications);

        }

        String uidAttributeName = configuration.getUidAttribute();
        if (LdapUtil.isDnAttribute(uidAttributeName)) {
            return new Uid(dn.toString());
        }

        Uid returnUid = uid;
        for (Attribute icfAttr : values) {
            if (icfAttr.is(uidAttributeName)) {
                returnUid = new Uid(SchemaUtil.getSingleStringNonBlankValue(icfAttr));
            }
        }

        // if UID is not in the set of modified attributes then assume that it has not changed.

        return returnUid;
    }

    protected void modify(Dn dn, List<Modification> modifications) {
        LdapNetworkConnection connection = connectionManager.getConnection(dn);
        try {
            PermissiveModify permissiveModifyControl = null;
            if (isUsePermissiveModify()) {
                permissiveModifyControl = new PermissiveModifyImpl();
            }
            if (LOG.isOk()) {
                OperationLog.logOperationReq(connection, "Modify REQ {0}: {1}, control={2}", dn,
                        dumpModifications(modifications), LdapUtil.toShortString(permissiveModifyControl));
            }
            ModifyRequest modRequest = new ModifyRequestImpl();
            modRequest.setName(dn);
            if (permissiveModifyControl != null) {
                modRequest.addControl(permissiveModifyControl);
            }
            // processModificationsBeforeUpdate must happen after logging. Otherwise passwords might be logged.
            for (Modification mod : processModificationsBeforeUpdate(modifications)) {
                modRequest.addModification(mod);
            }
            ModifyResponse modifyResponse = connection.modify(modRequest);

            if (LOG.isOk()) {
                OperationLog.logOperationRes(connection, "Modify RES {0}: {1}", dn, modifyResponse.getLdapResult());
            }

            if (modifyResponse.getLdapResult().getResultCode() != ResultCodeEnum.SUCCESS) {
                throw processModifyResult(dn, modifications, modifyResponse);
            }
        } catch (LdapException e) {
            OperationLog.logOperationErr(connection, "Modify ERROR {0}: {1}: {2}", dn,
                    dumpModifications(modifications), e.getMessage(), e);
            throw processModifyResult(dn.toString(), modifications, e);
        }
    }

    protected RuntimeException processModifyResult(Dn dn, List<Modification> modifications,
            ModifyResponse modifyResponse) {
        return processLdapResult("Error modifying LDAP entry " + dn + ": " + dumpModifications(modifications),
                modifyResponse.getLdapResult());
    }

    protected RuntimeException processModifyResult(String dn, List<Modification> modifications, LdapException e) {
        return processLdapException("Error modifying LDAP entry " + dn, e);
    }

    protected void addAttributeModification(Dn dn, List<Modification> modifications,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapStructuralObjectClass,
            ObjectClass icfObjectClass, Attribute icfAttr, ModificationOperation modOp) {
        AbstractSchemaTranslator<C> schemaTranslator = getSchemaTranslator();
        AttributeType attributeType = schemaTranslator.toLdapAttribute(ldapStructuralObjectClass,
                icfAttr.getName());
        if (attributeType == null && !configuration.isAllowUnknownAttributes()
                && !ArrayUtils.contains(configuration.getOperationalAttributes(), icfAttr.getName())) {
            throw new InvalidAttributeValueException(
                    "Unknown attribute " + icfAttr.getName() + " in object class " + icfObjectClass);
        }
        List<Value<Object>> ldapValues = schemaTranslator.toLdapValues(attributeType, icfAttr.getValue());
        if (ldapValues == null || ldapValues.isEmpty()) {
            // Do NOT set AttributeType here
            modifications.add(new DefaultModification(modOp, attributeType.getName()));
        } else {
            // Do NOT set AttributeType here
            modifications.add(new DefaultModification(modOp, attributeType.getName(),
                    ldapValues.toArray(new Value[ldapValues.size()])));
        }
    }

    protected void postUpdate(ObjectClass icfObjectClass, Uid uid, Set<Attribute> values, OperationOptions options,
            ModificationOperation modOp, Dn dn,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapStructuralObjectClass,
            List<Modification> modifications) {
        // Nothing to do here. Just for override in subclasses.
    }

    // We want to decrypt GuardedString at the very last moment
    private Modification[] processModificationsBeforeUpdate(List<Modification> modifications) {
        Modification[] out = new Modification[modifications.size()];
        int i = 0;
        for (final Modification modification : modifications) {
            if (modification.getAttribute() != null && modification.getAttribute().get() != null) {
                Value<?> val = modification.getAttribute().get();
                if (val instanceof GuardedStringValue) {
                    ((GuardedStringValue) val).getGuardedStringValue().access(new GuardedString.Accessor() {
                        @Override
                        public void access(char[] clearChars) {
                            DefaultAttribute attr = new DefaultAttribute(modification.getAttribute().getId(),
                                    new String(clearChars));
                            modification.setAttribute(attr);
                        }
                    });
                }
            }
            out[i] = modification;
            i++;
        }
        return out;
    }

    // We want to decrypt GuardedString at the very last moment
    private void processEntryBeforeCreate(Entry entry) {
        for (final org.apache.directory.api.ldap.model.entry.Attribute attribute : entry.getAttributes()) {
            Value<?> val = attribute.get();
            if (val instanceof GuardedStringValue) {
                attribute.remove(val);
                ((GuardedStringValue) val).getGuardedStringValue().access(new GuardedString.Accessor() {
                    @Override
                    public void access(char[] clearChars) {
                        try {
                            attribute.add(new String(clearChars));
                        } catch (LdapInvalidAttributeValueException e) {
                            throw new InvalidAttributeValueException(e.getMessage(), e);
                        }
                    }
                });
            }
        }

    }

    protected String dumpModifications(List<Modification> modifications) {
        if (modifications == null) {
            return null;
        }
        StringBuilder sb = new StringBuilder("[");
        for (Modification mod : modifications) {
            sb.append(mod.getOperation()).append(":");
            if (isSensitiveAttribute(mod.getAttribute())) {
                sb.append(mod.getAttribute().getUpId()).append("=");
                sb.append("..hidden.value..");
            } else {
                sb.append(mod.getAttribute());
            }
            sb.append(",");
        }
        sb.append("]");
        return sb.toString();
    }

    private boolean isSensitiveAttribute(org.apache.directory.api.ldap.model.entry.Attribute attribute) {
        return attribute.getId().equalsIgnoreCase(getConfiguration().getPasswordAttribute());
    }

    @Override
    public void sync(ObjectClass objectClass, SyncToken token, SyncResultsHandler handler,
            OperationOptions options) {
        prepareIcfSchema();
        SyncStrategy<C> strategy = chooseSyncStrategy();
        strategy.sync(objectClass, token, handler, options);
    }

    @Override
    public SyncToken getLatestSyncToken(ObjectClass objectClass) {
        SyncStrategy<C> strategy = chooseSyncStrategy();
        return strategy.getLatestSyncToken(objectClass);
    }

    private SyncStrategy<C> chooseSyncStrategy() {
        if (syncStrategy == null) {
            switch (configuration.getSynchronizationStrategy()) {
            case LdapConfiguration.SYNCHRONIZATION_STRATEGY_NONE:
                throw new UnsupportedOperationException("Synchronization disabled (synchronizationStrategy=none)");
            case LdapConfiguration.SYNCHRONIZATION_STRATEGY_SUN_CHANGE_LOG:
                syncStrategy = new SunChangelogSyncStrategy<>(configuration, connectionManager, getSchemaManager(),
                        getSchemaTranslator());
                break;
            case LdapConfiguration.SYNCHRONIZATION_STRATEGY_MODIFY_TIMESTAMP:
                syncStrategy = new ModifyTimestampSyncStrategy<>(configuration, connectionManager,
                        getSchemaManager(), getSchemaTranslator());
                break;
            case LdapConfiguration.SYNCHRONIZATION_STRATEGY_AD_DIR_SYNC:
                syncStrategy = new AdDirSyncStrategy<>(configuration, connectionManager, getSchemaManager(),
                        getSchemaTranslator());
                break;
            case LdapConfiguration.SYNCHRONIZATION_STRATEGY_AUTO:
                syncStrategy = chooseSyncStrategyAuto();
                break;
            default:
                throw new IllegalArgumentException(
                        "Unknown synchronization strategy '" + configuration.getSynchronizationStrategy() + "'");
            }
        }
        return syncStrategy;
    }

    private SyncStrategy<C> chooseSyncStrategyAuto() {
        Entry rootDse = LdapUtil.getRootDse(connectionManager,
                SunChangelogSyncStrategy.ROOT_DSE_ATTRIBUTE_CHANGELOG_NAME);
        org.apache.directory.api.ldap.model.entry.Attribute changelogAttribute = rootDse
                .get(SunChangelogSyncStrategy.ROOT_DSE_ATTRIBUTE_CHANGELOG_NAME);
        if (changelogAttribute != null) {
            LOG.ok("Choosing Sun ChangeLog sync stategy (found {0} attribute in root DSE)",
                    SunChangelogSyncStrategy.ROOT_DSE_ATTRIBUTE_CHANGELOG_NAME);
            return new SunChangelogSyncStrategy<>(configuration, connectionManager, getSchemaManager(),
                    getSchemaTranslator());
        }
        LOG.ok("Choosing modifyTimestamp sync stategy (fallback)");
        return new ModifyTimestampSyncStrategy<>(configuration, connectionManager, getSchemaManager(),
                getSchemaTranslator());
    }

    @Override
    public void delete(ObjectClass objectClass, Uid uid, OperationOptions options) {

        Dn dn;
        if (getConfiguration().isUseUnsafeNameHint() && uid.getNameHint() != null) {
            String dnHintString = uid.getNameHintValue();
            dn = getSchemaTranslator().toDn(dnHintString);
            LOG.ok("Using (unsafe) DN from the name hint: {0}", dn);
            try {

                deleteAttempt(dn, uid);

                return;

            } catch (Throwable e) {
                LOG.warn(
                        "Attempt to delete object with DN failed (DN taked from the name hint). The operation will continue with next attempt. Error: {0}",
                        e.getMessage(), e);
            }
        }

        dn = resolveDn(objectClass, uid, options);
        LOG.ok("Resolved DN: {0}", dn);

        deleteAttempt(dn, uid);
    }

    private void deleteAttempt(Dn dn, Uid uid) {
        LdapNetworkConnection connection = connectionManager.getConnection(dn);

        try {
            OperationLog.logOperationReq(connection, "Delete REQ {0}", dn);

            connection.delete(dn);

            OperationLog.logOperationRes(connection, "Delete RES {0}", dn);
        } catch (LdapException e) {
            OperationLog.logOperationErr(connection, "Delete ERROR {0}: {1}", dn, e.getMessage(), e);
            throw processLdapException("Failed to delete entry with DN " + dn + " (UID=" + uid + ")", e);
        }
    }

    /**
     * Very efficient method that translates ICF UID to Dn. In case that the ICF UID is
     * entryUUID we need to make LDAP search to translate it do DN. DN is needed for operations
     * such as modify or delete.
     * 
     * This is different from searchByUid() method in that it returns only the dn. Therefore
     * the search may be optimized. The searchByUid() method has to retrieve a complete object.
     */
    protected Dn resolveDn(ObjectClass objectClass, Uid uid, OperationOptions options) {
        Dn dn;
        String uidAttributeName = configuration.getUidAttribute();
        if (LdapUtil.isDnAttribute(uidAttributeName)) {
            dn = getSchemaTranslator().toDn(uid);
        } else {
            Dn baseDn = getBaseDn(options);
            checkBaseDnPresent(baseDn);
            SearchScope scope = getScope(options);
            AttributeType ldapAttributeType = null;
            SchemaManager schemaManager = getSchemaManager();
            try {
                ldapAttributeType = schemaManager.lookupAttributeTypeRegistry(uidAttributeName);
            } catch (LdapException e) {
                // E.g. ancient OpenLDAP does not have entryUUID in schema
                if (!configuration.isAllowUnknownAttributes()) {
                    throw new InvalidAttributeValueException(
                            "Cannot find schema for UID attribute " + uidAttributeName, e);
                }
                ldapAttributeType = schemaTranslator.createFauxAttributeType(uidAttributeName);
            }
            Value<Object> ldapValue = getSchemaTranslator().toLdapIdentifierValue(ldapAttributeType,
                    uid.getUidValue());
            ExprNode filterNode = new EqualityNode<Object>(ldapAttributeType, ldapValue);
            LOG.ok("Resolving DN for UID {0}", uid);
            Entry entry = searchSingleEntry(getConnectionManager(), baseDn, filterNode, scope,
                    new String[] { uidAttributeName }, "LDAP entry for UID " + uid);
            dn = entry.getDn();
        }

        return dn;
    }

    /**
     * The most efficient simple search for a single entry. Follows referrals based on the configured strategy.
     */
    protected Entry searchSingleEntry(ConnectionManager<C> connectionManager, Dn baseDn, ExprNode filterNode,
            SearchScope scope, String[] attributesToGet, String descMessage) {
        return searchSingleEntry(connectionManager, baseDn, filterNode, scope, attributesToGet, descMessage,
                baseDn);
    }

    /**
     * The most efficient simple search for a single entry. Follows referrals based on the configured strategy.
     * Additional parameter dnHint is used to select the server. But baseDn is still used as a base for search.
     * This is needed in case where the nameHing in the __NAME__ may be out of date and we need to search by
     * primary identifier. But we still want to use the nameHint to select the server. Chances are it is still
     * good for that. 
     */
    protected Entry searchSingleEntry(ConnectionManager<C> connectionManager, Dn baseDn, ExprNode filterNode,
            SearchScope scope, String[] attributesToGet, String descMessage, Dn dnHint) {

        LdapNetworkConnection connection = connectionManager.getConnection(dnHint);
        String filterString = filterNode.toString();

        Entry entry = null;
        int referralAttempts = 0;
        while (referralAttempts < configuration.getMaximumNumberOfAttempts()) {
            referralAttempts++;
            if (OperationLog.isLogOperations()) {
                OperationLog.logOperationReq(connection,
                        "Search REQ base={0}, filter={1}, scope={2}, attributes={3}, controls=null, dnHint={4}",
                        baseDn, filterString, scope, Arrays.toString(attributesToGet), dnHint);
            }

            SearchRequest searchReq = new SearchRequestImpl();
            searchReq.setBase(baseDn);
            searchReq.setFilter(filterNode);
            searchReq.setScope(scope);
            searchReq.addAttributes(attributesToGet);
            searchReq.setDerefAliases(AliasDerefMode.NEVER_DEREF_ALIASES);

            SearchCursor cursor = null;
            try {
                cursor = connection.search(searchReq);
                if (cursor.next()) {
                    Response response = cursor.get();
                    if (response instanceof SearchResultEntry) {
                        entry = ((SearchResultEntry) response).getEntry();
                        if (OperationLog.isLogOperations()) {
                            OperationLog.logOperationRes(connection, "Search RES {0}", entry);
                        }
                        break;
                    }
                } else {
                    // Something wrong happened, the entry was not created.
                    throw new UnknownUidException(descMessage + " was not found");
                }
            } catch (CursorLdapReferralException e) {
                LOG.ok("Got cursor referral exception while resolving {0}: {1}", descMessage, e.getReferralInfo());
                if (configuration.isReferralStrategyFollow()) {
                    LdapUrl referralUrl;
                    try {
                        referralUrl = new LdapUrl(e.getReferralInfo());
                    } catch (LdapURLEncodingException ee) {
                        throw new InvalidAttributeValueException(
                                "Invalid URL in referral '" + e.getReferralInfo() + ": " + ee.getMessage(), ee);
                    }
                    connection = connectionManager.getConnection(baseDn, referralUrl);
                    if (referralUrl.getDn() != null) {
                        baseDn = referralUrl.getDn();
                    }
                    if (LOG.isOk()) {
                        LOG.ok("Following referral to {0} / {1}", LdapUtil.formatConnectionInfo(connection),
                                baseDn);
                    }
                } else if (configuration.isReferralStrategyIgnore()) {
                    // We cannot really "ignore" this referral otherwise we cannot resolve DN
                    throw new ConfigurationException("Got referral to " + e.getReferralInfo()
                            + " while resolving DN. "
                            + "The referral strategy is set to ignore therefore we cannot follow the referral and complete"
                            + " DN resolving.");
                } else {
                    throw new ConnectorIOException("Error reading " + descMessage + ": " + e.getMessage(), e);
                }
            } catch (LdapException e) {
                throw processLdapException("Error reading " + descMessage, e);
            } catch (CursorException e) {
                throw new ConnectorIOException("Error reading " + descMessage + ": " + e.getMessage(), e);
            } finally {
                if (cursor != null) {
                    LdapUtil.closeCursor(cursor);
                }
            }
        }
        return entry;

    }

    @Override
    public void checkAlive() {
        if (!connectionManager.isAlive()) {
            LOG.ok("check alive: FAILED");
            throw new ConnectorException("Connection check failed");
        }
        LOG.ok("check alive: OK");
    }

    @Override
    public void dispose() {
        LOG.info("Disposing {0} connector instance {1}", this.getClass().getSimpleName(), this);
        configuration = null;
        if (connectionManager != null) {
            try {
                connectionManager.close();
            } catch (IOException e) {
                throw new ConnectorIOException(e.getMessage(), e);
            }
            connectionManager = null;
            schemaManager = null;
            schemaTranslator = null;
        } else {
            LOG.ok("Not closing connection because connection manager is already null");
        }
    }

    protected RuntimeException processLdapException(String message, LdapException ldapException) {
        return LdapUtil.processLdapException(message, ldapException);
    }

    protected RuntimeException processLdapResult(String message, LdapResult ldapResult) {
        return LdapUtil.processLdapResult(message, ldapResult);
    }

}