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