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