org.nuxeo.ecm.directory.ldap.LDAPReference.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.ecm.directory.ldap.LDAPReference.java

Source

/*
 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * 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.
 *
 * Contributors:
 *     Nuxeo - initial API and implementation
 *
 */

package org.nuxeo.ecm.directory.ldap;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.naming.CompositeName;
import javax.naming.InvalidNameException;
import javax.naming.Name;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SchemaViolationException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.lang.StringUtils;
import org.nuxeo.common.xmap.annotation.XNode;
import org.nuxeo.common.xmap.annotation.XNodeList;
import org.nuxeo.common.xmap.annotation.XObject;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.PropertyException;
import org.nuxeo.ecm.directory.AbstractReference;
import org.nuxeo.ecm.directory.BaseSession;
import org.nuxeo.ecm.directory.Directory;
import org.nuxeo.ecm.directory.DirectoryEntryNotFoundException;
import org.nuxeo.ecm.directory.DirectoryException;
import org.nuxeo.ecm.directory.DirectoryFieldMapper;
import org.nuxeo.ecm.directory.Session;
import org.nuxeo.ecm.directory.ldap.filter.FilterExpressionCorrector;
import org.nuxeo.ecm.directory.ldap.filter.FilterExpressionCorrector.FilterJobs;

import com.sun.jndi.ldap.LdapURL;

/**
 * Implementation of the directory Reference interface that leverage two common ways of storing relationships in LDAP
 * directories:
 * <ul>
 * <li>the static attribute strategy where a multi-valued attribute store the exhaustive list of distinguished names of
 * the refereed entries (eg. the uniqueMember attribute of the groupOfUniqueNames objectclass)</li>
 * <li>the dynamic attribute strategy where a potentially multi-valued attribute stores a ldap urls intensively
 * describing the refereed LDAP entries (eg. the memberURLs attribute of the groupOfURLs objectclass)</li>
 * </ul>
 * <p>
 * Please note that both static and dynamic references are resolved in read mode whereas only the static attribute
 * strategy is used when creating new references or when deleting existing ones (write / update mode).
 * <p>
 * Some design considerations behind the implementation of such reference can be found at:
 * http://jira.nuxeo.org/browse/NXP-1506
 *
 * @author Olivier Grisel <ogrisel@nuxeo.com>
 */
@XObject(value = "ldapReference")
public class LDAPReference extends AbstractReference {

    private static final Log log = LogFactory.getLog(LDAPReference.class);

    @XNodeList(value = "dynamicReference", type = LDAPDynamicReferenceDescriptor[].class, componentType = LDAPDynamicReferenceDescriptor.class)
    private LDAPDynamicReferenceDescriptor[] dynamicReferences;

    @XNode("@forceDnConsistencyCheck")
    public boolean forceDnConsistencyCheck;

    protected LDAPDirectoryDescriptor targetDirectoryDescriptor;

    /**
     * Resolve staticAttributeId as distinguished names (true by default) such as in the uniqueMember field of
     * groupOfUniqueNames. Set to false to resolve as simple id (as in memberUID of posixGroup for instance).
     */
    @XNode("@staticAttributeIdIsDn")
    private boolean staticAttributeIdIsDn = true;

    @XNode("@staticAttributeId")
    protected String staticAttributeId;

    @XNode("@dynamicAttributeId")
    protected String dynamicAttributeId;

    @XNode("@field")
    public void setFieldName(String fieldName) {
        this.fieldName = fieldName;
    }

    public static final List<String> EMPTY_STRING_LIST = Collections.emptyList();

    private LDAPFilterMatcher getFilterMatcher() {
        return new LDAPFilterMatcher();
    }

    /**
     * @return true if the reference should resolve statically refereed entries (identified by dn-valued attribute)
     * @throws DirectoryException
     */
    public boolean isStatic() throws DirectoryException {
        return getStaticAttributeId() != null;
    }

    public String getStaticAttributeId() throws DirectoryException {
        return getStaticAttributeId(null);
    }

    public String getStaticAttributeId(DirectoryFieldMapper sourceFM) throws DirectoryException {
        if (staticAttributeId != null) {
            // explicitly provided attributeId
            return staticAttributeId;
        }

        // sourceFM can be passed to avoid infinite loop in LDAPDirectory init
        if (sourceFM == null) {
            sourceFM = ((LDAPDirectory) getSourceDirectory()).getFieldMapper();
        }
        String backendFieldId = sourceFM.getBackendField(fieldName);
        if (fieldName.equals(backendFieldId)) {
            // no specific backendField found and no staticAttributeId found
            // either, this reference should not be statically resolved
            return null;
        } else {
            // BBB: the field mapper has been explicitly used to specify the
            // staticAttributeId value as this was the case before the
            // introduction of the staticAttributeId dynamicAttributeId duality
            log.warn(String.format(
                    "implicit static attribute definition through fieldMapping is deprecated, "
                            + "please update your setup with "
                            + "<ldapReference field=\"%s\" directory=\"%s\" staticAttributeId=\"%s\">",
                    fieldName, sourceDirectoryName, backendFieldId));
            return backendFieldId;
        }
    }

    public List<LDAPDynamicReferenceDescriptor> getDynamicAttributes() {
        return Arrays.asList(dynamicReferences);
    }

    public String getDynamicAttributeId() {
        return dynamicAttributeId;
    }

    /**
     * @return true if the reference should resolve dynamically refereed entries (identified by a LDAP url-valued
     *         attribute)
     */
    public boolean isDynamic() {
        return dynamicAttributeId != null;
    }

