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