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().getDescriptor(); 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.getDescriptor().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.getDescriptor().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.getDescriptor().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.getDescriptor().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.getDescriptor().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.getDescriptor().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.getDescriptor().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.getDescriptor().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.getDescriptor().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.getDescriptor().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.getDescriptor().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 public LDAPReference clone() { 1220 LDAPReference clone = (LDAPReference) super.clone(); 1221 // basic fields are already copied by super.clone() 1222 return clone; 1223 } 1224 1225}