    @Override
    @XNode("@directory")
    public void setTargetDirectoryName(String targetDirectoryName) {
        this.targetDirectoryName = targetDirectoryName;
    }

    @Override
    public Directory getSourceDirectory() throws DirectoryException {

        Directory sourceDir = super.getSourceDirectory();
        if (sourceDir instanceof LDAPDirectory) {
            return sourceDir;
        } else {
            throw new DirectoryException(sourceDirectoryName
                    + " is not a LDAPDirectory and thus cannot be used in a reference for " + fieldName);
        }
    }

    @Override
    public Directory getTargetDirectory() throws DirectoryException {
        Directory targetDir = super.getTargetDirectory();
        if (targetDir instanceof LDAPDirectory) {
            return targetDir;
        } else {
            throw new DirectoryException(targetDirectoryName
                    + " is not a LDAPDirectory and thus cannot be referenced as target by " + fieldName);
        }
    }

    protected LDAPDirectory getTargetLDAPDirectory() throws DirectoryException {
        return (LDAPDirectory) getTargetDirectory();
    }

    protected LDAPDirectory getSourceLDAPDirectory() throws DirectoryException {
        return (LDAPDirectory) getSourceDirectory();
    }

    protected LDAPDirectoryDescriptor getTargetDirectoryDescriptor() throws DirectoryException {
        if (targetDirectoryDescriptor == null) {
            targetDirectoryDescriptor = getTargetLDAPDirectory().getDescriptor();
        }
        return targetDirectoryDescriptor;
    }

