001/*
002 * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 * $Id: JOOoConvertPluginImpl.java 18651 2007-05-13 20:28:53Z sfermigier $
018 */
019
020package org.nuxeo.ecm.directory.ldap;
021
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.List;
027import java.util.Set;
028import java.util.TreeSet;
029
030import javax.naming.CompositeName;
031import javax.naming.InvalidNameException;
032import javax.naming.Name;
033import javax.naming.NamingEnumeration;
034import javax.naming.NamingException;
035import javax.naming.directory.Attribute;
036import javax.naming.directory.Attributes;
037import javax.naming.directory.BasicAttribute;
038import javax.naming.directory.BasicAttributes;
039import javax.naming.directory.DirContext;
040import javax.naming.directory.SchemaViolationException;
041import javax.naming.directory.SearchControls;
042import javax.naming.directory.SearchResult;
043import javax.naming.ldap.LdapName;
044import javax.naming.ldap.Rdn;
045
046import org.apache.commons.logging.Log;
047import org.apache.commons.logging.LogFactory;
048import org.nuxeo.common.utils.StringUtils;
049import org.nuxeo.common.xmap.annotation.XNode;
050import org.nuxeo.common.xmap.annotation.XNodeList;
051import org.nuxeo.common.xmap.annotation.XObject;
052import org.nuxeo.ecm.core.api.DocumentModel;
053import org.nuxeo.ecm.core.api.PropertyException;
054import org.nuxeo.ecm.directory.AbstractReference;
055import org.nuxeo.ecm.directory.BaseSession;
056import org.nuxeo.ecm.directory.Directory;
057import org.nuxeo.ecm.directory.DirectoryEntryNotFoundException;
058import org.nuxeo.ecm.directory.DirectoryException;
059import org.nuxeo.ecm.directory.DirectoryFieldMapper;
060import org.nuxeo.ecm.directory.Session;
061import org.nuxeo.ecm.directory.ldap.filter.FilterExpressionCorrector;
062import org.nuxeo.ecm.directory.ldap.filter.FilterExpressionCorrector.FilterJobs;
063
064import com.sun.jndi.ldap.LdapURL;
065
066/**
067 * Implementation of the directory Reference interface that leverage two common ways of storing relationships in LDAP
068 * directories:
069 * <ul>
070 * <li>the static attribute strategy where a multi-valued attribute store the exhaustive list of distinguished names of
071 * the refereed entries (eg. the uniqueMember attribute of the groupOfUniqueNames objectclass)</li>
072 * <li>the dynamic attribute strategy where a potentially multi-valued attribute stores a ldap urls intensively
073 * describing the refereed LDAP entries (eg. the memberURLs attribute of the groupOfURLs objectclass)</li>
074 * </ul>
075 * <p>
076 * Please note that both static and dynamic references are resolved in read mode whereas only the static attribute
077 * strategy is used when creating new references or when deleting existing ones (write / update mode).
078 * <p>
079 * Some design considerations behind the implementation of such reference can be found at:
080 * http://jira.nuxeo.org/browse/NXP-1506
081 *
082 * @author Olivier Grisel <ogrisel@nuxeo.com>
083 */
084@XObject(value = "ldapReference")
085public class LDAPReference extends AbstractReference {
086
087    private static final Log log = LogFactory.getLog(LDAPReference.class);
088
089    @XNodeList(value = "dynamicReference", type = LDAPDynamicReferenceDescriptor[].class, componentType = LDAPDynamicReferenceDescriptor.class)
090    private LDAPDynamicReferenceDescriptor[] dynamicReferences;
091
092    @XNode("@forceDnConsistencyCheck")
093    public boolean forceDnConsistencyCheck;
094
095    protected LDAPDirectoryDescriptor targetDirectoryDescriptor;
096
097    /**
098     * Resolve staticAttributeId as distinguished names (true by default) such as in the uniqueMember field of
099     * groupOfUniqueNames. Set to false to resolve as simple id (as in memberUID of posixGroup for instance).
100     */
101    @XNode("@staticAttributeIdIsDn")
102    private boolean staticAttributeIdIsDn = true;
103
104    @XNode("@staticAttributeId")
105    protected String staticAttributeId;
106
107    @XNode("@dynamicAttributeId")
108    protected String dynamicAttributeId;
109
110    @XNode("@field")
111    public void setFieldName(String fieldName) {
112        this.fieldName = fieldName;
113    }
114
115    public static final List<String> EMPTY_STRING_LIST = Collections.emptyList();
116
117    private LDAPFilterMatcher getFilterMatcher() {
118        return new LDAPFilterMatcher();
119    }
120
121    /**
122     * @return true if the reference should resolve statically refereed entries (identified by dn-valued attribute)
123     * @throws DirectoryException
124     */
125    public boolean isStatic() throws DirectoryException {
126        return getStaticAttributeId() != null;
127    }
128
129    public String getStaticAttributeId() throws DirectoryException {
130        return getStaticAttributeId(null);
131    }
132
133    public String getStaticAttributeId(DirectoryFieldMapper sourceFM) throws DirectoryException {
134        if (staticAttributeId != null) {
135            // explicitly provided attributeId
136            return staticAttributeId;
137        }
138
139        // sourceFM can be passed to avoid infinite loop in LDAPDirectory init
140        if (sourceFM == null) {
141            sourceFM = ((LDAPDirectory) getSourceDirectory()).getFieldMapper();
142        }
143        String backendFieldId = sourceFM.getBackendField(fieldName);
144        if (fieldName.equals(backendFieldId)) {
145            // no specific backendField found and no staticAttributeId found
146            // either, this reference should not be statically resolved
147            return null;
148        } else {
149            // BBB: the field mapper has been explicitly used to specify the
150            // staticAttributeId value as this was the case before the
151            // introduction of the staticAttributeId dynamicAttributeId duality
152            log.warn(String.format("implicit static attribute definition through fieldMapping is deprecated, "
153                    + "please update your setup with "
154                    + "<ldapReference field=\"%s\" directory=\"%s\" staticAttributeId=\"%s\">", fieldName,
155                    sourceDirectoryName, backendFieldId));
156            return backendFieldId;
157        }
158    }
159
160    public List<LDAPDynamicReferenceDescriptor> getDynamicAttributes() {
161        return Arrays.asList(dynamicReferences);
162    }
163
164    public String getDynamicAttributeId() {
165        return dynamicAttributeId;
166    }
167
168    /**
169     * @return true if the reference should resolve dynamically refereed entries (identified by a LDAP url-valued
170     *         attribute)
171     */
172    public boolean isDynamic() {
173        return dynamicAttributeId != null;
174    }
175
176    @Override
177    @XNode("@directory")
178    public void setTargetDirectoryName(String targetDirectoryName) {
179        this.targetDirectoryName = targetDirectoryName;
180    }
181
182    @Override
183    public Directory getSourceDirectory() throws DirectoryException {
184
185        Directory sourceDir = super.getSourceDirectory();
186        if (sourceDir instanceof LDAPDirectory) {
187            return sourceDir;
188        } else {
189            throw new DirectoryException(sourceDirectoryName
190                    + " is not a LDAPDirectory and thus cannot be used in a reference for " + fieldName);
191        }
192    }
193
194    @Override
195    public Directory getTargetDirectory() throws DirectoryException {
196        Directory targetDir = super.getTargetDirectory();
197        if (targetDir instanceof LDAPDirectory) {
198            return targetDir;
199        } else {
200            throw new DirectoryException(targetDirectoryName
201                    + " is not a LDAPDirectory and thus cannot be referenced as target by " + fieldName);
202        }
203    }
204
205    protected LDAPDirectory getTargetLDAPDirectory() throws DirectoryException {
206        return (LDAPDirectory) getTargetDirectory();
207    }
208
209    protected LDAPDirectory getSourceLDAPDirectory() throws DirectoryException {
210        return (LDAPDirectory) getSourceDirectory();
211    }
212
213    protected LDAPDirectoryDescriptor getTargetDirectoryDescriptor() throws DirectoryException {
214        if (targetDirectoryDescriptor == null) {
215            targetDirectoryDescriptor = getTargetLDAPDirectory().getConfig();
216        }
217        return targetDirectoryDescriptor;
218    }
219
220    /**
221     * Store new links using the LDAP staticAttributeId strategy.
222     *
223     * @see org.nuxeo.ecm.directory.Reference#addLinks(String, List)
224     */
225    @Override
226    public void addLinks(String sourceId, List<String> targetIds) throws DirectoryException {
227
228        if (targetIds.isEmpty()) {
229            // optim: nothing to do, return silently without further creating
230            // session instances
231            return;
232        }
233
234        LDAPDirectory targetDirectory = (LDAPDirectory) getTargetDirectory();
235        LDAPDirectory sourceDirectory = (LDAPDirectory) getSourceDirectory();
236        String attributeId = getStaticAttributeId();
237        if (attributeId == null) {
238            if (log.isTraceEnabled()) {
239                log.trace(String.format("trying to edit a non-static reference from %s in directory %s: ignoring",
240                        sourceId, sourceDirectory.getName()));
241            }
242            return;
243        }
244        try (LDAPSession targetSession = (LDAPSession) targetDirectory.getSession();
245                LDAPSession sourceSession = (LDAPSession) sourceDirectory.getSession()) {
246            // fetch the entry to be able to run the security policy
247            // implemented in an entry adaptor
248            DocumentModel sourceEntry = sourceSession.getEntry(sourceId, false);
249            if (sourceEntry == null) {
250                throw new DirectoryException(String.format("could not add links from unexisting %s in directory %s",
251                        sourceId, sourceDirectory.getName()));
252            }
253            if (!BaseSession.isReadOnlyEntry(sourceEntry)) {
254                SearchResult ldapEntry = sourceSession.getLdapEntry(sourceId);
255
256                String sourceDn = ldapEntry.getNameInNamespace();
257                Attribute storedAttr = ldapEntry.getAttributes().get(attributeId);
258                String emptyRefMarker = sourceDirectory.getConfig().getEmptyRefMarker();
259                Attribute attrToAdd = new BasicAttribute(attributeId);
260                for (String targetId : targetIds) {
261                    if (staticAttributeIdIsDn) {
262                        // TODO optim: avoid LDAP search request when targetDn
263                        // can be forged client side (rdnAttribute =
264                        // idAttribute
265                        // and scope is onelevel)
266                        ldapEntry = targetSession.getLdapEntry(targetId);
267                        if (ldapEntry == null) {
268                            log.warn(String.format(
269                                    "entry '%s' in directory '%s' not found: could not add link from '%s' in directory '%s' for '%s'",
270                                    targetId, targetDirectory.getName(), sourceId, sourceDirectory.getName(), this));
271                            continue;
272                        }
273                        String dn = ldapEntry.getNameInNamespace();
274                        if (storedAttr == null || !storedAttr.contains(dn)) {
275                            attrToAdd.add(dn);
276                        }
277                    } else {
278                        if (storedAttr == null || !storedAttr.contains(targetId)) {
279                            attrToAdd.add(targetId);
280                        }
281                    }
282                }
283                if (attrToAdd.size() > 0) {
284                    try {
285                        // do the LDAP request to store missing dns
286                        Attributes attrsToAdd = new BasicAttributes();
287                        attrsToAdd.put(attrToAdd);
288
289                        if (log.isDebugEnabled()) {
290                            log.debug(String.format("LDAPReference.addLinks(%s, [%s]): LDAP modifyAttributes dn='%s' "
291                                    + "mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]", sourceId,
292                                    StringUtils.join(targetIds, ", "), sourceDn, attrsToAdd, this));
293                        }
294                        sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.ADD_ATTRIBUTE, attrsToAdd);
295
296                        // robustly clean any existing empty marker now that we
297                        // are sure that the list in not empty
298                        if (storedAttr.contains(emptyRefMarker)) {
299                            Attributes cleanAttrs = new BasicAttributes(attributeId, emptyRefMarker);
300
301                            if (log.isDebugEnabled()) {
302                                log.debug(String.format(
303                                        "LDAPReference.addLinks(%s, [%s]): LDAP modifyAttributes dn='%s'"
304                                                + " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]", sourceId,
305                                        StringUtils.join(targetIds, ", "), sourceDn, cleanAttrs, this));
306                            }
307                            sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE, cleanAttrs);
308                        }
309                    } catch (SchemaViolationException e) {
310                        if (isDynamic()) {
311                            // we are editing an entry that has no static part
312                            log.warn(String.format("cannot update dynamic reference in field %s for source %s",
313                                    getFieldName(), sourceId));
314                        } else {
315                            // this is a real schema configuration problem,
316                            // wrap up the exception
317                            throw new DirectoryException(e);
318                        }
319                    }
320                }
321            }
322        } catch (NamingException e) {
323            throw new DirectoryException("addLinks failed: " + e.getMessage(), e);
324        }
325    }
326
327    /**
328     * Store new links using the LDAP staticAttributeId strategy.
329     *
330     * @see org.nuxeo.ecm.directory.Reference#addLinks(List, String)
331     */
332    @Override
333    public void addLinks(List<String> sourceIds, String targetId) throws DirectoryException {
334        String attributeId = getStaticAttributeId();
335        if (attributeId == null && !sourceIds.isEmpty()) {
336            log.warn("trying to edit a non-static reference: ignoring");
337            return;
338        }
339        LDAPDirectory targetDirectory = (LDAPDirectory) getTargetDirectory();
340        LDAPDirectory sourceDirectory = (LDAPDirectory) getSourceDirectory();
341
342        String emptyRefMarker = sourceDirectory.getConfig().getEmptyRefMarker();
343        try (LDAPSession targetSession = (LDAPSession) targetDirectory.getSession();
344                LDAPSession sourceSession = (LDAPSession) sourceDirectory.getSession()) {
345            if (!sourceSession.isReadOnly()) {
346                // compute the target dn to add to all the matching source
347                // entries
348                SearchResult ldapEntry = targetSession.getLdapEntry(targetId);
349                if (ldapEntry == null) {
350                    throw new DirectoryException(String.format("could not add links to unexisting %s in directory %s",
351                            targetId, targetDirectory.getName()));
352                }
353                String targetAttributeValue;
354                if (staticAttributeIdIsDn) {
355                    targetAttributeValue = ldapEntry.getNameInNamespace();
356                } else {
357                    targetAttributeValue = targetId;
358                }
359
360                for (String sourceId : sourceIds) {
361                    // fetch the entry to be able to run the security policy
362                    // implemented in an entry adaptor
363                    DocumentModel sourceEntry = sourceSession.getEntry(sourceId, false);
364                    if (sourceEntry == null) {
365                        log.warn(String.format(
366                                "entry %s in directory %s not found: could not add link to %s in directory %s",
367                                sourceId, sourceDirectory.getName(), targetId, targetDirectory.getName()));
368                        continue;
369                    }
370                    if (BaseSession.isReadOnlyEntry(sourceEntry)) {
371                        // skip this entry since it cannot be edited to add the
372                        // reference to targetId
373                        log.warn(String.format(
374                                "entry %s in directory %s is readonly: could not add link to %s in directory %s",
375                                sourceId, sourceDirectory.getName(), targetId, targetDirectory.getName()));
376                        continue;
377                    }
378                    ldapEntry = sourceSession.getLdapEntry(sourceId);
379                    String sourceDn = ldapEntry.getNameInNamespace();
380                    Attribute storedAttr = ldapEntry.getAttributes().get(attributeId);
381                    if (storedAttr.contains(targetAttributeValue)) {
382                        // no need to readd
383                        continue;
384                    }
385                    try {
386                        // add the new dn
387                        Attributes attrs = new BasicAttributes(attributeId, targetAttributeValue);
388
389                        if (log.isDebugEnabled()) {
390                            log.debug(String.format("LDAPReference.addLinks([%s], %s): LDAP modifyAttributes dn='%s'"
391                                    + " mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]", StringUtils.join(sourceIds, ", "),
392                                    targetId, sourceDn, attrs, this));
393                        }
394                        sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.ADD_ATTRIBUTE, attrs);
395
396                        // robustly clean any existing empty marker now that we
397                        // are sure that the list in not empty
398                        if (storedAttr.contains(emptyRefMarker)) {
399                            Attributes cleanAttrs = new BasicAttributes(attributeId, emptyRefMarker);
400                            if (log.isDebugEnabled()) {
401                                log.debug(String.format("LDAPReference.addLinks(%s, %s): LDAP modifyAttributes dn='%s'"
402                                        + " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
403                                        StringUtils.join(sourceIds, ", "), targetId, sourceDn, cleanAttrs.toString(),
404                                        this));
405                            }
406                            sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE, cleanAttrs);
407                        }
408                    } catch (SchemaViolationException e) {
409                        if (isDynamic()) {
410                            // we are editing an entry that has no static part
411                            log.warn(String.format("cannot add dynamic reference in field %s for target %s",
412                                    getFieldName(), targetId));
413                        } else {
414                            // this is a real schema configuration problem,
415                            // wrap the exception
416                            throw new DirectoryException(e);
417                        }
418                    }
419                }
420            }
421        } catch (NamingException e) {
422            throw new DirectoryException("addLinks failed: " + e.getMessage(), e);
423        }
424    }
425
426    /**
427     * Fetch both statically and dynamically defined references and merge the results.
428     *
429     * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
430     */
431    @Override
432    public List<String> getSourceIdsForTarget(String targetId) throws DirectoryException {
433
434        // container to hold merged references
435        Set<String> sourceIds = new TreeSet<String>();
436        SearchResult targetLdapEntry = null;
437        String targetDn = null;
438
439        // step #1: resolve static references
440        String staticAttributeId = getStaticAttributeId();
441        if (staticAttributeId != null) {
442            // step #1.1: fetch the dn of the targetId entry in the target
443            // directory by the static dn valued strategy
444            LDAPDirectory targetDir = getTargetLDAPDirectory();
445
446            if (staticAttributeIdIsDn) {
447                try (LDAPSession targetSession = (LDAPSession) targetDir.getSession()) {
448                    targetLdapEntry = targetSession.getLdapEntry(targetId, false);
449                    if (targetLdapEntry == null) {
450                        String msg = String.format("Failed to perform inverse lookup on LDAPReference"
451                                + " resolving field '%s' of '%s' to entries of '%s'"
452                                + " using the static content of attribute '%s':"
453                                + " entry '%s' cannot be found in '%s'", fieldName, sourceDirectory,
454                                targetDirectoryName, staticAttributeId, targetId, targetDirectoryName);
455                        throw new DirectoryEntryNotFoundException(msg);
456                    }
457                    targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
458
459                } catch (NamingException e) {
460                    throw new DirectoryException("error fetching " + targetId + " from " + targetDirectoryName + ": "
461                            + e.getMessage(), e);
462                }
463            }
464
465            // step #1.2: search for entries that reference that dn in the
466            // source directory and collect their ids
467            LDAPDirectory sourceDirectory = getSourceLDAPDirectory();
468
469            String filterExpr = String.format("(&(%s={0})%s)", staticAttributeId, sourceDirectory.getBaseFilter());
470            String[] filterArgs = new String[1];
471
472            if (staticAttributeIdIsDn) {
473                filterArgs[0] = targetDn;
474            } else {
475                filterArgs[0] = targetId;
476            }
477
478            String searchBaseDn = sourceDirectory.getConfig().getSearchBaseDn();
479            SearchControls sctls = sourceDirectory.getSearchControls();
480            try (LDAPSession sourceSession = (LDAPSession) sourceDirectory.getSession()) {
481                if (log.isDebugEnabled()) {
482                    log.debug(String.format("LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
483                            + " filter='%s' args='%s' scope='%s' [%s]", targetId, searchBaseDn, filterExpr,
484                            StringUtils.join(filterArgs, ", "), sctls.getSearchScope(), this));
485                }
486                NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(searchBaseDn, filterExpr,
487                        filterArgs, sctls);
488
489                try {
490                    while (results.hasMore()) {
491                        Attributes attributes = results.next().getAttributes();
492                        // NXP-2461: check that id field is filled
493                        Attribute attr = attributes.get(sourceSession.idAttribute);
494                        if (attr != null) {
495                            Object value = attr.get();
496                            if (value != null) {
497                                sourceIds.add(value.toString());
498                            }
499                        }
500                    }
501                } finally {
502                    results.close();
503                }
504            } catch (NamingException e) {
505                throw new DirectoryException("error during reference search for " + filterArgs[0], e);
506            }
507        }
508        // step #2: resolve dynamic references
509        String dynamicAttributeId = this.dynamicAttributeId;
510        if (dynamicAttributeId != null) {
511
512            LDAPDirectory sourceDirectory = getSourceLDAPDirectory();
513            LDAPDirectory targetDirectory = getTargetLDAPDirectory();
514            String searchBaseDn = sourceDirectory.getConfig().getSearchBaseDn();
515
516            try (LDAPSession sourceSession = (LDAPSession) sourceDirectory.getSession();
517                    LDAPSession targetSession = (LDAPSession) targetDirectory.getSession()) {
518                // step #2.1: fetch the target entry to apply the ldap url
519                // filters of the candidate sources on it
520                if (targetLdapEntry == null) {
521                    // only fetch the entry if not already fetched by the
522                    // static
523                    // attributes references resolution
524                    targetLdapEntry = targetSession.getLdapEntry(targetId, false);
525                }
526                if (targetLdapEntry == null) {
527                    String msg = String.format("Failed to perform inverse lookup on LDAPReference"
528                            + " resolving field '%s' of '%s' to entries of '%s'"
529                            + " using the dynamic content of attribute '%s':" + " entry '%s' cannot be found in '%s'",
530                            fieldName, sourceDirectory, targetDirectoryName, dynamicAttributeId, targetId,
531                            targetDirectoryName);
532                    throw new DirectoryException(msg);
533                }
534                targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
535                Attributes targetAttributes = targetLdapEntry.getAttributes();
536
537                // step #2.2: find the list of entries that hold candidate
538                // dynamic links in the source directory
539                SearchControls sctls = sourceDirectory.getSearchControls();
540                String filterExpr = String.format("%s=*", dynamicAttributeId);
541
542                if (log.isDebugEnabled()) {
543                    log.debug(String.format("LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
544                            + " filter='%s' scope='%s' [%s]", targetId, searchBaseDn, filterExpr,
545                            sctls.getSearchScope(), this));
546                }
547                NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(searchBaseDn, filterExpr,
548                        sctls);
549                try {
550                    while (results.hasMore()) {
551                        // step #2.3: for each sourceId and each ldapUrl test
552                        // whether the current target entry matches the
553                        // collected
554                        // URL
555                        Attributes sourceAttributes = results.next().getAttributes();
556
557                        NamingEnumeration<?> ldapUrls = sourceAttributes.get(dynamicAttributeId).getAll();
558                        try {
559                            while (ldapUrls.hasMore()) {
560                                LdapURL ldapUrl = new LdapURL(ldapUrls.next().toString());
561                                String candidateDN = pseudoNormalizeDn(ldapUrl.getDN());
562                                // check base URL
563                                if (!targetDn.endsWith(candidateDN)) {
564                                    continue;
565                                }
566
567                                // check onelevel scope constraints
568                                if ("onelevel".equals(ldapUrl.getScope())) {
569                                    int targetDnSize = new LdapName(targetDn).size();
570                                    int urlDnSize = new LdapName(candidateDN).size();
571                                    if (targetDnSize - urlDnSize > 1) {
572                                        // target is not a direct child of the
573                                        // DN of the
574                                        // LDAP URL
575                                        continue;
576                                    }
577                                }
578
579                                // check that the target entry matches the
580                                // filter
581                                if (getFilterMatcher().match(targetAttributes, ldapUrl.getFilter())) {
582                                    // the target match the source url, add it
583                                    // to the
584                                    // collected ids
585                                    sourceIds.add(sourceAttributes.get(sourceSession.idAttribute).get().toString());
586                                }
587                            }
588                        } finally {
589                            ldapUrls.close();
590                        }
591                    }
592                } finally {
593                    results.close();
594                }
595            } catch (NamingException e) {
596                throw new DirectoryException("error during reference search for " + targetId, e);
597            }
598        }
599
600        /*
601         * This kind of reference is not supported because Active Directory use filter expression not yet supported by
602         * LDAPFilterMatcher. See NXP-4562
603         */
604        if (dynamicReferences != null && dynamicReferences.length > 0) {
605            log.error("This kind of reference is not supported.");
606        }
607
608        return new ArrayList<String>(sourceIds);
609    }
610
611    /**
612     * Fetches both statically and dynamically defined references and merges the results.
613     *
614     * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
615     */
616    @Override
617    // XXX: broken, use getLdapTargetIds for a proper implementation
618    @SuppressWarnings("unchecked")
619    public List<String> getTargetIdsForSource(String sourceId) throws DirectoryException {
620        String schemaName = getSourceDirectory().getSchema();
621        try (Session session = getSourceDirectory().getSession()) {
622            try {
623                return (List<String>) session.getEntry(sourceId).getProperty(schemaName, fieldName);
624            } catch (PropertyException e) {
625                throw new DirectoryException(e);
626            }
627        }
628    }
629
630    /**
631     * Simple helper that replaces ", " by "," in the provided dn and returns the lower case version of the result for
632     * comparison purpose.
633     *
634     * @param dn the raw unnormalized dn
635     * @return lowercase version without whitespace after commas
636     * @throws InvalidNameException
637     */
638    protected static String pseudoNormalizeDn(String dn) throws InvalidNameException {
639        LdapName ldapName = new LdapName(dn);
640        List<String> rdns = new ArrayList<String>();
641        for (Rdn rdn : ldapName.getRdns()) {
642            String value = rdn.getValue().toString().toLowerCase().replaceAll(",", "\\\\,");
643            String rdnStr = rdn.getType().toLowerCase() + "=" + value;
644            rdns.add(0, rdnStr);
645        }
646        return StringUtils.join(rdns, ',');
647    }
648
649    /**
650     * Optimized method to spare a LDAP request when the caller is a LDAPSession object that has already fetched the
651     * LDAP Attribute instances.
652     * <p>
653     * This method should return the same results as the sister method: org.nuxeo
654     * .ecm.directory.Reference#getTargetIdsForSource(java.lang.String)
655     *
656     * @return target reference ids
657     * @throws DirectoryException
658     */
659    public List<String> getLdapTargetIds(Attributes attributes) throws DirectoryException {
660
661        Set<String> targetIds = new TreeSet<String>();
662
663        LDAPDirectory targetDirectory = (LDAPDirectory) getTargetDirectory();
664        LDAPDirectoryDescriptor targetDirconfig = getTargetDirectoryDescriptor();
665        String emptyRefMarker = targetDirectory.getConfig().getEmptyRefMarker();
666        try (LDAPSession targetSession = (LDAPSession) targetDirectory.getSession()) {
667            String baseDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
668
669            // step #1: fetch ids referenced by static attributes
670            String staticAttributeId = getStaticAttributeId();
671            Attribute staticAttribute = null;
672            if (staticAttributeId != null) {
673                staticAttribute = attributes.get(staticAttributeId);
674            }
675
676            if (staticAttribute != null && !staticAttributeIdIsDn) {
677                NamingEnumeration<?> staticContent = staticAttribute.getAll();
678                try {
679                    while (staticContent.hasMore()) {
680                        String value = staticContent.next().toString();
681                        if (!emptyRefMarker.equals(value)) {
682                            targetIds.add(value);
683                        }
684                    }
685                } finally {
686                    staticContent.close();
687                }
688            }
689
690            if (staticAttribute != null && staticAttributeIdIsDn) {
691                NamingEnumeration<?> targetDns = staticAttribute.getAll();
692                try {
693                    while (targetDns.hasMore()) {
694                        String targetDn = targetDns.next().toString();
695
696                        if (!pseudoNormalizeDn(targetDn).endsWith(baseDn)) {
697                            // optim: avoid network connections when obvious
698                            if (log.isTraceEnabled()) {
699                                log.trace(String.format("ignoring: dn='%s' (does not match '%s') for '%s'", targetDn,
700                                        baseDn, this));
701                            }
702                            continue;
703                        }
704                        // find the id of the referenced entry
705                        String id = null;
706
707                        if (targetSession.rdnMatchesIdField()) {
708                            // optim: do not fetch the entry to get its true id
709                            // but
710                            // guess it by reading the targetDn
711                            LdapName name = new LdapName(targetDn);
712                            String rdn = name.get(name.size() - 1);
713                            int pos = rdn.indexOf("=");
714                            id = rdn.substring(pos + 1);
715                        } else {
716                            id = getIdForDn(targetSession, targetDn);
717                            if (id == null) {
718                                log.warn(String.format(
719                                        "ignoring target '%s' (missing attribute '%s') while resolving reference '%s'",
720                                        targetDn, targetSession.idAttribute, this));
721                                continue;
722                            }
723                        }
724                        if (forceDnConsistencyCheck) {
725                            // check that the referenced entry is actually part
726                            // of
727                            // the target directory (takes care of the filters
728                            // and
729                            // the scope)
730                            // this check can be very expensive on large groups
731                            // and thus not enabled by default
732                            if (!targetSession.hasEntry(id)) {
733                                if (log.isTraceEnabled()) {
734                                    log.trace(String.format(
735                                            "ignoring target '%s' when resolving '%s' (not part of target"
736                                                    + " directory by forced DN consistency check)", targetDn, this));
737                                }
738                                continue;
739                            }
740                        }
741                        // NXP-2461: check that id field is filled
742                        if (id != null) {
743                            targetIds.add(id);
744                        }
745                    }
746                } finally {
747                    targetDns.close();
748                }
749            }
750            // step #2: fetched dynamically referenced ids
751            String dynamicAttributeId = this.dynamicAttributeId;
752            Attribute dynamicAttribute = null;
753            if (dynamicAttributeId != null) {
754                dynamicAttribute = attributes.get(dynamicAttributeId);
755            }
756            if (dynamicAttribute != null) {
757                NamingEnumeration<?> rawldapUrls = dynamicAttribute.getAll();
758                try {
759                    while (rawldapUrls.hasMore()) {
760                        LdapURL ldapUrl = new LdapURL(rawldapUrls.next().toString());
761                        String linkDn = pseudoNormalizeDn(ldapUrl.getDN());
762                        String directoryDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
763                        int scope = SearchControls.ONELEVEL_SCOPE;
764                        String scopePart = ldapUrl.getScope();
765                        if (scopePart != null && scopePart.toLowerCase().startsWith("sub")) {
766                            scope = SearchControls.SUBTREE_SCOPE;
767                        }
768                        if (!linkDn.endsWith(directoryDn) && !directoryDn.endsWith(linkDn)) {
769                            // optim #1: if the dns do not match, abort
770                            continue;
771                        } else if (directoryDn.endsWith(linkDn) && linkDn.length() < directoryDn.length()
772                                && scope == SearchControls.ONELEVEL_SCOPE) {
773                            // optim #2: the link dn is pointing to elements
774                            // that at
775                            // upperlevel than directory elements
776                            continue;
777                        } else {
778
779                            // Search for references elements
780                            targetIds.addAll(getReferencedElements(attributes, directoryDn, linkDn,
781                                    ldapUrl.getFilter(), scope));
782
783                        }
784                    }
785                } finally {
786                    rawldapUrls.close();
787                }
788            }
789
790            if (dynamicReferences != null && dynamicReferences.length > 0) {
791
792                // Only the first Dynamic Reference is used
793                LDAPDynamicReferenceDescriptor dynAtt = dynamicReferences[0];
794
795                Attribute baseDnsAttribute = attributes.get(dynAtt.baseDN);
796                Attribute filterAttribute = attributes.get(dynAtt.filter);
797
798                if (baseDnsAttribute != null && filterAttribute != null) {
799
800                    NamingEnumeration<?> baseDns = null;
801                    NamingEnumeration<?> filters = null;
802
803                    try {
804                        // Get the BaseDN value from the descriptor
805                        baseDns = baseDnsAttribute.getAll();
806                        String linkDnValue = baseDns.next().toString();
807                        baseDns.close();
808                        linkDnValue = pseudoNormalizeDn(linkDnValue);
809
810                        // Get the filter value from the descriptor
811                        filters = filterAttribute.getAll();
812                        String filterValue = filters.next().toString();
813                        filters.close();
814
815                        // Get the scope value from the descriptor
816                        int scope = "subtree".equalsIgnoreCase(dynAtt.type) ? SearchControls.SUBTREE_SCOPE
817                                : SearchControls.ONELEVEL_SCOPE;
818
819                        String directoryDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
820
821                        // if the dns match, and if the link dn is pointing to
822                        // elements that at upperlevel than directory elements
823                        if ((linkDnValue.endsWith(directoryDn) || directoryDn.endsWith(linkDnValue))
824                                && !(directoryDn.endsWith(linkDnValue) && linkDnValue.length() < directoryDn.length() && scope == SearchControls.ONELEVEL_SCOPE)) {
825
826                            // Correct the filter expression
827                            filterValue = FilterExpressionCorrector.correctFilter(filterValue, FilterJobs.CORRECT_NOT);
828
829                            // Search for references elements
830                            targetIds.addAll(getReferencedElements(attributes, directoryDn, linkDnValue, filterValue,
831                                    scope));
832
833                        }
834                    } finally {
835                        if (baseDns != null) {
836                            baseDns.close();
837                        }
838
839                        if (filters != null) {
840                            filters.close();
841                        }
842                    }
843
844                }
845
846            }
847            // return merged attributes
848            return new ArrayList<String>(targetIds);
849        } catch (NamingException e) {
850            throw new DirectoryException("error computing LDAP references", e);
851        }
852    }
853
854    protected String getIdForDn(LDAPSession session, String dn) {
855        // the entry id is not based on the rdn, we thus need to
856        // fetch the LDAP entry to grab it
857        String[] attributeIdsToCollect = { session.idAttribute };
858        Attributes entry;
859        try {
860
861            if (log.isDebugEnabled()) {
862                log.debug(String.format("LDAPReference.getIdForDn(session, %s): LDAP get dn='%s'"
863                        + " attribute ids to collect='%s' [%s]", dn, dn, StringUtils.join(attributeIdsToCollect, ", "),
864                        this));
865            }
866
867            Name name = new CompositeName().add(dn);
868            entry = session.dirContext.getAttributes(name, attributeIdsToCollect);
869        } catch (NamingException e) {
870            return null;
871        }
872        // NXP-2461: check that id field is filled
873        Attribute attr = entry.get(session.idAttribute);
874        if (attr != null) {
875            try {
876                return attr.get().toString();
877            } catch (NamingException e) {
878            }
879        }
880        return null;
881    }
882
883    /**
884     * Retrieve the elements referenced by the filter/BaseDN/Scope request.
885     *
886     * @param attributes Attributes of the referencer element
887     * @param directoryDn Dn of the Directory
888     * @param linkDn Dn specified in the parent
889     * @param filter Filter expression specified in the parent
890     * @param scope scope for the search
891     * @return The list of the referenced elements.
892     * @throws DirectoryException
893     * @throws NamingException
894     */
895    private Set<String> getReferencedElements(Attributes attributes, String directoryDn, String linkDn, String filter,
896            int scope) throws DirectoryException, NamingException {
897
898        Set<String> targetIds = new TreeSet<String>();
899
900        LDAPDirectoryDescriptor targetDirconfig = getTargetDirectoryDescriptor();
901        LDAPDirectory targetDirectory = (LDAPDirectory) getTargetDirectory();
902        LDAPSession targetSession = (LDAPSession) targetDirectory.getSession();
903
904        // use the most specific scope between the one specified in the
905        // Directory and the specified in the Parent
906        String dn = directoryDn.endsWith(linkDn) && directoryDn.length() > linkDn.length() ? directoryDn : linkDn;
907
908        // combine the ldapUrl search query with target
909        // directory own constraints
910        SearchControls scts = new SearchControls();
911
912        // use the most specific scope
913        scts.setSearchScope(Math.min(scope, targetDirconfig.getSearchScope()));
914
915        // only fetch the ids of the targets
916        scts.setReturningAttributes(new String[] { targetSession.idAttribute });
917
918        // combine the filter of the target directory with the
919        // provided filter if any
920        String targetFilter = targetDirconfig.getSearchFilter();
921        if (filter == null || filter.length() == 0) {
922            filter = targetFilter;
923        } else if (targetFilter != null && targetFilter.length() > 0) {
924            filter = String.format("(&(%s)(%s))", targetFilter, filter);
925        }
926
927        // perform the request and collect the ids
928        if (log.isDebugEnabled()) {
929            log.debug(String.format("LDAPReference.getLdapTargetIds(%s): LDAP search dn='%s' "
930                    + " filter='%s' scope='%s' [%s]", attributes, dn, dn, scts.getSearchScope(), this));
931        }
932
933        Name name = new CompositeName().add(dn);
934        NamingEnumeration<SearchResult> results = targetSession.dirContext.search(name, filter, scts);
935        try {
936            while (results.hasMore()) {
937                // NXP-2461: check that id field is filled
938                Attribute attr = results.next().getAttributes().get(targetSession.idAttribute);
939                if (attr != null) {
940                    String collectedId = attr.get().toString();
941                    if (collectedId != null) {
942                        targetIds.add(collectedId);
943                    }
944                }
945
946            }
947        } finally {
948            results.close();
949        }
950
951        return targetIds;
952    }
953
954    /**
955     * Remove existing statically defined links for the given source id (dynamic references remain unaltered)
956     *
957     * @see org.nuxeo.ecm.directory.Reference#removeLinksForSource(String)
958     */
959    @Override
960    public void removeLinksForSource(String sourceId) throws DirectoryException {
961        LDAPDirectory targetDirectory = (LDAPDirectory) getTargetDirectory();
962        LDAPDirectory sourceDirectory = (LDAPDirectory) getSourceDirectory();
963        String attributeId = getStaticAttributeId();
964        try (LDAPSession sourceSession = (LDAPSession) sourceDirectory.getSession();
965                LDAPSession targetSession = (LDAPSession) targetDirectory.getSession()) {
966            if (sourceSession.isReadOnly() || attributeId == null) {
967                // do not try to do anything on a read only server or to a
968                // purely dynamic reference
969                return;
970            }
971            // get the dn of the entry that matches sourceId
972            SearchResult sourceLdapEntry = sourceSession.getLdapEntry(sourceId);
973            if (sourceLdapEntry == null) {
974                throw new DirectoryException(String.format(
975                        "cannot edit the links hold by missing entry '%s' in directory '%s'", sourceId,
976                        sourceDirectory.getName()));
977            }
978            String sourceDn = pseudoNormalizeDn(sourceLdapEntry.getNameInNamespace());
979
980            Attribute oldAttr = sourceLdapEntry.getAttributes().get(attributeId);
981            if (oldAttr == null) {
982                // consider it as an empty attribute to simplify the following
983                // code
984                oldAttr = new BasicAttribute(attributeId);
985            }
986            Attribute attrToRemove = new BasicAttribute(attributeId);
987
988            NamingEnumeration<?> oldAttrs = oldAttr.getAll();
989            String targetBaseDn = pseudoNormalizeDn(targetDirectory.getConfig().getSearchBaseDn());
990            try {
991                while (oldAttrs.hasMore()) {
992                    String targetKeyAttr = oldAttrs.next().toString();
993
994                    if (staticAttributeIdIsDn) {
995                        String dn = pseudoNormalizeDn(targetKeyAttr);
996                        if (forceDnConsistencyCheck) {
997                            String id = getIdForDn(targetSession, dn);
998                            if (id != null && targetSession.hasEntry(id)) {
999                                // this is an entry managed by the current
1000                                // reference
1001                                attrToRemove.add(dn);
1002                            }
1003                        } else if (dn.endsWith(targetBaseDn)) {
1004                            // this is an entry managed by the current
1005                            // reference
1006                            attrToRemove.add(dn);
1007                        }
1008                    } else {
1009                        attrToRemove.add(targetKeyAttr);
1010                    }
1011                }
1012            } finally {
1013                oldAttrs.close();
1014            }
1015            try {
1016                if (attrToRemove.size() == oldAttr.size()) {
1017                    // use the empty ref marker to avoid empty attr
1018                    String emptyRefMarker = sourceDirectory.getConfig().getEmptyRefMarker();
1019                    Attributes emptyAttribute = new BasicAttributes(attributeId, emptyRefMarker);
1020                    if (log.isDebugEnabled()) {
1021                        log.debug(String.format(
1022                                "LDAPReference.removeLinksForSource(%s): LDAP modifyAttributes key='%s' "
1023                                        + " mod_op='REPLACE_ATTRIBUTE' attrs='%s' [%s]", sourceId, sourceDn,
1024                                emptyAttribute, this));
1025                    }
1026                    sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REPLACE_ATTRIBUTE, emptyAttribute);
1027                } else if (attrToRemove.size() > 0) {
1028                    // remove the attribute managed by the current reference
1029                    Attributes attrsToRemove = new BasicAttributes();
1030                    attrsToRemove.put(attrToRemove);
1031                    if (log.isDebugEnabled()) {
1032                        log.debug(String.format(
1033                                "LDAPReference.removeLinksForSource(%s): LDAP modifyAttributes dn='%s' "
1034                                        + " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]", sourceId, sourceDn,
1035                                attrsToRemove, this));
1036                    }
1037                    sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE, attrsToRemove);
1038                }
1039            } catch (SchemaViolationException e) {
1040                if (isDynamic()) {
1041                    // we are editing an entry that has no static part
1042                    log.warn(String.format("cannot remove dynamic reference in field %s for source %s", getFieldName(),
1043                            sourceId));
1044                } else {
1045                    // this is a real schma configuration problem, wrapup the
1046                    // exception
1047                    throw new DirectoryException(e);
1048                }
1049            }
1050        } catch (NamingException e) {
1051            throw new DirectoryException("removeLinksForSource failed: " + e.getMessage(), e);
1052        }
1053    }
1054
1055    /**
1056     * Remove existing statically defined links for the given target id (dynamic references remain unaltered)
1057     *
1058     * @see org.nuxeo.ecm.directory.Reference#removeLinksForTarget(String)
1059     */
1060    @Override
1061    public void removeLinksForTarget(String targetId) throws DirectoryException {
1062        if (!isStatic()) {
1063            // nothing to do: dynamic references cannot be updated
1064            return;
1065        }
1066        LDAPDirectory targetDirectory = (LDAPDirectory) getTargetDirectory();
1067        LDAPDirectory sourceDirectory = (LDAPDirectory) getSourceDirectory();
1068        String attributeId = getStaticAttributeId();
1069        try (LDAPSession targetSession = (LDAPSession) targetDirectory.getSession();
1070                LDAPSession sourceSession = (LDAPSession) sourceDirectory.getSession()) {
1071            if (!sourceSession.isReadOnly()) {
1072                // get the dn of the target that matches targetId
1073                String targetAttributeValue;
1074
1075                if (staticAttributeIdIsDn) {
1076                    SearchResult targetLdapEntry = targetSession.getLdapEntry(targetId);
1077                    if (targetLdapEntry == null) {
1078                        String rdnAttribute = targetDirectory.getConfig().getRdnAttribute();
1079                        if (!rdnAttribute.equals(targetSession.idAttribute)) {
1080                            log.warn(String.format(
1081                                    "cannot remove links to missing entry %s in directory %s for reference %s",
1082                                    targetId, targetDirectory.getName(), this));
1083                            return;
1084                        }
1085                        // the entry might have already been deleted, try to
1086                        // re-forge it if possible (might not work if scope is
1087                        // subtree)
1088                        targetAttributeValue = String.format("%s=%s,%s", rdnAttribute, targetId,
1089                                targetDirectory.getConfig().getSearchBaseDn());
1090                    } else {
1091                        targetAttributeValue = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
1092                    }
1093                } else {
1094                    targetAttributeValue = targetId;
1095                }
1096
1097                // build a LDAP query to find entries that point to the target
1098                String searchFilter = String.format("(%s=%s)", attributeId, targetAttributeValue);
1099                String sourceFilter = sourceDirectory.getBaseFilter();
1100
1101                if (sourceFilter != null && !"".equals(sourceFilter)) {
1102                    searchFilter = String.format("(&(%s)(%s))", searchFilter, sourceFilter);
1103                }
1104
1105                SearchControls scts = new SearchControls();
1106                scts.setSearchScope(sourceDirectory.getConfig().getSearchScope());
1107                scts.setReturningAttributes(new String[] { attributeId });
1108
1109                // find all source entries that point to the target key and
1110                // clean
1111                // those references
1112                if (log.isDebugEnabled()) {
1113                    log.debug(String.format("LDAPReference.removeLinksForTarget(%s): LDAP search baseDn='%s' "
1114                            + " filter='%s' scope='%s' [%s]", targetId, sourceSession.searchBaseDn, searchFilter,
1115                            scts.getSearchScope(), this));
1116                }
1117                NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(sourceSession.searchBaseDn,
1118                        searchFilter, scts);
1119                String emptyRefMarker = sourceDirectory.getConfig().getEmptyRefMarker();
1120                Attributes emptyAttribute = new BasicAttributes(attributeId, emptyRefMarker);
1121
1122                try {
1123                    while (results.hasMore()) {
1124                        SearchResult result = results.next();
1125                        Attributes attrs = result.getAttributes();
1126                        Attribute attr = attrs.get(attributeId);
1127                        try {
1128                            if (attr.size() == 1) {
1129                                // the attribute holds the last reference, put
1130                                // the
1131                                // empty ref. marker before removing the
1132                                // attribute
1133                                // since empty attribute are often not allowed
1134                                // by
1135                                // the server schema
1136                                if (log.isDebugEnabled()) {
1137                                    log.debug(String.format(
1138                                            "LDAPReference.removeLinksForTarget(%s): LDAP modifyAttributes key='%s' "
1139                                                    + "mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]", targetId,
1140                                            result.getNameInNamespace(), attrs, this));
1141                                }
1142                                sourceSession.dirContext.modifyAttributes(result.getNameInNamespace(),
1143                                        DirContext.ADD_ATTRIBUTE, emptyAttribute);
1144                            }
1145                            // remove the reference to the target key
1146                            attrs = new BasicAttributes();
1147                            attr = new BasicAttribute(attributeId);
1148                            attr.add(targetAttributeValue);
1149                            attrs.put(attr);
1150                            if (log.isDebugEnabled()) {
1151                                log.debug(String.format(
1152                                        "LDAPReference.removeLinksForTarget(%s): LDAP modifyAttributes key='%s' "
1153                                                + "mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]", targetId,
1154                                        result.getNameInNamespace(), attrs, this));
1155                            }
1156                            sourceSession.dirContext.modifyAttributes(result.getNameInNamespace(),
1157                                    DirContext.REMOVE_ATTRIBUTE, attrs);
1158                        } catch (SchemaViolationException e) {
1159                            if (isDynamic()) {
1160                                // we are editing an entry that has no static
1161                                // part
1162                                log.warn(String.format("cannot remove dynamic reference in field %s for target %s",
1163                                        getFieldName(), targetId));
1164                            } else {
1165                                // this is a real schema configuration problem,
1166                                // wrapup the exception
1167                                throw new DirectoryException(e);
1168                            }
1169                        }
1170                    }
1171                } finally {
1172                    results.close();
1173                }
1174            }
1175        } catch (NamingException e) {
1176            throw new DirectoryException("removeLinksForTarget failed: " + e.getMessage(), e);
1177        }
1178    }
1179
1180    /**
1181     * Edit the list of statically defined references for a given target (dynamic references remain unaltered)
1182     *
1183     * @see org.nuxeo.ecm.directory.Reference#setSourceIdsForTarget(String, List)
1184     */
1185    @Override
1186    public void setSourceIdsForTarget(String targetId, List<String> sourceIds) throws DirectoryException {
1187        removeLinksForTarget(targetId);
1188        addLinks(sourceIds, targetId);
1189    }
1190
1191    /**
1192     * Set the list of statically defined references for a given source (dynamic references remain unaltered)
1193     *
1194     * @see org.nuxeo.ecm.directory.Reference#setTargetIdsForSource(String, List)
1195     */
1196    @Override
1197    public void setTargetIdsForSource(String sourceId, List<String> targetIds) throws DirectoryException {
1198        removeLinksForSource(sourceId);
1199        addLinks(sourceId, targetIds);
1200    }
1201
1202    @Override
1203    // to build helpful debug logs
1204    public String toString() {
1205        return String.format("LDAPReference to resolve field='%s' of sourceDirectory='%s'"
1206                + " with targetDirectory='%s'" + " and staticAttributeId='%s', dynamicAttributeId='%s'", fieldName,
1207                sourceDirectoryName, targetDirectoryName, staticAttributeId, dynamicAttributeId);
1208    }
1209
1210    /**
1211     * @since 5.6
1212     */
1213    @Override
1214    protected AbstractReference newInstance() {
1215        return new LDAPReference();
1216    }
1217
1218    /**
1219     * @since 5.6
1220     */
1221    @Override
1222    public LDAPReference clone() {
1223        LDAPReference clone = (LDAPReference) super.clone();
1224        clone.forceDnConsistencyCheck = forceDnConsistencyCheck;
1225        clone.staticAttributeIdIsDn = staticAttributeIdIsDn;
1226        clone.staticAttributeId = staticAttributeId;
1227        clone.dynamicAttributeId = dynamicAttributeId;
1228        clone.fieldName = fieldName;
1229        return clone;
1230    }
1231
1232}