001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2016 ForgeRock AS. 015 */ 016package org.opends.server.backends; 017 018import static org.opends.messages.BackendMessages.*; 019import static org.opends.messages.ReplicationMessages.*; 020import static org.opends.server.config.ConfigConstants.*; 021import static org.opends.server.core.DirectoryServer.*; 022import static org.opends.server.replication.plugin.MultimasterReplication.*; 023import static org.opends.server.replication.server.changelog.api.DBCursor.KeyMatchingStrategy.*; 024import static org.opends.server.replication.server.changelog.api.DBCursor.PositionStrategy.*; 025import static org.opends.server.util.LDIFWriter.*; 026import static org.opends.server.util.ServerConstants.*; 027import static org.opends.server.util.StaticUtils.*; 028 029import java.text.SimpleDateFormat; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.Date; 033import java.util.Iterator; 034import java.util.LinkedHashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.Set; 038import java.util.TimeZone; 039import java.util.concurrent.ConcurrentLinkedQueue; 040import java.util.concurrent.ConcurrentSkipListMap; 041import java.util.concurrent.atomic.AtomicReference; 042 043import org.forgerock.i18n.LocalizableMessage; 044import org.forgerock.i18n.slf4j.LocalizedLogger; 045import org.forgerock.opendj.config.server.ConfigException; 046import org.forgerock.opendj.ldap.ByteString; 047import org.forgerock.opendj.ldap.ConditionResult; 048import org.forgerock.opendj.ldap.DN; 049import org.forgerock.opendj.ldap.ModificationType; 050import org.forgerock.opendj.ldap.RDN; 051import org.forgerock.opendj.ldap.ResultCode; 052import org.forgerock.opendj.ldap.SearchScope; 053import org.forgerock.opendj.ldap.schema.AttributeType; 054import org.opends.server.admin.Configuration; 055import org.opends.server.api.Backend; 056import org.opends.server.config.ConfigConstants; 057import org.opends.server.controls.EntryChangelogNotificationControl; 058import org.opends.server.controls.ExternalChangelogRequestControl; 059import org.opends.server.core.AddOperation; 060import org.opends.server.core.DeleteOperation; 061import org.opends.server.core.DirectoryServer; 062import org.opends.server.core.ModifyDNOperation; 063import org.opends.server.core.ModifyOperation; 064import org.opends.server.core.PersistentSearch; 065import org.opends.server.core.SearchOperation; 066import org.opends.server.core.ServerContext; 067import org.opends.server.replication.common.CSN; 068import org.opends.server.replication.common.MultiDomainServerState; 069import org.opends.server.replication.common.ServerState; 070import org.opends.server.replication.protocol.AddMsg; 071import org.opends.server.replication.protocol.DeleteMsg; 072import org.opends.server.replication.protocol.LDAPUpdateMsg; 073import org.opends.server.replication.protocol.ModifyCommonMsg; 074import org.opends.server.replication.protocol.ModifyDNMsg; 075import org.opends.server.replication.protocol.UpdateMsg; 076import org.opends.server.replication.server.ReplicationServer; 077import org.opends.server.replication.server.ReplicationServerDomain; 078import org.opends.server.replication.server.changelog.api.ChangeNumberIndexDB; 079import org.opends.server.replication.server.changelog.api.ChangeNumberIndexRecord; 080import org.opends.server.replication.server.changelog.api.ChangelogDB; 081import org.opends.server.replication.server.changelog.api.ChangelogException; 082import org.opends.server.replication.server.changelog.api.DBCursor; 083import org.opends.server.replication.server.changelog.api.DBCursor.CursorOptions; 084import org.opends.server.replication.server.changelog.api.ReplicaId; 085import org.opends.server.replication.server.changelog.api.ReplicationDomainDB; 086import org.opends.server.replication.server.changelog.file.ECLEnabledDomainPredicate; 087import org.opends.server.replication.server.changelog.file.ECLMultiDomainDBCursor; 088import org.opends.server.replication.server.changelog.file.MultiDomainDBCursor; 089import org.opends.server.types.Attribute; 090import org.opends.server.types.Attributes; 091import org.opends.server.types.BackupConfig; 092import org.opends.server.types.BackupDirectory; 093import org.opends.server.types.CanceledOperationException; 094import org.opends.server.types.Control; 095import org.opends.server.types.DirectoryException; 096import org.opends.server.types.Entry; 097import org.opends.server.types.FilterType; 098import org.opends.server.types.IndexType; 099import org.opends.server.types.InitializationException; 100import org.opends.server.types.LDIFExportConfig; 101import org.opends.server.types.LDIFImportConfig; 102import org.opends.server.types.LDIFImportResult; 103import org.opends.server.types.Modification; 104import org.opends.server.types.ObjectClass; 105import org.opends.server.types.Privilege; 106import org.opends.server.types.RawAttribute; 107import org.opends.server.types.RestoreConfig; 108import org.opends.server.types.SearchFilter; 109import org.opends.server.types.WritabilityMode; 110import org.opends.server.util.StaticUtils; 111 112/** 113 * A backend that provides access to the changelog, i.e. the "cn=changelog" 114 * suffix. It is a read-only backend that is created by a 115 * {@link ReplicationServer} and is not configurable. 116 * <p> 117 * There are two modes to search the changelog: 118 * <ul> 119 * <li>Cookie mode: when a "ECL Cookie Exchange Control" is provided with the 120 * request. The cookie provided in the control is used to retrieve entries from 121 * the ReplicaDBs. The <code>changeNumber</code> attribute is not returned with 122 * the entries.</li> 123 * <li>Change number mode: when no "ECL Cookie Exchange Control" is provided 124 * with the request. The entries are retrieved using the ChangeNumberIndexDB and 125 * their attributes are set with the information from the ReplicasDBs. The 126 * <code>changeNumber</code> attribute value is set from the content of 127 * ChangeNumberIndexDB.</li> 128 * </ul> 129 * <h3>Searches flow</h3> 130 * <p> 131 * Here is the flow of searches within the changelog backend APIs: 132 * <ul> 133 * <li>Normal searches only go through: 134 * <ol> 135 * <li>{@link ChangelogBackend#search(SearchOperation)} (once, single threaded)</li> 136 * </ol> 137 * </li> 138 * <li>Persistent searches with <code>changesOnly=false</code> go through: 139 * <ol> 140 * <li>{@link ChangelogBackend#registerPersistentSearch(PersistentSearch)} 141 * (once, single threaded),</li> 142 * <li> 143 * {@link ChangelogBackend#search(SearchOperation)} (once, single threaded)</li> 144 * <li>{@link ChangelogBackend#notify*EntryAdded()} (multiple times, multi 145 * threaded)</li> 146 * </ol> 147 * </li> 148 * <li>Persistent searches with <code>changesOnly=true</code> go through: 149 * <ol> 150 * <li>{@link ChangelogBackend#registerPersistentSearch(PersistentSearch)} 151 * (once, single threaded)</li> 152 * <li> 153 * {@link ChangelogBackend#notify*EntryAdded()} (multiple times, multi 154 * threaded)</li> 155 * </ol> 156 * </li> 157 * </ul> 158 * 159 * @see ReplicationServer 160 */ 161public class ChangelogBackend extends Backend<Configuration> 162{ 163 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 164 165 /** The id of this backend. */ 166 public static final String BACKEND_ID = "changelog"; 167 168 private static final long CHANGE_NUMBER_FOR_EMPTY_CURSOR = 0L; 169 170 private static final String CHANGE_NUMBER_ATTR = "changeNumber"; 171 private static final String ENTRY_SENDER_ATTACHMENT = OID_ECL_COOKIE_EXCHANGE_CONTROL + ".entrySender"; 172 173 /** The set of objectclasses that will be used in root entry. */ 174 private static final Map<ObjectClass, String> 175 CHANGELOG_ROOT_OBJECT_CLASSES = new LinkedHashMap<>(2); 176 static 177 { 178 CHANGELOG_ROOT_OBJECT_CLASSES.put(DirectoryServer.getObjectClass(OC_TOP, true), OC_TOP); 179 CHANGELOG_ROOT_OBJECT_CLASSES.put(DirectoryServer.getObjectClass("container", true), "container"); 180 } 181 182 /** The set of objectclasses that will be used in ECL entries. */ 183 private static final Map<ObjectClass, String> 184 CHANGELOG_ENTRY_OBJECT_CLASSES = new LinkedHashMap<>(2); 185 static 186 { 187 CHANGELOG_ENTRY_OBJECT_CLASSES.put(DirectoryServer.getObjectClass(OC_TOP, true), OC_TOP); 188 CHANGELOG_ENTRY_OBJECT_CLASSES.put(DirectoryServer.getObjectClass(OC_CHANGELOG_ENTRY, true), OC_CHANGELOG_ENTRY); 189 } 190 191 /** The attribute type for the "creatorsName" attribute. */ 192 private static final AttributeType CREATORS_NAME_TYPE = getAttributeType(OP_ATTR_CREATORS_NAME); 193 /** The attribute type for the "modifiersName" attribute. */ 194 private static final AttributeType MODIFIERS_NAME_TYPE = getAttributeType(OP_ATTR_MODIFIERS_NAME); 195 196 /** The base DN for the external change log. */ 197 public static final DN CHANGELOG_BASE_DN = DN.valueOf(DN_EXTERNAL_CHANGELOG_ROOT); 198 199 /** The set of base DNs for this backend. */ 200 private DN[] baseDNs; 201 /** The set of supported controls for this backend. */ 202 private final Set<String> supportedControls = Collections.singleton(OID_ECL_COOKIE_EXCHANGE_CONTROL); 203 /** Whether the base changelog entry has subordinates. */ 204 private Boolean baseEntryHasSubordinates; 205 206 /** The replication server on which the changelog is read. */ 207 private final ReplicationServer replicationServer; 208 private final ECLEnabledDomainPredicate domainPredicate; 209 210 /** The set of cookie-based persistent searches registered with this backend. */ 211 private final ConcurrentLinkedQueue<PersistentSearch> cookieBasedPersistentSearches = new ConcurrentLinkedQueue<>(); 212 /** The set of change number-based persistent searches registered with this backend. */ 213 private final ConcurrentLinkedQueue<PersistentSearch> changeNumberBasedPersistentSearches = 214 new ConcurrentLinkedQueue<>(); 215 216 /** 217 * Creates a new backend with the provided replication server. 218 * 219 * @param replicationServer 220 * The replication server on which the changes are read. 221 * @param domainPredicate 222 * Returns whether a domain is enabled for the external changelog. 223 */ 224 public ChangelogBackend(final ReplicationServer replicationServer, final ECLEnabledDomainPredicate domainPredicate) 225 { 226 this.replicationServer = replicationServer; 227 this.domainPredicate = domainPredicate; 228 setBackendID(BACKEND_ID); 229 setWritabilityMode(WritabilityMode.DISABLED); 230 setPrivateBackend(true); 231 } 232 233 private ChangelogDB getChangelogDB() 234 { 235 return replicationServer.getChangelogDB(); 236 } 237 238 /** 239 * Returns the ChangelogBackend configured for "cn=changelog" in this directory server. 240 * 241 * @return the ChangelogBackend configured for "cn=changelog" in this directory server 242 * @deprecated instead inject the required object where needed 243 */ 244 @Deprecated 245 public static ChangelogBackend getInstance() 246 { 247 return (ChangelogBackend) DirectoryServer.getBackend(CHANGELOG_BASE_DN); 248 } 249 250 @Override 251 public void configureBackend(final Configuration config, ServerContext serverContext) throws ConfigException 252 { 253 throw new UnsupportedOperationException("The changelog backend is not configurable"); 254 } 255 256 @Override 257 public void openBackend() throws InitializationException 258 { 259 baseDNs = new DN[] { CHANGELOG_BASE_DN }; 260 261 try 262 { 263 DirectoryServer.registerBaseDN(CHANGELOG_BASE_DN, this, true); 264 } 265 catch (final DirectoryException e) 266 { 267 throw new InitializationException( 268 ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(DN_EXTERNAL_CHANGELOG_ROOT, getExceptionMessage(e)), e); 269 } 270 } 271 272 @Override 273 public void closeBackend() 274 { 275 try 276 { 277 DirectoryServer.deregisterBaseDN(CHANGELOG_BASE_DN); 278 } 279 catch (final DirectoryException e) 280 { 281 logger.traceException(e); 282 } 283 } 284 285 @Override 286 public DN[] getBaseDNs() 287 { 288 return baseDNs; 289 } 290 291 @Override 292 public boolean isIndexed(final AttributeType attributeType, final IndexType indexType) 293 { 294 return true; 295 } 296 297 @Override 298 public Entry getEntry(final DN entryDN) throws DirectoryException 299 { 300 if (entryDN == null) 301 { 302 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 303 ERR_BACKEND_GET_ENTRY_NULL.get(getBackendID())); 304 } 305 throw new RuntimeException("Not implemented"); 306 } 307 308 @Override 309 public ConditionResult hasSubordinates(final DN entryDN) throws DirectoryException 310 { 311 if (CHANGELOG_BASE_DN.equals(entryDN)) 312 { 313 final Boolean hasSubs = baseChangelogHasSubordinates(); 314 if (hasSubs == null) 315 { 316 return ConditionResult.UNDEFINED; 317 } 318 return ConditionResult.valueOf(hasSubs); 319 } 320 return ConditionResult.FALSE; 321 } 322 323 private Boolean baseChangelogHasSubordinates() throws DirectoryException 324 { 325 if (baseEntryHasSubordinates == null) 326 { 327 // compute its value 328 try 329 { 330 final ReplicationDomainDB replicationDomainDB = getChangelogDB().getReplicationDomainDB(); 331 CursorOptions options = new CursorOptions(GREATER_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY); 332 try (final MultiDomainDBCursor cursor = 333 replicationDomainDB.getCursorFrom(new MultiDomainServerState(), options, getExcludedBaseDNs())) 334 { 335 baseEntryHasSubordinates = cursor.next(); 336 } 337 } 338 catch (ChangelogException e) 339 { 340 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_CHANGELOG_BACKEND_ATTRIBUTE.get( 341 "hasSubordinates", DN_EXTERNAL_CHANGELOG_ROOT, stackTraceToSingleLineString(e))); 342 } 343 } 344 return baseEntryHasSubordinates; 345 } 346 347 @Override 348 public long getNumberOfEntriesInBaseDN(final DN baseDN) throws DirectoryException 349 { 350 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_NUM_SUBORDINATES_NOT_SUPPORTED.get()); 351 } 352 353 @Override 354 public long getNumberOfChildren(final DN parentDN) throws DirectoryException 355 { 356 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_NUM_SUBORDINATES_NOT_SUPPORTED.get()); 357 } 358 359 /** 360 * Notifies persistent searches of this backend that a new cookie entry was added to it. 361 * <p> 362 * Note: This method correspond to the "persistent search" phase. 363 * It is executed multiple times per persistent search, multi-threaded, until the persistent search is cancelled. 364 * <p> 365 * This method must only be called after the provided data have been persisted to disk. 366 * 367 * @param baseDN 368 * the baseDN of the newly added entry. 369 * @param updateMsg 370 * the update message of the newly added entry 371 * @throws ChangelogException 372 * If a problem occurs while notifying of the newly added entry. 373 */ 374 public void notifyCookieEntryAdded(DN baseDN, UpdateMsg updateMsg) throws ChangelogException 375 { 376 if (!(updateMsg instanceof LDAPUpdateMsg)) 377 { 378 return; 379 } 380 381 try 382 { 383 for (PersistentSearch pSearch : cookieBasedPersistentSearches) 384 { 385 final SearchOperation searchOp = pSearch.getSearchOperation(); 386 final CookieEntrySender entrySender = searchOp.getAttachment(ENTRY_SENDER_ATTACHMENT); 387 entrySender.persistentSearchSendEntry(baseDN, updateMsg); 388 } 389 } 390 catch (DirectoryException e) 391 { 392 throw new ChangelogException(e.getMessageObject(), e); 393 } 394 } 395 396 /** 397 * Notifies persistent searches of this backend that a new change number entry was added to it. 398 * <p> 399 * Note: This method correspond to the "persistent search" phase. 400 * It is executed multiple times per persistent search, multi-threaded, until the persistent search is cancelled. 401 * <p> 402 * This method must only be called after the provided data have been persisted to disk. 403 * 404 * @param baseDN 405 * the baseDN of the newly added entry. 406 * @param changeNumber 407 * the change number of the newly added entry. It will be greater 408 * than zero for entries added to the change number index and less 409 * than or equal to zero for entries added to any replica DB 410 * @param cookieString 411 * a string representing the cookie of the newly added entry. 412 * This is only meaningful for entries added to the change number index 413 * @param updateMsg 414 * the update message of the newly added entry 415 * @throws ChangelogException 416 * If a problem occurs while notifying of the newly added entry. 417 */ 418 public void notifyChangeNumberEntryAdded(DN baseDN, long changeNumber, String cookieString, UpdateMsg updateMsg) 419 throws ChangelogException 420 { 421 if (!(updateMsg instanceof LDAPUpdateMsg) 422 || changeNumberBasedPersistentSearches.isEmpty()) 423 { 424 return; 425 } 426 427 try 428 { 429 // changeNumber entry can be shared with multiple persistent searches 430 final Entry changeNumberEntry = createEntryFromMsg(baseDN, changeNumber, cookieString, updateMsg); 431 for (PersistentSearch pSearch : changeNumberBasedPersistentSearches) 432 { 433 final SearchOperation searchOp = pSearch.getSearchOperation(); 434 final ChangeNumberEntrySender entrySender = searchOp.getAttachment(ENTRY_SENDER_ATTACHMENT); 435 entrySender.persistentSearchSendEntry(changeNumber, changeNumberEntry); 436 } 437 } 438 catch (DirectoryException e) 439 { 440 throw new ChangelogException(e.getMessageObject(), e); 441 } 442 } 443 444 private boolean isCookieBased(final SearchOperation searchOp) 445 { 446 for (Control c : searchOp.getRequestControls()) 447 { 448 if (OID_ECL_COOKIE_EXCHANGE_CONTROL.equals(c.getOID())) 449 { 450 return true; 451 } 452 } 453 return false; 454 } 455 456 @Override 457 public void addEntry(Entry entry, AddOperation addOperation) 458 throws DirectoryException, CanceledOperationException 459 { 460 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 461 ERR_BACKEND_ADD_NOT_SUPPORTED.get(String.valueOf(entry.getName()), getBackendID())); 462 } 463 464 @Override 465 public void deleteEntry(DN entryDN, DeleteOperation deleteOperation) 466 throws DirectoryException, CanceledOperationException 467 { 468 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 469 ERR_BACKEND_DELETE_NOT_SUPPORTED.get(String.valueOf(entryDN), getBackendID())); 470 } 471 472 @Override 473 public void replaceEntry(Entry oldEntry, Entry newEntry, 474 ModifyOperation modifyOperation) throws DirectoryException, 475 CanceledOperationException 476 { 477 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 478 ERR_BACKEND_MODIFY_NOT_SUPPORTED.get(String.valueOf(newEntry.getName()), getBackendID())); 479 } 480 481 @Override 482 public void renameEntry(DN currentDN, Entry entry, 483 ModifyDNOperation modifyDNOperation) throws DirectoryException, 484 CanceledOperationException 485 { 486 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 487 ERR_BACKEND_MODIFY_DN_NOT_SUPPORTED.get(String.valueOf(currentDN), getBackendID())); 488 } 489 490 /** 491 * {@inheritDoc} 492 * <p> 493 * Runs the "initial search" phase (as opposed to a "persistent search" 494 * phase). The "initial search" phase is the only search run by normal 495 * searches, but it is also run by persistent searches with 496 * <code>changesOnly=false</code>. Persistent searches with 497 * <code>changesOnly=true</code> never execute this code. 498 * <p> 499 * Note: this method is executed only once per persistent search, single 500 * threaded. 501 */ 502 @Override 503 public void search(final SearchOperation searchOperation) throws DirectoryException 504 { 505 checkChangelogReadPrivilege(searchOperation); 506 507 final Set<DN> excludedBaseDNs = getExcludedBaseDNs(); 508 final MultiDomainServerState cookie = getCookieFromControl(searchOperation, excludedBaseDNs); 509 510 final ChangeNumberRange range = optimizeSearch(searchOperation.getBaseDN(), searchOperation.getFilter()); 511 try 512 { 513 final boolean isPersistentSearch = isPersistentSearch(searchOperation); 514 if (cookie != null) 515 { 516 initialSearchFromCookie( 517 getCookieEntrySender(SearchPhase.INITIAL, searchOperation, cookie, excludedBaseDNs, isPersistentSearch)); 518 } 519 else 520 { 521 initialSearchFromChangeNumber( 522 getChangeNumberEntrySender(SearchPhase.INITIAL, searchOperation, range, isPersistentSearch)); 523 } 524 } 525 catch (ChangelogException e) 526 { 527 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_CHANGELOG_BACKEND_SEARCH.get( 528 searchOperation.getBaseDN(), searchOperation.getFilter(), stackTraceToSingleLineString(e))); 529 } 530 } 531 532 private MultiDomainServerState getCookieFromControl(final SearchOperation searchOperation, Set<DN> excludedBaseDNs) 533 throws DirectoryException 534 { 535 final ExternalChangelogRequestControl eclRequestControl = 536 searchOperation.getRequestControl(ExternalChangelogRequestControl.DECODER); 537 if (eclRequestControl != null) 538 { 539 final MultiDomainServerState cookie = eclRequestControl.getCookie(); 540 validateProvidedCookie(cookie, excludedBaseDNs); 541 return cookie; 542 } 543 return null; 544 } 545 546 @Override 547 public Set<String> getSupportedControls() 548 { 549 return supportedControls; 550 } 551 552 @Override 553 public Set<String> getSupportedFeatures() 554 { 555 return Collections.emptySet(); 556 } 557 558 @Override 559 public boolean supports(BackendOperation backendOperation) 560 { 561 return false; 562 } 563 564 @Override 565 public void exportLDIF(final LDIFExportConfig exportConfig) 566 throws DirectoryException 567 { 568 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 569 ERR_BACKEND_IMPORT_AND_EXPORT_NOT_SUPPORTED.get(getBackendID())); 570 } 571 572 @Override 573 public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext) 574 throws DirectoryException 575 { 576 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 577 ERR_BACKEND_IMPORT_AND_EXPORT_NOT_SUPPORTED.get(getBackendID())); 578 } 579 580 @Override 581 public void createBackup(BackupConfig backupConfig) throws DirectoryException 582 { 583 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 584 ERR_BACKEND_BACKUP_AND_RESTORE_NOT_SUPPORTED.get(getBackendID())); 585 } 586 587 @Override 588 public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException 589 { 590 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 591 ERR_BACKEND_BACKUP_AND_RESTORE_NOT_SUPPORTED.get(getBackendID())); 592 } 593 594 @Override 595 public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException 596 { 597 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 598 ERR_BACKEND_BACKUP_AND_RESTORE_NOT_SUPPORTED.get(getBackendID())); 599 } 600 601 @Override 602 public long getEntryCount() 603 { 604 try 605 { 606 return getNumberOfEntriesInBaseDN(CHANGELOG_BASE_DN) + 1; 607 } 608 catch (DirectoryException e) 609 { 610 logger.traceException(e); 611 return -1; 612 } 613 } 614 615 /** 616 * Represent the change number range targeted by a search operation. 617 * <p> 618 * This class should be visible for tests. 619 */ 620 static final class ChangeNumberRange 621 { 622 private long lowerBound = -1; 623 private long upperBound = -1; 624 625 /** 626 * Returns the lowest change number to retrieve (inclusive). 627 * 628 * @return the lowest change number 629 */ 630 long getLowerBound() 631 { 632 return lowerBound; 633 } 634 635 /** 636 * Returns the highest change number to retrieve (inclusive). 637 * 638 * @return the highest change number 639 */ 640 long getUpperBound() 641 { 642 return upperBound; 643 } 644 } 645 646 /** 647 * Returns the set of DNs to exclude from the search. 648 * 649 * @return the DNs corresponding to domains to exclude from the search. 650 * @throws DirectoryException 651 * If a DN can't be decoded. 652 */ 653 private static Set<DN> getExcludedBaseDNs() throws DirectoryException 654 { 655 return getExcludedChangelogDomains(); 656 } 657 658 /** 659 * Optimize the search parameters by analyzing the DN and filter. 660 * It also performs validation on some search parameters 661 * for both cookie and change number based changelogs. 662 * 663 * @param baseDN the provided search baseDN. 664 * @param userFilter the provided search filter. 665 * @return the optimized change number range 666 * @throws DirectoryException when an exception occurs. 667 */ 668 ChangeNumberRange optimizeSearch(final DN baseDN, final SearchFilter userFilter) throws DirectoryException 669 { 670 SearchFilter equalityFilter = null; 671 switch (baseDN.size()) 672 { 673 case 1: 674 // "cn=changelog" : use user-provided search filter. 675 break; 676 case 2: 677 // It is probably "changeNumber=xxx,cn=changelog", use equality filter 678 // But it also could be "<service-id>,cn=changelog" so need to check on attribute 679 equalityFilter = buildSearchFilterFrom(baseDN, CHANGE_NUMBER_ATTR); 680 break; 681 default: 682 // "replicationCSN=xxx,<service-id>,cn=changelog" : use equality filter 683 equalityFilter = buildSearchFilterFrom(baseDN, "replicationCSN"); 684 break; 685 } 686 687 return optimizeSearchUsingFilter(equalityFilter != null ? equalityFilter : userFilter); 688 } 689 690 /** 691 * Build a search filter from given DN and attribute. 692 * 693 * @return the search filter or {@code null} if attribute is not present in 694 * the provided DN 695 */ 696 private SearchFilter buildSearchFilterFrom(final DN baseDN, final String attrName) 697 { 698 final RDN rdn = baseDN.rdn(); 699 AttributeType attrType = DirectoryServer.getAttributeType(attrName); 700 final ByteString attrValue = rdn.getAttributeValue(attrType); 701 if (attrValue != null) 702 { 703 return SearchFilter.createEqualityFilter(attrType, attrValue); 704 } 705 return null; 706 } 707 708 private ChangeNumberRange optimizeSearchUsingFilter(final SearchFilter filter) throws DirectoryException 709 { 710 final ChangeNumberRange range = new ChangeNumberRange(); 711 if (filter == null) 712 { 713 return range; 714 } 715 716 if (matches(filter, FilterType.GREATER_OR_EQUAL, CHANGE_NUMBER_ATTR)) 717 { 718 range.lowerBound = decodeChangeNumber(filter.getAssertionValue()); 719 } 720 else if (matches(filter, FilterType.LESS_OR_EQUAL, CHANGE_NUMBER_ATTR)) 721 { 722 range.upperBound = decodeChangeNumber(filter.getAssertionValue()); 723 } 724 else if (matches(filter, FilterType.EQUALITY, CHANGE_NUMBER_ATTR)) 725 { 726 final long number = decodeChangeNumber(filter.getAssertionValue()); 727 range.lowerBound = number; 728 range.upperBound = number; 729 } 730 else if (matches(filter, FilterType.EQUALITY, "replicationcsn")) 731 { 732 // == exact CSN 733 // validate provided CSN is correct 734 new CSN(filter.getAssertionValue().toString()); 735 } 736 else if (filter.getFilterType() == FilterType.AND) 737 { 738 // TODO: it looks like it could be generalized to N components, not only two 739 final Collection<SearchFilter> components = filter.getFilterComponents(); 740 final SearchFilter filters[] = components.toArray(new SearchFilter[0]); 741 long upper1 = -1; 742 long lower1 = -1; 743 long upper2 = -1; 744 long lower2 = -1; 745 if (filters.length > 0) 746 { 747 ChangeNumberRange range1 = optimizeSearchUsingFilter(filters[0]); 748 upper1 = range1.upperBound; 749 lower1 = range1.lowerBound; 750 } 751 if (filters.length > 1) 752 { 753 ChangeNumberRange range2 = optimizeSearchUsingFilter(filters[1]); 754 upper2 = range2.upperBound; 755 lower2 = range2.lowerBound; 756 } 757 if (upper1 == -1) 758 { 759 range.upperBound = upper2; 760 } 761 else if (upper2 == -1) 762 { 763 range.upperBound = upper1; 764 } 765 else 766 { 767 range.upperBound = Math.min(upper1, upper2); 768 } 769 770 range.lowerBound = Math.max(lower1, lower2); 771 } 772 return range; 773 } 774 775 private static long decodeChangeNumber(final ByteString assertionValue) 776 throws DirectoryException 777 { 778 try 779 { 780 return Long.decode(assertionValue.toString()); 781 } 782 catch (NumberFormatException e) 783 { 784 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 785 LocalizableMessage.raw("Could not convert value '%s' to long", assertionValue)); 786 } 787 } 788 789 private boolean matches(SearchFilter filter, FilterType filterType, String primaryName) 790 { 791 return filter.getFilterType() == filterType 792 && filter.getAttributeType() != null 793 && filter.getAttributeType().getNameOrOID().equalsIgnoreCase(primaryName); 794 } 795 796 /** Search the changelog when a cookie control is provided. */ 797 private void initialSearchFromCookie(final CookieEntrySender entrySender) 798 throws DirectoryException, ChangelogException 799 { 800 if (!sendBaseChangelogEntry(entrySender.searchOp)) 801 { // only return the base entry: stop here 802 return; 803 } 804 805 final ReplicationDomainDB replicationDomainDB = getChangelogDB().getReplicationDomainDB(); 806 CursorOptions options = new CursorOptions(GREATER_THAN_OR_EQUAL_TO_KEY, AFTER_MATCHING_KEY); 807 try (final MultiDomainDBCursor cursor = 808 replicationDomainDB.getCursorFrom(entrySender.cookie, options, entrySender.excludedBaseDNs); 809 ECLMultiDomainDBCursor replicaUpdatesCursor = new ECLMultiDomainDBCursor(domainPredicate, cursor)) 810 { 811 if (sendCookieEntriesFromCursor(entrySender, replicaUpdatesCursor)) 812 { 813 entrySender.transitioningToPersistentSearchPhase(); 814 sendCookieEntriesFromCursor(entrySender, replicaUpdatesCursor); 815 } 816 } 817 finally 818 { 819 entrySender.finalizeInitialSearch(); 820 } 821 } 822 823 private CookieEntrySender getCookieEntrySender(SearchPhase startPhase, final SearchOperation searchOperation, 824 MultiDomainServerState cookie, Set<DN> excludedBaseDNs, boolean isPersistentSearch) 825 { 826 if (isPersistentSearch && SearchPhase.INITIAL.equals(startPhase)) 827 { 828 return searchOperation.getAttachment(ENTRY_SENDER_ATTACHMENT); 829 } 830 return new CookieEntrySender(searchOperation, startPhase, cookie, excludedBaseDNs); 831 } 832 833 private boolean sendCookieEntriesFromCursor(final CookieEntrySender entrySender, 834 final ECLMultiDomainDBCursor replicaUpdatesCursor) throws ChangelogException, DirectoryException 835 { 836 boolean continueSearch = true; 837 while (continueSearch && replicaUpdatesCursor.next()) 838 { 839 final UpdateMsg updateMsg = replicaUpdatesCursor.getRecord(); 840 final DN domainBaseDN = replicaUpdatesCursor.getData(); 841 continueSearch = entrySender.initialSearchSendEntry(updateMsg, domainBaseDN); 842 } 843 return continueSearch; 844 } 845 846 private boolean isPersistentSearch(SearchOperation op) 847 { 848 for (PersistentSearch pSearch : getPersistentSearches()) 849 { 850 if (op == pSearch.getSearchOperation()) 851 { 852 return true; 853 } 854 } 855 return false; 856 } 857 858 @Override 859 public void registerPersistentSearch(PersistentSearch pSearch) throws DirectoryException 860 { 861 initializePersistentSearch(pSearch); 862 863 if (isCookieBased(pSearch.getSearchOperation())) 864 { 865 cookieBasedPersistentSearches.add(pSearch); 866 } 867 else 868 { 869 changeNumberBasedPersistentSearches.add(pSearch); 870 } 871 super.registerPersistentSearch(pSearch); 872 } 873 874 private void initializePersistentSearch(PersistentSearch pSearch) throws DirectoryException 875 { 876 final SearchOperation searchOp = pSearch.getSearchOperation(); 877 878 // Validation must be done during registration for changes only persistent searches. 879 // Otherwise, when there is an initial search phase, 880 // validation is performed by the search() method. 881 if (pSearch.isChangesOnly()) 882 { 883 checkChangelogReadPrivilege(searchOp); 884 } 885 final ChangeNumberRange range = optimizeSearch(searchOp.getBaseDN(), searchOp.getFilter()); 886 887 final SearchPhase startPhase = pSearch.isChangesOnly() ? SearchPhase.PERSISTENT : SearchPhase.INITIAL; 888 if (isCookieBased(searchOp)) 889 { 890 final Set<DN> excludedBaseDNs = getExcludedBaseDNs(); 891 final MultiDomainServerState cookie = getCookie(pSearch.isChangesOnly(), searchOp, excludedBaseDNs); 892 searchOp.setAttachment(ENTRY_SENDER_ATTACHMENT, 893 new CookieEntrySender(searchOp, startPhase, cookie, excludedBaseDNs)); 894 } 895 else 896 { 897 searchOp.setAttachment(ENTRY_SENDER_ATTACHMENT, 898 new ChangeNumberEntrySender(searchOp, startPhase, range)); 899 } 900 } 901 902 private MultiDomainServerState getCookie(boolean isChangesOnly, SearchOperation searchOp, Set<DN> excludedBaseDNs) 903 throws DirectoryException 904 { 905 if (isChangesOnly) 906 { 907 // this changesOnly persistent search will not go through #initialSearch() 908 // so we must initialize the cookie here 909 return getNewestCookie(searchOp); 910 } 911 return getCookieFromControl(searchOp, excludedBaseDNs); 912 } 913 914 private MultiDomainServerState getNewestCookie(SearchOperation searchOp) 915 { 916 if (!isCookieBased(searchOp)) 917 { 918 return null; 919 } 920 921 final MultiDomainServerState cookie = new MultiDomainServerState(); 922 for (final Iterator<ReplicationServerDomain> it = 923 replicationServer.getDomainIterator(); it.hasNext();) 924 { 925 final DN baseDN = it.next().getBaseDN(); 926 final ServerState state = getChangelogDB().getReplicationDomainDB().getDomainNewestCSNs(baseDN); 927 cookie.update(baseDN, state); 928 } 929 return cookie; 930 } 931 932 /** 933 * Validates the cookie contained in search parameters by checking its content 934 * with the actual replication server state. 935 * 936 * @throws DirectoryException 937 * If the state is not valid 938 */ 939 private void validateProvidedCookie(final MultiDomainServerState cookie, Set<DN> excludedBaseDNs) 940 throws DirectoryException 941 { 942 if (cookie != null && !cookie.isEmpty()) 943 { 944 replicationServer.validateCookie(cookie, excludedBaseDNs); 945 } 946 } 947 948 /** Search the changelog using change number(s). */ 949 private void initialSearchFromChangeNumber(final ChangeNumberEntrySender entrySender) 950 throws ChangelogException, DirectoryException 951 { 952 if (!sendBaseChangelogEntry(entrySender.searchOp)) 953 { // only return the base entry: stop here 954 return; 955 } 956 957 final AtomicReference<MultiDomainDBCursor> replicaUpdatesCursor = new AtomicReference<>(); 958 try (DBCursor<ChangeNumberIndexRecord> cnIndexDBCursor = getCNIndexDBCursor(entrySender.lowestChangeNumber)) 959 { 960 final MultiDomainServerState cookie = new MultiDomainServerState(); 961 962 if (sendChangeNumberEntriesFromCursors(entrySender, cnIndexDBCursor, replicaUpdatesCursor, cookie)) 963 { 964 entrySender.transitioningToPersistentSearchPhase(); 965 sendChangeNumberEntriesFromCursors(entrySender, cnIndexDBCursor, replicaUpdatesCursor, cookie); 966 } 967 } 968 finally 969 { 970 entrySender.finalizeInitialSearch(); 971 StaticUtils.close(replicaUpdatesCursor.get()); 972 } 973 } 974 975 private ChangeNumberEntrySender getChangeNumberEntrySender(SearchPhase startPhase, 976 final SearchOperation searchOperation, ChangeNumberRange range, boolean isPersistentSearch) 977 { 978 if (isPersistentSearch && SearchPhase.INITIAL.equals(startPhase)) 979 { 980 return searchOperation.getAttachment(ENTRY_SENDER_ATTACHMENT); 981 } 982 return new ChangeNumberEntrySender(searchOperation, SearchPhase.INITIAL, range); 983 } 984 985 private boolean sendChangeNumberEntriesFromCursors(final ChangeNumberEntrySender entrySender, 986 DBCursor<ChangeNumberIndexRecord> cnIndexDBCursor, AtomicReference<MultiDomainDBCursor> replicaUpdatesCursor, 987 MultiDomainServerState cookie) throws ChangelogException, DirectoryException 988 { 989 boolean continueSearch = true; 990 while (continueSearch && cnIndexDBCursor.next()) 991 { 992 // Handle the current cnIndex record 993 final ChangeNumberIndexRecord cnIndexRecord = cnIndexDBCursor.getRecord(); 994 if (replicaUpdatesCursor.get() == null) 995 { 996 replicaUpdatesCursor.set(initializeReplicaUpdatesCursor(cnIndexRecord)); 997 initializeCookieForChangeNumberMode(cookie, cnIndexRecord); 998 } 999 else 1000 { 1001 cookie.update(cnIndexRecord.getBaseDN(), cnIndexRecord.getCSN()); 1002 } 1003 continueSearch = entrySender.changeNumberIsInRange(cnIndexRecord.getChangeNumber()); 1004 if (continueSearch) 1005 { 1006 final UpdateMsg updateMsg = findReplicaUpdateMessage(replicaUpdatesCursor.get(), cnIndexRecord.getCSN()); 1007 if (updateMsg != null) 1008 { 1009 continueSearch = entrySender.initialSearchSendEntry(cnIndexRecord, updateMsg, cookie); 1010 replicaUpdatesCursor.get().next(); 1011 } 1012 } 1013 } 1014 return continueSearch; 1015 } 1016 1017 /** Initialize the provided cookie from the provided change number index record. */ 1018 private void initializeCookieForChangeNumberMode( 1019 MultiDomainServerState cookie, final ChangeNumberIndexRecord cnIndexRecord) throws ChangelogException 1020 { 1021 // Initialize the multi domain cursor only from the change number index record. 1022 // The cookie is always empty at this stage. 1023 CursorOptions options = new CursorOptions(LESS_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY, cnIndexRecord.getCSN()); 1024 MultiDomainServerState unused = new MultiDomainServerState(); 1025 MultiDomainDBCursor cursor = getChangelogDB().getReplicationDomainDB().getCursorFrom(unused, options); 1026 try (ECLMultiDomainDBCursor eclCursor = new ECLMultiDomainDBCursor(domainPredicate, cursor)) 1027 { 1028 updateCookieToMediumConsistencyPoint(cookie, eclCursor, cnIndexRecord); 1029 } 1030 } 1031 1032 /** 1033 * Rebuilds the changelogcookie starting at the newest change number index record. 1034 * <p> 1035 * It updates the provided cookie with the changes from the provided ECL cursor, 1036 * up to (and including) the provided change number index record. 1037 * <p> 1038 * Therefore, after calling this method, the cursor is positioned 1039 * to the change immediately following the provided change number index record. 1040 * 1041 * @param cookie the cookie to update 1042 * @param cursor the cursor where to read changes from 1043 * @param cnIndexRecord the change number index record to go right after 1044 * @throws ChangelogException if any problem occurs 1045 */ 1046 public static void updateCookieToMediumConsistencyPoint( 1047 MultiDomainServerState cookie, ECLMultiDomainDBCursor cursor, ChangeNumberIndexRecord cnIndexRecord) 1048 throws ChangelogException 1049 { 1050 if (cnIndexRecord == null) 1051 { 1052 return; 1053 } 1054 1055 while (cursor.next()) 1056 { 1057 UpdateMsg updateMsg = cursor.getRecord(); 1058 if (updateMsg.getCSN().compareTo(cnIndexRecord.getCSN()) > 0) 1059 { 1060 break; 1061 } 1062 cookie.update(cursor.getData(), updateMsg.getCSN()); 1063 } 1064 } 1065 1066 private MultiDomainDBCursor initializeReplicaUpdatesCursor( 1067 final ChangeNumberIndexRecord cnIndexRecord) throws ChangelogException 1068 { 1069 final MultiDomainServerState state = new MultiDomainServerState(); 1070 state.update(cnIndexRecord.getBaseDN(), cnIndexRecord.getCSN()); 1071 1072 // No need for ECLMultiDomainDBCursor in this case 1073 // as updateMsg will be matched with cnIndexRecord 1074 CursorOptions options = new CursorOptions(GREATER_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY); 1075 final MultiDomainDBCursor replicaUpdatesCursor = 1076 getChangelogDB().getReplicationDomainDB().getCursorFrom(state, options); 1077 replicaUpdatesCursor.next(); 1078 return replicaUpdatesCursor; 1079 } 1080 1081 /** 1082 * Returns the replica update message corresponding to the provided 1083 * cnIndexRecord. 1084 * 1085 * @return the update message, which may be {@code null} if the update message 1086 * could not be found because it was purged or because corresponding 1087 * baseDN was removed from the changelog 1088 * @throws DirectoryException 1089 * If inconsistency is detected between the available update 1090 * messages and the provided cnIndexRecord 1091 */ 1092 private UpdateMsg findReplicaUpdateMessage(final MultiDomainDBCursor replicaUpdatesCursor, CSN csn) 1093 throws ChangelogException, DirectoryException 1094 { 1095 while (true) 1096 { 1097 final UpdateMsg updateMsg = replicaUpdatesCursor.getRecord(); 1098 final int compareIndexWithUpdateMsg = csn.compareTo(updateMsg.getCSN()); 1099 if (compareIndexWithUpdateMsg < 0) { 1100 // Either update message has been purged or baseDN has been removed from changelogDB, 1101 // ignore current index record and go to the next one 1102 return null; 1103 } 1104 else if (compareIndexWithUpdateMsg == 0) 1105 { 1106 // Found the matching update message 1107 return updateMsg; 1108 } 1109 // Case compareIndexWithUpdateMsg > 0 : the update message has not bean reached yet 1110 if (!replicaUpdatesCursor.next()) 1111 { 1112 // Should never happen, as it means some messages have disappeared 1113 // TODO : put the correct I18N message 1114 throw new DirectoryException(ResultCode.OPERATIONS_ERROR, 1115 LocalizableMessage.raw("Could not find replica update message matching index record. " + 1116 "No more replica update messages with a csn newer than " + updateMsg.getCSN() + " exist.")); 1117 } 1118 } 1119 } 1120 1121 /** Returns a cursor on CNIndexDB for the provided first change number. */ 1122 private DBCursor<ChangeNumberIndexRecord> getCNIndexDBCursor( 1123 final long firstChangeNumber) throws ChangelogException 1124 { 1125 final ChangeNumberIndexDB cnIndexDB = getChangelogDB().getChangeNumberIndexDB(); 1126 long changeNumberToUse = firstChangeNumber; 1127 if (changeNumberToUse <= 1) 1128 { 1129 final ChangeNumberIndexRecord oldestRecord = cnIndexDB.getOldestRecord(); 1130 changeNumberToUse = oldestRecord == null ? CHANGE_NUMBER_FOR_EMPTY_CURSOR : oldestRecord.getChangeNumber(); 1131 } 1132 return cnIndexDB.getCursorFrom(changeNumberToUse); 1133 } 1134 1135 /** Creates a changelog entry. */ 1136 private static Entry createEntryFromMsg(final DN baseDN, final long changeNumber, final String cookie, 1137 final UpdateMsg msg) throws DirectoryException 1138 { 1139 if (msg instanceof AddMsg) 1140 { 1141 return createAddMsg(baseDN, changeNumber, cookie, msg); 1142 } 1143 else if (msg instanceof ModifyCommonMsg) 1144 { 1145 return createModifyMsg(baseDN, changeNumber, cookie, msg); 1146 } 1147 else if (msg instanceof DeleteMsg) 1148 { 1149 final DeleteMsg delMsg = (DeleteMsg) msg; 1150 return createChangelogEntry(baseDN, changeNumber, cookie, delMsg, null, "delete", delMsg.getInitiatorsName()); 1151 } 1152 throw new DirectoryException(ResultCode.OPERATIONS_ERROR, 1153 LocalizableMessage.raw("Unexpected message type when trying to create changelog entry for dn %s : %s", baseDN, 1154 msg.getClass())); 1155 } 1156 1157 /** 1158 * Creates an entry from an add message. 1159 * <p> 1160 * Map addMsg to an LDIF string for the 'changes' attribute, and pull out 1161 * change initiators name if available which is contained in the creatorsName 1162 * attribute. 1163 */ 1164 private static Entry createAddMsg(final DN baseDN, final long changeNumber, final String cookie, final UpdateMsg msg) 1165 throws DirectoryException 1166 { 1167 final AddMsg addMsg = (AddMsg) msg; 1168 String changeInitiatorsName = null; 1169 String ldifChanges = null; 1170 try 1171 { 1172 final StringBuilder builder = new StringBuilder(256); 1173 for (Attribute attr : addMsg.getAttributes()) 1174 { 1175 if (attr.getAttributeDescription().getAttributeType().equals(CREATORS_NAME_TYPE) && !attr.isEmpty()) 1176 { 1177 // This attribute is not multi-valued. 1178 changeInitiatorsName = attr.iterator().next().toString(); 1179 } 1180 final String attrName = attr.getNameWithOptions(); 1181 for (ByteString value : attr) 1182 { 1183 builder.append(attrName); 1184 appendLDIFSeparatorAndValue(builder, value); 1185 builder.append('\n'); 1186 } 1187 } 1188 ldifChanges = builder.toString(); 1189 } 1190 catch (Exception e) 1191 { 1192 logEncodingMessageError("add", addMsg.getDN(), e); 1193 } 1194 1195 return createChangelogEntry(baseDN, changeNumber, cookie, addMsg, ldifChanges, "add", changeInitiatorsName); 1196 } 1197 1198 /** 1199 * Creates an entry from a modify message. 1200 * <p> 1201 * Map the modifyMsg to an LDIF string for the 'changes' attribute, and pull 1202 * out change initiators name if available which is contained in the 1203 * modifiersName attribute. 1204 */ 1205 private static Entry createModifyMsg(final DN baseDN, final long changeNumber, final String cookie, 1206 final UpdateMsg msg) throws DirectoryException 1207 { 1208 final ModifyCommonMsg modifyMsg = (ModifyCommonMsg) msg; 1209 String changeInitiatorsName = null; 1210 String ldifChanges = null; 1211 try 1212 { 1213 final StringBuilder builder = new StringBuilder(128); 1214 for (Modification mod : modifyMsg.getMods()) 1215 { 1216 final Attribute attr = mod.getAttribute(); 1217 if (mod.getModificationType() == ModificationType.REPLACE 1218 && attr.getAttributeDescription().getAttributeType().equals(MODIFIERS_NAME_TYPE) 1219 && !attr.isEmpty()) 1220 { 1221 // This attribute is not multi-valued. 1222 changeInitiatorsName = attr.iterator().next().toString(); 1223 } 1224 final String attrName = attr.getNameWithOptions(); 1225 builder.append(mod.getModificationType()); 1226 builder.append(": "); 1227 builder.append(attrName); 1228 builder.append('\n'); 1229 1230 for (ByteString value : attr) 1231 { 1232 builder.append(attrName); 1233 appendLDIFSeparatorAndValue(builder, value); 1234 builder.append('\n'); 1235 } 1236 builder.append("-\n"); 1237 } 1238 ldifChanges = builder.toString(); 1239 } 1240 catch (Exception e) 1241 { 1242 logEncodingMessageError("modify", modifyMsg.getDN(), e); 1243 } 1244 1245 final boolean isModifyDNMsg = modifyMsg instanceof ModifyDNMsg; 1246 final Entry entry = createChangelogEntry(baseDN, changeNumber, cookie, modifyMsg, ldifChanges, 1247 isModifyDNMsg ? "modrdn" : "modify", changeInitiatorsName); 1248 1249 if (isModifyDNMsg) 1250 { 1251 final ModifyDNMsg modDNMsg = (ModifyDNMsg) modifyMsg; 1252 addAttribute(entry, "newrdn", modDNMsg.getNewRDN()); 1253 if (modDNMsg.getNewSuperior() != null) 1254 { 1255 addAttribute(entry, "newsuperior", modDNMsg.getNewSuperior()); 1256 } 1257 addAttribute(entry, "deleteoldrdn", String.valueOf(modDNMsg.deleteOldRdn())); 1258 } 1259 return entry; 1260 } 1261 1262 /** 1263 * Log an encoding message error. 1264 * 1265 * @param messageType 1266 * String identifying type of message. Should be "add" or "modify". 1267 * @param entryDN 1268 * DN of original entry 1269 */ 1270 private static void logEncodingMessageError(String messageType, DN entryDN, Exception exception) 1271 { 1272 logger.traceException(exception); 1273 logger.error(LocalizableMessage.raw( 1274 "An exception was encountered while trying to encode a replication " + messageType + " message for entry \"" 1275 + entryDN + "\" into an External Change Log entry: " + exception.getMessage())); 1276 } 1277 1278 private void checkChangelogReadPrivilege(SearchOperation searchOp) throws DirectoryException 1279 { 1280 if (!searchOp.getClientConnection().hasPrivilege(Privilege.CHANGELOG_READ, searchOp)) 1281 { 1282 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 1283 NOTE_SEARCH_CHANGELOG_INSUFFICIENT_PRIVILEGES.get()); 1284 } 1285 } 1286 1287 /** 1288 * Create a changelog entry from a set of provided information. This is the part of 1289 * entry creation common to all types of msgs (ADD, DEL, MOD, MODDN). 1290 */ 1291 private static Entry createChangelogEntry(final DN baseDN, final long changeNumber, final String cookie, 1292 final LDAPUpdateMsg msg, final String ldifChanges, final String changeType, 1293 final String changeInitiatorsName) throws DirectoryException 1294 { 1295 final CSN csn = msg.getCSN(); 1296 String dnString; 1297 if (changeNumber > 0) 1298 { 1299 // change number mode 1300 dnString = "changeNumber=" + changeNumber + "," + DN_EXTERNAL_CHANGELOG_ROOT; 1301 } 1302 else 1303 { 1304 // Cookie mode 1305 dnString = "replicationCSN=" + csn + "," + baseDN + "," + DN_EXTERNAL_CHANGELOG_ROOT; 1306 } 1307 1308 final Map<AttributeType, List<Attribute>> userAttrs = new LinkedHashMap<>(); 1309 final Map<AttributeType, List<Attribute>> opAttrs = new LinkedHashMap<>(); 1310 1311 // Operational standard attributes 1312 addAttributeByType(ATTR_SUBSCHEMA_SUBENTRY_LC, ATTR_SUBSCHEMA_SUBENTRY_LC, 1313 ConfigConstants.DN_DEFAULT_SCHEMA_ROOT, userAttrs, opAttrs); 1314 addAttributeByType("numsubordinates", "numSubordinates", "0", userAttrs, opAttrs); 1315 addAttributeByType("hassubordinates", "hasSubordinates", "false", userAttrs, opAttrs); 1316 addAttributeByType("entrydn", "entryDN", dnString, userAttrs, opAttrs); 1317 1318 // REQUIRED attributes 1319 if (changeNumber > 0) 1320 { 1321 addAttributeByType("changenumber", "changeNumber", String.valueOf(changeNumber), userAttrs, opAttrs); 1322 } 1323 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_GMT_TIME); 1324 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); // ?? 1325 final String format = dateFormat.format(new Date(csn.getTime())); 1326 addAttributeByType("changetime", "changeTime", format, userAttrs, opAttrs); 1327 addAttributeByType("changetype", "changeType", changeType, userAttrs, opAttrs); 1328 addAttributeByType("targetdn", "targetDN", msg.getDN().toString(), userAttrs, opAttrs); 1329 1330 // NON REQUESTED attributes 1331 addAttributeByType("replicationcsn", "replicationCSN", csn.toString(), userAttrs, opAttrs); 1332 addAttributeByType("replicaidentifier", "replicaIdentifier", Integer.toString(csn.getServerId()), 1333 userAttrs, opAttrs); 1334 1335 if (ldifChanges != null) 1336 { 1337 addAttributeByType("changes", "changes", ldifChanges, userAttrs, opAttrs); 1338 } 1339 if (changeInitiatorsName != null) 1340 { 1341 addAttributeByType("changeinitiatorsname", "changeInitiatorsName", changeInitiatorsName, userAttrs, opAttrs); 1342 } 1343 1344 final String targetUUID = msg.getEntryUUID(); 1345 if (targetUUID != null) 1346 { 1347 addAttributeByType("targetentryuuid", "targetEntryUUID", targetUUID, userAttrs, opAttrs); 1348 } 1349 final String cookie2 = cookie != null ? cookie : ""; 1350 addAttributeByType("changelogcookie", "changeLogCookie", cookie2, userAttrs, opAttrs); 1351 1352 final List<RawAttribute> includedAttributes = msg.getEclIncludes(); 1353 if (includedAttributes != null && !includedAttributes.isEmpty()) 1354 { 1355 final StringBuilder builder = new StringBuilder(256); 1356 for (final RawAttribute includedAttribute : includedAttributes) 1357 { 1358 final String name = includedAttribute.getAttributeType(); 1359 for (final ByteString value : includedAttribute.getValues()) 1360 { 1361 builder.append(name); 1362 appendLDIFSeparatorAndValue(builder, value); 1363 builder.append('\n'); 1364 } 1365 } 1366 final String includedAttributesLDIF = builder.toString(); 1367 addAttributeByType("includedattributes", "includedAttributes", includedAttributesLDIF, userAttrs, opAttrs); 1368 } 1369 1370 return new Entry(DN.valueOf(dnString), CHANGELOG_ENTRY_OBJECT_CLASSES, userAttrs, opAttrs); 1371 } 1372 1373 /** 1374 * Sends the entry if it matches the base, scope and filter of the current search operation. 1375 * It will also send the base changelog entry if it needs to be sent and was not sent before. 1376 * 1377 * @return {@code true} if search should continue, {@code false} otherwise 1378 */ 1379 private static boolean sendEntryIfMatches(SearchOperation searchOp, Entry entry, String cookie) 1380 throws DirectoryException 1381 { 1382 if (matchBaseAndScopeAndFilter(searchOp, entry)) 1383 { 1384 return searchOp.returnEntry(entry, getControls(cookie)); 1385 } 1386 // maybe the next entry will match? 1387 return true; 1388 } 1389 1390 /** Indicates if the provided entry matches the filter, base and scope. */ 1391 private static boolean matchBaseAndScopeAndFilter(SearchOperation searchOp, Entry entry) throws DirectoryException 1392 { 1393 return entry.matchesBaseAndScope(searchOp.getBaseDN(), searchOp.getScope()) 1394 && searchOp.getFilter().matchesEntry(entry); 1395 } 1396 1397 private static List<Control> getControls(String cookie) 1398 { 1399 if (cookie != null) 1400 { 1401 final Control c = new EntryChangelogNotificationControl(true, cookie); 1402 return Collections.singletonList(c); 1403 } 1404 return Collections.emptyList(); 1405 } 1406 1407 /** 1408 * Create and returns the base changelog entry to the underlying search operation. 1409 * <p> 1410 * "initial search" phase must return the base entry immediately. 1411 * 1412 * @return {@code true} if search should continue, {@code false} otherwise 1413 */ 1414 private boolean sendBaseChangelogEntry(SearchOperation searchOp) throws DirectoryException 1415 { 1416 final DN baseDN = searchOp.getBaseDN(); 1417 final SearchFilter filter = searchOp.getFilter(); 1418 final SearchScope scope = searchOp.getScope(); 1419 1420 if (ChangelogBackend.CHANGELOG_BASE_DN.isInScopeOf(baseDN, scope)) 1421 { 1422 final Entry entry = buildBaseChangelogEntry(); 1423 if (filter.matchesEntry(entry) && !searchOp.returnEntry(entry, null)) 1424 { 1425 // Abandon, size limit reached. 1426 return false; 1427 } 1428 } 1429 return !baseDN.equals(ChangelogBackend.CHANGELOG_BASE_DN) 1430 || !scope.equals(SearchScope.BASE_OBJECT); 1431 } 1432 1433 private Entry buildBaseChangelogEntry() throws DirectoryException 1434 { 1435 final String hasSubordinatesStr = Boolean.toString(baseChangelogHasSubordinates()); 1436 1437 final Map<AttributeType, List<Attribute>> userAttrs = new LinkedHashMap<>(); 1438 final Map<AttributeType, List<Attribute>> operationalAttrs = new LinkedHashMap<>(); 1439 1440 // We never return the numSubordinates attribute for the base changelog entry 1441 // and there is a very good reason for that: 1442 // - Either we compute it before sending the entries, 1443 // -- then we risk returning more entries if new entries come in after we computed numSubordinates 1444 // -- or we risk returning less entries if purge kicks in after we computed numSubordinates 1445 // - Or we accumulate all the entries that must be returned before sending them => OutOfMemoryError 1446 1447 addAttributeByUppercaseName(ATTR_COMMON_NAME, ATTR_COMMON_NAME, BACKEND_ID, userAttrs, operationalAttrs); 1448 addAttributeByUppercaseName(ATTR_SUBSCHEMA_SUBENTRY_LC, ATTR_SUBSCHEMA_SUBENTRY, 1449 ConfigConstants.DN_DEFAULT_SCHEMA_ROOT, userAttrs, operationalAttrs); 1450 addAttributeByUppercaseName("hassubordinates", "hasSubordinates", hasSubordinatesStr, userAttrs, operationalAttrs); 1451 addAttributeByUppercaseName("entrydn", "entryDN", DN_EXTERNAL_CHANGELOG_ROOT, userAttrs, operationalAttrs); 1452 return new Entry(CHANGELOG_BASE_DN, CHANGELOG_ROOT_OBJECT_CLASSES, userAttrs, operationalAttrs); 1453 } 1454 1455 private static void addAttribute(final Entry e, final String attrType, final String attrValue) 1456 { 1457 e.addAttribute(Attributes.create(attrType, attrValue), null); 1458 } 1459 1460 private static void addAttributeByType(String attrNameLowercase, 1461 String attrNameUppercase, String attrValue, 1462 Map<AttributeType, List<Attribute>> userAttrs, 1463 Map<AttributeType, List<Attribute>> operationalAttrs) 1464 { 1465 addAttribute(attrNameLowercase, attrNameUppercase, attrValue, userAttrs, operationalAttrs, true); 1466 } 1467 1468 private static void addAttributeByUppercaseName(String attrNameLowercase, 1469 String attrNameUppercase, String attrValue, 1470 Map<AttributeType, List<Attribute>> userAttrs, 1471 Map<AttributeType, List<Attribute>> operationalAttrs) 1472 { 1473 addAttribute(attrNameLowercase, attrNameUppercase, attrValue, userAttrs, operationalAttrs, false); 1474 } 1475 1476 private static void addAttribute(final String attrNameLowercase, 1477 final String attrNameUppercase, final String attrValue, 1478 final Map<AttributeType, List<Attribute>> userAttrs, 1479 final Map<AttributeType, List<Attribute>> operationalAttrs, final boolean addByType) 1480 { 1481 AttributeType attrType = DirectoryServer.getAttributeType(attrNameUppercase); 1482 final Attribute a = addByType 1483 ? Attributes.create(attrType, attrValue) 1484 : Attributes.create(attrNameUppercase, attrValue); 1485 final List<Attribute> attrList = Collections.singletonList(a); 1486 if (attrType.isOperational()) 1487 { 1488 operationalAttrs.put(attrType, attrList); 1489 } 1490 else 1491 { 1492 userAttrs.put(attrType, attrList); 1493 } 1494 } 1495 1496 /** Describes the current search phase. */ 1497 private enum SearchPhase 1498 { 1499 /** 1500 * "Initial search" phase. The "initial search" phase is running 1501 * concurrently. All update notifications are ignored. 1502 */ 1503 INITIAL, 1504 /** 1505 * Transitioning from the "initial search" phase to the "persistent search" 1506 * phase. "Initial search" phase has finished reading from the DB. It now 1507 * verifies if any more updates have been persisted to the DB since stopping 1508 * and send them. All update notifications are blocked. 1509 */ 1510 TRANSITIONING, 1511 /** 1512 * "Persistent search" phase. "Initial search" phase has completed. All 1513 * update notifications are published. 1514 */ 1515 PERSISTENT; 1516 } 1517 1518 /** 1519 * Contains data to ensure that the same change is not sent twice to clients 1520 * because of race conditions between the "initial search" phase and the 1521 * "persistent search" phase. 1522 */ 1523 private static class SendEntryData<K extends Comparable<K>> 1524 { 1525 private final AtomicReference<SearchPhase> searchPhase = new AtomicReference<>(SearchPhase.INITIAL); 1526 private final Object transitioningLock = new Object(); 1527 private volatile K lastKeySentByInitialSearch; 1528 1529 private SendEntryData(SearchPhase startPhase) 1530 { 1531 searchPhase.set(startPhase); 1532 } 1533 1534 private void finalizeInitialSearch() 1535 { 1536 searchPhase.set(SearchPhase.PERSISTENT); 1537 synchronized (transitioningLock) 1538 { // initial search phase has completed, release all persistent searches 1539 transitioningLock.notifyAll(); 1540 } 1541 } 1542 1543 public void transitioningToPersistentSearchPhase() 1544 { 1545 searchPhase.set(SearchPhase.TRANSITIONING); 1546 } 1547 1548 private void initialSearchSendsEntry(final K key) 1549 { 1550 lastKeySentByInitialSearch = key; 1551 } 1552 1553 private boolean persistentSearchCanSendEntry(K key) 1554 { 1555 final SearchPhase stateValue = searchPhase.get(); 1556 switch (stateValue) 1557 { 1558 case INITIAL: 1559 return false; 1560 case TRANSITIONING: 1561 synchronized (transitioningLock) 1562 { 1563 while (SearchPhase.TRANSITIONING.equals(searchPhase.get())) 1564 { 1565 // "initial search" phase is over, and is now verifying whether new 1566 // changes have been published to the DB. 1567 // Wait for this check to complete 1568 try 1569 { 1570 transitioningLock.wait(); 1571 } 1572 catch (InterruptedException e) 1573 { 1574 Thread.currentThread().interrupt(); 1575 // Shutdown must have been called. Stop sending entries. 1576 return false; 1577 } 1578 } 1579 } 1580 return key.compareTo(lastKeySentByInitialSearch) > 0; 1581 case PERSISTENT: 1582 return true; 1583 default: 1584 throw new RuntimeException("Not implemented for " + stateValue); 1585 } 1586 } 1587 } 1588 1589 /** Sends entries to clients for change number searches. */ 1590 private static class ChangeNumberEntrySender 1591 { 1592 private final SearchOperation searchOp; 1593 private final long lowestChangeNumber; 1594 private final long highestChangeNumber; 1595 private final SendEntryData<Long> sendEntryData; 1596 1597 private ChangeNumberEntrySender(SearchOperation searchOp, SearchPhase startPhase, ChangeNumberRange range) 1598 { 1599 this.searchOp = searchOp; 1600 this.sendEntryData = new SendEntryData<>(startPhase); 1601 this.lowestChangeNumber = range.lowerBound; 1602 this.highestChangeNumber = range.upperBound; 1603 } 1604 1605 /** 1606 * Indicates if provided change number is compatible with last change 1607 * number. 1608 * 1609 * @param changeNumber 1610 * The change number to test. 1611 * @return {@code true} if and only if the provided change number is in the 1612 * range of the last change number. 1613 */ 1614 boolean changeNumberIsInRange(long changeNumber) 1615 { 1616 return highestChangeNumber == -1 || changeNumber <= highestChangeNumber; 1617 } 1618 1619 private void finalizeInitialSearch() 1620 { 1621 sendEntryData.finalizeInitialSearch(); 1622 } 1623 1624 private void transitioningToPersistentSearchPhase() 1625 { 1626 sendEntryData.transitioningToPersistentSearchPhase(); 1627 } 1628 1629 /** 1630 * @return {@code true} if search should continue, {@code false} otherwise 1631 */ 1632 private boolean initialSearchSendEntry(ChangeNumberIndexRecord cnIndexRecord, UpdateMsg updateMsg, 1633 MultiDomainServerState cookie) throws DirectoryException 1634 { 1635 final DN baseDN = cnIndexRecord.getBaseDN(); 1636 sendEntryData.initialSearchSendsEntry(cnIndexRecord.getChangeNumber()); 1637 final Entry entry = createEntryFromMsg(baseDN, cnIndexRecord.getChangeNumber(), cookie.toString(), updateMsg); 1638 return sendEntryIfMatches(searchOp, entry, null); 1639 } 1640 1641 private void persistentSearchSendEntry(long changeNumber, Entry entry) throws DirectoryException 1642 { 1643 if (sendEntryData.persistentSearchCanSendEntry(changeNumber)) 1644 { 1645 sendEntryIfMatches(searchOp, entry, null); 1646 } 1647 } 1648 } 1649 1650 /** Sends entries to clients for cookie-based searches. */ 1651 private static class CookieEntrySender { 1652 private final SearchOperation searchOp; 1653 private final SearchPhase startPhase; 1654 private final Set<DN> excludedBaseDNs; 1655 private final MultiDomainServerState cookie; 1656 private final ConcurrentSkipListMap<ReplicaId, SendEntryData<CSN>> replicaIdToSendEntryData = 1657 new ConcurrentSkipListMap<>(); 1658 1659 private CookieEntrySender(SearchOperation searchOp, SearchPhase startPhase, MultiDomainServerState cookie, 1660 Set<DN> excludedBaseDNs) 1661 { 1662 this.searchOp = searchOp; 1663 this.startPhase = startPhase; 1664 this.cookie = cookie; 1665 this.excludedBaseDNs = excludedBaseDNs; 1666 } 1667 1668 private void finalizeInitialSearch() 1669 { 1670 for (SendEntryData<CSN> sendEntryData : replicaIdToSendEntryData.values()) 1671 { 1672 sendEntryData.finalizeInitialSearch(); 1673 } 1674 } 1675 1676 private void transitioningToPersistentSearchPhase() 1677 { 1678 for (SendEntryData<CSN> sendEntryData : replicaIdToSendEntryData.values()) 1679 { 1680 sendEntryData.transitioningToPersistentSearchPhase(); 1681 } 1682 } 1683 1684 private SendEntryData<CSN> getSendEntryData(DN baseDN, CSN csn) 1685 { 1686 final ReplicaId replicaId = ReplicaId.of(baseDN, csn.getServerId()); 1687 SendEntryData<CSN> data = replicaIdToSendEntryData.get(replicaId); 1688 if (data == null) 1689 { 1690 final SendEntryData<CSN> newData = new SendEntryData<>(startPhase); 1691 data = replicaIdToSendEntryData.putIfAbsent(replicaId, newData); 1692 return data == null ? newData : data; 1693 } 1694 return data; 1695 } 1696 1697 private boolean initialSearchSendEntry(final UpdateMsg updateMsg, final DN baseDN) throws DirectoryException 1698 { 1699 final CSN csn = updateMsg.getCSN(); 1700 final SendEntryData<CSN> sendEntryData = getSendEntryData(baseDN, csn); 1701 sendEntryData.initialSearchSendsEntry(csn); 1702 final String cookieString = updateCookie(baseDN, updateMsg.getCSN()); 1703 final Entry entry = createEntryFromMsg(baseDN, 0, cookieString, updateMsg); 1704 return sendEntryIfMatches(searchOp, entry, cookieString); 1705 } 1706 1707 private void persistentSearchSendEntry(DN baseDN, UpdateMsg updateMsg) 1708 throws DirectoryException 1709 { 1710 final CSN csn = updateMsg.getCSN(); 1711 final SendEntryData<CSN> sendEntryData = getSendEntryData(baseDN, csn); 1712 if (sendEntryData.persistentSearchCanSendEntry(csn)) 1713 { 1714 // multi threaded case: wait for the "initial search" phase to set the cookie 1715 final String cookieString = updateCookie(baseDN, updateMsg.getCSN()); 1716 final Entry cookieEntry = createEntryFromMsg(baseDN, 0, cookieString, updateMsg); 1717 // FIXME JNR use this instead of previous line: 1718 // entry.replaceAttribute(Attributes.create("changelogcookie", cookieString)); 1719 sendEntryIfMatches(searchOp, cookieEntry, cookieString); 1720 } 1721 } 1722 1723 private String updateCookie(DN baseDN, final CSN csn) 1724 { 1725 synchronized (cookie) 1726 { // forbid concurrent updates to the cookie 1727 cookie.update(baseDN, csn); 1728 return cookie.toString(); 1729 } 1730 } 1731 } 1732}