    /**
     * Store new links using the LDAP staticAttributeId strategy.
     *
     * @see org.nuxeo.ecm.directory.Reference#addLinks(String, List)
     */
    @Override
    public void addLinks(String sourceId, List<String> targetIds) throws DirectoryException {

        if (targetIds.isEmpty()) {
            // optim: nothing to do, return silently without further creating
            // session instances
            return;
        }

        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
        LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
        String attributeId = getStaticAttributeId();
        if (attributeId == null) {
            if (log.isTraceEnabled()) {
                log.trace(String.format("trying to edit a non-static reference from %s in directory %s: ignoring",
                        sourceId, ldapSourceDirectory.getName()));
            }
            return;
        }
        try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
                LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
            // fetch the entry to be able to run the security policy
            // implemented in an entry adaptor
            DocumentModel sourceEntry = sourceSession.getEntry(sourceId, false);
            if (sourceEntry == null) {
                throw new DirectoryException(String.format("could not add links from unexisting %s in directory %s",
                        sourceId, ldapSourceDirectory.getName()));
            }
            if (!BaseSession.isReadOnlyEntry(sourceEntry)) {
                SearchResult ldapEntry = sourceSession.getLdapEntry(sourceId);

                String sourceDn = ldapEntry.getNameInNamespace();
                Attribute storedAttr = ldapEntry.getAttributes().get(attributeId);
                String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
                Attribute attrToAdd = new BasicAttribute(attributeId);
                for (String targetId : targetIds) {
                    if (staticAttributeIdIsDn) {
                        // TODO optim: avoid LDAP search request when targetDn
                        // can be forged client side (rdnAttribute = idAttribute and scope is onelevel)
                        ldapEntry = targetSession.getLdapEntry(targetId);
                        if (ldapEntry == null) {
                            log.warn(String.format(
                                    "entry '%s' in directory '%s' not found: could not add link from '%s' in directory '%s' for '%s'",
                                    targetId, ldapTargetDirectory.getName(), sourceId,
                                    ldapSourceDirectory.getName(), this));
                            continue;
                        }
                        String dn = ldapEntry.getNameInNamespace();
                        if (storedAttr == null || !storedAttr.contains(dn)) {
                            attrToAdd.add(dn);
                        }
                    } else {
                        if (storedAttr == null || !storedAttr.contains(targetId)) {
                            attrToAdd.add(targetId);
                        }
                    }
                }
                if (attrToAdd.size() > 0) {
                    try {
                        // do the LDAP request to store missing dns
                        Attributes attrsToAdd = new BasicAttributes();
                        attrsToAdd.put(attrToAdd);

                        if (log.isDebugEnabled()) {
                            log.debug(String.format(
                                    "LDAPReference.addLinks(%s, [%s]): LDAP modifyAttributes dn='%s' "
                                            + "mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]",
                                    sourceId, StringUtils.join(targetIds, ", "), sourceDn, attrsToAdd, this));
                        }
                        sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.ADD_ATTRIBUTE, attrsToAdd);

                        // robustly clean any existing empty marker now that we are sure that the list in not empty
                        if (storedAttr.contains(emptyRefMarker)) {
                            Attributes cleanAttrs = new BasicAttributes(attributeId, emptyRefMarker);

                            if (log.isDebugEnabled()) {
                                log.debug(String.format(
                                        "LDAPReference.addLinks(%s, [%s]): LDAP modifyAttributes dn='%s'"
                                                + " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
                                        sourceId, StringUtils.join(targetIds, ", "), sourceDn, cleanAttrs, this));
                            }
                            sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE,
                                    cleanAttrs);
                        }
                    } catch (SchemaViolationException e) {
                        if (isDynamic()) {
                            // we are editing an entry that has no static part
                            log.warn(String.format("cannot update dynamic reference in field %s for source %s",
                                    getFieldName(), sourceId));
                        } else {
                            // this is a real schema configuration problem,
                            // wrap up the exception
                            throw new DirectoryException(e);
                        }
                    }
                }
            }
        } catch (NamingException e) {
            throw new DirectoryException("addLinks failed: " + e.getMessage(), e);
        }
    }

    /**
     * Store new links using the LDAP staticAttributeId strategy.
     *
     * @see org.nuxeo.ecm.directory.Reference#addLinks(List, String)
     */
    @Override
    public void addLinks(List<String> sourceIds, String targetId) throws DirectoryException {
        String attributeId = getStaticAttributeId();
        if (attributeId == null && !sourceIds.isEmpty()) {
            log.warn("trying to edit a non-static reference: ignoring");
            return;
        }
        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
        LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();

        String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
        try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
                LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
            if (!sourceSession.isReadOnly()) {
                // compute the target dn to add to all the matching source
                // entries
                SearchResult ldapEntry = targetSession.getLdapEntry(targetId);
                if (ldapEntry == null) {
                    throw new DirectoryException(
                            String.format("could not add links to unexisting %s in directory %s", targetId,
                                    ldapTargetDirectory.getName()));
                }
                String targetAttributeValue;
                if (staticAttributeIdIsDn) {
                    targetAttributeValue = ldapEntry.getNameInNamespace();
                } else {
                    targetAttributeValue = targetId;
                }

                for (String sourceId : sourceIds) {
                    // fetch the entry to be able to run the security policy
                    // implemented in an entry adaptor
                    DocumentModel sourceEntry = sourceSession.getEntry(sourceId, false);
                    if (sourceEntry == null) {
                        log.warn(String.format(
                                "entry %s in directory %s not found: could not add link to %s in directory %s",
                                sourceId, ldapSourceDirectory.getName(), targetId, ldapTargetDirectory.getName()));
                        continue;
                    }
                    if (BaseSession.isReadOnlyEntry(sourceEntry)) {
                        // skip this entry since it cannot be edited to add the
                        // reference to targetId
                        log.warn(String.format(
                                "entry %s in directory %s is readonly: could not add link to %s in directory %s",
                                sourceId, ldapSourceDirectory.getName(), targetId, ldapTargetDirectory.getName()));
                        continue;
                    }
                    ldapEntry = sourceSession.getLdapEntry(sourceId);
                    String sourceDn = ldapEntry.getNameInNamespace();
                    Attribute storedAttr = ldapEntry.getAttributes().get(attributeId);
                    if (storedAttr.contains(targetAttributeValue)) {
                        // no need to readd
                        continue;
                    }
                    try {
                        // add the new dn
                        Attributes attrs = new BasicAttributes(attributeId, targetAttributeValue);

                        if (log.isDebugEnabled()) {
                            log.debug(String.format(
                                    "LDAPReference.addLinks([%s], %s): LDAP modifyAttributes dn='%s'"
                                            + " mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]",
                                    StringUtils.join(sourceIds, ", "), targetId, sourceDn, attrs, this));
                        }
                        sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.ADD_ATTRIBUTE, attrs);

                        // robustly clean any existing empty marker now that we
                        // are sure that the list in not empty
                        if (storedAttr.contains(emptyRefMarker)) {
                            Attributes cleanAttrs = new BasicAttributes(attributeId, emptyRefMarker);
                            if (log.isDebugEnabled()) {
                                log.debug(String.format(
                                        "LDAPReference.addLinks(%s, %s): LDAP modifyAttributes dn='%s'"
                                                + " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
                                        StringUtils.join(sourceIds, ", "), targetId, sourceDn,
                                        cleanAttrs.toString(), this));
                            }
                            sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE,
                                    cleanAttrs);
                        }
                    } catch (SchemaViolationException e) {
                        if (isDynamic()) {
                            // we are editing an entry that has no static part
                            log.warn(String.format("cannot add dynamic reference in field %s for target %s",
                                    getFieldName(), targetId));
                        } else {
                            // this is a real schema configuration problem,
                            // wrap the exception
                            throw new DirectoryException(e);
                        }
                    }
                }
            }
        } catch (NamingException e) {
            throw new DirectoryException("addLinks failed: " + e.getMessage(), e);
        }
    }

    /**
     * Fetch both statically and dynamically defined references and merge the results.
     *
     * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
     */
    @Override
    public List<String> getSourceIdsForTarget(String targetId) throws DirectoryException {

        // container to hold merged references
        Set<String> sourceIds = new TreeSet<>();
        SearchResult targetLdapEntry = null;
        String targetDn = null;

        // step #1: resolve static references
        String staticAttributeId = getStaticAttributeId();
        if (staticAttributeId != null) {
            // step #1.1: fetch the dn of the targetId entry in the target
            // directory by the static dn valued strategy
            LDAPDirectory targetDir = getTargetLDAPDirectory();

            if (staticAttributeIdIsDn) {
                try (LDAPSession targetSession = (LDAPSession) targetDir.getSession()) {
                    targetLdapEntry = targetSession.getLdapEntry(targetId, false);
                    if (targetLdapEntry == null) {
                        String msg = String.format(
                                "Failed to perform inverse lookup on LDAPReference"
                                        + " resolving field '%s' of '%s' to entries of '%s'"
                                        + " using the static content of attribute '%s':"
                                        + " entry '%s' cannot be found in '%s'",
                                fieldName, sourceDirectory, targetDirectoryName, staticAttributeId, targetId,
                                targetDirectoryName);
                        throw new DirectoryEntryNotFoundException(msg);
                    }
                    targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());

                } catch (NamingException e) {
                    throw new DirectoryException(
                            "error fetching " + targetId + " from " + targetDirectoryName + ": " + e.getMessage(),
                            e);
                }
            }

            // step #1.2: search for entries that reference that dn in the
            // source directory and collect their ids
            LDAPDirectory ldapSourceDirectory = getSourceLDAPDirectory();

            String filterExpr = String.format("(&(%s={0})%s)", staticAttributeId,
                    ldapSourceDirectory.getBaseFilter());
            String[] filterArgs = new String[1];

            if (staticAttributeIdIsDn) {
                filterArgs[0] = targetDn;
            } else {
                filterArgs[0] = targetId;
            }

            String searchBaseDn = ldapSourceDirectory.getDescriptor().getSearchBaseDn();
            SearchControls sctls = ldapSourceDirectory.getSearchControls();
            try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
                if (log.isDebugEnabled()) {
                    log.debug(String.format(
                            "LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
                                    + " filter='%s' args='%s' scope='%s' [%s]",
                            targetId, searchBaseDn, filterExpr, StringUtils.join(filterArgs, ", "),
                            sctls.getSearchScope(), this));
                }
                NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(searchBaseDn, filterExpr,
                        filterArgs, sctls);

                try {
                    while (results.hasMore()) {
                        Attributes attributes = results.next().getAttributes();
                        // NXP-2461: check that id field is filled
                        Attribute attr = attributes.get(sourceSession.idAttribute);
                        if (attr != null) {
                            Object value = attr.get();
                            if (value != null) {
                                sourceIds.add(value.toString());
                            }
                        }
                    }
                } finally {
                    results.close();
                }
            } catch (NamingException e) {
                throw new DirectoryException("error during reference search for " + filterArgs[0], e);
            }
        }
        // step #2: resolve dynamic references
        String dynamicAttributeId = this.dynamicAttributeId;
        if (dynamicAttributeId != null) {

            LDAPDirectory ldapSourceDirectory = getSourceLDAPDirectory();
            LDAPDirectory ldapTargetDirectory = getTargetLDAPDirectory();
            String searchBaseDn = ldapSourceDirectory.getDescriptor().getSearchBaseDn();

            try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession();
                    LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) {
                // step #2.1: fetch the target entry to apply the ldap url
                // filters of the candidate sources on it
                if (targetLdapEntry == null) {
                    // only fetch the entry if not already fetched by the
                    // static
                    // attributes references resolution
                    targetLdapEntry = targetSession.getLdapEntry(targetId, false);
                }
                if (targetLdapEntry == null) {
                    String msg = String.format(
                            "Failed to perform inverse lookup on LDAPReference"
                                    + " resolving field '%s' of '%s' to entries of '%s'"
                                    + " using the dynamic content of attribute '%s':"
                                    + " entry '%s' cannot be found in '%s'",
                            fieldName, ldapSourceDirectory, targetDirectoryName, dynamicAttributeId, targetId,
                            targetDirectoryName);
                    throw new DirectoryException(msg);
                }
                targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
                Attributes targetAttributes = targetLdapEntry.getAttributes();

                // step #2.2: find the list of entries that hold candidate
                // dynamic links in the source directory
                SearchControls sctls = ldapSourceDirectory.getSearchControls();
                sctls.setReturningAttributes(new String[] { sourceSession.idAttribute, dynamicAttributeId });
                String filterExpr = String.format("%s=*", dynamicAttributeId);

                if (log.isDebugEnabled()) {
                    log.debug(String.format(
                            "LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
                                    + " filter='%s' scope='%s' [%s]",
                            targetId, searchBaseDn, filterExpr, sctls.getSearchScope(), this));
                }
                NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(searchBaseDn, filterExpr,
                        sctls);
                try {
                    while (results.hasMore()) {
                        // step #2.3: for each sourceId and each ldapUrl test
                        // whether the current target entry matches the
                        // collected
                        // URL
                        Attributes sourceAttributes = results.next().getAttributes();

                        NamingEnumeration<?> ldapUrls = sourceAttributes.get(dynamicAttributeId).getAll();
                        try {
                            while (ldapUrls.hasMore()) {
                                LdapURL ldapUrl = new LdapURL(ldapUrls.next().toString());
                                String candidateDN = pseudoNormalizeDn(ldapUrl.getDN());
                                // check base URL
                                if (!targetDn.endsWith(candidateDN)) {
                                    continue;
                                }

                                // check onelevel scope constraints
                                if ("onelevel".equals(ldapUrl.getScope())) {
                                    int targetDnSize = new LdapName(targetDn).size();
                                    int urlDnSize = new LdapName(candidateDN).size();
                                    if (targetDnSize - urlDnSize > 1) {
                                        // target is not a direct child of the
                                        // DN of the
                                        // LDAP URL
                                        continue;
                                    }
                                }

                                // check that the target entry matches the
                                // filter
                                if (getFilterMatcher().match(targetAttributes, ldapUrl.getFilter())) {
                                    // the target match the source url, add it
                                    // to the
                                    // collected ids
                                    sourceIds.add(sourceAttributes.get(sourceSession.idAttribute).get().toString());
                                }
                            }
                        } finally {
                            ldapUrls.close();
                        }
                    }
                } finally {
                    results.close();
                }
            } catch (NamingException e) {
                throw new DirectoryException("error during reference search for " + targetId, e);
            }
        }

        /*
         * This kind of reference is not supported because Active Directory use filter expression not yet supported by
         * LDAPFilterMatcher. See NXP-4562
         */
        if (dynamicReferences != null && dynamicReferences.length > 0) {
            log.error("This kind of reference is not supported.");
        }

        return new ArrayList<>(sourceIds);
    }

    /**
     * Fetches both statically and dynamically defined references and merges the results.
     *
     * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
     */
    @Override
    // XXX: broken, use getLdapTargetIds for a proper implementation
    @SuppressWarnings("unchecked")
    public List<String> getTargetIdsForSource(String sourceId) throws DirectoryException {
        String schemaName = getSourceDirectory().getSchema();
        try (Session session = getSourceDirectory().getSession()) {
            try {
                return (List<String>) session.getEntry(sourceId).getProperty(schemaName, fieldName);
            } catch (PropertyException e) {
                throw new DirectoryException(e);
            }
        }
    }

    /**
     * Simple helper that replaces ", " by "," in the provided dn and returns the lower case version of the result for
     * comparison purpose.
     *
     * @param dn the raw unnormalized dn
     * @return lowercase version without whitespace after commas
     * @throws InvalidNameException
     */
    protected static String pseudoNormalizeDn(String dn) throws InvalidNameException {
        LdapName ldapName = new LdapName(dn);
        List<String> rdns = new ArrayList<>();
        for (Rdn rdn : ldapName.getRdns()) {
            String value = rdn.getValue().toString().toLowerCase().replaceAll(",", "\\\\,");
            String rdnStr = rdn.getType().toLowerCase() + "=" + value;
            rdns.add(0, rdnStr);
        }
        return StringUtils.join(rdns, ',');
    }

    /**
     * Optimized method to spare a LDAP request when the caller is a LDAPSession object that has already fetched the
     * LDAP Attribute instances.
     * <p>
     * This method should return the same results as the sister method: org.nuxeo
     * .ecm.directory.Reference#getTargetIdsForSource(java.lang.String)
     *
     * @return target reference ids
     * @throws DirectoryException
     */
    public List<String> getLdapTargetIds(Attributes attributes) throws DirectoryException {

        Set<String> targetIds = new TreeSet<>();

        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
        LDAPDirectoryDescriptor targetDirconfig = getTargetDirectoryDescriptor();
        String emptyRefMarker = ldapTargetDirectory.getDescriptor().getEmptyRefMarker();
        try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) {
            String baseDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());

            // step #1: fetch ids referenced by static attributes
            String staticAttributeId = getStaticAttributeId();
            Attribute staticAttribute = null;
            if (staticAttributeId != null) {
                staticAttribute = attributes.get(staticAttributeId);
            }

            if (staticAttribute != null && !staticAttributeIdIsDn) {
                NamingEnumeration<?> staticContent = staticAttribute.getAll();
                try {
                    while (staticContent.hasMore()) {
                        String value = staticContent.next().toString();
                        if (!emptyRefMarker.equals(value)) {
                            targetIds.add(value);
                        }
                    }
                } finally {
                    staticContent.close();
                }
            }

            if (staticAttribute != null && staticAttributeIdIsDn) {
                NamingEnumeration<?> targetDns = staticAttribute.getAll();
                try {
                    while (targetDns.hasMore()) {
                        String targetDn = targetDns.next().toString();

                        if (!pseudoNormalizeDn(targetDn).endsWith(baseDn)) {
                            // optim: avoid network connections when obvious
                            if (log.isTraceEnabled()) {
                                log.trace(String.format("ignoring: dn='%s' (does not match '%s') for '%s'",
                                        targetDn, baseDn, this));
                            }
                            continue;
                        }
                        // find the id of the referenced entry
                        String id = null;

                        if (targetSession.rdnMatchesIdField()) {
                            // optim: do not fetch the entry to get its true id
                            // but
                            // guess it by reading the targetDn
                            LdapName name = new LdapName(targetDn);
                            String rdn = name.get(name.size() - 1);
                            int pos = rdn.indexOf("=");
                            id = rdn.substring(pos + 1);
                        } else {
                            id = getIdForDn(targetSession, targetDn);
                            if (id == null) {
                                log.warn(String.format(
                                        "ignoring target '%s' (missing attribute '%s') while resolving reference '%s'",
                                        targetDn, targetSession.idAttribute, this));
                                continue;
                            }
                        }
                        if (forceDnConsistencyCheck) {
                            // check that the referenced entry is actually part
                            // of
                            // the target directory (takes care of the filters
                            // and
                            // the scope)
                            // this check can be very expensive on large groups
                            // and thus not enabled by default
                            if (!targetSession.hasEntry(id)) {
                                if (log.isTraceEnabled()) {
                                    log.trace(String.format(
                                            "ignoring target '%s' when resolving '%s' (not part of target"
                                                    + " directory by forced DN consistency check)",
                                            targetDn, this));
                                }
                                continue;
                            }
                        }
                        // NXP-2461: check that id field is filled
                        if (id != null) {
                            targetIds.add(id);
                        }
                    }
                } finally {
                    targetDns.close();
                }
            }
            // step #2: fetched dynamically referenced ids
            String dynamicAttributeId = this.dynamicAttributeId;
            Attribute dynamicAttribute = null;
            if (dynamicAttributeId != null) {
                dynamicAttribute = attributes.get(dynamicAttributeId);
            }
            if (dynamicAttribute != null) {
                NamingEnumeration<?> rawldapUrls = dynamicAttribute.getAll();
                try {
                    while (rawldapUrls.hasMore()) {
                        LdapURL ldapUrl = new LdapURL(rawldapUrls.next().toString());
                        String linkDn = pseudoNormalizeDn(ldapUrl.getDN());
                        String directoryDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
                        int scope = SearchControls.ONELEVEL_SCOPE;
                        String scopePart = ldapUrl.getScope();
                        if (scopePart != null && scopePart.toLowerCase().startsWith("sub")) {
                            scope = SearchControls.SUBTREE_SCOPE;
                        }
                        if (!linkDn.endsWith(directoryDn) && !directoryDn.endsWith(linkDn)) {
                            // optim #1: if the dns do not match, abort
                            continue;
                        } else if (directoryDn.endsWith(linkDn) && linkDn.length() < directoryDn.length()
                                && scope == SearchControls.ONELEVEL_SCOPE) {
                            // optim #2: the link dn is pointing to elements
                            // that at
                            // upperlevel than directory elements
                            continue;
                        } else {

                            // Search for references elements
                            targetIds.addAll(getReferencedElements(attributes, directoryDn, linkDn,
                                    ldapUrl.getFilter(), scope));

                        }
                    }
                } finally {
                    rawldapUrls.close();
                }
            }

            if (dynamicReferences != null && dynamicReferences.length > 0) {

                // Only the first Dynamic Reference is used
                LDAPDynamicReferenceDescriptor dynAtt = dynamicReferences[0];

                Attribute baseDnsAttribute = attributes.get(dynAtt.baseDN);
                Attribute filterAttribute = attributes.get(dynAtt.filter);

                if (baseDnsAttribute != null && filterAttribute != null) {

                    NamingEnumeration<?> baseDns = null;
                    NamingEnumeration<?> filters = null;

                    try {
                        // Get the BaseDN value from the descriptor
                        baseDns = baseDnsAttribute.getAll();
                        String linkDnValue = baseDns.next().toString();
                        baseDns.close();
                        linkDnValue = pseudoNormalizeDn(linkDnValue);

                        // Get the filter value from the descriptor
                        filters = filterAttribute.getAll();
                        String filterValue = filters.next().toString();
                        filters.close();

                        // Get the scope value from the descriptor
                        int scope = "subtree".equalsIgnoreCase(dynAtt.type) ? SearchControls.SUBTREE_SCOPE
                                : SearchControls.ONELEVEL_SCOPE;

                        String directoryDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());

                        // if the dns match, and if the link dn is pointing to
                        // elements that at upperlevel than directory elements
                        if ((linkDnValue.endsWith(directoryDn) || directoryDn.endsWith(linkDnValue))
                                && !(directoryDn.endsWith(linkDnValue)
                                        && linkDnValue.length() < directoryDn.length()
                                        && scope == SearchControls.ONELEVEL_SCOPE)) {

                            // Correct the filter expression
                            filterValue = FilterExpressionCorrector.correctFilter(filterValue,
                                    FilterJobs.CORRECT_NOT);

                            // Search for references elements
                            targetIds.addAll(getReferencedElements(attributes, directoryDn, linkDnValue,
                                    filterValue, scope));

                        }
                    } finally {
                        if (baseDns != null) {
                            baseDns.close();
                        }

                        if (filters != null) {
                            filters.close();
                        }
                    }

                }

            }
            // return merged attributes
            return new ArrayList<String>(targetIds);
        } catch (NamingException e) {
            throw new DirectoryException("error computing LDAP references", e);
        }
    }

    protected String getIdForDn(LDAPSession session, String dn) {
        // the entry id is not based on the rdn, we thus need to
        // fetch the LDAP entry to grab it
        String[] attributeIdsToCollect = { session.idAttribute };
        Attributes entry;
        try {

            if (log.isDebugEnabled()) {
                log.debug(String.format(
                        "LDAPReference.getIdForDn(session, %s): LDAP get dn='%s'"
                                + " attribute ids to collect='%s' [%s]",
                        dn, dn, StringUtils.join(attributeIdsToCollect, ", "), this));
            }

            Name name = new CompositeName().add(dn);
            entry = session.dirContext.getAttributes(name, attributeIdsToCollect);
        } catch (NamingException e) {
            return null;
        }
        // NXP-2461: check that id field is filled
        Attribute attr = entry.get(session.idAttribute);
        if (attr != null) {
            try {
                return attr.get().toString();
            } catch (NamingException e) {
            }
        }
        return null;
    }

    /**
     * Retrieve the elements referenced by the filter/BaseDN/Scope request.
     *
     * @param attributes Attributes of the referencer element
     * @param directoryDn Dn of the Directory
     * @param linkDn Dn specified in the parent
     * @param filter Filter expression specified in the parent
     * @param scope scope for the search
     * @return The list of the referenced elements.
     * @throws DirectoryException
     * @throws NamingException
     */
    private Set<String> getReferencedElements(Attributes attributes, String directoryDn, String linkDn,
            String filter, int scope) throws DirectoryException, NamingException {

        Set<String> targetIds = new TreeSet<>();

        LDAPDirectoryDescriptor targetDirconfig = getTargetDirectoryDescriptor();
        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
        LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();

        // use the most specific scope between the one specified in the
        // Directory and the specified in the Parent
        String dn = directoryDn.endsWith(linkDn) && directoryDn.length() > linkDn.length() ? directoryDn : linkDn;

        // combine the ldapUrl search query with target
        // directory own constraints
        SearchControls scts = new SearchControls();

        // use the most specific scope
        scts.setSearchScope(Math.min(scope, targetDirconfig.getSearchScope()));

        // only fetch the ids of the targets
        scts.setReturningAttributes(new String[] { targetSession.idAttribute });

        // combine the filter of the target directory with the
        // provided filter if any
        String targetFilter = targetDirconfig.getSearchFilter();
        if (filter == null || filter.length() == 0) {
            filter = targetFilter;
        } else if (targetFilter != null && targetFilter.length() > 0) {
            filter = String.format("(&(%s)(%s))", targetFilter, filter);
        }

        // perform the request and collect the ids
        if (log.isDebugEnabled()) {
            log.debug(String.format(
                    "LDAPReference.getLdapTargetIds(%s): LDAP search dn='%s' " + " filter='%s' scope='%s' [%s]",
                    attributes, dn, dn, scts.getSearchScope(), this));
        }

        Name name = new CompositeName().add(dn);
        NamingEnumeration<SearchResult> results = targetSession.dirContext.search(name, filter, scts);
        try {
            while (results.hasMore()) {
                // NXP-2461: check that id field is filled
                Attribute attr = results.next().getAttributes().get(targetSession.idAttribute);
                if (attr != null) {
                    String collectedId = attr.get().toString();
                    if (collectedId != null) {
                        targetIds.add(collectedId);
                    }
                }

            }
        } finally {
            results.close();
        }

        return targetIds;
    }

    /**
     * Remove existing statically defined links for the given source id (dynamic references remain unaltered)
     *
     * @see org.nuxeo.ecm.directory.Reference#removeLinksForSource(String)
     */
    @Override
    public void removeLinksForSource(String sourceId) throws DirectoryException {
        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
        LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
        String attributeId = getStaticAttributeId();
        try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession();
                LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) {
            if (sourceSession.isReadOnly() || attributeId == null) {
                // do not try to do anything on a read only server or to a
                // purely dynamic reference
                return;
            }
            // get the dn of the entry that matches sourceId
            SearchResult sourceLdapEntry = sourceSession.getLdapEntry(sourceId);
            if (sourceLdapEntry == null) {
                throw new DirectoryException(
                        String.format("cannot edit the links hold by missing entry '%s' in directory '%s'",
                                sourceId, ldapSourceDirectory.getName()));
            }
            String sourceDn = pseudoNormalizeDn(sourceLdapEntry.getNameInNamespace());

            Attribute oldAttr = sourceLdapEntry.getAttributes().get(attributeId);
            if (oldAttr == null) {
                // consider it as an empty attribute to simplify the following
                // code
                oldAttr = new BasicAttribute(attributeId);
            }
            Attribute attrToRemove = new BasicAttribute(attributeId);

            NamingEnumeration<?> oldAttrs = oldAttr.getAll();
            String targetBaseDn = pseudoNormalizeDn(ldapTargetDirectory.getDescriptor().getSearchBaseDn());
            try {
                while (oldAttrs.hasMore()) {
                    String targetKeyAttr = oldAttrs.next().toString();

                    if (staticAttributeIdIsDn) {
                        String dn = pseudoNormalizeDn(targetKeyAttr);
                        if (forceDnConsistencyCheck) {
                            String id = getIdForDn(targetSession, dn);
                            if (id != null && targetSession.hasEntry(id)) {
                                // this is an entry managed by the current
                                // reference
                                attrToRemove.add(dn);
                            }
                        } else if (dn.endsWith(targetBaseDn)) {
                            // this is an entry managed by the current
                            // reference
                            attrToRemove.add(dn);
                        }
                    } else {
                        attrToRemove.add(targetKeyAttr);
                    }
                }
            } finally {
                oldAttrs.close();
            }
            try {
                if (attrToRemove.size() == oldAttr.size()) {
                    // use the empty ref marker to avoid empty attr
                    String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
                    Attributes emptyAttribute = new BasicAttributes(attributeId, emptyRefMarker);
                    if (log.isDebugEnabled()) {
                        log.debug(String.format(
                                "LDAPReference.removeLinksForSource(%s): LDAP modifyAttributes key='%s' "
                                        + " mod_op='REPLACE_ATTRIBUTE' attrs='%s' [%s]",
                                sourceId, sourceDn, emptyAttribute, this));
                    }
                    sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REPLACE_ATTRIBUTE,
                            emptyAttribute);
                } else if (attrToRemove.size() > 0) {
                    // remove the attribute managed by the current reference
                    Attributes attrsToRemove = new BasicAttributes();
                    attrsToRemove.put(attrToRemove);
                    if (log.isDebugEnabled()) {
                        log.debug(String.format(
                                "LDAPReference.removeLinksForSource(%s): LDAP modifyAttributes dn='%s' "
                                        + " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
                                sourceId, sourceDn, attrsToRemove, this));
                    }
                    sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE, attrsToRemove);
                }
            } catch (SchemaViolationException e) {
                if (isDynamic()) {
                    // we are editing an entry that has no static part
                    log.warn(String.format("cannot remove dynamic reference in field %s for source %s",
                            getFieldName(), sourceId));
                } else {
                    // this is a real schma configuration problem, wrapup the
                    // exception
                    throw new DirectoryException(e);
                }
            }
        } catch (NamingException e) {
            throw new DirectoryException("removeLinksForSource failed: " + e.getMessage(), e);
        }
    }

    /**
     * Remove existing statically defined links for the given target id (dynamic references remain unaltered)
     *
     * @see org.nuxeo.ecm.directory.Reference#removeLinksForTarget(String)
     */
    @Override
    public void removeLinksForTarget(String targetId) throws DirectoryException {
        if (!isStatic()) {
            // nothing to do: dynamic references cannot be updated
            return;
        }
        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
        LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
        String attributeId = getStaticAttributeId();
        try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
                LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
            if (!sourceSession.isReadOnly()) {
                // get the dn of the target that matches targetId
                String targetAttributeValue;

                if (staticAttributeIdIsDn) {
                    SearchResult targetLdapEntry = targetSession.getLdapEntry(targetId);
                    if (targetLdapEntry == null) {
                        String rdnAttribute = ldapTargetDirectory.getDescriptor().getRdnAttribute();
                        if (!rdnAttribute.equals(targetSession.idAttribute)) {
                            log.warn(String.format(
                                    "cannot remove links to missing entry %s in directory %s for reference %s",
                                    targetId, ldapTargetDirectory.getName(), this));
                            return;
                        }
                        // the entry might have already been deleted, try to
                        // re-forge it if possible (might not work if scope is
                        // subtree)
                        targetAttributeValue = String.format("%s=%s,%s", rdnAttribute, targetId,
                                ldapTargetDirectory.getDescriptor().getSearchBaseDn());
                    } else {
                        targetAttributeValue = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
                    }
                } else {
                    targetAttributeValue = targetId;
                }

                // build a LDAP query to find entries that point to the target
                String searchFilter = String.format("(%s=%s)", attributeId, targetAttributeValue);
                String sourceFilter = ldapSourceDirectory.getBaseFilter();

                if (sourceFilter != null && !"".equals(sourceFilter)) {
                    searchFilter = String.format("(&(%s)(%s))", searchFilter, sourceFilter);
                }

                SearchControls scts = new SearchControls();
                scts.setSearchScope(ldapSourceDirectory.getDescriptor().getSearchScope());
                scts.setReturningAttributes(new String[] { attributeId });

                // find all source entries that point to the target key and
                // clean
                // those references
                if (log.isDebugEnabled()) {
                    log.debug(String.format(
                            "LDAPReference.removeLinksForTarget(%s): LDAP search baseDn='%s' "
                                    + " filter='%s' scope='%s' [%s]",
                            targetId, sourceSession.searchBaseDn, searchFilter, scts.getSearchScope(), this));
                }
                NamingEnumeration<SearchResult> results = sourceSession.dirContext
                        .search(sourceSession.searchBaseDn, searchFilter, scts);
                String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
                Attributes emptyAttribute = new BasicAttributes(attributeId, emptyRefMarker);

                try {
                    while (results.hasMore()) {
                        SearchResult result = results.next();
                        Attributes attrs = result.getAttributes();
                        Attribute attr = attrs.get(attributeId);
                        try {
                            if (attr.size() == 1) {
                                // the attribute holds the last reference, put
                                // the
                                // empty ref. marker before removing the
                                // attribute
                                // since empty attribute are often not allowed
                                // by
                                // the server schema
                                if (log.isDebugEnabled()) {
                                    log.debug(String.format(
                                            "LDAPReference.removeLinksForTarget(%s): LDAP modifyAttributes key='%s' "
                                                    + "mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]",
                                            targetId, result.getNameInNamespace(), attrs, this));
                                }
                                sourceSession.dirContext.modifyAttributes(result.getNameInNamespace(),
                                        DirContext.ADD_ATTRIBUTE, emptyAttribute);
                            }
                            // remove the reference to the target key
                            attrs = new BasicAttributes();
                            attr = new BasicAttribute(attributeId);
                            attr.add(targetAttributeValue);
                            attrs.put(attr);
                            if (log.isDebugEnabled()) {
                                log.debug(String.format(
                                        "LDAPReference.removeLinksForTarget(%s): LDAP modifyAttributes key='%s' "
                                                + "mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
                                        targetId, result.getNameInNamespace(), attrs, this));
                            }
                            sourceSession.dirContext.modifyAttributes(result.getNameInNamespace(),
                                    DirContext.REMOVE_ATTRIBUTE, attrs);
                        } catch (SchemaViolationException e) {
                            if (isDynamic()) {
                                // we are editing an entry that has no static
                                // part
                                log.warn(String.format("cannot remove dynamic reference in field %s for target %s",
                                        getFieldName(), targetId));
                            } else {
                                // this is a real schema configuration problem,
                                // wrapup the exception
                                throw new DirectoryException(e);
                            }
                        }
                    }
                } finally {
                    results.close();
                }
            }
        } catch (NamingException e) {
            throw new DirectoryException("removeLinksForTarget failed: " + e.getMessage(), e);
        }
    }

    /**
     * Edit the list of statically defined references for a given target (dynamic references remain unaltered)
     *
     * @see org.nuxeo.ecm.directory.Reference#setSourceIdsForTarget(String, List)
     */
    @Override
    public void setSourceIdsForTarget(String targetId, List<String> sourceIds) throws DirectoryException {
        removeLinksForTarget(targetId);
        addLinks(sourceIds, targetId);
    }

    /**
     * Set the list of statically defined references for a given source (dynamic references remain unaltered)
     *
     * @see org.nuxeo.ecm.directory.Reference#setTargetIdsForSource(String, List)
     */
    @Override
    public void setTargetIdsForSource(String sourceId, List<String> targetIds) throws DirectoryException {
        removeLinksForSource(sourceId);
        addLinks(sourceId, targetIds);
    }

    @Override
    // to build helpful debug logs
    public String toString() {
        return String.format(
                "LDAPReference to resolve field='%s' of sourceDirectory='%s'" + " with targetDirectory='%s'"
                        + " and staticAttributeId='%s', dynamicAttributeId='%s'",
                fieldName, sourceDirectoryName, targetDirectoryName, staticAttributeId, dynamicAttributeId);
    }

    /**
     * @since 5.6
     */
    @Override
    public LDAPReference clone() {
        LDAPReference clone = (LDAPReference) super.clone();
        // basic fields are already copied by super.clone()
        return clone;
    }

}