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