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 2006-2010 Sun Microsystems, Inc. 015 * Portions Copyright 2011-2016 ForgeRock AS. 016 */ 017package org.opends.server.replication.plugin; 018 019import static org.forgerock.opendj.ldap.ResultCode.*; 020import static org.opends.messages.ReplicationMessages.*; 021import static org.opends.messages.ToolMessages.*; 022import static org.opends.server.protocols.internal.InternalClientConnection.*; 023import static org.opends.server.protocols.internal.Requests.*; 024import static org.opends.server.replication.plugin.EntryHistorical.*; 025import static org.opends.server.replication.protocol.OperationContext.*; 026import static org.opends.server.util.CollectionUtils.*; 027import static org.opends.server.util.ServerConstants.*; 028import static org.opends.server.util.StaticUtils.*; 029 030import java.io.File; 031import java.io.InputStream; 032import java.io.OutputStream; 033import java.io.StringReader; 034import java.util.ArrayList; 035import java.util.Collection; 036import java.util.Collections; 037import java.util.Date; 038import java.util.HashMap; 039import java.util.HashSet; 040import java.util.Iterator; 041import java.util.LinkedHashMap; 042import java.util.LinkedHashSet; 043import java.util.LinkedList; 044import java.util.List; 045import java.util.Map; 046import java.util.NoSuchElementException; 047import java.util.Set; 048import java.util.SortedMap; 049import java.util.StringTokenizer; 050import java.util.TreeMap; 051import java.util.concurrent.BlockingQueue; 052import java.util.concurrent.TimeUnit; 053import java.util.concurrent.TimeoutException; 054import java.util.concurrent.atomic.AtomicBoolean; 055import java.util.concurrent.atomic.AtomicInteger; 056import java.util.concurrent.atomic.AtomicReference; 057import java.util.zip.DataFormatException; 058 059import org.forgerock.i18n.LocalizableMessage; 060import org.forgerock.i18n.LocalizedIllegalArgumentException; 061import org.forgerock.i18n.slf4j.LocalizedLogger; 062import org.forgerock.opendj.config.server.ConfigChangeResult; 063import org.forgerock.opendj.config.server.ConfigException; 064import org.forgerock.opendj.ldap.AVA; 065import org.forgerock.opendj.ldap.ByteString; 066import org.forgerock.opendj.ldap.DN; 067import org.forgerock.opendj.ldap.DecodeException; 068import org.forgerock.opendj.ldap.ModificationType; 069import org.forgerock.opendj.ldap.RDN; 070import org.forgerock.opendj.ldap.ResultCode; 071import org.forgerock.opendj.ldap.SearchScope; 072import org.forgerock.opendj.ldap.schema.AttributeType; 073import org.opends.server.admin.server.ConfigurationChangeListener; 074import org.opends.server.admin.std.meta.ReplicationDomainCfgDefn.IsolationPolicy; 075import org.opends.server.admin.std.server.ExternalChangelogDomainCfg; 076import org.opends.server.admin.std.server.ReplicationDomainCfg; 077import org.opends.server.api.AlertGenerator; 078import org.opends.server.api.Backend; 079import org.opends.server.api.Backend.BackendOperation; 080import org.opends.server.api.BackendInitializationListener; 081import org.opends.server.api.DirectoryThread; 082import org.opends.server.api.MonitorData; 083import org.opends.server.api.ServerShutdownListener; 084import org.opends.server.api.SynchronizationProvider; 085import org.opends.server.backends.task.Task; 086import org.opends.server.config.ConfigConstants; 087import org.opends.server.controls.PagedResultsControl; 088import org.opends.server.core.AddOperation; 089import org.opends.server.core.DeleteOperation; 090import org.opends.server.core.DirectoryServer; 091import org.opends.server.core.LockFileManager; 092import org.opends.server.core.ModifyDNOperation; 093import org.opends.server.core.ModifyDNOperationBasis; 094import org.opends.server.core.ModifyOperation; 095import org.opends.server.core.ModifyOperationBasis; 096import org.opends.server.protocols.internal.InternalClientConnection; 097import org.opends.server.protocols.internal.InternalSearchListener; 098import org.opends.server.protocols.internal.InternalSearchOperation; 099import org.opends.server.protocols.internal.Requests; 100import org.opends.server.protocols.internal.SearchRequest; 101import org.opends.server.protocols.ldap.LDAPAttribute; 102import org.opends.server.protocols.ldap.LDAPControl; 103import org.opends.server.protocols.ldap.LDAPFilter; 104import org.opends.server.protocols.ldap.LDAPModification; 105import org.opends.server.replication.common.CSN; 106import org.opends.server.replication.common.ServerState; 107import org.opends.server.replication.common.ServerStatus; 108import org.opends.server.replication.common.StatusMachineEvent; 109import org.opends.server.replication.protocol.AddContext; 110import org.opends.server.replication.protocol.AddMsg; 111import org.opends.server.replication.protocol.DeleteContext; 112import org.opends.server.replication.protocol.DeleteMsg; 113import org.opends.server.replication.protocol.LDAPUpdateMsg; 114import org.opends.server.replication.protocol.ModifyContext; 115import org.opends.server.replication.protocol.ModifyDNMsg; 116import org.opends.server.replication.protocol.ModifyDnContext; 117import org.opends.server.replication.protocol.ModifyMsg; 118import org.opends.server.replication.protocol.OperationContext; 119import org.opends.server.replication.protocol.RoutableMsg; 120import org.opends.server.replication.protocol.UpdateMsg; 121import org.opends.server.replication.service.DSRSShutdownSync; 122import org.opends.server.replication.service.ReplicationBroker; 123import org.opends.server.replication.service.ReplicationDomain; 124import org.opends.server.tasks.PurgeConflictsHistoricalTask; 125import org.opends.server.tasks.TaskUtils; 126import org.opends.server.types.AdditionalLogItem; 127import org.opends.server.types.Attribute; 128import org.opends.server.types.AttributeBuilder; 129import org.opends.server.types.Attributes; 130import org.opends.server.types.Control; 131import org.opends.server.types.DirectoryException; 132import org.opends.server.types.Entry; 133import org.opends.server.types.ExistingFileBehavior; 134import org.opends.server.types.LDAPException; 135import org.opends.server.types.LDIFExportConfig; 136import org.opends.server.types.LDIFImportConfig; 137import org.opends.server.types.Modification; 138import org.opends.server.types.ObjectClass; 139import org.opends.server.types.Operation; 140import org.opends.server.types.OperationType; 141import org.opends.server.types.RawModification; 142import org.opends.server.types.Schema; 143import org.opends.server.types.SearchFilter; 144import org.opends.server.types.SearchResultEntry; 145import org.opends.server.types.SearchResultReference; 146import org.opends.server.types.SynchronizationProviderResult; 147import org.opends.server.types.operation.PluginOperation; 148import org.opends.server.types.operation.PostOperationAddOperation; 149import org.opends.server.types.operation.PostOperationDeleteOperation; 150import org.opends.server.types.operation.PostOperationModifyDNOperation; 151import org.opends.server.types.operation.PostOperationModifyOperation; 152import org.opends.server.types.operation.PostOperationOperation; 153import org.opends.server.types.operation.PreOperationAddOperation; 154import org.opends.server.types.operation.PreOperationDeleteOperation; 155import org.opends.server.types.operation.PreOperationModifyDNOperation; 156import org.opends.server.types.operation.PreOperationModifyOperation; 157import org.opends.server.util.LDIFReader; 158import org.opends.server.util.TimeThread; 159import org.opends.server.workflowelement.localbackend.LocalBackendModifyOperation; 160 161/** 162 * This class implements the bulk part of the Directory Server side 163 * of the replication code. 164 * It contains the root method for publishing a change, 165 * processing a change received from the replicationServer service, 166 * handle conflict resolution, 167 * handle protocol messages from the replicationServer. 168 * <p> 169 * FIXME Move this class to org.opends.server.replication.service 170 * or the equivalent package once this code is moved to a maven module. 171 */ 172public final class LDAPReplicationDomain extends ReplicationDomain 173 implements ConfigurationChangeListener<ReplicationDomainCfg>, 174 AlertGenerator, BackendInitializationListener, ServerShutdownListener 175{ 176 /** 177 * Set of attributes that will return all the user attributes and the 178 * replication related operational attributes when used in a search operation. 179 */ 180 private static final Set<String> USER_AND_REPL_OPERATIONAL_ATTRS = 181 newHashSet(HISTORICAL_ATTRIBUTE_NAME, ENTRYUUID_ATTRIBUTE_NAME, "*"); 182 183 /** 184 * Initializing replication for the domain initiates backend finalization/initialization 185 * This flag prevents the Replication Domain to disable/enable itself when 186 * it is the event initiator 187 */ 188 private boolean ignoreBackendInitializationEvent; 189 190 private volatile boolean serverShutdownRequested; 191 192 @Override 193 public String getShutdownListenerName() { 194 return "LDAPReplicationDomain " + getBaseDN(); 195 } 196 197 @Override 198 public void processServerShutdown(LocalizableMessage reason) { 199 serverShutdownRequested = true; 200 } 201 202 203 /** 204 * This class is used in the session establishment phase 205 * when no Replication Server with all the local changes has been found 206 * and we therefore need to recover them. 207 * A search is then performed on the database using this 208 * internalSearchListener. 209 */ 210 private class ScanSearchListener implements InternalSearchListener 211 { 212 private final CSN startCSN; 213 private final CSN endCSN; 214 215 public ScanSearchListener(CSN startCSN, CSN endCSN) 216 { 217 this.startCSN = startCSN; 218 this.endCSN = endCSN; 219 } 220 221 @Override 222 public void handleInternalSearchEntry( 223 InternalSearchOperation searchOperation, SearchResultEntry searchEntry) 224 throws DirectoryException 225 { 226 // Build the list of Operations that happened on this entry after startCSN 227 // and before endCSN and add them to the replayOperations list 228 Iterable<FakeOperation> updates = 229 EntryHistorical.generateFakeOperations(searchEntry); 230 231 for (FakeOperation op : updates) 232 { 233 CSN csn = op.getCSN(); 234 if (csn.isNewerThan(startCSN) && csn.isOlderThan(endCSN)) 235 { 236 synchronized (replayOperations) 237 { 238 replayOperations.put(csn, op); 239 } 240 } 241 } 242 } 243 244 @Override 245 public void handleInternalSearchReference( 246 InternalSearchOperation searchOperation, 247 SearchResultReference searchReference) throws DirectoryException 248 { 249 // Nothing to do. 250 } 251 } 252 253 @Override 254 public void performBackendPreInitializationProcessing(Backend<?> backend) { 255 // Nothing to do 256 } 257 258 @Override 259 public void performBackendPostFinalizationProcessing(Backend<?> backend) { 260 // Nothing to do 261 } 262 263 @Override 264 public void performBackendPostInitializationProcessing(Backend<?> backend) { 265 if (!ignoreBackendInitializationEvent 266 && getBackend().getBackendID().equals(backend.getBackendID())) { 267 enable(); 268 } 269 } 270 271 @Override 272 public void performBackendPreFinalizationProcessing(Backend<?> backend) { 273 // Do not disable itself during a shutdown 274 // And ignore the event if this replica is the event trigger (e.g. importing). 275 if (!ignoreBackendInitializationEvent 276 && !serverShutdownRequested 277 && getBackend().getBackendID().equals(backend.getBackendID())) { 278 disable(); 279 } 280 } 281 282 /** The fully-qualified name of this class. */ 283 private static final String CLASS_NAME = LDAPReplicationDomain.class.getName(); 284 285 /** 286 * The attribute used to mark conflicting entries. 287 * The value of this attribute should be the dn that this entry was 288 * supposed to have when it was marked as conflicting. 289 */ 290 public static final String DS_SYNC_CONFLICT = "ds-sync-conflict"; 291 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 292 293 private final DSRSShutdownSync dsrsShutdownSync; 294 /** 295 * The update to replay message queue where the listener thread is going to 296 * push incoming update messages. 297 */ 298 private final BlockingQueue<UpdateToReplay> updateToReplayQueue; 299 /** The number of naming conflicts successfully resolved. */ 300 private final AtomicInteger numResolvedNamingConflicts = new AtomicInteger(); 301 /** The number of modify conflicts successfully resolved. */ 302 private final AtomicInteger numResolvedModifyConflicts = new AtomicInteger(); 303 /** The number of unresolved naming conflicts. */ 304 private final AtomicInteger numUnresolvedNamingConflicts = 305 new AtomicInteger(); 306 /** The number of updates replayed successfully by the replication. */ 307 private final AtomicInteger numReplayedPostOpCalled = new AtomicInteger(); 308 309 private final PersistentServerState state; 310 private volatile boolean generationIdSavedStatus; 311 312 /** 313 * This object is used to store the list of update currently being done on the local database. 314 * It is useful to make sure that the local operations are sent in a correct order to the 315 * replication server and that the ServerState is not updated too early. 316 */ 317 private final PendingChanges pendingChanges; 318 private final AtomicReference<RSUpdater> rsUpdater = new AtomicReference<>(null); 319 320 /** 321 * It contain the updates that were done on other servers, transmitted by the 322 * replication server and that are currently replayed. 323 * <p> 324 * It is useful to make sure that dependencies between operations are 325 * correctly fulfilled and to make sure that the ServerState is not updated 326 * too early. 327 */ 328 private final RemotePendingChanges remotePendingChanges; 329 private boolean solveConflictFlag = true; 330 331 private final InternalClientConnection conn = getRootConnection(); 332 private final AtomicBoolean shutdown = new AtomicBoolean(); 333 private volatile boolean disabled; 334 private volatile boolean stateSavingDisabled; 335 336 /** 337 * This list is used to temporary store operations that needs to be replayed 338 * at session establishment time. 339 */ 340 private final SortedMap<CSN, FakeOperation> replayOperations = new TreeMap<>(); 341 342 private ExternalChangelogDomain eclDomain; 343 344 /** A boolean indicating if the thread used to save the persistentServerState is terminated. */ 345 private volatile boolean done = true; 346 347 private final ServerStateFlush flushThread; 348 349 /** The attribute name used to store the generation id in the backend. */ 350 private static final String REPLICATION_GENERATION_ID = "ds-sync-generation-id"; 351 /** The attribute name used to store the fractional include configuration in the backend. */ 352 static final String REPLICATION_FRACTIONAL_INCLUDE = "ds-sync-fractional-include"; 353 /** The attribute name used to store the fractional exclude configuration in the backend. */ 354 static final String REPLICATION_FRACTIONAL_EXCLUDE = "ds-sync-fractional-exclude"; 355 356 /** 357 * Fractional replication variables. 358 */ 359 360 /** Holds the fractional configuration for this domain, if any. */ 361 private final FractionalConfig fractionalConfig; 362 363 /** The list of attributes that cannot be used in fractional replication configuration. */ 364 private static final String[] FRACTIONAL_PROHIBITED_ATTRIBUTES = new String[] 365 { 366 "objectClass", 367 "2.5.4.0" // objectClass OID 368 }; 369 370 /** 371 * When true, this flag is used to force the domain status to be put in bad 372 * data set just after the connection to the replication server. 373 * This must be used when fractional replication is enabled with a 374 * configuration different from the previous one (or at the very first 375 * fractional usage time) : after connection, a ChangeStatusMsg is sent 376 * requesting the bad data set status. Then none of the update messages 377 * received from the replication server are taken into account until the 378 * backend is synchronized with brand new data set compliant with the new 379 * fractional configuration (i.e with compliant fractional configuration in 380 * domain root entry). 381 */ 382 private boolean forceBadDataSet; 383 384 /** 385 * The message id to be used when an import is stopped with error by 386 * the fractional replication ldif import plugin. 387 */ 388 private int importErrorMessageId = -1; 389 /** LocalizableMessage type for ERR_FULL_UPDATE_IMPORT_FRACTIONAL_BAD_REMOTE. */ 390 static final int IMPORT_ERROR_MESSAGE_BAD_REMOTE = 1; 391 /** LocalizableMessage type for ERR_FULL_UPDATE_IMPORT_FRACTIONAL_REMOTE_IS_FRACTIONAL. */ 392 static final int IMPORT_ERROR_MESSAGE_REMOTE_IS_FRACTIONAL = 2; 393 394 /* 395 * Definitions for the return codes of the 396 * fractionalFilterOperation(PreOperationModifyOperation 397 * modifyOperation, boolean performFiltering) method 398 */ 399 /** 400 * The operation contains attributes subject to fractional filtering according 401 * to the fractional configuration. 402 */ 403 private static final int FRACTIONAL_HAS_FRACTIONAL_FILTERED_ATTRIBUTES = 1; 404 /** 405 * The operation contains no attributes subject to fractional filtering 406 * according to the fractional configuration. 407 */ 408 private static final int FRACTIONAL_HAS_NO_FRACTIONAL_FILTERED_ATTRIBUTES = 2; 409 /** The operation should become a no-op. */ 410 private static final int FRACTIONAL_BECOME_NO_OP = 3; 411 412 /** 413 * The last CSN purged in this domain. Allows to have a continuous purging 414 * process from one purge processing (task run) to the next one. Values 0 when 415 * the server starts. 416 */ 417 private CSN lastCSNPurgedFromHist = new CSN(0,0,0); 418 419 /** 420 * The thread that periodically saves the ServerState of this 421 * LDAPReplicationDomain in the database. 422 */ 423 private class ServerStateFlush extends DirectoryThread 424 { 425 protected ServerStateFlush() 426 { 427 super("Replica DS(" + getServerId() + ") state checkpointer for domain \"" + getBaseDN() + "\""); 428 } 429 430 @Override 431 public void run() 432 { 433 done = false; 434 435 while (!isShutdownInitiated()) 436 { 437 try 438 { 439 synchronized (this) 440 { 441 wait(1000); 442 if (!disabled && !stateSavingDisabled) 443 { 444 // save the ServerState 445 state.save(); 446 } 447 } 448 } 449 catch (InterruptedException e) 450 { 451 // Thread interrupted: check for shutdown. 452 Thread.currentThread().interrupt(); 453 } 454 } 455 state.save(); 456 457 done = true; 458 } 459 } 460 461 /** 462 * The thread that is responsible to update the RS to which this domain is 463 * connected in case it is late and there is no RS which is up to date. 464 */ 465 private class RSUpdater extends DirectoryThread 466 { 467 private final CSN startCSN; 468 469 protected RSUpdater(CSN replServerMaxCSN) 470 { 471 super("Replica DS(" + getServerId() + ") missing change publisher for domain \"" + getBaseDN() + "\""); 472 this.startCSN = replServerMaxCSN; 473 } 474 475 @Override 476 public void run() 477 { 478 // Replication server is missing some of our changes: 479 // let's send them to him. 480 logger.trace(DEBUG_GOING_TO_SEARCH_FOR_CHANGES); 481 482 /* 483 * Get all the changes that have not been seen by this 484 * replication server and publish them. 485 */ 486 try 487 { 488 if (buildAndPublishMissingChanges(startCSN, broker)) 489 { 490 logger.trace(DEBUG_CHANGES_SENT); 491 synchronized(replayOperations) 492 { 493 replayOperations.clear(); 494 } 495 } 496 else 497 { 498 /* 499 * An error happened trying to search for the updates 500 * This server will start accepting again new updates but 501 * some inconsistencies will stay between servers. 502 * Log an error for the repair tool 503 * that will need to re-synchronize the servers. 504 */ 505 logger.error(ERR_CANNOT_RECOVER_CHANGES, getBaseDN()); 506 } 507 } 508 catch (Exception e) 509 { 510 /* 511 * An error happened trying to search for the updates 512 * This server will start accepting again new updates but 513 * some inconsistencies will stay between servers. 514 * Log an error for the repair tool 515 * that will need to re-synchronize the servers. 516 */ 517 logger.error(ERR_CANNOT_RECOVER_CHANGES, getBaseDN()); 518 } 519 finally 520 { 521 broker.setRecoveryRequired(false); 522 // RSUpdater thread has finished its work, let's remove it from memory 523 // so another RSUpdater thread can be started if needed. 524 rsUpdater.compareAndSet(this, null); 525 } 526 } 527 } 528 529 /** 530 * Creates a new ReplicationDomain using configuration from configEntry. 531 * 532 * @param configuration The configuration of this ReplicationDomain. 533 * @param updateToReplayQueue The queue for update messages to replay. 534 * @param dsrsShutdownSync Synchronization object for shutdown of combined DS/RS instances. 535 * @throws ConfigException In case of invalid configuration. 536 */ 537 LDAPReplicationDomain(ReplicationDomainCfg configuration, 538 BlockingQueue<UpdateToReplay> updateToReplayQueue, 539 DSRSShutdownSync dsrsShutdownSync) throws ConfigException 540 { 541 super(configuration, -1); 542 543 this.updateToReplayQueue = updateToReplayQueue; 544 this.dsrsShutdownSync = dsrsShutdownSync; 545 546 // Get assured configuration 547 readAssuredConfig(configuration, false); 548 549 // Get fractional configuration 550 fractionalConfig = new FractionalConfig(getBaseDN()); 551 readFractionalConfig(configuration, false); 552 storeECLConfiguration(configuration); 553 solveConflictFlag = isSolveConflict(configuration); 554 555 Backend<?> backend = getBackend(); 556 if (backend == null) 557 { 558 throw new ConfigException(ERR_SEARCHING_DOMAIN_BACKEND.get(getBaseDN())); 559 } 560 561 try 562 { 563 generationId = loadGenerationId(); 564 } 565 catch (DirectoryException e) 566 { 567 logger.error(ERR_LOADING_GENERATION_ID, getBaseDN(), stackTraceToSingleLineString(e)); 568 } 569 570 /* 571 * Create a new Persistent Server State that will be used to store 572 * the last CSN seen from all LDAP servers in the topology. 573 */ 574 state = new PersistentServerState(getBaseDN(), getServerId(), 575 getServerState()); 576 flushThread = new ServerStateFlush(); 577 578 /* 579 * CSNGenerator is used to create new unique CSNs for each operation done on 580 * this replication domain. 581 * 582 * The generator time is adjusted to the time of the last CSN received from 583 * remote other servers. 584 */ 585 pendingChanges = new PendingChanges(getGenerator(), this); 586 remotePendingChanges = new RemotePendingChanges(getServerState()); 587 588 // listen for changes on the configuration 589 configuration.addChangeListener(this); 590 591 // register as an AlertGenerator 592 DirectoryServer.registerAlertGenerator(this); 593 594 DirectoryServer.registerBackendInitializationListener(this); 595 DirectoryServer.registerShutdownListener(this); 596 597 startPublishService(); 598 } 599 600 /** 601 * Modify conflicts are solved for all suffixes but the schema suffix because 602 * we don't want to store extra information in the schema ldif files. This has 603 * no negative impact because the changes on schema should not produce 604 * conflicts. 605 */ 606 private boolean isSolveConflict(ReplicationDomainCfg cfg) 607 { 608 return !getBaseDN().equals(DirectoryServer.getSchemaDN()) 609 && cfg.isSolveConflicts(); 610 } 611 612 /** 613 * Sets the error message id to be used when online import is stopped with 614 * error by the fractional replication ldif import plugin. 615 * @param importErrorMessageId The message to use. 616 */ 617 void setImportErrorMessageId(int importErrorMessageId) 618 { 619 this.importErrorMessageId = importErrorMessageId; 620 } 621 622 /** 623 * This flag is used by the fractional replication ldif import plugin to stop 624 * the (online) import process if a fractional configuration inconsistency is 625 * detected by it. 626 * 627 * @return true if the online import currently in progress should continue, 628 * false otherwise. 629 */ 630 private boolean isFollowImport() 631 { 632 return importErrorMessageId == -1; 633 } 634 635 /** 636 * Gets and stores the fractional replication configuration parameters. 637 * @param configuration The configuration object 638 * @param allowReconnection Tells if one must reconnect if significant changes 639 * occurred 640 */ 641 private void readFractionalConfig(ReplicationDomainCfg configuration, 642 boolean allowReconnection) 643 { 644 // Read the configuration entry 645 FractionalConfig newFractionalConfig; 646 try 647 { 648 newFractionalConfig = FractionalConfig.toFractionalConfig(configuration); 649 } 650 catch(ConfigException e) 651 { 652 // Should not happen as normally already called without problem in 653 // isConfigurationChangeAcceptable or isConfigurationAcceptable 654 // if we come up to this method 655 logger.info(NOTE_ERR_FRACTIONAL, getBaseDN(), stackTraceToSingleLineString(e)); 656 return; 657 } 658 659 /** 660 * Is there any change in fractional configuration ? 661 */ 662 663 // Compute current configuration 664 boolean needReconnection; 665 try 666 { 667 needReconnection = !FractionalConfig. 668 isFractionalConfigEquivalent(fractionalConfig, newFractionalConfig); 669 } 670 catch (ConfigException e) 671 { 672 // Should not happen 673 logger.info(NOTE_ERR_FRACTIONAL, getBaseDN(), stackTraceToSingleLineString(e)); 674 return; 675 } 676 677 // Disable service if configuration changed 678 final boolean needRestart = needReconnection && allowReconnection; 679 if (needRestart) 680 { 681 disableService(); 682 } 683 // Set new configuration 684 int newFractionalMode = newFractionalConfig.fractionalConfigToInt(); 685 fractionalConfig.setFractional(newFractionalMode != 686 FractionalConfig.NOT_FRACTIONAL); 687 if (fractionalConfig.isFractional()) 688 { 689 // Set new fractional configuration values 690 fractionalConfig.setFractionalExclusive( 691 newFractionalMode == FractionalConfig.EXCLUSIVE_FRACTIONAL); 692 fractionalConfig.setFractionalSpecificClassesAttributes( 693 newFractionalConfig.getFractionalSpecificClassesAttributes()); 694 fractionalConfig.setFractionalAllClassesAttributes( 695 newFractionalConfig.fractionalAllClassesAttributes); 696 } else 697 { 698 // Reset default values 699 fractionalConfig.setFractionalExclusive(true); 700 fractionalConfig.setFractionalSpecificClassesAttributes( 701 new HashMap<String, Set<String>>()); 702 fractionalConfig.setFractionalAllClassesAttributes(new HashSet<String>()); 703 } 704 705 // Reconnect if required 706 if (needRestart) 707 { 708 enableService(); 709 } 710 } 711 712 /** 713 * Return true if the fractional configuration stored in the domain root 714 * entry of the backend is equivalent to the fractional configuration stored 715 * in the local variables. 716 */ 717 private boolean isBackendFractionalConfigConsistent() 718 { 719 // Read config stored in domain root entry 720 if (logger.isTraceEnabled()) 721 { 722 logger.trace("Attempt to read the potential fractional config in domain root entry " + getBaseDN()); 723 } 724 725 // Search the domain root entry that is used to save the generation id 726 SearchRequest request = newSearchRequest(getBaseDN(), SearchScope.BASE_OBJECT) 727 .addAttribute(REPLICATION_GENERATION_ID, REPLICATION_FRACTIONAL_EXCLUDE, REPLICATION_FRACTIONAL_INCLUDE); 728 InternalSearchOperation search = conn.processSearch(request); 729 730 if (search.getResultCode() != ResultCode.SUCCESS 731 && search.getResultCode() != ResultCode.NO_SUCH_OBJECT) 732 { 733 String errorMsg = search.getResultCode().getName() + " " + search.getErrorMessage(); 734 logger.error(ERR_SEARCHING_GENERATION_ID, errorMsg, getBaseDN()); 735 return false; 736 } 737 738 SearchResultEntry resultEntry = findReplicationSearchResultEntry(search); 739 if (resultEntry == null) 740 { 741 /* 742 * The backend is probably empty: if there is some fractional 743 * configuration in memory, we do not let the domain being connected, 744 * otherwise, it's ok 745 */ 746 return !fractionalConfig.isFractional(); 747 } 748 749 // Now extract fractional configuration if any 750 Iterator<ByteString> exclIt = getAttributeValueIterator(resultEntry, REPLICATION_FRACTIONAL_EXCLUDE); 751 Iterator<ByteString> inclIt = getAttributeValueIterator(resultEntry, REPLICATION_FRACTIONAL_INCLUDE); 752 753 // Compare backend and local fractional configuration 754 return isFractionalConfigConsistent(fractionalConfig, exclIt, inclIt); 755 } 756 757 private SearchResultEntry findReplicationSearchResultEntry( 758 InternalSearchOperation searchOperation) 759 { 760 final SearchResultEntry resultEntry = getFirstResult(searchOperation); 761 if (resultEntry != null) 762 { 763 AttributeType synchronizationGenIDType = DirectoryServer.getAttributeType(REPLICATION_GENERATION_ID); 764 List<Attribute> attrs = resultEntry.getAttribute(synchronizationGenIDType); 765 if (!attrs.isEmpty()) 766 { 767 Attribute attr = attrs.get(0); 768 if (attr.size() == 1) 769 { 770 return resultEntry; 771 } 772 if (attr.size() > 1) 773 { 774 String errorMsg = "#Values=" + attr.size() + " Must be exactly 1 in entry " + resultEntry.toLDIFString(); 775 logger.error(ERR_LOADING_GENERATION_ID, getBaseDN(), errorMsg); 776 } 777 } 778 } 779 return null; 780 } 781 782 private Iterator<ByteString> getAttributeValueIterator(SearchResultEntry resultEntry, String attrName) 783 { 784 AttributeType attrType = DirectoryServer.getAttributeType(attrName); 785 List<Attribute> exclAttrs = resultEntry.getAttribute(attrType); 786 if (!exclAttrs.isEmpty()) 787 { 788 Attribute exclAttr = exclAttrs.get(0); 789 if (exclAttr != null) 790 { 791 return exclAttr.iterator(); 792 } 793 } 794 return null; 795 } 796 797 /** 798 * Return true if the fractional configuration passed as fractional 799 * configuration attribute values is equivalent to the fractional 800 * configuration stored in the local variables. 801 * @param fractionalConfig The local fractional configuration 802 * @param exclIt Fractional exclude mode configuration attribute values to 803 * analyze. 804 * @param inclIt Fractional include mode configuration attribute values to 805 * analyze. 806 * @return True if the fractional configuration passed as fractional 807 * configuration attribute values is equivalent to the fractional 808 * configuration stored in the local variables. 809 */ 810 static boolean isFractionalConfigConsistent( 811 FractionalConfig fractionalConfig, Iterator<ByteString> exclIt, Iterator<ByteString> inclIt) 812 { 813 // Parse fractional configuration stored in passed fractional configuration attributes values 814 Map<String, Set<String>> storedFractionalSpecificClassesAttributes = new HashMap<>(); 815 Set<String> storedFractionalAllClassesAttributes = new HashSet<>(); 816 817 int storedFractionalMode; 818 try 819 { 820 storedFractionalMode = FractionalConfig.parseFractionalConfig(exclIt, 821 inclIt, storedFractionalSpecificClassesAttributes, 822 storedFractionalAllClassesAttributes); 823 } catch (ConfigException e) 824 { 825 // Should not happen as configuration in domain root entry is flushed 826 // from valid configuration in local variables 827 logger.info(NOTE_ERR_FRACTIONAL, fractionalConfig.getBaseDn(), stackTraceToSingleLineString(e)); 828 return false; 829 } 830 831 FractionalConfig storedFractionalConfig = new FractionalConfig( 832 fractionalConfig.getBaseDn()); 833 storedFractionalConfig.setFractional(storedFractionalMode != 834 FractionalConfig.NOT_FRACTIONAL); 835 // Set stored fractional configuration values 836 if (storedFractionalConfig.isFractional()) 837 { 838 storedFractionalConfig.setFractionalExclusive( 839 storedFractionalMode == FractionalConfig.EXCLUSIVE_FRACTIONAL); 840 } 841 storedFractionalConfig.setFractionalSpecificClassesAttributes( 842 storedFractionalSpecificClassesAttributes); 843 storedFractionalConfig.setFractionalAllClassesAttributes( 844 storedFractionalAllClassesAttributes); 845 846 /* 847 * Compare configuration stored in passed fractional configuration 848 * attributes with local variable one 849 */ 850 try 851 { 852 return FractionalConfig. 853 isFractionalConfigEquivalent(fractionalConfig, storedFractionalConfig); 854 } catch (ConfigException e) 855 { 856 // Should not happen as configuration in domain root entry is flushed 857 // from valid configuration in local variables so both should have already 858 // been checked 859 logger.info(NOTE_ERR_FRACTIONAL, fractionalConfig.getBaseDn(), stackTraceToSingleLineString(e)); 860 return false; 861 } 862 } 863 864 /** 865 * Compare 2 attribute collections and returns true if they are equivalent. 866 * 867 * @param attributes1 868 * First attribute collection to compare. 869 * @param attributes2 870 * Second attribute collection to compare. 871 * @return True if both attribute collection are equivalent. 872 * @throws ConfigException 873 * If some attributes could not be retrieved from the schema. 874 */ 875 private static boolean areAttributesEquivalent( 876 Collection<String> attributes1, Collection<String> attributes2) 877 throws ConfigException 878 { 879 // Compare all classes attributes 880 if (attributes1.size() != attributes2.size()) 881 { 882 return false; 883 } 884 885 // Check consistency of all classes attributes 886 Schema schema = DirectoryServer.getSchema(); 887 /* 888 * For each attribute in attributes1, check there is the matching 889 * one in attributes2. 890 */ 891 for (String attrName1 : attributes1) 892 { 893 // Get attribute from attributes1 894 AttributeType attributeType1 = schema.getAttributeType(attrName1); 895 if (attributeType1.isPlaceHolder()) 896 { 897 throw new ConfigException( 898 NOTE_ERR_FRACTIONAL_CONFIG_UNKNOWN_ATTRIBUTE_TYPE.get(attrName1)); 899 } 900 // Look for matching one in attributes2 901 boolean foundAttribute = false; 902 for (String attrName2 : attributes2) 903 { 904 AttributeType attributeType2 = schema.getAttributeType(attrName2); 905 if (attributeType2.isPlaceHolder()) 906 { 907 throw new ConfigException( 908 NOTE_ERR_FRACTIONAL_CONFIG_UNKNOWN_ATTRIBUTE_TYPE.get(attrName2)); 909 } 910 if (attributeType1.equals(attributeType2)) 911 { 912 foundAttribute = true; 913 break; 914 } 915 } 916 // Found matching attribute ? 917 if (!foundAttribute) 918 { 919 return false; 920 } 921 } 922 923 return true; 924 } 925 926 /** 927 * Check that the passed fractional configuration is acceptable 928 * regarding configuration syntax, schema constraints... 929 * Throws an exception if the configuration is not acceptable. 930 * @param configuration The configuration to analyze. 931 * @throws org.opends.server.config.ConfigException if the configuration is 932 * not acceptable. 933 */ 934 private static void isFractionalConfigAcceptable( 935 ReplicationDomainCfg configuration) throws ConfigException 936 { 937 /* 938 * Parse fractional configuration 939 */ 940 941 // Read the configuration entry 942 FractionalConfig newFractionalConfig = FractionalConfig.toFractionalConfig( 943 configuration); 944 945 if (!newFractionalConfig.isFractional()) 946 { 947 // Nothing to check 948 return; 949 } 950 951 // Prepare variables to be filled with config 952 Map<String, Set<String>> newFractionalSpecificClassesAttributes = 953 newFractionalConfig.getFractionalSpecificClassesAttributes(); 954 Set<String> newFractionalAllClassesAttributes = 955 newFractionalConfig.getFractionalAllClassesAttributes(); 956 957 /* 958 * Check attributes consistency : we only allow to filter MAY (optional) 959 * attributes of a class : to be compliant with the schema, no MUST 960 * (mandatory) attribute can be filtered by fractional replication. 961 */ 962 963 // Check consistency of specific classes attributes 964 Schema schema = DirectoryServer.getSchema(); 965 int fractionalMode = newFractionalConfig.fractionalConfigToInt(); 966 for (String className : newFractionalSpecificClassesAttributes.keySet()) 967 { 968 // Does the class exist ? 969 ObjectClass fractionalClass = schema.getObjectClass( 970 className.toLowerCase()); 971 if (fractionalClass == null) 972 { 973 throw new ConfigException( 974 NOTE_ERR_FRACTIONAL_CONFIG_UNKNOWN_OBJECT_CLASS.get(className)); 975 } 976 977 boolean isExtensibleObjectClass = 978 "extensibleObject".equalsIgnoreCase(className); 979 980 Set<String> attributes = 981 newFractionalSpecificClassesAttributes.get(className); 982 983 for (String attrName : attributes) 984 { 985 // Not a prohibited attribute ? 986 if (isFractionalProhibitedAttr(attrName)) 987 { 988 throw new ConfigException( 989 NOTE_ERR_FRACTIONAL_CONFIG_PROHIBITED_ATTRIBUTE.get(attrName)); 990 } 991 992 // Does the attribute exist ? 993 AttributeType attributeType = schema.getAttributeType(attrName); 994 if (!attributeType.isPlaceHolder()) 995 { 996 // No more checking for the extensibleObject class 997 if (!isExtensibleObjectClass 998 && fractionalMode == FractionalConfig.EXCLUSIVE_FRACTIONAL 999 // Exclusive mode : the attribute must be optional 1000 && !fractionalClass.isOptional(attributeType)) 1001 { 1002 throw new ConfigException( 1003 NOTE_ERR_FRACTIONAL_CONFIG_NOT_OPTIONAL_ATTRIBUTE.get(attrName, 1004 className)); 1005 } 1006 } 1007 else 1008 { 1009 throw new ConfigException( 1010 NOTE_ERR_FRACTIONAL_CONFIG_UNKNOWN_ATTRIBUTE_TYPE.get(attrName)); 1011 } 1012 } 1013 } 1014 1015 // Check consistency of all classes attributes 1016 for (String attrName : newFractionalAllClassesAttributes) 1017 { 1018 // Not a prohibited attribute ? 1019 if (isFractionalProhibitedAttr(attrName)) 1020 { 1021 throw new ConfigException( 1022 NOTE_ERR_FRACTIONAL_CONFIG_PROHIBITED_ATTRIBUTE.get(attrName)); 1023 } 1024 1025 // Does the attribute exist ? 1026 if (schema.getAttributeType(attrName) == null) 1027 { 1028 throw new ConfigException( 1029 NOTE_ERR_FRACTIONAL_CONFIG_UNKNOWN_ATTRIBUTE_TYPE.get(attrName)); 1030 } 1031 } 1032 } 1033 1034 /** 1035 * Test if the passed attribute is not allowed to be used in configuration of 1036 * fractional replication. 1037 * @param attr Attribute to test. 1038 * @return true if the attribute is prohibited. 1039 */ 1040 private static boolean isFractionalProhibitedAttr(String attr) 1041 { 1042 for (String forbiddenAttr : FRACTIONAL_PROHIBITED_ATTRIBUTES) 1043 { 1044 if (forbiddenAttr.equalsIgnoreCase(attr)) 1045 { 1046 return true; 1047 } 1048 } 1049 return false; 1050 } 1051 1052 /** 1053 * If fractional replication is enabled, this analyzes the operation and 1054 * suppresses the forbidden attributes in it so that they are not added in 1055 * the local backend. 1056 * 1057 * @param addOperation The operation to modify based on fractional 1058 * replication configuration 1059 * @param performFiltering Tells if the effective attribute filtering should 1060 * be performed or if the call is just to analyze if there are some 1061 * attributes filtered by fractional configuration 1062 * @return true if the operation contains some attributes subject to filtering 1063 * by the fractional configuration 1064 */ 1065 private boolean fractionalFilterOperation( 1066 PreOperationAddOperation addOperation, boolean performFiltering) 1067 { 1068 return fractionalRemoveAttributesFromEntry(fractionalConfig, 1069 addOperation.getEntryDN().rdn(), addOperation.getObjectClasses(), 1070 addOperation.getUserAttributes(), performFiltering); 1071 } 1072 1073 /** 1074 * If fractional replication is enabled, this analyzes the operation and 1075 * suppresses the forbidden attributes in it so that they are not added in 1076 * the local backend. 1077 * 1078 * @param modifyDNOperation The operation to modify based on fractional 1079 * replication configuration 1080 * @param performFiltering Tells if the effective modifications should 1081 * be performed or if the call is just to analyze if there are some 1082 * inconsistency with fractional configuration 1083 * @return true if the operation is inconsistent with fractional 1084 * configuration 1085 */ 1086 private boolean fractionalFilterOperation( 1087 PreOperationModifyDNOperation modifyDNOperation, boolean performFiltering) 1088 { 1089 // Quick exit if not called for analyze and 1090 if (performFiltering && modifyDNOperation.deleteOldRDN()) 1091 { 1092 // The core will remove any occurrence of attribute that was part of the 1093 // old RDN, nothing more to do. 1094 return true; // Will not be used as analyze was not requested 1095 } 1096 1097 // Create a list of filtered attributes for this entry 1098 Entry concernedEntry = modifyDNOperation.getOriginalEntry(); 1099 Set<AttributeType> fractionalConcernedAttributes = 1100 createFractionalConcernedAttrList(fractionalConfig, 1101 concernedEntry.getObjectClasses().keySet()); 1102 1103 boolean fractionalExclusive = fractionalConfig.isFractionalExclusive(); 1104 if (fractionalExclusive && fractionalConcernedAttributes.isEmpty()) 1105 { 1106 // No attributes to filter 1107 return false; 1108 } 1109 1110 /* 1111 * Analyze the old and new rdn to see if they are some attributes to be 1112 * removed: if the oldRDN contains some forbidden attributes (for instance 1113 * it is possible if the entry was created with an add operation and the 1114 * RDN used contains a forbidden attribute: in this case the attribute value 1115 * has been kept to be consistent with the dn of the entry.) that are no 1116 * more part of the new RDN, we must remove any attribute of this type by 1117 * putting a modification to delete the attribute. 1118 */ 1119 1120 boolean inconsistentOperation = false; 1121 RDN rdn = modifyDNOperation.getEntryDN().rdn(); 1122 RDN newRdn = modifyDNOperation.getNewRDN(); 1123 1124 // Go through each attribute of the old RDN 1125 for (AVA ava : rdn) 1126 { 1127 AttributeType attributeType = ava.getAttributeType(); 1128 // Is it present in the fractional attributes established list ? 1129 boolean foundAttribute = 1130 fractionalConcernedAttributes.contains(attributeType); 1131 if (canRemoveAttribute(fractionalExclusive, foundAttribute) 1132 && !newRdn.hasAttributeType(attributeType) 1133 && !modifyDNOperation.deleteOldRDN()) 1134 { 1135 /* 1136 * A forbidden attribute is in the old RDN and no more in the new RDN, 1137 * and it has not been requested to remove attributes from old RDN: 1138 * let's remove the attribute from the entry to stay consistent with 1139 * fractional configuration 1140 */ 1141 Modification modification = new Modification(ModificationType.DELETE, 1142 Attributes.empty(attributeType)); 1143 modifyDNOperation.addModification(modification); 1144 inconsistentOperation = true; 1145 } 1146 } 1147 1148 return inconsistentOperation; 1149 } 1150 1151 /** 1152 * Remove attributes from an entry, according to the passed fractional 1153 * configuration. The entry is represented by the 2 passed parameters. 1154 * The attributes to be removed are removed using the remove method on the 1155 * passed iterator for the attributes in the entry. 1156 * @param fractionalConfig The fractional configuration to use 1157 * @param entryRdn The rdn of the entry to add 1158 * @param classes The object classes representing the entry to modify 1159 * @param attributesMap The map of attributes/values to be potentially removed 1160 * from the entry. 1161 * @param performFiltering Tells if the effective attribute filtering should 1162 * be performed or if the call is just an analyze to see if there are some 1163 * attributes filtered by fractional configuration 1164 * @return true if the operation contains some attributes subject to filtering 1165 * by the fractional configuration 1166 */ 1167 static boolean fractionalRemoveAttributesFromEntry( 1168 FractionalConfig fractionalConfig, RDN entryRdn, 1169 Map<ObjectClass,String> classes, Map<AttributeType, 1170 List<Attribute>> attributesMap, boolean performFiltering) 1171 { 1172 boolean hasSomeAttributesToFilter = false; 1173 /* 1174 * Prepare a list of attributes to be included/excluded according to the 1175 * fractional replication configuration 1176 */ 1177 1178 Set<AttributeType> fractionalConcernedAttributes = 1179 createFractionalConcernedAttrList(fractionalConfig, classes.keySet()); 1180 boolean fractionalExclusive = fractionalConfig.isFractionalExclusive(); 1181 if (fractionalExclusive && fractionalConcernedAttributes.isEmpty()) 1182 { 1183 return false; // No attributes to filter 1184 } 1185 1186 // Prepare list of object classes of the added entry 1187 Set<ObjectClass> entryClasses = classes.keySet(); 1188 1189 /* 1190 * Go through the user attributes and remove those that match filtered one 1191 * - exclude mode : remove only attributes that are in 1192 * fractionalConcernedAttributes 1193 * - include mode : remove any attribute that is not in 1194 * fractionalConcernedAttributes 1195 */ 1196 List<List<Attribute>> newRdnAttrLists = new ArrayList<>(); 1197 List<AttributeType> rdnAttrTypes = new ArrayList<>(); 1198 final Set<AttributeType> attrTypes = attributesMap.keySet(); 1199 for (Iterator<AttributeType> iter = attrTypes.iterator(); iter.hasNext();) 1200 { 1201 AttributeType attributeType = iter.next(); 1202 1203 // Only optional attributes may be removed 1204 if (isMandatoryAttribute(entryClasses, attributeType) 1205 // Do not remove an attribute if it is a prohibited one 1206 || isFractionalProhibited(attributeType) 1207 || !canRemoveAttribute(attributeType, fractionalExclusive, fractionalConcernedAttributes)) 1208 { 1209 continue; 1210 } 1211 1212 if (!performFiltering) 1213 { 1214 // The call was just to check : at least one attribute to filter 1215 // found, return immediately the answer; 1216 return true; 1217 } 1218 1219 // Do not remove an attribute/value that is part of the RDN of the 1220 // entry as it is forbidden 1221 if (entryRdn.hasAttributeType(attributeType)) 1222 { 1223 /* 1224 We must remove all values of the attributes map for this 1225 attribute type but the one that has the value which is in the RDN 1226 of the entry. In fact the (underlying )attribute list does not 1227 support remove so we have to create a new list, keeping only the 1228 attribute value which is the same as in the RDN 1229 */ 1230 ByteString rdnAttributeValue = 1231 entryRdn.getAttributeValue(attributeType); 1232 List<Attribute> attrList = attributesMap.get(attributeType); 1233 ByteString sameAttrValue = null; 1234 // Locate the attribute value identical to the one in the RDN 1235 for (Attribute attr : attrList) 1236 { 1237 if (attr.contains(rdnAttributeValue)) 1238 { 1239 for (ByteString attrValue : attr) { 1240 if (rdnAttributeValue.equals(attrValue)) { 1241 // Keep the value we want 1242 sameAttrValue = attrValue; 1243 } else { 1244 hasSomeAttributesToFilter = true; 1245 } 1246 } 1247 } 1248 else 1249 { 1250 hasSomeAttributesToFilter = true; 1251 } 1252 } 1253 // Recreate the attribute list with only the RDN attribute value 1254 if (sameAttrValue != null) 1255 // Paranoia check: should never be the case as we should always 1256 // find the attribute/value pair matching the pair in the RDN 1257 { 1258 // Construct and store new attribute list 1259 newRdnAttrLists.add(Attributes.createAsList(attributeType, sameAttrValue)); 1260 /* 1261 Store matching attribute type 1262 The mapping will be done using object from rdnAttrTypes as key 1263 and object from newRdnAttrLists (at same index) as value in 1264 the user attribute map to be modified 1265 */ 1266 rdnAttrTypes.add(attributeType); 1267 } 1268 } 1269 else 1270 { 1271 // Found an attribute to remove, remove it from the list. 1272 iter.remove(); 1273 hasSomeAttributesToFilter = true; 1274 } 1275 } 1276 // Now overwrite the attribute values for the attribute types present in the 1277 // RDN, if there are some filtered attributes in the RDN 1278 for (int index = 0 ; index < rdnAttrTypes.size() ; index++) 1279 { 1280 attributesMap.put(rdnAttrTypes.get(index), newRdnAttrLists.get(index)); 1281 } 1282 return hasSomeAttributesToFilter; 1283 } 1284 1285 private static boolean isMandatoryAttribute(Set<ObjectClass> entryClasses, AttributeType attributeType) 1286 { 1287 for (ObjectClass objectClass : entryClasses) 1288 { 1289 if (objectClass.isRequired(attributeType)) 1290 { 1291 return true; 1292 } 1293 } 1294 return false; 1295 } 1296 1297 private static boolean isFractionalProhibited(AttributeType attrType) 1298 { 1299 String attributeName = attrType.getNameOrOID(); 1300 return (attributeName != null && isFractionalProhibitedAttr(attributeName)) 1301 || isFractionalProhibitedAttr(attrType.getOID()); 1302 } 1303 1304 private static boolean canRemoveAttribute(AttributeType attributeType, 1305 boolean fractionalExclusive, Set<AttributeType> fractionalConcernedAttributes) 1306 { 1307 // Now remove the attribute or modification if: 1308 // - exclusive mode and attribute is in configuration 1309 // - inclusive mode and attribute is not in configuration 1310 return canRemoveAttribute(fractionalExclusive, 1311 fractionalConcernedAttributes.contains(attributeType)); 1312 } 1313 1314 private static boolean canRemoveAttribute(boolean fractionalExclusive, boolean foundAttribute) 1315 { 1316 return (foundAttribute && fractionalExclusive) 1317 || (!foundAttribute && !fractionalExclusive); 1318 } 1319 1320 /** 1321 * Prepares a list of attributes of interest for the fractional feature. 1322 * @param fractionalConfig The fractional configuration to use 1323 * @param entryObjectClasses The object classes of an entry on which an 1324 * operation is going to be performed. 1325 * @return The list of attributes of the entry to be excluded/included 1326 * when the operation will be performed. 1327 */ 1328 private static Set<AttributeType> createFractionalConcernedAttrList( 1329 FractionalConfig fractionalConfig, Set<ObjectClass> entryObjectClasses) 1330 { 1331 /* 1332 * Is the concerned entry of a type concerned by fractional replication configuration ? 1333 * If yes, add the matching attribute names to a set of attributes 1334 * to take into account for filtering (inclusive or exclusive mode). 1335 * Using a Set to avoid duplicate attributes (from 2 inheriting classes for instance) 1336 */ 1337 Set<String> fractionalConcernedAttributes = new HashSet<>(); 1338 1339 // Get object classes the entry matches 1340 Set<String> fractionalAllClassesAttributes = 1341 fractionalConfig.getFractionalAllClassesAttributes(); 1342 Map<String, Set<String>> fractionalSpecificClassesAttributes = 1343 fractionalConfig.getFractionalSpecificClassesAttributes(); 1344 1345 Set<String> fractionalClasses = 1346 fractionalSpecificClassesAttributes.keySet(); 1347 for (ObjectClass entryObjectClass : entryObjectClasses) 1348 { 1349 for(String fractionalClass : fractionalClasses) 1350 { 1351 if (entryObjectClass.hasNameOrOID(fractionalClass.toLowerCase())) 1352 { 1353 fractionalConcernedAttributes.addAll( 1354 fractionalSpecificClassesAttributes.get(fractionalClass)); 1355 } 1356 } 1357 } 1358 1359 // Add to the set any attribute which is class independent 1360 fractionalConcernedAttributes.addAll(fractionalAllClassesAttributes); 1361 1362 Set<AttributeType> results = new HashSet<>(); 1363 for (String attrName : fractionalConcernedAttributes) 1364 { 1365 results.add(DirectoryServer.getAttributeType(attrName)); 1366 } 1367 return results; 1368 } 1369 1370 /** 1371 * If fractional replication is enabled, this analyzes the operation and 1372 * suppresses the forbidden attributes in it so that they are not added/ 1373 * deleted/modified in the local backend. 1374 * 1375 * @param modifyOperation The operation to modify based on fractional 1376 * replication configuration 1377 * @param performFiltering Tells if the effective attribute filtering should 1378 * be performed or if the call is just to analyze if there are some 1379 * attributes filtered by fractional configuration 1380 * @return FRACTIONAL_HAS_FRACTIONAL_FILTERED_ATTRIBUTES, 1381 * FRACTIONAL_HAS_NO_FRACTIONAL_FILTERED_ATTRIBUTES or FRACTIONAL_BECOME_NO_OP 1382 */ 1383 private int fractionalFilterOperation(PreOperationModifyOperation 1384 modifyOperation, boolean performFiltering) 1385 { 1386 /* 1387 * Prepare a list of attributes to be included/excluded according to the 1388 * fractional replication configuration 1389 */ 1390 1391 Entry modifiedEntry = modifyOperation.getCurrentEntry(); 1392 Set<AttributeType> fractionalConcernedAttributes = 1393 createFractionalConcernedAttrList(fractionalConfig, modifiedEntry.getObjectClasses().keySet()); 1394 boolean fractionalExclusive = fractionalConfig.isFractionalExclusive(); 1395 if (fractionalExclusive && fractionalConcernedAttributes.isEmpty()) 1396 { 1397 // No attributes to filter 1398 return FRACTIONAL_HAS_NO_FRACTIONAL_FILTERED_ATTRIBUTES; 1399 } 1400 1401 // Prepare list of object classes of the modified entry 1402 DN entryToModifyDn = modifyOperation.getEntryDN(); 1403 Entry entryToModify; 1404 try 1405 { 1406 entryToModify = DirectoryServer.getEntry(entryToModifyDn); 1407 } 1408 catch(DirectoryException e) 1409 { 1410 logger.info(NOTE_ERR_FRACTIONAL, getBaseDN(), stackTraceToSingleLineString(e)); 1411 return FRACTIONAL_HAS_NO_FRACTIONAL_FILTERED_ATTRIBUTES; 1412 } 1413 Set<ObjectClass> entryClasses = entryToModify.getObjectClasses().keySet(); 1414 1415 /* 1416 * Now go through the attribute modifications and filter the mods according 1417 * to the fractional configuration (using the just established concerned 1418 * attributes list): 1419 * - delete attributes: remove them if regarding a filtered attribute 1420 * - add attributes: remove them if regarding a filtered attribute 1421 * - modify attributes: remove them if regarding a filtered attribute 1422 */ 1423 1424 int result = FRACTIONAL_HAS_NO_FRACTIONAL_FILTERED_ATTRIBUTES; 1425 List<Modification> mods = modifyOperation.getModifications(); 1426 Iterator<Modification> modsIt = mods.iterator(); 1427 while (modsIt.hasNext()) 1428 { 1429 Modification mod = modsIt.next(); 1430 Attribute attr = mod.getAttribute(); 1431 AttributeType attrType = attr.getAttributeDescription().getAttributeType(); 1432 // Fractional replication ignores operational attributes 1433 if (attrType.isOperational() 1434 || isMandatoryAttribute(entryClasses, attrType) 1435 || isFractionalProhibited(attrType) 1436 || !canRemoveAttribute(attrType, fractionalExclusive, 1437 fractionalConcernedAttributes)) 1438 { 1439 continue; 1440 } 1441 1442 if (!performFiltering) 1443 { 1444 // The call was just to check : at least one attribute to filter 1445 // found, return immediately the answer; 1446 return FRACTIONAL_HAS_FRACTIONAL_FILTERED_ATTRIBUTES; 1447 } 1448 1449 // Found a modification to remove, remove it from the list. 1450 modsIt.remove(); 1451 result = FRACTIONAL_HAS_FRACTIONAL_FILTERED_ATTRIBUTES; 1452 if (mods.isEmpty()) 1453 { 1454 // This operation must become a no-op as no more modification in it 1455 return FRACTIONAL_BECOME_NO_OP; 1456 } 1457 } 1458 1459 return result; 1460 } 1461 1462 /** 1463 * This is overwritten to allow stopping the (online) import process by the 1464 * fractional ldif import plugin when it detects that the (imported) remote 1465 * data set is not consistent with the local fractional configuration. 1466 * {@inheritDoc} 1467 */ 1468 @Override 1469 protected byte[] receiveEntryBytes() 1470 { 1471 if (isFollowImport()) 1472 { 1473 // Ok, next entry is allowed to be received 1474 return super.receiveEntryBytes(); 1475 } 1476 1477 // Fractional ldif import plugin detected inconsistency between local and 1478 // remote server fractional configuration and is stopping the import 1479 // process: 1480 // This is an error termination during the import 1481 // The error is stored and the import is ended by returning null 1482 final ImportExportContext ieCtx = getImportExportContext(); 1483 LocalizableMessage msg = null; 1484 switch (importErrorMessageId) 1485 { 1486 case IMPORT_ERROR_MESSAGE_BAD_REMOTE: 1487 msg = NOTE_ERR_FULL_UPDATE_IMPORT_FRACTIONAL_BAD_REMOTE.get(getBaseDN(), ieCtx.getImportSource()); 1488 break; 1489 case IMPORT_ERROR_MESSAGE_REMOTE_IS_FRACTIONAL: 1490 msg = NOTE_ERR_FULL_UPDATE_IMPORT_FRACTIONAL_REMOTE_IS_FRACTIONAL.get(getBaseDN(), ieCtx.getImportSource()); 1491 break; 1492 } 1493 ieCtx.setException(new DirectoryException(UNWILLING_TO_PERFORM, msg)); 1494 return null; 1495 } 1496 1497 /** 1498 * This is overwritten to allow stopping the (online) export process if the 1499 * local domain is fractional and the destination is all other servers: 1500 * This make no sense to have only fractional servers in a replicated 1501 * topology. This prevents from administrator manipulation error that would 1502 * lead to whole topology data corruption. 1503 * {@inheritDoc} 1504 */ 1505 @Override 1506 protected void initializeRemote(int target, int requestorID, 1507 Task initTask, int initWindow) throws DirectoryException 1508 { 1509 if (target == RoutableMsg.ALL_SERVERS && fractionalConfig.isFractional()) 1510 { 1511 LocalizableMessage msg = NOTE_ERR_FRACTIONAL_FORBIDDEN_FULL_UPDATE_FRACTIONAL.get(getBaseDN(), getServerId()); 1512 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, msg); 1513 } 1514 1515 super.initializeRemote(target, requestorID, initTask, initWindow); 1516 } 1517 1518 /** 1519 * Implement the handleConflictResolution phase of the deleteOperation. 1520 * 1521 * @param deleteOperation The deleteOperation. 1522 * @return A SynchronizationProviderResult indicating if the operation 1523 * can continue. 1524 */ 1525 SynchronizationProviderResult handleConflictResolution( 1526 PreOperationDeleteOperation deleteOperation) 1527 { 1528 if (!deleteOperation.isSynchronizationOperation() && !brokerIsConnected()) 1529 { 1530 LocalizableMessage msg = ERR_REPLICATION_COULD_NOT_CONNECT.get(getBaseDN()); 1531 return new SynchronizationProviderResult.StopProcessing( 1532 ResultCode.UNWILLING_TO_PERFORM, msg); 1533 } 1534 1535 DeleteContext ctx = 1536 (DeleteContext) deleteOperation.getAttachment(SYNCHROCONTEXT); 1537 Entry deletedEntry = deleteOperation.getEntryToDelete(); 1538 1539 if (ctx != null) 1540 { 1541 /* 1542 * This is a replication operation 1543 * Check that the modified entry has the same entryuuid 1544 * as it was in the original message. 1545 */ 1546 String operationEntryUUID = ctx.getEntryUUID(); 1547 String deletedEntryUUID = getEntryUUID(deletedEntry); 1548 if (!operationEntryUUID.equals(deletedEntryUUID)) 1549 { 1550 /* 1551 * The changes entry is not the same entry as the one on 1552 * the original change was performed. 1553 * Probably the original entry was renamed and replaced with 1554 * another entry. 1555 * We must not let the change proceed, return a negative 1556 * result and set the result code to NO_SUCH_OBJECT. 1557 * When the operation will return, the thread that started the operation 1558 * will try to find the correct entry and restart a new operation. 1559 */ 1560 return new SynchronizationProviderResult.StopProcessing( 1561 ResultCode.NO_SUCH_OBJECT, null); 1562 } 1563 } 1564 else 1565 { 1566 // There is no replication context attached to the operation 1567 // so this is not a replication operation. 1568 CSN csn = generateCSN(deleteOperation); 1569 String modifiedEntryUUID = getEntryUUID(deletedEntry); 1570 ctx = new DeleteContext(csn, modifiedEntryUUID); 1571 deleteOperation.setAttachment(SYNCHROCONTEXT, ctx); 1572 1573 synchronized (replayOperations) 1574 { 1575 int size = replayOperations.size(); 1576 if (size >= 10000) 1577 { 1578 replayOperations.remove(replayOperations.firstKey()); 1579 } 1580 FakeOperation op = new FakeDelOperation( 1581 deleteOperation.getEntryDN(), csn, modifiedEntryUUID); 1582 replayOperations.put(csn, op); 1583 } 1584 } 1585 1586 return new SynchronizationProviderResult.ContinueProcessing(); 1587 } 1588 1589 /** 1590 * Implement the handleConflictResolution phase of the addOperation. 1591 * 1592 * @param addOperation The AddOperation. 1593 * @return A SynchronizationProviderResult indicating if the operation 1594 * can continue. 1595 */ 1596 SynchronizationProviderResult handleConflictResolution( 1597 PreOperationAddOperation addOperation) 1598 { 1599 if (!addOperation.isSynchronizationOperation() && !brokerIsConnected()) 1600 { 1601 LocalizableMessage msg = ERR_REPLICATION_COULD_NOT_CONNECT.get(getBaseDN()); 1602 return new SynchronizationProviderResult.StopProcessing( 1603 ResultCode.UNWILLING_TO_PERFORM, msg); 1604 } 1605 1606 if (fractionalConfig.isFractional()) 1607 { 1608 if (addOperation.isSynchronizationOperation()) 1609 { 1610 /* 1611 * Filter attributes here for fractional replication. If fractional 1612 * replication is enabled, we analyze the operation to suppress the 1613 * forbidden attributes in it so that they are not added in the local 1614 * backend. This must be called before any other plugin is called, to 1615 * keep coherency across plugin calls. 1616 */ 1617 fractionalFilterOperation(addOperation, true); 1618 } 1619 else 1620 { 1621 /* 1622 * Direct access from an LDAP client : if some attributes are to be 1623 * removed according to the fractional configuration, simply forbid 1624 * the operation 1625 */ 1626 if (fractionalFilterOperation(addOperation, false)) 1627 { 1628 LocalizableMessage msg = NOTE_ERR_FRACTIONAL_FORBIDDEN_OPERATION.get(getBaseDN(), addOperation); 1629 return new SynchronizationProviderResult.StopProcessing( 1630 ResultCode.UNWILLING_TO_PERFORM, msg); 1631 } 1632 } 1633 } 1634 1635 if (addOperation.isSynchronizationOperation()) 1636 { 1637 AddContext ctx = (AddContext) addOperation.getAttachment(SYNCHROCONTEXT); 1638 /* 1639 * If an entry with the same entry uniqueID already exist then 1640 * this operation has already been replayed in the past. 1641 */ 1642 String uuid = ctx.getEntryUUID(); 1643 if (findEntryDN(uuid) != null) 1644 { 1645 return new SynchronizationProviderResult.StopProcessing( 1646 ResultCode.NO_OPERATION, null); 1647 } 1648 1649 /* The parent entry may have been renamed here since the change was done 1650 * on the first server, and another entry have taken the former dn 1651 * of the parent entry 1652 */ 1653 1654 String parentEntryUUID = ctx.getParentEntryUUID(); 1655 // root entry have no parent, there is no need to check for it. 1656 if (parentEntryUUID != null) 1657 { 1658 // There is a potential of perfs improvement here 1659 // if we could avoid the following parent entry retrieval 1660 DN parentDnFromCtx = findEntryDN(ctx.getParentEntryUUID()); 1661 if (parentDnFromCtx == null) 1662 { 1663 // The parent does not exist with the specified unique id 1664 // stop the operation with NO_SUCH_OBJECT and let the 1665 // conflict resolution or the dependency resolution solve this. 1666 return new SynchronizationProviderResult.StopProcessing( 1667 ResultCode.NO_SUCH_OBJECT, null); 1668 } 1669 1670 DN entryDN = addOperation.getEntryDN(); 1671 DN parentDnFromEntryDn = DirectoryServer.getParentDNInSuffix(entryDN); 1672 if (parentDnFromEntryDn != null 1673 && !parentDnFromCtx.equals(parentDnFromEntryDn)) 1674 { 1675 // parentEntry has been renamed 1676 // replication name conflict resolution is expected to fix that 1677 // later in the flow 1678 return new SynchronizationProviderResult.StopProcessing( 1679 ResultCode.NO_SUCH_OBJECT, null); 1680 } 1681 } 1682 } 1683 return new SynchronizationProviderResult.ContinueProcessing(); 1684 } 1685 1686 /** 1687 * Check that the broker associated to this ReplicationDomain has found 1688 * a Replication Server and that this LDAP server is therefore able to 1689 * process operations. 1690 * If not, set the ResultCode, the response message, 1691 * interrupt the operation, and return false 1692 * 1693 * @return true when it OK to process the Operation, false otherwise. 1694 * When false is returned the resultCode and the response message 1695 * is also set in the Operation. 1696 */ 1697 private boolean brokerIsConnected() 1698 { 1699 final IsolationPolicy isolationPolicy = config.getIsolationPolicy(); 1700 if (isolationPolicy.equals(IsolationPolicy.ACCEPT_ALL_UPDATES)) 1701 { 1702 // this policy imply that we always accept updates. 1703 return true; 1704 } 1705 if (isolationPolicy.equals(IsolationPolicy.REJECT_ALL_UPDATES)) 1706 { 1707 // this isolation policy specifies that the updates are denied 1708 // when the broker had problems during the connection phase 1709 // Updates are still accepted if the broker is currently connecting.. 1710 return !hasConnectionError(); 1711 } 1712 // we should never get there as the only possible policies are 1713 // ACCEPT_ALL_UPDATES and REJECT_ALL_UPDATES 1714 return true; 1715 } 1716 1717 /** 1718 * Implement the handleConflictResolution phase of the ModifyDNOperation. 1719 * 1720 * @param modifyDNOperation The ModifyDNOperation. 1721 * @return A SynchronizationProviderResult indicating if the operation 1722 * can continue. 1723 */ 1724 SynchronizationProviderResult handleConflictResolution( 1725 PreOperationModifyDNOperation modifyDNOperation) 1726 { 1727 if (!modifyDNOperation.isSynchronizationOperation() && !brokerIsConnected()) 1728 { 1729 LocalizableMessage msg = ERR_REPLICATION_COULD_NOT_CONNECT.get(getBaseDN()); 1730 return new SynchronizationProviderResult.StopProcessing( 1731 ResultCode.UNWILLING_TO_PERFORM, msg); 1732 } 1733 1734 if (fractionalConfig.isFractional()) 1735 { 1736 if (modifyDNOperation.isSynchronizationOperation()) 1737 { 1738 /* 1739 * Filter operation here for fractional replication. If fractional 1740 * replication is enabled, we analyze the operation and modify it if 1741 * necessary to stay consistent with what is defined in fractional 1742 * configuration. 1743 */ 1744 fractionalFilterOperation(modifyDNOperation, true); 1745 } 1746 else 1747 { 1748 /* 1749 * Direct access from an LDAP client : something is inconsistent with 1750 * the fractional configuration, forbid the operation. 1751 */ 1752 if (fractionalFilterOperation(modifyDNOperation, false)) 1753 { 1754 LocalizableMessage msg = NOTE_ERR_FRACTIONAL_FORBIDDEN_OPERATION.get(getBaseDN(), modifyDNOperation); 1755 return new SynchronizationProviderResult.StopProcessing( 1756 ResultCode.UNWILLING_TO_PERFORM, msg); 1757 } 1758 } 1759 } 1760 1761 ModifyDnContext ctx = 1762 (ModifyDnContext) modifyDNOperation.getAttachment(SYNCHROCONTEXT); 1763 if (ctx != null) 1764 { 1765 /* 1766 * This is a replication operation 1767 * Check that the modified entry has the same entryuuid 1768 * as was in the original message. 1769 */ 1770 final String modifiedEntryUUID = 1771 getEntryUUID(modifyDNOperation.getOriginalEntry()); 1772 if (!modifiedEntryUUID.equals(ctx.getEntryUUID())) 1773 { 1774 /* 1775 * The modified entry is not the same entry as the one on 1776 * the original change was performed. 1777 * Probably the original entry was renamed and replaced with 1778 * another entry. 1779 * We must not let the change proceed, return a negative 1780 * result and set the result code to NO_SUCH_OBJECT. 1781 * When the operation will return, the thread that started the operation 1782 * will try to find the correct entry and restart a new operation. 1783 */ 1784 return new SynchronizationProviderResult.StopProcessing( 1785 ResultCode.NO_SUCH_OBJECT, null); 1786 } 1787 1788 if (modifyDNOperation.getNewSuperior() != null) 1789 { 1790 /* 1791 * Also check that the current id of the 1792 * parent is the same as when the operation was performed. 1793 */ 1794 String newParentId = findEntryUUID(modifyDNOperation.getNewSuperior()); 1795 if (newParentId != null && ctx.getNewSuperiorEntryUUID() != null 1796 && !newParentId.equals(ctx.getNewSuperiorEntryUUID())) 1797 { 1798 return new SynchronizationProviderResult.StopProcessing( 1799 ResultCode.NO_SUCH_OBJECT, null); 1800 } 1801 } 1802 1803 /* 1804 * If the object has been renamed more recently than this 1805 * operation, cancel the operation. 1806 */ 1807 EntryHistorical hist = EntryHistorical.newInstanceFromEntry( 1808 modifyDNOperation.getOriginalEntry()); 1809 if (hist.addedOrRenamedAfter(ctx.getCSN())) 1810 { 1811 return new SynchronizationProviderResult.StopProcessing( 1812 ResultCode.NO_OPERATION, null); 1813 } 1814 } 1815 else 1816 { 1817 // There is no replication context attached to the operation 1818 // so this is not a replication operation. 1819 CSN csn = generateCSN(modifyDNOperation); 1820 String newParentId = null; 1821 if (modifyDNOperation.getNewSuperior() != null) 1822 { 1823 newParentId = findEntryUUID(modifyDNOperation.getNewSuperior()); 1824 } 1825 1826 Entry modifiedEntry = modifyDNOperation.getOriginalEntry(); 1827 String modifiedEntryUUID = getEntryUUID(modifiedEntry); 1828 ctx = new ModifyDnContext(csn, modifiedEntryUUID, newParentId); 1829 modifyDNOperation.setAttachment(SYNCHROCONTEXT, ctx); 1830 } 1831 return new SynchronizationProviderResult.ContinueProcessing(); 1832 } 1833 1834 /** 1835 * Handle the conflict resolution. 1836 * Called by the core server after locking the entry and before 1837 * starting the actual modification. 1838 * @param modifyOperation the operation 1839 * @return code indicating is operation must proceed 1840 */ 1841 SynchronizationProviderResult handleConflictResolution( 1842 PreOperationModifyOperation modifyOperation) 1843 { 1844 if (!modifyOperation.isSynchronizationOperation() && !brokerIsConnected()) 1845 { 1846 LocalizableMessage msg = ERR_REPLICATION_COULD_NOT_CONNECT.get(getBaseDN()); 1847 return new SynchronizationProviderResult.StopProcessing( 1848 ResultCode.UNWILLING_TO_PERFORM, msg); 1849 } 1850 1851 if (fractionalConfig.isFractional()) 1852 { 1853 if (modifyOperation.isSynchronizationOperation()) 1854 { 1855 /* 1856 * Filter attributes here for fractional replication. If fractional 1857 * replication is enabled, we analyze the operation and modify it so 1858 * that no forbidden attribute is added/modified/deleted in the local 1859 * backend. This must be called before any other plugin is called, to 1860 * keep coherency across plugin calls. 1861 */ 1862 if (fractionalFilterOperation(modifyOperation, true) == 1863 FRACTIONAL_BECOME_NO_OP) 1864 { 1865 // Every modifications filtered in this operation: the operation 1866 // becomes a no-op 1867 return new SynchronizationProviderResult.StopProcessing( 1868 ResultCode.NO_OPERATION, null); 1869 } 1870 } 1871 else 1872 { 1873 /* 1874 * Direct access from an LDAP client : if some attributes are to be 1875 * removed according to the fractional configuration, simply forbid 1876 * the operation 1877 */ 1878 switch(fractionalFilterOperation(modifyOperation, false)) 1879 { 1880 case FRACTIONAL_HAS_NO_FRACTIONAL_FILTERED_ATTRIBUTES: 1881 // Ok, let the operation happen 1882 break; 1883 case FRACTIONAL_HAS_FRACTIONAL_FILTERED_ATTRIBUTES: 1884 // Some attributes not compliant with fractional configuration : 1885 // forbid the operation 1886 LocalizableMessage msg = NOTE_ERR_FRACTIONAL_FORBIDDEN_OPERATION.get(getBaseDN(), modifyOperation); 1887 return new SynchronizationProviderResult.StopProcessing( 1888 ResultCode.UNWILLING_TO_PERFORM, msg); 1889 } 1890 } 1891 } 1892 1893 ModifyContext ctx = 1894 (ModifyContext) modifyOperation.getAttachment(SYNCHROCONTEXT); 1895 1896 Entry modifiedEntry = modifyOperation.getModifiedEntry(); 1897 if (ctx == null) 1898 { 1899 // No replication ctx attached => not a replicated operation 1900 // - create a ctx with : CSN, entryUUID 1901 // - attach the context to the op 1902 1903 CSN csn = generateCSN(modifyOperation); 1904 ctx = new ModifyContext(csn, getEntryUUID(modifiedEntry)); 1905 1906 modifyOperation.setAttachment(SYNCHROCONTEXT, ctx); 1907 } 1908 else 1909 { 1910 // Replication ctx attached => this is a replicated operation being 1911 // replayed here, it is necessary to 1912 // - check if the entry has been renamed 1913 // - check for conflicts 1914 String modifiedEntryUUID = ctx.getEntryUUID(); 1915 String currentEntryUUID = getEntryUUID(modifiedEntry); 1916 if (currentEntryUUID != null 1917 && !currentEntryUUID.equals(modifiedEntryUUID)) 1918 { 1919 /* 1920 * The current modified entry is not the same entry as the one on 1921 * the original modification was performed. 1922 * Probably the original entry was renamed and replaced with 1923 * another entry. 1924 * We must not let the modification proceed, return a negative 1925 * result and set the result code to NO_SUCH_OBJECT. 1926 * When the operation will return, the thread that started the 1927 * operation will try to find the correct entry and restart a new 1928 * operation. 1929 */ 1930 return new SynchronizationProviderResult.StopProcessing( 1931 ResultCode.NO_SUCH_OBJECT, null); 1932 } 1933 1934 // Solve the conflicts between modify operations 1935 EntryHistorical historicalInformation = 1936 EntryHistorical.newInstanceFromEntry(modifiedEntry); 1937 modifyOperation.setAttachment(EntryHistorical.HISTORICAL, 1938 historicalInformation); 1939 1940 if (historicalInformation.replayOperation(modifyOperation, modifiedEntry)) 1941 { 1942 numResolvedModifyConflicts.incrementAndGet(); 1943 } 1944 } 1945 return new SynchronizationProviderResult.ContinueProcessing(); 1946 } 1947 1948 /** 1949 * The preOperation phase for the add Operation. 1950 * Its job is to generate the replication context associated to the 1951 * operation. It is necessary to do it in this phase because contrary to 1952 * the other operations, the entry UUID is not set when the handleConflict 1953 * phase is called. 1954 * 1955 * @param addOperation The Add Operation. 1956 */ 1957 void doPreOperation(PreOperationAddOperation addOperation) 1958 { 1959 final CSN csn = generateCSN(addOperation); 1960 final String entryUUID = getEntryUUID(addOperation); 1961 final AddContext ctx = new AddContext(csn, entryUUID, 1962 findEntryUUID(DirectoryServer.getParentDNInSuffix(addOperation.getEntryDN()))); 1963 addOperation.setAttachment(SYNCHROCONTEXT, ctx); 1964 } 1965 1966 @Override 1967 public void publishReplicaOfflineMsg() 1968 { 1969 pendingChanges.putReplicaOfflineMsg(); 1970 dsrsShutdownSync.replicaOfflineMsgSent(getBaseDN()); 1971 } 1972 1973 /** 1974 * Check if an operation must be synchronized. 1975 * Also update the list of pending changes and the server RUV 1976 * @param op the operation 1977 */ 1978 void synchronize(PostOperationOperation op) 1979 { 1980 ResultCode result = op.getResultCode(); 1981 // Note that a failed non-replication operation might not have a change 1982 // number. 1983 CSN curCSN = OperationContext.getCSN(op); 1984 if (curCSN != null && config.isLogChangenumber()) 1985 { 1986 op.addAdditionalLogItem(AdditionalLogItem.unquotedKeyValue(getClass(), 1987 "replicationCSN", curCSN)); 1988 } 1989 1990 if (result == ResultCode.SUCCESS) 1991 { 1992 if (op.isSynchronizationOperation()) 1993 { // Replaying a sync operation 1994 numReplayedPostOpCalled.incrementAndGet(); 1995 try 1996 { 1997 remotePendingChanges.commit(curCSN); 1998 } 1999 catch (NoSuchElementException e) 2000 { 2001 logger.error(ERR_OPERATION_NOT_FOUND_IN_PENDING, op, curCSN); 2002 return; 2003 } 2004 } 2005 else 2006 { 2007 // Generate a replication message for a successful non-replication 2008 // operation. 2009 LDAPUpdateMsg msg = LDAPUpdateMsg.generateMsg(op); 2010 2011 if (msg == null) 2012 { 2013 /* 2014 * This is an operation type that we do not know about 2015 * It should never happen. 2016 */ 2017 pendingChanges.remove(curCSN); 2018 logger.error(ERR_UNKNOWN_TYPE, op.getOperationType()); 2019 return; 2020 } 2021 2022 addEntryAttributesForCL(msg,op); 2023 2024 // If assured replication is configured, this will prepare blocking 2025 // mechanism. If assured replication is disabled, this returns 2026 // immediately 2027 prepareWaitForAckIfAssuredEnabled(msg); 2028 try 2029 { 2030 msg.encode(); 2031 pendingChanges.commitAndPushCommittedChanges(curCSN, msg); 2032 } 2033 catch (NoSuchElementException e) 2034 { 2035 logger.error(ERR_OPERATION_NOT_FOUND_IN_PENDING, op, curCSN); 2036 return; 2037 } 2038 // If assured replication is enabled, this will wait for the matching 2039 // ack or time out. If assured replication is disabled, this returns 2040 // immediately 2041 try 2042 { 2043 waitForAckIfAssuredEnabled(msg); 2044 } catch (TimeoutException ex) 2045 { 2046 // This exception may only be raised if assured replication is enabled 2047 logger.info(NOTE_DS_ACK_TIMEOUT, getBaseDN(), getAssuredTimeout(), msg); 2048 } 2049 } 2050 2051 /* 2052 * If the operation is a DELETE on the base entry of the suffix 2053 * that is replicated, the generation is now lost because the 2054 * DB is empty. We need to save it again the next time we add an entry. 2055 */ 2056 if (OperationType.DELETE.equals(op.getOperationType()) 2057 && ((PostOperationDeleteOperation) op) 2058 .getEntryDN().equals(getBaseDN())) 2059 { 2060 generationIdSavedStatus = false; 2061 } 2062 2063 if (!generationIdSavedStatus) 2064 { 2065 saveGenerationId(generationId); 2066 } 2067 } 2068 else if (!op.isSynchronizationOperation() && curCSN != null) 2069 { 2070 // Remove an unsuccessful non-replication operation from the pending 2071 // changes list. 2072 pendingChanges.remove(curCSN); 2073 pendingChanges.pushCommittedChanges(); 2074 } 2075 2076 checkForClearedConflict(op); 2077 } 2078 2079 /** 2080 * Check if the operation that just happened has cleared a conflict : 2081 * Clearing a conflict happens if the operation has free a DN that 2082 * for which an other entry was in conflict. 2083 * Steps: 2084 * - get the DN freed by a DELETE or MODRDN op 2085 * - search for entries put in the conflict space (dn=entryUUID'+'....) 2086 * because the expected DN was not available (ds-sync-conflict=expected DN) 2087 * - retain the entry with the oldest conflict 2088 * - rename this entry with the freedDN as it was expected originally 2089 */ 2090 private void checkForClearedConflict(PostOperationOperation op) 2091 { 2092 OperationType type = op.getOperationType(); 2093 if (op.getResultCode() != ResultCode.SUCCESS) 2094 { 2095 // those operations cannot have cleared a conflict 2096 return; 2097 } 2098 2099 DN freedDN; 2100 if (type == OperationType.DELETE) 2101 { 2102 freedDN = ((PostOperationDeleteOperation) op).getEntryDN(); 2103 } 2104 else if (type == OperationType.MODIFY_DN) 2105 { 2106 freedDN = ((PostOperationModifyDNOperation) op).getEntryDN(); 2107 } 2108 else 2109 { 2110 return; 2111 } 2112 2113 SearchFilter filter; 2114 try 2115 { 2116 filter = LDAPFilter.createEqualityFilter(DS_SYNC_CONFLICT, 2117 ByteString.valueOfUtf8(freedDN.toString())).toSearchFilter(); 2118 } 2119 catch (DirectoryException e) 2120 { 2121 // can not happen? 2122 logger.traceException(e); 2123 return; 2124 } 2125 2126 SearchRequest request = newSearchRequest(getBaseDN(), SearchScope.WHOLE_SUBTREE, filter) 2127 .addAttribute(USER_AND_REPL_OPERATIONAL_ATTRS); 2128 InternalSearchOperation searchOp = conn.processSearch(request); 2129 2130 Entry entryToRename = null; 2131 CSN entryToRenameCSN = null; 2132 for (SearchResultEntry entry : searchOp.getSearchEntries()) 2133 { 2134 EntryHistorical history = EntryHistorical.newInstanceFromEntry(entry); 2135 if (entryToRename == null) 2136 { 2137 entryToRename = entry; 2138 entryToRenameCSN = history.getDNDate(); 2139 } 2140 else if (!history.addedOrRenamedAfter(entryToRenameCSN)) 2141 { 2142 // this conflict is older than the previous, keep it. 2143 entryToRename = entry; 2144 entryToRenameCSN = history.getDNDate(); 2145 } 2146 } 2147 2148 if (entryToRename != null) 2149 { 2150 DN entryDN = entryToRename.getName(); 2151 ModifyDNOperation newOp = renameEntry( 2152 entryDN, freedDN.rdn(), freedDN.parent(), false); 2153 2154 ResultCode res = newOp.getResultCode(); 2155 if (res != ResultCode.SUCCESS) 2156 { 2157 logger.error(ERR_COULD_NOT_SOLVE_CONFLICT, entryDN, res); 2158 } 2159 } 2160 } 2161 2162 /** 2163 * Rename an Entry Using a synchronization, non-replicated operation. 2164 * This method should be used instead of the InternalConnection methods 2165 * when the operation that need to be run must be local only and therefore 2166 * not replicated to the RS. 2167 * 2168 * @param targetDN The DN of the entry to rename. 2169 * @param newRDN The new RDN to be used. 2170 * @param parentDN The parentDN to be used. 2171 * @param markConflict A boolean indicating is this entry should be marked 2172 * as a conflicting entry. In such case the 2173 * DS_SYNC_CONFLICT attribute will be added to the entry 2174 * with the value of its original DN. 2175 * If false, the DS_SYNC_CONFLICT attribute will be 2176 * cleared. 2177 * 2178 * @return The operation that was run to rename the entry. 2179 */ 2180 private ModifyDNOperation renameEntry(DN targetDN, RDN newRDN, DN parentDN, 2181 boolean markConflict) 2182 { 2183 ModifyDNOperation newOp = new ModifyDNOperationBasis( 2184 conn, nextOperationID(), nextMessageID(), new ArrayList<Control>(0), 2185 targetDN, newRDN, false, parentDN); 2186 2187 if (markConflict) 2188 { 2189 Attribute attr = Attributes.create(DS_SYNC_CONFLICT, targetDN.toString()); 2190 newOp.addModification(new Modification(ModificationType.REPLACE, attr)); 2191 } 2192 else 2193 { 2194 Attribute attr = Attributes.empty(DS_SYNC_CONFLICT); 2195 newOp.addModification(new Modification(ModificationType.DELETE, attr)); 2196 } 2197 2198 runAsSynchronizedOperation(newOp); 2199 return newOp; 2200 } 2201 2202 private void runAsSynchronizedOperation(Operation op) 2203 { 2204 op.setInternalOperation(true); 2205 op.setSynchronizationOperation(true); 2206 op.setDontSynchronize(true); 2207 op.run(); 2208 } 2209 2210 /** Delete this ReplicationDomain. */ 2211 void delete() 2212 { 2213 shutdown(); 2214 removeECLDomainCfg(); 2215 } 2216 2217 /** Shutdown this ReplicationDomain. */ 2218 public void shutdown() 2219 { 2220 if (shutdown.compareAndSet(false, true)) 2221 { 2222 final RSUpdater rsUpdater = this.rsUpdater.get(); 2223 if (rsUpdater != null) 2224 { 2225 rsUpdater.initiateShutdown(); 2226 } 2227 2228 // stop the thread in charge of flushing the ServerState. 2229 if (flushThread != null) 2230 { 2231 flushThread.initiateShutdown(); 2232 synchronized (flushThread) 2233 { 2234 flushThread.notify(); 2235 } 2236 } 2237 2238 DirectoryServer.deregisterAlertGenerator(this); 2239 DirectoryServer.deregisterBackendInitializationListener(this); 2240 DirectoryServer.deregisterShutdownListener(this); 2241 2242 // stop the ReplicationDomain 2243 disableService(); 2244 } 2245 2246 // wait for completion of the ServerStateFlush thread. 2247 try 2248 { 2249 while (!done) 2250 { 2251 Thread.sleep(50); 2252 } 2253 } catch (InterruptedException e) 2254 { 2255 Thread.currentThread().interrupt(); 2256 } 2257 } 2258 2259 /** 2260 * Marks the specified message as the one currently processed by a replay thread. 2261 * @param msg the message being processed 2262 */ 2263 void markInProgress(LDAPUpdateMsg msg) 2264 { 2265 remotePendingChanges.markInProgress(msg); 2266 } 2267 2268 /** 2269 * Create and replay a synchronized Operation from an UpdateMsg. 2270 * 2271 * @param msg 2272 * The UpdateMsg to be replayed. 2273 * @param shutdown 2274 * whether the server initiated shutdown 2275 */ 2276 void replay(LDAPUpdateMsg msg, AtomicBoolean shutdown) 2277 { 2278 // Try replay the operation, then flush (replaying) any pending operation 2279 // whose dependency has been replayed until no more left. 2280 do 2281 { 2282 Operation op = null; // the last operation on which replay was attempted 2283 boolean dependency = false; 2284 String replayErrorMsg = null; 2285 CSN csn = null; 2286 try 2287 { 2288 // The next operation for which to attempt replay. 2289 // This local variable allow to keep error messages in the "op" local 2290 // variable until the next loop iteration starts. 2291 // "op" is already initialized to the next Operation because of the 2292 // error handling paths. 2293 Operation nextOp = op = msg.createOperation(conn); 2294 dependency = remotePendingChanges.checkDependencies(op, msg); 2295 boolean replayDone = false; 2296 int retryCount = 10; 2297 while (!dependency && !replayDone && retryCount-- > 0) 2298 { 2299 if (shutdown.get()) 2300 { 2301 // shutdown initiated, let's leave 2302 return; 2303 } 2304 // Try replay the operation 2305 op = nextOp; 2306 op.setInternalOperation(true); 2307 op.setSynchronizationOperation(true); 2308 2309 // Always add the ManageDSAIT control so that updates to referrals 2310 // are processed locally. 2311 op.addRequestControl(new LDAPControl(OID_MANAGE_DSAIT_CONTROL)); 2312 2313 csn = OperationContext.getCSN(op); 2314 op.run(); 2315 2316 ResultCode result = op.getResultCode(); 2317 2318 if (result != ResultCode.SUCCESS) 2319 { 2320 if (result == ResultCode.NO_OPERATION) 2321 { 2322 // Pre-operation conflict resolution detected that the operation 2323 // was a no-op. For example, an add which has already been 2324 // replayed, or a modify DN operation on an entry which has been 2325 // renamed by a more recent modify DN. 2326 replayDone = true; 2327 } 2328 else if (result == ResultCode.BUSY) 2329 { 2330 /* 2331 * We probably could not get a lock (OPENDJ-885). Give the server 2332 * another chance to process this operation immediately. 2333 */ 2334 Thread.yield(); 2335 continue; 2336 } 2337 else if (result == ResultCode.UNAVAILABLE) 2338 { 2339 /* 2340 * It can happen when a rebuild is performed or the backend is 2341 * offline (OPENDJ-49). Give the server another chance to process 2342 * this operation after some time. 2343 */ 2344 Thread.sleep(50); 2345 continue; 2346 } 2347 else if (op instanceof ModifyOperation) 2348 { 2349 ModifyOperation castOp = (ModifyOperation) op; 2350 dependency = remotePendingChanges.checkDependencies(castOp); 2351 ModifyMsg modifyMsg = (ModifyMsg) msg; 2352 replayDone = !dependency && solveNamingConflict(castOp, modifyMsg); 2353 } 2354 else if (op instanceof DeleteOperation) 2355 { 2356 DeleteOperation castOp = (DeleteOperation) op; 2357 dependency = remotePendingChanges.checkDependencies(castOp); 2358 replayDone = !dependency && solveNamingConflict(castOp, msg); 2359 } 2360 else if (op instanceof AddOperation) 2361 { 2362 AddOperation castOp = (AddOperation) op; 2363 AddMsg addMsg = (AddMsg) msg; 2364 dependency = remotePendingChanges.checkDependencies(castOp); 2365 replayDone = !dependency && solveNamingConflict(castOp, addMsg); 2366 } 2367 else if (op instanceof ModifyDNOperation) 2368 { 2369 ModifyDNOperation castOp = (ModifyDNOperation) op; 2370 ModifyDNMsg modifyDNMsg = (ModifyDNMsg) msg; 2371 dependency = remotePendingChanges.checkDependencies(modifyDNMsg); 2372 replayDone = !dependency && solveNamingConflict(castOp, modifyDNMsg); 2373 } 2374 else 2375 { 2376 replayDone = true; // unknown type of operation ?! 2377 } 2378 2379 if (replayDone) 2380 { 2381 // the update became a dummy update and the result 2382 // of the conflict resolution phase is to do nothing. 2383 // however we still need to push this change to the serverState 2384 updateError(csn); 2385 } 2386 else 2387 { 2388 /* 2389 * Create a new operation reflecting the new state of the UpdateMsg after conflict resolution 2390 * modified it and try replaying it again. Dependencies might have been replayed by now. 2391 * Note: When msg is a DeleteMsg, the DeleteOperation is properly 2392 * created with subtreeDelete request control when needed. 2393 */ 2394 nextOp = msg.createOperation(conn); 2395 } 2396 } 2397 else 2398 { 2399 replayDone = true; 2400 } 2401 } 2402 2403 if (!replayDone && !dependency) 2404 { 2405 // Continue with the next change but the servers could now become 2406 // inconsistent. 2407 // Let the repair tool know about this. 2408 final LocalizableMessage message = ERR_LOOP_REPLAYING_OPERATION.get( 2409 op, op.getErrorMessage()); 2410 logger.error(message); 2411 numUnresolvedNamingConflicts.incrementAndGet(); 2412 replayErrorMsg = message.toString(); 2413 updateError(csn); 2414 } 2415 } catch (DecodeException | LDAPException | DataFormatException e) 2416 { 2417 replayErrorMsg = logDecodingOperationError(msg, e); 2418 } catch (Exception e) 2419 { 2420 if (csn != null) 2421 { 2422 /* 2423 * An Exception happened during the replay process. 2424 * Continue with the next change but the servers will now start 2425 * to be inconsistent. 2426 * Let the repair tool know about this. 2427 */ 2428 LocalizableMessage message = 2429 ERR_EXCEPTION_REPLAYING_OPERATION.get( 2430 stackTraceToSingleLineString(e), op); 2431 logger.error(message); 2432 replayErrorMsg = message.toString(); 2433 updateError(csn); 2434 } else 2435 { 2436 replayErrorMsg = logDecodingOperationError(msg, e); 2437 } 2438 } finally 2439 { 2440 if (!dependency) 2441 { 2442 processUpdateDone(msg, replayErrorMsg); 2443 } 2444 } 2445 2446 // Now replay any pending update that had a dependency and whose 2447 // dependency has been replayed, do that until no more updates of that 2448 // type left... 2449 msg = remotePendingChanges.getNextUpdate(); 2450 } while (msg != null); 2451 } 2452 2453 private String logDecodingOperationError(LDAPUpdateMsg msg, Exception e) 2454 { 2455 LocalizableMessage message = 2456 ERR_EXCEPTION_DECODING_OPERATION.get(msg + " " + stackTraceToSingleLineString(e)); 2457 logger.error(message); 2458 return message.toString(); 2459 } 2460 2461 /** 2462 * This method is called when an error happens while replaying 2463 * an operation. 2464 * It is necessary because the postOperation does not always get 2465 * called when error or Exceptions happen during the operation replay. 2466 * 2467 * @param csn the CSN of the operation with error. 2468 */ 2469 private void updateError(CSN csn) 2470 { 2471 try 2472 { 2473 remotePendingChanges.commit(csn); 2474 } 2475 catch (NoSuchElementException e) 2476 { 2477 // A failure occurred after the change had been removed from the pending 2478 // changes table. 2479 if (logger.isTraceEnabled()) 2480 { 2481 logger.trace( 2482 "LDAPReplicationDomain.updateError: Unable to find remote " 2483 + "pending change for CSN %s", csn); 2484 } 2485 } 2486 } 2487 2488 /** 2489 * Generate a new CSN and insert it in the pending list. 2490 * 2491 * @param operation 2492 * The operation for which the CSN must be generated. 2493 * @return The new CSN. 2494 */ 2495 private CSN generateCSN(PluginOperation operation) 2496 { 2497 return pendingChanges.putLocalOperation(operation); 2498 } 2499 2500 /** 2501 * Find the Unique Id of the entry with the provided DN by doing a 2502 * search of the entry and extracting its entryUUID from its attributes. 2503 * 2504 * @param dn The dn of the entry for which the unique Id is searched. 2505 * 2506 * @return The unique Id of the entry with the provided DN. 2507 */ 2508 static String findEntryUUID(DN dn) 2509 { 2510 if (dn == null) 2511 { 2512 return null; 2513 } 2514 final SearchRequest request = newSearchRequest(dn, SearchScope.BASE_OBJECT) 2515 .addAttribute(ENTRYUUID_ATTRIBUTE_NAME); 2516 final InternalSearchOperation search = getRootConnection().processSearch(request); 2517 final SearchResultEntry resultEntry = getFirstResult(search); 2518 if (resultEntry != null) 2519 { 2520 return getEntryUUID(resultEntry); 2521 } 2522 return null; 2523 } 2524 2525 private static SearchResultEntry getFirstResult(InternalSearchOperation search) 2526 { 2527 if (search.getResultCode() == ResultCode.SUCCESS) 2528 { 2529 final LinkedList<SearchResultEntry> results = search.getSearchEntries(); 2530 if (!results.isEmpty()) 2531 { 2532 return results.getFirst(); 2533 } 2534 } 2535 return null; 2536 } 2537 2538 /** 2539 * Find the current DN of an entry from its entry UUID. 2540 * 2541 * @param uuid the Entry Unique ID. 2542 * @return The current DN of the entry or null if there is no entry with 2543 * the specified UUID. 2544 */ 2545 private DN findEntryDN(String uuid) 2546 { 2547 try 2548 { 2549 final SearchRequest request = newSearchRequest(getBaseDN(), SearchScope.WHOLE_SUBTREE, "entryuuid=" + uuid); 2550 InternalSearchOperation search = conn.processSearch(request); 2551 final SearchResultEntry resultEntry = getFirstResult(search); 2552 if (resultEntry != null) 2553 { 2554 return resultEntry.getName(); 2555 } 2556 } 2557 catch (DirectoryException e) 2558 { 2559 // never happens because the filter is always valid. 2560 } 2561 return null; 2562 } 2563 2564 /** 2565 * Solve a conflict detected when replaying a modify operation. 2566 * 2567 * @param op The operation that triggered the conflict detection. 2568 * @param msg The operation that triggered the conflict detection. 2569 * @return true if the process is completed, false if it must continue.. 2570 */ 2571 private boolean solveNamingConflict(ModifyOperation op, ModifyMsg msg) 2572 { 2573 ResultCode result = op.getResultCode(); 2574 ModifyContext ctx = (ModifyContext) op.getAttachment(SYNCHROCONTEXT); 2575 String entryUUID = ctx.getEntryUUID(); 2576 2577 if (result == ResultCode.NO_SUCH_OBJECT) 2578 { 2579 /* 2580 * The operation is a modification but 2581 * the entry has been renamed on a different master in the same time. 2582 * search if the entry has been renamed, and return the new dn 2583 * of the entry. 2584 */ 2585 DN newDN = findEntryDN(entryUUID); 2586 if (newDN != null) 2587 { 2588 // There is an entry with the same unique id as this modify operation 2589 // replay the modify using the current dn of this entry. 2590 msg.setDN(newDN); 2591 numResolvedNamingConflicts.incrementAndGet(); 2592 return false; 2593 } 2594 else 2595 { 2596 // This entry does not exist anymore. 2597 // It has probably been deleted, stop the processing of this operation 2598 numResolvedNamingConflicts.incrementAndGet(); 2599 return true; 2600 } 2601 } 2602 else if (result == ResultCode.NOT_ALLOWED_ON_RDN) 2603 { 2604 DN currentDN = findEntryDN(entryUUID); 2605 RDN currentRDN; 2606 if (currentDN != null) 2607 { 2608 currentRDN = currentDN.rdn(); 2609 } 2610 else 2611 { 2612 // The entry does not exist anymore. 2613 numResolvedNamingConflicts.incrementAndGet(); 2614 return true; 2615 } 2616 2617 // The modify operation is trying to delete the value that is 2618 // currently used in the RDN. We need to alter the modify so that it does 2619 // not remove the current RDN value(s). 2620 2621 List<Modification> mods = op.getModifications(); 2622 for (Modification mod : mods) 2623 { 2624 AttributeType modAttrType = mod.getAttribute().getAttributeDescription().getAttributeType(); 2625 if ((mod.getModificationType() == ModificationType.DELETE 2626 || mod.getModificationType() == ModificationType.REPLACE) 2627 && currentRDN.hasAttributeType(modAttrType)) 2628 { 2629 // the attribute can't be deleted because it is used in the RDN, 2630 // turn this operation is a replace with the current RDN value(s); 2631 mod.setModificationType(ModificationType.REPLACE); 2632 Attribute newAttribute = mod.getAttribute(); 2633 AttributeBuilder attrBuilder = new AttributeBuilder(newAttribute); 2634 attrBuilder.add(currentRDN.getAttributeValue(modAttrType)); 2635 mod.setAttribute(attrBuilder.toAttribute()); 2636 } 2637 } 2638 msg.setMods(mods); 2639 numResolvedNamingConflicts.incrementAndGet(); 2640 return false; 2641 } 2642 else 2643 { 2644 // The other type of errors can not be caused by naming conflicts. 2645 // Log a message for the repair tool. 2646 logger.error(ERR_ERROR_REPLAYING_OPERATION, 2647 op, ctx.getCSN(), result, op.getErrorMessage()); 2648 return true; 2649 } 2650 } 2651 2652 /** 2653 * Solve a conflict detected when replaying a delete operation. 2654 * 2655 * @param op The operation that triggered the conflict detection. 2656 * @param msg The operation that triggered the conflict detection. 2657 * @return true if the process is completed, false if it must continue.. 2658 */ 2659 private boolean solveNamingConflict(DeleteOperation op, LDAPUpdateMsg msg) 2660 { 2661 ResultCode result = op.getResultCode(); 2662 DeleteContext ctx = (DeleteContext) op.getAttachment(SYNCHROCONTEXT); 2663 String entryUUID = ctx.getEntryUUID(); 2664 2665 if (result == ResultCode.NO_SUCH_OBJECT) 2666 { 2667 /* Find if the entry is still in the database. */ 2668 DN currentDN = findEntryDN(entryUUID); 2669 if (currentDN == null) 2670 { 2671 /* 2672 * The entry has already been deleted, either because this delete 2673 * has already been replayed or because another concurrent delete 2674 * has already done the job. 2675 * In any case, there is nothing more to do. 2676 */ 2677 numResolvedNamingConflicts.incrementAndGet(); 2678 return true; 2679 } 2680 else 2681 { 2682 // This entry has been renamed, replay the delete using its new DN. 2683 msg.setDN(currentDN); 2684 numResolvedNamingConflicts.incrementAndGet(); 2685 return false; 2686 } 2687 } 2688 else if (result == ResultCode.NOT_ALLOWED_ON_NONLEAF) 2689 { 2690 /* 2691 * This may happen when we replay a DELETE done on a master 2692 * but children of this entry have been added on another master. 2693 * 2694 * Rename all the children by adding entryuuid in dn and delete this entry. 2695 * 2696 * The action taken here must be consistent with the actions 2697 * done in the solveNamingConflict(AddOperation) method 2698 * when we are adding an entry whose parent entry has already been deleted. 2699 */ 2700 if (findAndRenameChild(op.getEntryDN(), op)) 2701 { 2702 numUnresolvedNamingConflicts.incrementAndGet(); 2703 } 2704 2705 return false; 2706 } 2707 else 2708 { 2709 // The other type of errors can not be caused by naming conflicts. 2710 // Log a message for the repair tool. 2711 logger.error(ERR_ERROR_REPLAYING_OPERATION, 2712 op, ctx.getCSN(), result, op.getErrorMessage()); 2713 return true; 2714 } 2715 } 2716 2717/** 2718 * Solve a conflict detected when replaying a Modify DN operation. 2719 * 2720 * @param op The operation that triggered the conflict detection. 2721 * @param msg The operation that triggered the conflict detection. 2722 * @return true if the process is completed, false if it must continue. 2723 * @throws Exception When the operation is not valid. 2724 */ 2725private boolean solveNamingConflict(ModifyDNOperation op, LDAPUpdateMsg msg) 2726 throws Exception 2727{ 2728 ResultCode result = op.getResultCode(); 2729 ModifyDnContext ctx = (ModifyDnContext) op.getAttachment(SYNCHROCONTEXT); 2730 String entryUUID = ctx.getEntryUUID(); 2731 String newSuperiorID = ctx.getNewSuperiorEntryUUID(); 2732 2733 /* 2734 * four possible cases : 2735 * - the modified entry has been renamed 2736 * - the new parent has been renamed 2737 * - the operation is replayed for the second time. 2738 * - the entry has been deleted 2739 * action : 2740 * - change the target dn and the new parent dn and 2741 * restart the operation, 2742 * - don't do anything if the operation is replayed. 2743 */ 2744 2745 // get the current DN of this entry in the database. 2746 DN currentDN = findEntryDN(entryUUID); 2747 2748 // Construct the new DN to use for the entry. 2749 DN entryDN = op.getEntryDN(); 2750 DN newSuperior; 2751 RDN newRDN = op.getNewRDN(); 2752 2753 if (newSuperiorID != null) 2754 { 2755 newSuperior = findEntryDN(newSuperiorID); 2756 } 2757 else 2758 { 2759 newSuperior = entryDN.parent(); 2760 } 2761 2762 //If we could not find the new parent entry, we missed this entry 2763 // earlier or it has disappeared from the database 2764 // Log this information for the repair tool and mark the entry 2765 // as conflicting. 2766 // stop the processing. 2767 if (newSuperior == null) 2768 { 2769 markConflictEntry(op, currentDN, currentDN.parent().child(newRDN)); 2770 numUnresolvedNamingConflicts.incrementAndGet(); 2771 return true; 2772 } 2773 2774 DN newDN = newSuperior.child(newRDN); 2775 2776 if (currentDN == null) 2777 { 2778 // The entry targeted by the Modify DN is not in the database 2779 // anymore. 2780 // This is a conflict between a delete and this modify DN. 2781 // The entry has been deleted, we can safely assume 2782 // that the operation is completed. 2783 numResolvedNamingConflicts.incrementAndGet(); 2784 return true; 2785 } 2786 2787 // if the newDN and the current DN match then the operation 2788 // is a no-op (this was probably a second replay) 2789 // don't do anything. 2790 if (newDN.equals(currentDN)) 2791 { 2792 numResolvedNamingConflicts.incrementAndGet(); 2793 return true; 2794 } 2795 2796 if (result == ResultCode.NO_SUCH_OBJECT 2797 || result == ResultCode.UNWILLING_TO_PERFORM 2798 || result == ResultCode.OBJECTCLASS_VIOLATION) 2799 { 2800 /* 2801 * The entry or it's new parent has not been found 2802 * reconstruct the operation with the DN we just built 2803 */ 2804 ModifyDNMsg modifyDnMsg = (ModifyDNMsg) msg; 2805 modifyDnMsg.setDN(currentDN); 2806 modifyDnMsg.setNewSuperior(newSuperior.toString()); 2807 numResolvedNamingConflicts.incrementAndGet(); 2808 return false; 2809 } 2810 else if (result == ResultCode.ENTRY_ALREADY_EXISTS) 2811 { 2812 /* 2813 * This may happen when two modifyDn operation 2814 * are done on different servers but with the same target DN 2815 * add the conflict object class to the entry 2816 * and rename it using its entryuuid. 2817 */ 2818 ModifyDNMsg modifyDnMsg = (ModifyDNMsg) msg; 2819 markConflictEntry(op, op.getEntryDN(), newDN); 2820 modifyDnMsg.setNewRDN(generateConflictRDN(entryUUID, 2821 modifyDnMsg.getNewRDN())); 2822 modifyDnMsg.setNewSuperior(newSuperior.toString()); 2823 numUnresolvedNamingConflicts.incrementAndGet(); 2824 return false; 2825 } 2826 else 2827 { 2828 // The other type of errors can not be caused by naming conflicts. 2829 // Log a message for the repair tool. 2830 logger.error(ERR_ERROR_REPLAYING_OPERATION, 2831 op, ctx.getCSN(), result, op.getErrorMessage()); 2832 return true; 2833 } 2834} 2835 2836 /** 2837 * Solve a conflict detected when replaying a ADD operation. 2838 * 2839 * @param op The operation that triggered the conflict detection. 2840 * @param msg The message that triggered the conflict detection. 2841 * @return true if the process is completed, false if it must continue. 2842 * @throws Exception When the operation is not valid. 2843 */ 2844 private boolean solveNamingConflict(AddOperation op, AddMsg msg) 2845 throws Exception 2846 { 2847 ResultCode result = op.getResultCode(); 2848 AddContext ctx = (AddContext) op.getAttachment(SYNCHROCONTEXT); 2849 String entryUUID = ctx.getEntryUUID(); 2850 String parentUniqueId = ctx.getParentEntryUUID(); 2851 2852 if (result == ResultCode.NO_SUCH_OBJECT) 2853 { 2854 /* 2855 * This can happen if the parent has been renamed or deleted 2856 * find the parent dn and calculate a new dn for the entry 2857 */ 2858 if (parentUniqueId == null) 2859 { 2860 /* 2861 * This entry is the base dn of the backend. 2862 * It is quite surprising that the operation result be NO_SUCH_OBJECT. 2863 * There is nothing more we can do except log a 2864 * message for the repair tool to look at this problem. 2865 * TODO : Log the message 2866 */ 2867 return true; 2868 } 2869 DN parentDn = findEntryDN(parentUniqueId); 2870 if (parentDn == null) 2871 { 2872 /* 2873 * The parent has been deleted 2874 * rename the entry as a conflicting entry. 2875 * The action taken here must be consistent with the actions 2876 * done when in the solveNamingConflict(DeleteOperation) method 2877 * when we are deleting an entry that have some child entries. 2878 */ 2879 addConflict(msg); 2880 2881 String conflictRDN = 2882 generateConflictRDN(entryUUID, op.getEntryDN().rdn().toString()); 2883 msg.setDN(DN.valueOf(conflictRDN + "," + getBaseDN())); 2884 // reset the parent entryUUID so that the check done is the 2885 // handleConflict phase does not fail. 2886 msg.setParentEntryUUID(null); 2887 numUnresolvedNamingConflicts.incrementAndGet(); 2888 } 2889 else 2890 { 2891 msg.setDN(DN.valueOf(msg.getDN().rdn() + "," + parentDn)); 2892 numResolvedNamingConflicts.incrementAndGet(); 2893 } 2894 return false; 2895 } 2896 else if (result == ResultCode.ENTRY_ALREADY_EXISTS) 2897 { 2898 /* 2899 * This can happen if 2900 * - two adds are done on different servers but with the 2901 * same target DN. 2902 * - the same ADD is being replayed for the second time on this server. 2903 * if the entryUUID already exist, assume this is a replay and 2904 * don't do anything 2905 * if the entry unique id do not exist, generate conflict. 2906 */ 2907 if (findEntryDN(entryUUID) != null) 2908 { 2909 // entry already exist : this is a replay 2910 return true; 2911 } 2912 else 2913 { 2914 addConflict(msg); 2915 String conflictRDN = 2916 generateConflictRDN(entryUUID, msg.getDN().toString()); 2917 msg.setDN(DN.valueOf(conflictRDN)); 2918 numUnresolvedNamingConflicts.incrementAndGet(); 2919 return false; 2920 } 2921 } 2922 else 2923 { 2924 // The other type of errors can not be caused by naming conflicts. 2925 // log a message for the repair tool. 2926 logger.error(ERR_ERROR_REPLAYING_OPERATION, 2927 op, ctx.getCSN(), result, op.getErrorMessage()); 2928 return true; 2929 } 2930 } 2931 2932 /** 2933 * Find all the entries below the provided DN and rename them 2934 * so that they stay below the baseDN of this replicationDomain and 2935 * use the conflicting name and attribute. 2936 * 2937 * @param entryDN The DN of the entry whose child must be renamed. 2938 * @param conflictOp The Operation that generated the conflict. 2939 */ 2940 private boolean findAndRenameChild(DN entryDN, Operation conflictOp) 2941 { 2942 /* 2943 * TODO JNR Ludo thinks that: "Ideally, the operation should verify that the 2944 * entryUUID has not changed or try to use the entryUUID rather than the 2945 * DN.". entryUUID can be obtained from the caller of the current method. 2946 */ 2947 boolean conflict = false; 2948 2949 // Find and rename child entries. 2950 final SearchRequest request = newSearchRequest(entryDN, SearchScope.SINGLE_LEVEL) 2951 .addAttribute(ENTRYUUID_ATTRIBUTE_NAME, HISTORICAL_ATTRIBUTE_NAME); 2952 InternalSearchOperation op = conn.processSearch(request); 2953 if (op.getResultCode() == ResultCode.SUCCESS) 2954 { 2955 for (SearchResultEntry entry : op.getSearchEntries()) 2956 { 2957 /* 2958 * Check the ADD and ModRDN date of the child entry 2959 * (All of them, not only the one that are newer than the DEL op) 2960 * and keep the entry as a conflicting entry. 2961 */ 2962 conflict = true; 2963 renameConflictEntry(conflictOp, entry.getName(), getEntryUUID(entry)); 2964 } 2965 } 2966 else 2967 { 2968 // log error and information for the REPAIR tool. 2969 logger.error(ERR_CANNOT_RENAME_CONFLICT_ENTRY, entryDN, conflictOp, op.getResultCode()); 2970 } 2971 2972 return conflict; 2973 } 2974 2975 /** 2976 * Rename an entry that was conflicting so that it stays below the 2977 * baseDN of the replicationDomain. 2978 * 2979 * @param conflictOp The Operation that caused the conflict. 2980 * @param dn The DN of the entry to be renamed. 2981 * @param entryUUID The uniqueID of the entry to be renamed. 2982 */ 2983 private void renameConflictEntry(Operation conflictOp, DN dn, 2984 String entryUUID) 2985 { 2986 LocalizableMessage alertMessage = NOTE_UNRESOLVED_CONFLICT.get(dn); 2987 DirectoryServer.sendAlertNotification(this, 2988 ALERT_TYPE_REPLICATION_UNRESOLVED_CONFLICT, alertMessage); 2989 2990 RDN newRDN = generateDeleteConflictDn(entryUUID, dn); 2991 ModifyDNOperation newOp = renameEntry(dn, newRDN, getBaseDN(), true); 2992 2993 if (newOp.getResultCode() != ResultCode.SUCCESS) 2994 { 2995 // log information for the repair tool. 2996 logger.error(ERR_CANNOT_RENAME_CONFLICT_ENTRY, 2997 dn, conflictOp, newOp.getResultCode()); 2998 } 2999 } 3000 3001 /** 3002 * Generate a modification to add the conflict attribute to an entry 3003 * whose Dn is now conflicting with another entry. 3004 * 3005 * @param op The operation causing the conflict. 3006 * @param currentDN The current DN of the operation to mark as conflicting. 3007 * @param conflictDN The newDn on which the conflict happened. 3008 */ 3009 private void markConflictEntry(Operation op, DN currentDN, DN conflictDN) 3010 { 3011 // create new internal modify operation and run it. 3012 Attribute attr = Attributes.create(DS_SYNC_CONFLICT, conflictDN.toString()); 3013 List<Modification> mods = newArrayList(new Modification(ModificationType.REPLACE, attr)); 3014 3015 ModifyOperation newOp = new ModifyOperationBasis( 3016 conn, nextOperationID(), nextMessageID(), new ArrayList<Control>(0), 3017 currentDN, mods); 3018 runAsSynchronizedOperation(newOp); 3019 3020 if (newOp.getResultCode() != ResultCode.SUCCESS) 3021 { 3022 // Log information for the repair tool. 3023 logger.error(ERR_CANNOT_ADD_CONFLICT_ATTRIBUTE, op, newOp.getResultCode()); 3024 } 3025 3026 // Generate an alert to let the administration know that some 3027 // conflict could not be solved. 3028 LocalizableMessage alertMessage = NOTE_UNRESOLVED_CONFLICT.get(conflictDN); 3029 DirectoryServer.sendAlertNotification(this, 3030 ALERT_TYPE_REPLICATION_UNRESOLVED_CONFLICT, alertMessage); 3031 } 3032 3033 /** 3034 * Add the conflict attribute to an entry that could 3035 * not be added because it is conflicting with another entry. 3036 * 3037 * @param msg The conflicting Add Operation. 3038 * 3039 * @throws DecodeException When an encoding error happened manipulating the 3040 * msg. 3041 */ 3042 private void addConflict(AddMsg msg) throws DecodeException 3043 { 3044 String normalizedDN = msg.getDN().toString(); 3045 3046 // Generate an alert to let the administrator know that some 3047 // conflict could not be solved. 3048 LocalizableMessage alertMessage = NOTE_UNRESOLVED_CONFLICT.get(normalizedDN); 3049 DirectoryServer.sendAlertNotification(this, 3050 ALERT_TYPE_REPLICATION_UNRESOLVED_CONFLICT, alertMessage); 3051 3052 // Add the conflict attribute 3053 msg.addAttribute(DS_SYNC_CONFLICT, normalizedDN); 3054 } 3055 3056 /** 3057 * Generate the Dn to use for a conflicting entry. 3058 * 3059 * @param entryUUID The unique identifier of the entry involved in the 3060 * conflict. 3061 * @param rdn Original rdn. 3062 * @return The generated RDN for a conflicting entry. 3063 */ 3064 private String generateConflictRDN(String entryUUID, String rdn) 3065 { 3066 return "entryuuid=" + entryUUID + "+" + rdn; 3067 } 3068 3069 /** 3070 * Generate the RDN to use for a conflicting entry whose father was deleted. 3071 * 3072 * @param entryUUID The unique identifier of the entry involved in the 3073 * conflict. 3074 * @param dn The original DN of the entry. 3075 * 3076 * @return The generated RDN for a conflicting entry. 3077 */ 3078 private RDN generateDeleteConflictDn(String entryUUID, DN dn) 3079 { 3080 String newRDN = "entryuuid=" + entryUUID + "+" + dn.rdn(); 3081 try 3082 { 3083 return RDN.valueOf(newRDN); 3084 } 3085 catch (LocalizedIllegalArgumentException e) 3086 { 3087 // cannot happen 3088 return null; 3089 } 3090 } 3091 3092 /** 3093 * Check if the domain solve conflicts. 3094 * 3095 * @return a boolean indicating if the domain should solve conflicts. 3096 */ 3097 boolean solveConflict() 3098 { 3099 return solveConflictFlag; 3100 } 3101 3102 /** 3103 * Disable the replication on this domain. 3104 * The session to the replication server will be stopped. 3105 * The domain will not be destroyed but call to the pre-operation 3106 * methods will result in failure. 3107 * The listener thread will be destroyed. 3108 * The monitor informations will still be accessible. 3109 */ 3110 public void disable() 3111 { 3112 state.save(); 3113 state.clearInMemory(); 3114 disabled = true; 3115 disableService(); // This will cut the session and wake up the listener 3116 } 3117 3118 /** 3119 * Do what necessary when the data have changed : load state, load 3120 * generation Id. 3121 * If there is no such information check if there is a 3122 * ReplicaUpdateVector entry and translate it into a state 3123 * and generationId. 3124 * @exception DirectoryException Thrown when an error occurs. 3125 */ 3126 private void loadDataState() throws DirectoryException 3127 { 3128 state.clearInMemory(); 3129 state.loadState(); 3130 getGenerator().adjust(state.getMaxCSN(getServerId())); 3131 3132 // Retrieves the generation ID associated with the data imported 3133 generationId = loadGenerationId(); 3134 } 3135 3136 /** 3137 * Enable back the domain after a previous disable. 3138 * The domain will connect back to a replication Server and 3139 * will recreate threads to listen for messages from the Synchronization 3140 * server. 3141 * The generationId will be retrieved or computed if necessary. 3142 * The ServerState will also be read again from the local database. 3143 */ 3144 public void enable() 3145 { 3146 try 3147 { 3148 loadDataState(); 3149 } 3150 catch (Exception e) 3151 { 3152 /* TODO should mark that replicationServer service is 3153 * not available, log an error and retry upon timeout 3154 * should we stop the modifications ? 3155 */ 3156 logger.error(ERR_LOADING_GENERATION_ID, getBaseDN(), stackTraceToSingleLineString(e)); 3157 return; 3158 } 3159 3160 enableService(); 3161 3162 disabled = false; 3163 } 3164 3165 /** 3166 * Compute the data generationId associated with the current data present 3167 * in the backend for this domain. 3168 * @return The computed generationId. 3169 * @throws DirectoryException When an error occurs. 3170 */ 3171 private long computeGenerationId() throws DirectoryException 3172 { 3173 final long genId = exportBackend(null, true); 3174 if (logger.isTraceEnabled()) 3175 { 3176 logger.trace("Computed generationId: generationId=" + genId); 3177 } 3178 return genId; 3179 } 3180 3181 /** 3182 * Run a modify operation to update the entry whose DN is given as 3183 * a parameter with the generationID information. 3184 * 3185 * @param entryDN The DN of the entry to be updated. 3186 * @param generationId The value of the generationID to be saved. 3187 * 3188 * @return A ResultCode indicating if the operation was successful. 3189 */ 3190 private ResultCode runSaveGenerationId(DN entryDN, long generationId) 3191 { 3192 // The generationId is stored in the root entry of the domain. 3193 final ByteString asn1BaseDn = ByteString.valueOfUtf8(entryDN.toString()); 3194 3195 LDAPAttribute attr = new LDAPAttribute(REPLICATION_GENERATION_ID, Long.toString(generationId)); 3196 List<RawModification> mods = new ArrayList<>(1); 3197 mods.add(new LDAPModification(ModificationType.REPLACE, attr)); 3198 3199 ModifyOperation op = new ModifyOperationBasis( 3200 conn, nextOperationID(), nextMessageID(), new ArrayList<Control>(0), 3201 asn1BaseDn, mods); 3202 runAsSynchronizedOperation(op); 3203 return op.getResultCode(); 3204 } 3205 3206 /** 3207 * Stores the value of the generationId. 3208 * @param generationId The value of the generationId. 3209 * @return a ResultCode indicating if the method was successful. 3210 */ 3211 private ResultCode saveGenerationId(long generationId) 3212 { 3213 ResultCode result = runSaveGenerationId(getBaseDN(), generationId); 3214 if (result != ResultCode.SUCCESS) 3215 { 3216 generationIdSavedStatus = false; 3217 if (result == ResultCode.NO_SUCH_OBJECT) 3218 { 3219 // If the base entry does not exist, save the generation 3220 // ID in the config entry 3221 result = runSaveGenerationId(config.dn(), generationId); 3222 } 3223 3224 if (result != ResultCode.SUCCESS) 3225 { 3226 logger.error(ERR_UPDATING_GENERATION_ID, result.getName(), getBaseDN()); 3227 } 3228 } 3229 else 3230 { 3231 generationIdSavedStatus = true; 3232 } 3233 return result; 3234 } 3235 3236 /** 3237 * Load the GenerationId from the root entry of the domain 3238 * from the REPLICATION_GENERATION_ID attribute in database 3239 * to memory, or compute it if not found. 3240 * 3241 * @return generationId The retrieved value of generationId 3242 * @throws DirectoryException When an error occurs. 3243 */ 3244 private long loadGenerationId() throws DirectoryException 3245 { 3246 if (logger.isTraceEnabled()) 3247 { 3248 logger.trace("Attempt to read generation ID from DB " + getBaseDN()); 3249 } 3250 3251 // Search the database entry that is used to periodically save the generation id 3252 final SearchRequest request = newSearchRequest(getBaseDN(), SearchScope.BASE_OBJECT) 3253 .addAttribute(REPLICATION_GENERATION_ID); 3254 InternalSearchOperation search = conn.processSearch(request); 3255 if (search.getResultCode() == ResultCode.NO_SUCH_OBJECT) 3256 { 3257 // if the base entry does not exist look for the generationID 3258 // in the config entry. 3259 request.setName(config.dn()); 3260 search = conn.processSearch(request); 3261 } 3262 3263 boolean found = false; 3264 long aGenerationId = -1; 3265 if (search.getResultCode() != ResultCode.SUCCESS) 3266 { 3267 if (search.getResultCode() != ResultCode.NO_SUCH_OBJECT) 3268 { 3269 String errorMsg = search.getResultCode().getName() + " " + search.getErrorMessage(); 3270 logger.error(ERR_SEARCHING_GENERATION_ID, errorMsg, getBaseDN()); 3271 } 3272 } 3273 else 3274 { 3275 List<SearchResultEntry> result = search.getSearchEntries(); 3276 SearchResultEntry resultEntry = result.get(0); 3277 if (resultEntry != null) 3278 { 3279 AttributeType synchronizationGenIDType = DirectoryServer.getAttributeType(REPLICATION_GENERATION_ID); 3280 List<Attribute> attrs = resultEntry.getAttribute(synchronizationGenIDType); 3281 if (!attrs.isEmpty()) 3282 { 3283 Attribute attr = attrs.get(0); 3284 if (attr.size()>1) 3285 { 3286 String errorMsg = "#Values=" + attr.size() + " Must be exactly 1 in entry " + resultEntry.toLDIFString(); 3287 logger.error(ERR_LOADING_GENERATION_ID, getBaseDN(), errorMsg); 3288 } 3289 else if (attr.size() == 1) 3290 { 3291 found = true; 3292 try 3293 { 3294 aGenerationId = Long.decode(attr.iterator().next().toString()); 3295 } 3296 catch(Exception e) 3297 { 3298 logger.error(ERR_LOADING_GENERATION_ID, getBaseDN(), stackTraceToSingleLineString(e)); 3299 } 3300 } 3301 } 3302 } 3303 } 3304 3305 if (!found) 3306 { 3307 aGenerationId = computeGenerationId(); 3308 saveGenerationId(aGenerationId); 3309 3310 if (logger.isTraceEnabled()) 3311 { 3312 logger.trace("Generation ID created for domain baseDN=" + getBaseDN() + " generationId=" + aGenerationId); 3313 } 3314 } 3315 else 3316 { 3317 generationIdSavedStatus = true; 3318 if (logger.isTraceEnabled()) 3319 { 3320 logger.trace("Generation ID successfully read from domain baseDN=" + getBaseDN() 3321 + " generationId=" + aGenerationId); 3322 } 3323 } 3324 return aGenerationId; 3325 } 3326 3327 /** 3328 * Do whatever is needed when a backup is started. 3329 * We need to make sure that the serverState is correctly save. 3330 */ 3331 void backupStart() 3332 { 3333 state.save(); 3334 } 3335 3336 /** Do whatever is needed when a backup is finished. */ 3337 void backupEnd() 3338 { 3339 // Nothing is needed at the moment 3340 } 3341 3342 /* 3343 * Total Update >> 3344 */ 3345 3346 /** 3347 * This method trigger an export of the replicated data. 3348 * 3349 * @param output The OutputStream where the export should 3350 * be produced. 3351 * @throws DirectoryException When needed. 3352 */ 3353 @Override 3354 protected void exportBackend(OutputStream output) throws DirectoryException 3355 { 3356 exportBackend(output, false); 3357 } 3358 3359 /** 3360 * Export the entries from the backend and/or compute the generation ID. 3361 * The ieContext must have been set before calling. 3362 * 3363 * @param output The OutputStream where the export should 3364 * be produced. 3365 * @param checksumOutput A boolean indicating if this export is 3366 * invoked to perform a checksum only 3367 * 3368 * @return The computed GenerationID. 3369 * 3370 * @throws DirectoryException when an error occurred 3371 */ 3372 private long exportBackend(OutputStream output, boolean checksumOutput) 3373 throws DirectoryException 3374 { 3375 Backend<?> backend = getBackend(); 3376 3377 // Acquire a shared lock for the backend. 3378 try 3379 { 3380 String lockFile = LockFileManager.getBackendLockFileName(backend); 3381 StringBuilder failureReason = new StringBuilder(); 3382 if (! LockFileManager.acquireSharedLock(lockFile, failureReason)) 3383 { 3384 LocalizableMessage message = 3385 ERR_LDIFEXPORT_CANNOT_LOCK_BACKEND.get(backend.getBackendID(), failureReason); 3386 logger.error(message); 3387 throw new DirectoryException(ResultCode.OTHER, message); 3388 } 3389 } 3390 catch (Exception e) 3391 { 3392 LocalizableMessage message = 3393 ERR_LDIFEXPORT_CANNOT_LOCK_BACKEND.get(backend.getBackendID(), 3394 stackTraceToSingleLineString(e)); 3395 logger.error(message); 3396 throw new DirectoryException(ResultCode.OTHER, message); 3397 } 3398 3399 long numberOfEntries = backend.getNumberOfEntriesInBaseDN(getBaseDN()); 3400 long entryCount = Math.min(numberOfEntries, 1000); 3401 OutputStream os; 3402 ReplLDIFOutputStream ros = null; 3403 if (checksumOutput) 3404 { 3405 ros = new ReplLDIFOutputStream(entryCount); 3406 os = ros; 3407 try 3408 { 3409 os.write(Long.toString(numberOfEntries).getBytes()); 3410 } 3411 catch(Exception e) 3412 { 3413 // Should never happen 3414 } 3415 } 3416 else 3417 { 3418 os = output; 3419 } 3420 3421 // baseDN branch is the only one included in the export 3422 LDIFExportConfig exportConfig = new LDIFExportConfig(os); 3423 exportConfig.setIncludeBranches(newArrayList(getBaseDN())); 3424 3425 // For the checksum computing mode, only consider the 'stable' attributes 3426 if (checksumOutput) 3427 { 3428 String includeAttributeStrings[] = { "objectclass", "sn", "cn", "entryuuid" }; 3429 Set<AttributeType> includeAttributes = new HashSet<>(); 3430 for (String attrName : includeAttributeStrings) 3431 { 3432 includeAttributes.add(DirectoryServer.getAttributeType(attrName)); 3433 } 3434 exportConfig.setIncludeAttributes(includeAttributes); 3435 } 3436 3437 // Launch the export. 3438 long genID = 0; 3439 try 3440 { 3441 backend.exportLDIF(exportConfig); 3442 } 3443 catch (DirectoryException de) 3444 { 3445 if (ros == null || ros.getNumExportedEntries() < entryCount) 3446 { 3447 LocalizableMessage message = ERR_LDIFEXPORT_ERROR_DURING_EXPORT.get(de.getMessageObject()); 3448 logger.error(message); 3449 throw new DirectoryException(ResultCode.OTHER, message); 3450 } 3451 } 3452 catch (Exception e) 3453 { 3454 LocalizableMessage message = ERR_LDIFEXPORT_ERROR_DURING_EXPORT.get(stackTraceToSingleLineString(e)); 3455 logger.error(message); 3456 throw new DirectoryException(ResultCode.OTHER, message); 3457 } 3458 finally 3459 { 3460 // Clean up after the export by closing the export config. 3461 // Will also flush the export and export the remaining entries. 3462 exportConfig.close(); 3463 3464 if (checksumOutput) 3465 { 3466 genID = ros.getChecksumValue(); 3467 } 3468 3469 // Release the shared lock on the backend. 3470 try 3471 { 3472 String lockFile = LockFileManager.getBackendLockFileName(backend); 3473 StringBuilder failureReason = new StringBuilder(); 3474 if (! LockFileManager.releaseLock(lockFile, failureReason)) 3475 { 3476 LocalizableMessage message = 3477 WARN_LDIFEXPORT_CANNOT_UNLOCK_BACKEND.get(backend.getBackendID(), failureReason); 3478 logger.warn(message); 3479 throw new DirectoryException(ResultCode.OTHER, message); 3480 } 3481 } 3482 catch (Exception e) 3483 { 3484 LocalizableMessage message = 3485 WARN_LDIFEXPORT_CANNOT_UNLOCK_BACKEND.get(backend.getBackendID(), 3486 stackTraceToSingleLineString(e)); 3487 logger.warn(message); 3488 throw new DirectoryException(ResultCode.OTHER, message); 3489 } 3490 } 3491 return genID; 3492 } 3493 3494 /** 3495 * Process backend before import. 3496 * 3497 * @param backend 3498 * The backend. 3499 * @throws DirectoryException 3500 * If the backend could not be disabled or locked exclusively. 3501 */ 3502 private void preBackendImport(Backend<?> backend) throws DirectoryException 3503 { 3504 // Stop saving state 3505 stateSavingDisabled = true; 3506 3507 // Prevent the processing of the backend finalisation event as the import will disable the attached backend 3508 ignoreBackendInitializationEvent = true; 3509 3510 // FIXME setBackendEnabled should be part of TaskUtils ? 3511 TaskUtils.disableBackend(backend.getBackendID()); 3512 3513 // Acquire an exclusive lock for the backend. 3514 String lockFile = LockFileManager.getBackendLockFileName(backend); 3515 StringBuilder failureReason = new StringBuilder(); 3516 if (! LockFileManager.acquireExclusiveLock(lockFile, failureReason)) 3517 { 3518 LocalizableMessage message = ERR_INIT_CANNOT_LOCK_BACKEND.get(backend.getBackendID(), failureReason); 3519 logger.error(message); 3520 throw new DirectoryException(ResultCode.OTHER, message); 3521 } 3522 } 3523 3524 /** 3525 * This method triggers an import of the replicated data. 3526 * 3527 * @param input The InputStream from which the data are read. 3528 * @throws DirectoryException When needed. 3529 */ 3530 @Override 3531 protected void importBackend(InputStream input) throws DirectoryException 3532 { 3533 Backend<?> backend = getBackend(); 3534 3535 LDIFImportConfig importConfig = null; 3536 ImportExportContext ieCtx = getImportExportContext(); 3537 try 3538 { 3539 if (!backend.supports(BackendOperation.LDIF_IMPORT)) 3540 { 3541 ieCtx.setExceptionIfNoneSet(new DirectoryException(OTHER, 3542 ERR_INIT_IMPORT_NOT_SUPPORTED.get(backend.getBackendID()))); 3543 return; 3544 } 3545 3546 importConfig = new LDIFImportConfig(input); 3547 importConfig.setIncludeBranches(newLinkedHashSet(getBaseDN())); 3548 importConfig.setSkipDNValidation(true); 3549 // We should not validate schema for replication 3550 importConfig.setValidateSchema(false); 3551 // Allow fractional replication ldif import plugin to be called 3552 importConfig.setInvokeImportPlugins(true); 3553 // Reset the follow import flag and message before starting the import 3554 importErrorMessageId = -1; 3555 3556 // TODO How to deal with rejected entries during the import 3557 File rejectsFile = 3558 getFileForPath("logs" + File.separator + "replInitRejectedEntries"); 3559 importConfig.writeRejectedEntries(rejectsFile.getAbsolutePath(), 3560 ExistingFileBehavior.OVERWRITE); 3561 3562 // Process import 3563 preBackendImport(backend); 3564 backend.importLDIF(importConfig, DirectoryServer.getInstance().getServerContext()); 3565 3566 stateSavingDisabled = false; 3567 } 3568 catch(Exception e) 3569 { 3570 ieCtx.setExceptionIfNoneSet(new DirectoryException(ResultCode.OTHER, 3571 ERR_INIT_IMPORT_FAILURE.get(stackTraceToSingleLineString(e)))); 3572 } 3573 finally 3574 { 3575 try 3576 { 3577 // Cleanup 3578 if (importConfig != null) 3579 { 3580 importConfig.close(); 3581 closeBackendImport(backend); // Re-enable backend 3582 backend = getBackend(); 3583 } 3584 3585 loadDataState(); 3586 3587 if (ieCtx.getException() != null) 3588 { 3589 // When an error occurred during an import, most of times 3590 // the generationId coming in the root entry of the imported data, 3591 // is not valid anymore (partial data in the backend). 3592 generationId = computeGenerationId(); 3593 saveGenerationId(generationId); 3594 } 3595 } 3596 catch (DirectoryException fe) 3597 { 3598 // If we already catch an Exception it's quite possible 3599 // that the loadDataState() and setGenerationId() fail 3600 // so we don't bother about the new Exception. 3601 // However if there was no Exception before we want 3602 // to return this Exception to the task creator. 3603 ieCtx.setExceptionIfNoneSet(new DirectoryException( 3604 ResultCode.OTHER, 3605 ERR_INIT_IMPORT_FAILURE.get(stackTraceToSingleLineString(fe)))); 3606 } 3607 } 3608 3609 if (ieCtx.getException() != null) 3610 { 3611 throw ieCtx.getException(); 3612 } 3613 } 3614 3615 /** 3616 * Make post import operations. 3617 * @param backend The backend implied in the import. 3618 * @exception DirectoryException Thrown when an error occurs. 3619 */ 3620 private void closeBackendImport(Backend<?> backend) throws DirectoryException 3621 { 3622 String lockFile = LockFileManager.getBackendLockFileName(backend); 3623 StringBuilder failureReason = new StringBuilder(); 3624 3625 // Release lock 3626 if (!LockFileManager.releaseLock(lockFile, failureReason)) 3627 { 3628 LocalizableMessage message = 3629 WARN_LDIFIMPORT_CANNOT_UNLOCK_BACKEND.get(backend.getBackendID(), failureReason); 3630 logger.warn(message); 3631 throw new DirectoryException(ResultCode.OTHER, message); 3632 } 3633 3634 TaskUtils.enableBackend(backend.getBackendID()); 3635 3636 // Restore the processing of backend finalization events. 3637 ignoreBackendInitializationEvent = false; 3638 3639 } 3640 3641 /** 3642 * Retrieves a replication domain based on the baseDN. 3643 * 3644 * @param baseDN The baseDN of the domain to retrieve 3645 * @return The domain retrieved 3646 * @throws DirectoryException When an error occurred or no domain 3647 * match the provided baseDN. 3648 */ 3649 public static LDAPReplicationDomain retrievesReplicationDomain(DN baseDN) 3650 throws DirectoryException 3651 { 3652 LDAPReplicationDomain replicationDomain = null; 3653 3654 // Retrieves the domain 3655 for (SynchronizationProvider<?> provider : 3656 DirectoryServer.getSynchronizationProviders()) 3657 { 3658 if (!(provider instanceof MultimasterReplication)) 3659 { 3660 LocalizableMessage message = ERR_INVALID_PROVIDER.get(); 3661 throw new DirectoryException(ResultCode.OTHER, message); 3662 } 3663 3664 // From the domainDN retrieves the replication domain 3665 LDAPReplicationDomain domain = 3666 MultimasterReplication.findDomain(baseDN, null); 3667 if (domain == null) 3668 { 3669 break; 3670 } 3671 if (replicationDomain != null) 3672 { 3673 // Should never happen 3674 LocalizableMessage message = ERR_MULTIPLE_MATCHING_DOMAIN.get(); 3675 throw new DirectoryException(ResultCode.OTHER, message); 3676 } 3677 replicationDomain = domain; 3678 } 3679 3680 if (replicationDomain == null) 3681 { 3682 throw new DirectoryException(ResultCode.OTHER, ERR_NO_MATCHING_DOMAIN.get(baseDN)); 3683 } 3684 return replicationDomain; 3685 } 3686 3687 /** 3688 * Returns the backend associated to this domain. 3689 * @return The associated backend. 3690 */ 3691 private Backend<?> getBackend() 3692 { 3693 return DirectoryServer.getBackend(getBaseDN()); 3694 } 3695 3696 /* 3697 * <<Total Update 3698 */ 3699 3700 /** 3701 * Push the schema modifications contained in the given parameter as a 3702 * modification that would happen on a local server. The modifications are not 3703 * applied to the local schema backend and historical information is not 3704 * updated; but a CSN is generated and the ServerState associated to the 3705 * schema domain is updated. 3706 * 3707 * @param modifications 3708 * The schema modifications to push 3709 */ 3710 void synchronizeSchemaModifications(List<Modification> modifications) 3711 { 3712 ModifyOperation op = new ModifyOperationBasis( 3713 conn, nextOperationID(), nextMessageID(), null, 3714 DirectoryServer.getSchemaDN(), modifications); 3715 3716 final Entry schema; 3717 try 3718 { 3719 schema = DirectoryServer.getEntry(DirectoryServer.getSchemaDN()); 3720 } 3721 catch (DirectoryException e) 3722 { 3723 logger.traceException(e); 3724 logger.error(ERR_BACKEND_SEARCH_ENTRY.get(DirectoryServer.getSchemaDN().toString(), 3725 stackTraceToSingleLineString(e))); 3726 return; 3727 } 3728 3729 LocalBackendModifyOperation localOp = new LocalBackendModifyOperation(op); 3730 CSN csn = generateCSN(localOp); 3731 OperationContext ctx = new ModifyContext(csn, getEntryUUID(schema)); 3732 localOp.setAttachment(SYNCHROCONTEXT, ctx); 3733 localOp.setResultCode(ResultCode.SUCCESS); 3734 synchronize(localOp); 3735 } 3736 3737 /** 3738 * Check if the provided configuration is acceptable for add. 3739 * 3740 * @param configuration The configuration to check. 3741 * @param unacceptableReasons When the configuration is not acceptable, this 3742 * table is use to return the reasons why this 3743 * configuration is not acceptable. 3744 * 3745 * @return true if the configuration is acceptable, false other wise. 3746 */ 3747 static boolean isConfigurationAcceptable(ReplicationDomainCfg configuration, 3748 List<LocalizableMessage> unacceptableReasons) 3749 { 3750 // Check that there is not already a domain with the same DN 3751 final DN dn = configuration.getBaseDN(); 3752 LDAPReplicationDomain domain = MultimasterReplication.findDomain(dn, null); 3753 if (domain != null && domain.getBaseDN().equals(dn)) 3754 { 3755 unacceptableReasons.add(ERR_SYNC_INVALID_DN.get()); 3756 return false; 3757 } 3758 3759 // Check that the base DN is configured as a base-dn of the directory server 3760 if (DirectoryServer.getBackend(dn) == null) 3761 { 3762 unacceptableReasons.add(ERR_UNKNOWN_DN.get(dn)); 3763 return false; 3764 } 3765 3766 // Check fractional configuration 3767 try 3768 { 3769 isFractionalConfigAcceptable(configuration); 3770 } catch (ConfigException e) 3771 { 3772 unacceptableReasons.add(e.getMessageObject()); 3773 return false; 3774 } 3775 3776 return true; 3777 } 3778 3779 @Override 3780 public ConfigChangeResult applyConfigurationChange( 3781 ReplicationDomainCfg configuration) 3782 { 3783 this.config = configuration; 3784 changeConfig(configuration); 3785 3786 // Read assured + fractional configuration and each time reconnect if needed 3787 readAssuredConfig(configuration, true); 3788 readFractionalConfig(configuration, true); 3789 3790 solveConflictFlag = isSolveConflict(configuration); 3791 3792 final ConfigChangeResult ccr = new ConfigChangeResult(); 3793 try 3794 { 3795 storeECLConfiguration(configuration); 3796 } 3797 catch(Exception e) 3798 { 3799 ccr.setResultCode(ResultCode.OTHER); 3800 } 3801 return ccr; 3802 } 3803 3804 @Override 3805 public boolean isConfigurationChangeAcceptable( 3806 ReplicationDomainCfg configuration, List<LocalizableMessage> unacceptableReasons) 3807 { 3808 // Check that a import/export is not in progress 3809 if (ieRunning()) 3810 { 3811 unacceptableReasons.add( 3812 NOTE_ERR_CANNOT_CHANGE_CONFIG_DURING_TOTAL_UPDATE.get()); 3813 return false; 3814 } 3815 3816 // Check fractional configuration 3817 try 3818 { 3819 isFractionalConfigAcceptable(configuration); 3820 return true; 3821 } 3822 catch (ConfigException e) 3823 { 3824 unacceptableReasons.add(e.getMessageObject()); 3825 return false; 3826 } 3827 } 3828 3829 @Override 3830 public Map<String, String> getAlerts() 3831 { 3832 Map<String, String> alerts = new LinkedHashMap<>(); 3833 3834 alerts.put(ALERT_TYPE_REPLICATION_UNRESOLVED_CONFLICT, 3835 ALERT_DESCRIPTION_REPLICATION_UNRESOLVED_CONFLICT); 3836 return alerts; 3837 } 3838 3839 @Override 3840 public String getClassName() 3841 { 3842 return CLASS_NAME; 3843 } 3844 3845 @Override 3846 public DN getComponentEntryDN() 3847 { 3848 return config.dn(); 3849 } 3850 3851 /** Starts the Replication Domain. */ 3852 public void start() 3853 { 3854 // Create the ServerStateFlush thread 3855 flushThread.start(); 3856 3857 startListenService(); 3858 } 3859 3860 /** Remove the configuration of the external changelog from this domain configuration. */ 3861 private void removeECLDomainCfg() 3862 { 3863 try 3864 { 3865 DN eclConfigEntryDN = DN.valueOf("cn=external changeLog," + config.dn()); 3866 if (DirectoryServer.getConfigHandler().entryExists(eclConfigEntryDN)) 3867 { 3868 DirectoryServer.getConfigHandler().deleteEntry(eclConfigEntryDN, null); 3869 } 3870 } 3871 catch(Exception e) 3872 { 3873 logger.traceException(e); 3874 logger.error(ERR_CHECK_CREATE_REPL_BACKEND_FAILED, stackTraceToSingleLineString(e)); 3875 } 3876 } 3877 3878 /** 3879 * Store the provided ECL configuration for the domain. 3880 * @param domCfg The provided configuration. 3881 * @throws ConfigException When an error occurred. 3882 */ 3883 private void storeECLConfiguration(ReplicationDomainCfg domCfg) 3884 throws ConfigException 3885 { 3886 ExternalChangelogDomainCfg eclDomCfg = null; 3887 // create the ecl config if it does not exist 3888 // There may not be any config entry related to this domain in some 3889 // unit test cases 3890 try 3891 { 3892 DN configDn = config.dn(); 3893 if (DirectoryServer.getConfigHandler().entryExists(configDn)) 3894 { 3895 try 3896 { eclDomCfg = domCfg.getExternalChangelogDomain(); 3897 } catch(Exception e) { /* do nothing */ } 3898 // domain with no config entry only when running unit tests 3899 if (eclDomCfg == null) 3900 { 3901 // no ECL config provided hence create a default one 3902 // create the default one 3903 DN eclConfigEntryDN = DN.valueOf("cn=external changelog," + configDn); 3904 if (!DirectoryServer.getConfigHandler().entryExists(eclConfigEntryDN)) 3905 { 3906 // no entry exist yet for the ECL config for this domain 3907 // create it 3908 String ldif = makeLdif( 3909 "dn: cn=external changelog," + configDn, 3910 "objectClass: top", 3911 "objectClass: ds-cfg-external-changelog-domain", 3912 "cn: external changelog", 3913 "ds-cfg-enabled: " + !getBackend().isPrivateBackend()); 3914 LDIFImportConfig ldifImportConfig = new LDIFImportConfig( 3915 new StringReader(ldif)); 3916 // No need to validate schema in replication 3917 ldifImportConfig.setValidateSchema(false); 3918 LDIFReader reader = new LDIFReader(ldifImportConfig); 3919 Entry eclEntry = reader.readEntry(); 3920 DirectoryServer.getConfigHandler().addEntry(eclEntry, null); 3921 ldifImportConfig.close(); 3922 } 3923 } 3924 } 3925 eclDomCfg = domCfg.getExternalChangelogDomain(); 3926 if (eclDomain != null) 3927 { 3928 eclDomain.applyConfigurationChange(eclDomCfg); 3929 } 3930 else 3931 { 3932 // Create the ECL domain object 3933 eclDomain = new ExternalChangelogDomain(this, eclDomCfg); 3934 } 3935 } 3936 catch (Exception e) 3937 { 3938 throw new ConfigException(NOTE_ERR_UNABLE_TO_ENABLE_ECL.get( 3939 "Replication Domain on " + getBaseDN(), stackTraceToSingleLineString(e)), e); 3940 } 3941 } 3942 3943 private static String makeLdif(String... lines) 3944 { 3945 final StringBuilder buffer = new StringBuilder(); 3946 for (String line : lines) { 3947 buffer.append(line).append(EOL); 3948 } 3949 // Append an extra line so we can append LDIF Strings. 3950 buffer.append(EOL); 3951 return buffer.toString(); 3952 } 3953 3954 @Override 3955 public void sessionInitiated(ServerStatus initStatus, ServerState rsState) 3956 { 3957 // Check domain fractional configuration consistency with local 3958 // configuration variables 3959 forceBadDataSet = !isBackendFractionalConfigConsistent(); 3960 3961 super.sessionInitiated(initStatus, rsState); 3962 3963 // Now for bad data set status if needed 3964 if (forceBadDataSet) 3965 { 3966 signalNewStatus(StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT); 3967 logger.info(NOTE_FRACTIONAL_BAD_DATA_SET_NEED_RESYNC, getBaseDN()); 3968 return; // Do not send changes to the replication server 3969 } 3970 3971 try 3972 { 3973 /* 3974 * We must not publish changes to a replicationServer that has 3975 * not seen all our previous changes because this could cause 3976 * some other ldap servers to miss those changes. 3977 * Check that the ReplicationServer has seen all our previous 3978 * changes. 3979 */ 3980 CSN replServerMaxCSN = rsState.getCSN(getServerId()); 3981 3982 // we don't want to update from here (a DS) an empty RS because 3983 // normally the RS should have been updated by other RSes except for 3984 // very last changes lost if the local connection was broken 3985 // ... hence the RS we are connected to should not be empty 3986 // ... or if it is empty, it is due to a voluntary reset 3987 // and we don't want to update it with our changes that could be huge. 3988 if (replServerMaxCSN != null && replServerMaxCSN.getSeqnum() != 0) 3989 { 3990 CSN ourMaxCSN = state.getMaxCSN(getServerId()); 3991 if (ourMaxCSN != null 3992 && !ourMaxCSN.isOlderThanOrEqualTo(replServerMaxCSN)) 3993 { 3994 pendingChanges.setRecovering(true); 3995 broker.setRecoveryRequired(true); 3996 final RSUpdater rsUpdater = new RSUpdater(replServerMaxCSN); 3997 if (this.rsUpdater.compareAndSet(null, rsUpdater)) 3998 { 3999 rsUpdater.start(); 4000 } 4001 } 4002 } 4003 } catch (Exception e) 4004 { 4005 logger.error(ERR_PUBLISHING_FAKE_OPS, getBaseDN(), stackTraceToSingleLineString(e)); 4006 } 4007 } 4008 4009 /** 4010 * Build the list of changes that have been processed by this server after the 4011 * CSN given as a parameter and publish them using the given session. 4012 * 4013 * @param startCSN 4014 * The CSN where we need to start the search 4015 * @param session 4016 * The session to use to publish the changes 4017 * @return A boolean indicating he success of the operation. 4018 * @throws Exception 4019 * if an Exception happens during the search. 4020 */ 4021 boolean buildAndPublishMissingChanges(CSN startCSN, ReplicationBroker session) 4022 throws Exception 4023 { 4024 // Trim the changes in replayOperations that are older than the startCSN. 4025 synchronized (replayOperations) 4026 { 4027 Iterator<CSN> it = replayOperations.keySet().iterator(); 4028 while (it.hasNext()) 4029 { 4030 if (shutdown.get()) 4031 { 4032 return false; 4033 } 4034 if (it.next().isNewerThan(startCSN)) 4035 { 4036 break; 4037 } 4038 it.remove(); 4039 } 4040 } 4041 4042 CSN lastRetrievedChange; 4043 InternalSearchOperation op; 4044 CSN currentStartCSN = startCSN; 4045 do 4046 { 4047 if (shutdown.get()) 4048 { 4049 return false; 4050 } 4051 4052 lastRetrievedChange = null; 4053 // We can't do the search in one go because we need to store the results 4054 // so that we are sure we send the operations in order and because the 4055 // list might be large. 4056 // So we search by interval of 10 seconds and store the results in the 4057 // replayOperations list so that they are sorted before sending them. 4058 long missingChangesDelta = currentStartCSN.getTime() + 10000; 4059 CSN endCSN = new CSN(missingChangesDelta, 0xffffffff, getServerId()); 4060 4061 ScanSearchListener listener = 4062 new ScanSearchListener(currentStartCSN, endCSN); 4063 op = searchForChangedEntries(getBaseDN(), currentStartCSN, endCSN, 4064 listener); 4065 4066 // Publish and remove all the changes from the replayOperations list 4067 // that are older than the endCSN. 4068 final List<FakeOperation> opsToSend = new LinkedList<>(); 4069 synchronized (replayOperations) 4070 { 4071 Iterator<FakeOperation> itOp = replayOperations.values().iterator(); 4072 while (itOp.hasNext()) 4073 { 4074 if (shutdown.get()) 4075 { 4076 return false; 4077 } 4078 FakeOperation fakeOp = itOp.next(); 4079 if (fakeOp.getCSN().isNewerThan(endCSN) // sanity check 4080 || !state.cover(fakeOp.getCSN()) 4081 // do not look for replay operations in the future 4082 || currentStartCSN.isNewerThan(now())) 4083 { 4084 break; 4085 } 4086 4087 lastRetrievedChange = fakeOp.getCSN(); 4088 opsToSend.add(fakeOp); 4089 itOp.remove(); 4090 } 4091 } 4092 4093 for (FakeOperation opToSend : opsToSend) 4094 { 4095 if (shutdown.get()) 4096 { 4097 return false; 4098 } 4099 session.publishRecovery(opToSend.generateMessage()); 4100 } 4101 4102 if (lastRetrievedChange != null) 4103 { 4104 if (logger.isDebugEnabled()) 4105 { 4106 logger.debug(LocalizableMessage.raw("publish loop" 4107 + " >=" + currentStartCSN + " <=" + endCSN 4108 + " nentries=" + op.getEntriesSent() 4109 + " result=" + op.getResultCode() 4110 + " lastRetrievedChange=" + lastRetrievedChange)); 4111 } 4112 currentStartCSN = lastRetrievedChange; 4113 } 4114 else 4115 { 4116 if (logger.isDebugEnabled()) 4117 { 4118 logger.debug(LocalizableMessage.raw("publish loop" 4119 + " >=" + currentStartCSN + " <=" + endCSN 4120 + " nentries=" + op.getEntriesSent() 4121 + " result=" + op.getResultCode() 4122 + " no changes")); 4123 } 4124 currentStartCSN = endCSN; 4125 } 4126 } while (pendingChanges.recoveryUntil(currentStartCSN) 4127 && op.getResultCode().equals(ResultCode.SUCCESS)); 4128 4129 return op.getResultCode().equals(ResultCode.SUCCESS); 4130 } 4131 4132 private static CSN now() 4133 { 4134 // ensure now() will always come last with isNewerThan() test, 4135 // even though the timestamp, or the timestamp and seqnum would be the same 4136 return new CSN(TimeThread.getTime(), Integer.MAX_VALUE, Integer.MAX_VALUE); 4137 } 4138 4139 /** 4140 * Search for the changes that happened since fromCSN based on the historical 4141 * attribute. The only changes that will be send will be the one generated on 4142 * the serverId provided in fromCSN. 4143 * 4144 * @param baseDN 4145 * the base DN 4146 * @param fromCSN 4147 * The CSN from which we want the changes 4148 * @param lastCSN 4149 * The max CSN that the search should return 4150 * @param resultListener 4151 * The listener that will process the entries returned 4152 * @return the internal search operation 4153 * @throws Exception 4154 * when raised. 4155 */ 4156 private static InternalSearchOperation searchForChangedEntries(DN baseDN, 4157 CSN fromCSN, CSN lastCSN, InternalSearchListener resultListener) 4158 throws Exception 4159 { 4160 String maxValueForId; 4161 if (lastCSN == null) 4162 { 4163 final Integer serverId = fromCSN.getServerId(); 4164 maxValueForId = "ffffffffffffffff" + String.format("%04x", serverId) 4165 + "ffffffff"; 4166 } 4167 else 4168 { 4169 maxValueForId = lastCSN.toString(); 4170 } 4171 4172 String filter = 4173 "(&(" + HISTORICAL_ATTRIBUTE_NAME + ">=dummy:" + fromCSN + ")" + 4174 "(" + HISTORICAL_ATTRIBUTE_NAME + "<=dummy:" + maxValueForId + "))"; 4175 SearchRequest request = Requests.newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter) 4176 .addAttribute(USER_AND_REPL_OPERATIONAL_ATTRS); 4177 return getRootConnection().processSearch(request, resultListener); 4178 } 4179 4180 /** 4181 * Search for the changes that happened since fromCSN based on the historical 4182 * attribute. The only changes that will be send will be the one generated on 4183 * the serverId provided in fromCSN. 4184 * 4185 * @param baseDN 4186 * the base DN 4187 * @param fromCSN 4188 * The CSN from which we want the changes 4189 * @param resultListener 4190 * that will process the entries returned. 4191 * @return the internal search operation 4192 * @throws Exception 4193 * when raised. 4194 */ 4195 static InternalSearchOperation searchForChangedEntries(DN baseDN, 4196 CSN fromCSN, InternalSearchListener resultListener) throws Exception 4197 { 4198 return searchForChangedEntries(baseDN, fromCSN, null, resultListener); 4199 } 4200 4201 /** 4202 * This method should return the total number of objects in the 4203 * replicated domain. 4204 * This count will be used for reporting. 4205 * 4206 * @throws DirectoryException when needed. 4207 * 4208 * @return The number of objects in the replication domain. 4209 */ 4210 @Override 4211 public long countEntries() throws DirectoryException 4212 { 4213 Backend<?> backend = getBackend(); 4214 if (!backend.supports(BackendOperation.LDIF_EXPORT)) 4215 { 4216 LocalizableMessage msg = ERR_INIT_EXPORT_NOT_SUPPORTED.get(backend.getBackendID()); 4217 logger.error(msg); 4218 throw new DirectoryException(ResultCode.OTHER, msg); 4219 } 4220 4221 return backend.getNumberOfEntriesInBaseDN(getBaseDN()); 4222 } 4223 4224 @Override 4225 public boolean processUpdate(UpdateMsg updateMsg) 4226 { 4227 // Ignore message if fractional configuration is inconsistent and 4228 // we have been passed into bad data set status 4229 if (forceBadDataSet) 4230 { 4231 return false; 4232 } 4233 4234 if (updateMsg instanceof LDAPUpdateMsg) 4235 { 4236 LDAPUpdateMsg msg = (LDAPUpdateMsg) updateMsg; 4237 4238 // Put the UpdateMsg in the RemotePendingChanges list. 4239 if (!remotePendingChanges.putRemoteUpdate(msg)) 4240 { 4241 /* 4242 * Already received this change so ignore it. This may happen if there 4243 * are uncommitted changes in the queue and session failover occurs 4244 * causing a recovery of all changes since the current committed server 4245 * state. See OPENDJ-1115. 4246 */ 4247 if (logger.isTraceEnabled()) 4248 { 4249 logger.trace( 4250 "LDAPReplicationDomain.processUpdate: ignoring " 4251 + "duplicate change %s", msg.getCSN()); 4252 } 4253 return true; 4254 } 4255 4256 // Put update message into the replay queue 4257 // (block until some place in the queue is available) 4258 final UpdateToReplay updateToReplay = new UpdateToReplay(msg, this); 4259 while (!isListenerShuttingDown()) 4260 { 4261 // loop until we can offer to the queue or shutdown was initiated 4262 try 4263 { 4264 if (updateToReplayQueue.offer(updateToReplay, 1, TimeUnit.SECONDS)) 4265 { 4266 // successful offer to the queue, let's exit the loop 4267 break; 4268 } 4269 } 4270 catch (InterruptedException e) 4271 { 4272 // Thread interrupted: check for shutdown. 4273 Thread.currentThread().interrupt(); 4274 } 4275 } 4276 4277 return false; 4278 } 4279 4280 // unknown message type, this should not happen, just ignore it. 4281 return true; 4282 } 4283 4284 @Override 4285 public void addAdditionalMonitoring(MonitorData attributes) 4286 { 4287 attributes.add("pending-updates", pendingChanges.size()); 4288 attributes.add("replayed-updates-ok", numReplayedPostOpCalled); 4289 attributes.add("resolved-modify-conflicts", numResolvedModifyConflicts); 4290 attributes.add("resolved-naming-conflicts", numResolvedNamingConflicts); 4291 attributes.add("unresolved-naming-conflicts", numUnresolvedNamingConflicts); 4292 attributes.add("remote-pending-changes-size", remotePendingChanges.getQueueSize()); 4293 attributes.add("dependent-changes-size", remotePendingChanges.getDependentChangesSize()); 4294 attributes.add("changes-in-progress-size", remotePendingChanges.changesInProgressSize()); 4295 } 4296 4297 /** 4298 * Verifies that the given string represents a valid source 4299 * from which this server can be initialized. 4300 * @param sourceString The string representing the source 4301 * @return The source as a integer value 4302 * @throws DirectoryException if the string is not valid 4303 */ 4304 public int decodeSource(String sourceString) throws DirectoryException 4305 { 4306 int source = 0; 4307 try 4308 { 4309 source = Integer.decode(sourceString); 4310 if (source >= -1 && source != getServerId()) 4311 { 4312 // TODO Verifies serverID is in the domain 4313 // We should check here that this is a server implied 4314 // in the current domain. 4315 return source; 4316 } 4317 } 4318 catch (Exception e) 4319 { 4320 LocalizableMessage message = ERR_INVALID_IMPORT_SOURCE.get( 4321 getBaseDN(), getServerId(), sourceString, stackTraceToSingleLineString(e)); 4322 throw new DirectoryException(ResultCode.OTHER, message, e); 4323 } 4324 4325 LocalizableMessage message = ERR_INVALID_IMPORT_SOURCE.get(getBaseDN(), getServerId(), source, ""); 4326 throw new DirectoryException(ResultCode.OTHER, message); 4327 } 4328 4329 /** 4330 * Called by synchronize post op plugin in order to add the entry historical 4331 * attributes to the UpdateMsg. 4332 * @param msg an replication update message 4333 * @param op the operation in progress 4334 */ 4335 private void addEntryAttributesForCL(UpdateMsg msg, 4336 PostOperationOperation op) 4337 { 4338 if (op instanceof PostOperationDeleteOperation) 4339 { 4340 PostOperationDeleteOperation delOp = (PostOperationDeleteOperation) op; 4341 final Set<String> names = getEclIncludesForDeletes(); 4342 Entry entry = delOp.getEntryToDelete(); 4343 final DeleteMsg deleteMsg = (DeleteMsg) msg; 4344 deleteMsg.setEclIncludes(getIncludedAttributes(entry, names)); 4345 4346 // For delete only, add the Authorized DN since it's required in the 4347 // ECL entry but is not part of rest of the message. 4348 DN deleterDN = delOp.getAuthorizationDN(); 4349 if (deleterDN != null) 4350 { 4351 deleteMsg.setInitiatorsName(deleterDN.toString()); 4352 } 4353 } 4354 else if (op instanceof PostOperationModifyOperation) 4355 { 4356 PostOperationModifyOperation modOp = (PostOperationModifyOperation) op; 4357 Set<String> names = getEclIncludes(); 4358 Entry entry = modOp.getCurrentEntry(); 4359 ((ModifyMsg) msg).setEclIncludes(getIncludedAttributes(entry, names)); 4360 } 4361 else if (op instanceof PostOperationModifyDNOperation) 4362 { 4363 PostOperationModifyDNOperation modDNOp = 4364 (PostOperationModifyDNOperation) op; 4365 Set<String> names = getEclIncludes(); 4366 Entry entry = modDNOp.getOriginalEntry(); 4367 ((ModifyDNMsg) msg).setEclIncludes(getIncludedAttributes(entry, names)); 4368 } 4369 else if (op instanceof PostOperationAddOperation) 4370 { 4371 PostOperationAddOperation addOp = (PostOperationAddOperation) op; 4372 Set<String> names = getEclIncludes(); 4373 Entry entry = addOp.getEntryToAdd(); 4374 ((AddMsg) msg).setEclIncludes(getIncludedAttributes(entry, names)); 4375 } 4376 } 4377 4378 private Collection<Attribute> getIncludedAttributes(Entry entry, 4379 Set<String> names) 4380 { 4381 if (names.isEmpty()) 4382 { 4383 // Fast-path. 4384 return Collections.emptySet(); 4385 } 4386 else if (names.size() == 1 && names.contains("*")) 4387 { 4388 // Potential fast-path for delete operations. 4389 List<Attribute> attributes = new LinkedList<>(); 4390 for (List<Attribute> attributeList : entry.getUserAttributes().values()) 4391 { 4392 attributes.addAll(attributeList); 4393 } 4394 Attribute objectClassAttribute = entry.getObjectClassAttribute(); 4395 if (objectClassAttribute != null) 4396 { 4397 attributes.add(objectClassAttribute); 4398 } 4399 return attributes; 4400 } 4401 else 4402 { 4403 // Expand @objectclass references in attribute list if needed. 4404 // We do this now in order to take into account dynamic schema changes. 4405 final Set<String> expandedNames = getExpandedNames(names); 4406 final Entry filteredEntry = 4407 entry.filterEntry(expandedNames, false, false, false); 4408 return filteredEntry.getAttributes(); 4409 } 4410 } 4411 4412 private Set<String> getExpandedNames(Set<String> names) 4413 { 4414 // Only rebuild the attribute set if necessary. 4415 if (!needsExpanding(names)) 4416 { 4417 return names; 4418 } 4419 4420 final Set<String> expandedNames = new HashSet<>(names.size()); 4421 for (String name : names) 4422 { 4423 if (name.startsWith("@")) 4424 { 4425 String ocName = name.substring(1); 4426 ObjectClass objectClass = 4427 DirectoryServer.getObjectClass(toLowerCase(ocName)); 4428 if (objectClass != null) 4429 { 4430 for (AttributeType at : objectClass.getRequiredAttributeChain()) 4431 { 4432 expandedNames.add(at.getNameOrOID()); 4433 } 4434 for (AttributeType at : objectClass.getOptionalAttributeChain()) 4435 { 4436 expandedNames.add(at.getNameOrOID()); 4437 } 4438 } 4439 } 4440 else 4441 { 4442 expandedNames.add(name); 4443 } 4444 } 4445 return expandedNames; 4446 } 4447 4448 private boolean needsExpanding(Set<String> names) 4449 { 4450 for (String name : names) 4451 { 4452 if (name.startsWith("@")) 4453 { 4454 return true; 4455 } 4456 } 4457 return false; 4458 } 4459 4460 /** 4461 * Gets the fractional configuration of this domain. 4462 * @return The fractional configuration of this domain. 4463 */ 4464 FractionalConfig getFractionalConfig() 4465 { 4466 return fractionalConfig; 4467 } 4468 4469 /** 4470 * This bean is a utility class used for holding the parsing 4471 * result of a fractional configuration. It also contains some facility 4472 * methods like fractional configuration comparison... 4473 */ 4474 static class FractionalConfig 4475 { 4476 /** 4477 * Tells if fractional replication is enabled or not (some fractional 4478 * constraints have been put in place). If this is true then 4479 * fractionalExclusive explains the configuration mode and either 4480 * fractionalSpecificClassesAttributes or fractionalAllClassesAttributes or 4481 * both should be filled with something. 4482 */ 4483 private boolean fractional; 4484 4485 /** 4486 * - If true, tells that the configured fractional replication is exclusive: 4487 * Every attributes contained in fractionalSpecificClassesAttributes and 4488 * fractionalAllClassesAttributes should be ignored when replaying operation 4489 * in local backend. 4490 * - If false, tells that the configured fractional replication is 4491 * inclusive: 4492 * Only attributes contained in fractionalSpecificClassesAttributes and 4493 * fractionalAllClassesAttributes should be taken into account in local 4494 * backend. 4495 */ 4496 private boolean fractionalExclusive = true; 4497 4498 /** 4499 * Used in fractional replication: holds attributes of a specific object class. 4500 * - key = object class (name or OID of the class) 4501 * - value = the attributes of that class that should be taken into account 4502 * (inclusive or exclusive fractional replication) (name or OID of the 4503 * attribute) 4504 * When an operation coming from the network is to be locally replayed, if 4505 * the concerned entry has an objectClass attribute equals to 'key': 4506 * - inclusive mode: only the attributes in 'value' will be added/deleted/modified 4507 * - exclusive mode: the attributes in 'value' will not be added/deleted/modified 4508 */ 4509 private Map<String, Set<String>> fractionalSpecificClassesAttributes = new HashMap<>(); 4510 4511 /** 4512 * Used in fractional replication: holds attributes of any object class. 4513 * When an operation coming from the network is to be locally replayed: 4514 * - inclusive mode: only attributes of the matching entry not present in 4515 * fractionalAllClassesAttributes will be added/deleted/modified 4516 * - exclusive mode: attributes of the matching entry present in 4517 * fractionalAllClassesAttributes will not be added/deleted/modified 4518 * The attributes may be in human readable form of OID form. 4519 */ 4520 private Set<String> fractionalAllClassesAttributes = new HashSet<>(); 4521 4522 /** Base DN the fractional configuration is for. */ 4523 private final DN baseDN; 4524 4525 /** 4526 * Constructs a new fractional configuration object. 4527 * @param baseDN The base DN the object is for. 4528 */ 4529 private FractionalConfig(DN baseDN) 4530 { 4531 this.baseDN = baseDN; 4532 } 4533 4534 /** 4535 * Getter for fractional. 4536 * @return True if the configuration has fractional enabled 4537 */ 4538 boolean isFractional() 4539 { 4540 return fractional; 4541 } 4542 4543 /** 4544 * Set the fractional parameter. 4545 * @param fractional The fractional parameter 4546 */ 4547 private void setFractional(boolean fractional) 4548 { 4549 this.fractional = fractional; 4550 } 4551 4552 /** 4553 * Getter for fractionalExclusive. 4554 * @return True if the configuration has fractional exclusive enabled 4555 */ 4556 boolean isFractionalExclusive() 4557 { 4558 return fractionalExclusive; 4559 } 4560 4561 /** 4562 * Set the fractionalExclusive parameter. 4563 * @param fractionalExclusive The fractionalExclusive parameter 4564 */ 4565 private void setFractionalExclusive(boolean fractionalExclusive) 4566 { 4567 this.fractionalExclusive = fractionalExclusive; 4568 } 4569 4570 /** 4571 * Getter for fractionalSpecificClassesAttributes attribute. 4572 * @return The fractionalSpecificClassesAttributes attribute. 4573 */ 4574 Map<String, Set<String>> getFractionalSpecificClassesAttributes() 4575 { 4576 return fractionalSpecificClassesAttributes; 4577 } 4578 4579 /** 4580 * Set the fractionalSpecificClassesAttributes parameter. 4581 * @param fractionalSpecificClassesAttributes The 4582 * fractionalSpecificClassesAttributes parameter to set. 4583 */ 4584 private void setFractionalSpecificClassesAttributes( 4585 Map<String, Set<String>> fractionalSpecificClassesAttributes) 4586 { 4587 this.fractionalSpecificClassesAttributes = 4588 fractionalSpecificClassesAttributes; 4589 } 4590 4591 /** 4592 * Getter for fractionalSpecificClassesAttributes attribute. 4593 * @return The fractionalSpecificClassesAttributes attribute. 4594 */ 4595 Set<String> getFractionalAllClassesAttributes() 4596 { 4597 return fractionalAllClassesAttributes; 4598 } 4599 4600 /** 4601 * Set the fractionalAllClassesAttributes parameter. 4602 * @param fractionalAllClassesAttributes The 4603 * fractionalSpecificClassesAttributes parameter to set. 4604 */ 4605 private void setFractionalAllClassesAttributes( 4606 Set<String> fractionalAllClassesAttributes) 4607 { 4608 this.fractionalAllClassesAttributes = fractionalAllClassesAttributes; 4609 } 4610 4611 /** 4612 * Getter for the base baseDN. 4613 * @return The baseDN attribute. 4614 */ 4615 DN getBaseDn() 4616 { 4617 return baseDN; 4618 } 4619 4620 /** 4621 * Extract the fractional configuration from the passed domain configuration 4622 * entry. 4623 * @param configuration The configuration object 4624 * @return The fractional replication configuration. 4625 * @throws ConfigException If an error occurred. 4626 */ 4627 static FractionalConfig toFractionalConfig( 4628 ReplicationDomainCfg configuration) throws ConfigException 4629 { 4630 // Prepare fractional configuration variables to parse 4631 Iterator<String> exclIt = configuration.getFractionalExclude().iterator(); 4632 Iterator<String> inclIt = configuration.getFractionalInclude().iterator(); 4633 4634 // Get potentially new fractional configuration 4635 Map<String, Set<String>> newFractionalSpecificClassesAttributes = new HashMap<>(); 4636 Set<String> newFractionalAllClassesAttributes = new HashSet<>(); 4637 4638 int newFractionalMode = parseFractionalConfig(exclIt, inclIt, 4639 newFractionalSpecificClassesAttributes, 4640 newFractionalAllClassesAttributes); 4641 4642 // Create matching parsed config object 4643 FractionalConfig result = new FractionalConfig(configuration.getBaseDN()); 4644 switch (newFractionalMode) 4645 { 4646 case NOT_FRACTIONAL: 4647 result.setFractional(false); 4648 result.setFractionalExclusive(true); 4649 break; 4650 case EXCLUSIVE_FRACTIONAL: 4651 case INCLUSIVE_FRACTIONAL: 4652 result.setFractional(true); 4653 result.setFractionalExclusive( 4654 newFractionalMode == EXCLUSIVE_FRACTIONAL); 4655 break; 4656 } 4657 result.setFractionalSpecificClassesAttributes( 4658 newFractionalSpecificClassesAttributes); 4659 result.setFractionalAllClassesAttributes( 4660 newFractionalAllClassesAttributes); 4661 return result; 4662 } 4663 4664 /** 4665 * Parses a fractional replication configuration, filling the empty passed 4666 * variables and returning the used fractional mode. The 2 passed variables 4667 * to fill should be initialized (not null) and empty. 4668 * @param exclIt The list of fractional exclude configuration values (may be 4669 * null) 4670 * @param inclIt The list of fractional include configuration values (may be 4671 * null) 4672 * @param fractionalSpecificClassesAttributes An empty map to be filled with 4673 * what is read from the fractional configuration properties. 4674 * @param fractionalAllClassesAttributes An empty list to be filled with 4675 * what is read from the fractional configuration properties. 4676 * @return the fractional mode deduced from the passed configuration: 4677 * not fractional, exclusive fractional or inclusive fractional 4678 * modes 4679 */ 4680 private static int parseFractionalConfig( 4681 Iterator<?> exclIt, Iterator<?> inclIt, 4682 Map<String, Set<String>> fractionalSpecificClassesAttributes, 4683 Set<String> fractionalAllClassesAttributes) throws ConfigException 4684 { 4685 // Determine if fractional-exclude or fractional-include property is used: 4686 // only one of them is allowed 4687 int fractionalMode; 4688 Iterator<?> iterator; 4689 if (exclIt != null && exclIt.hasNext()) 4690 { 4691 if (inclIt != null && inclIt.hasNext()) 4692 { 4693 throw new ConfigException( 4694 NOTE_ERR_FRACTIONAL_CONFIG_BOTH_MODES.get()); 4695 } 4696 4697 fractionalMode = EXCLUSIVE_FRACTIONAL; 4698 iterator = exclIt; 4699 } 4700 else 4701 { 4702 if (inclIt != null && inclIt.hasNext()) 4703 { 4704 fractionalMode = INCLUSIVE_FRACTIONAL; 4705 iterator = inclIt; 4706 } 4707 else 4708 { 4709 return NOT_FRACTIONAL; 4710 } 4711 } 4712 4713 while (iterator.hasNext()) 4714 { 4715 // Parse a value with the form class:attr1,attr2... 4716 // or *:attr1,attr2... 4717 String fractCfgStr = iterator.next().toString(); 4718 StringTokenizer st = new StringTokenizer(fractCfgStr, ":"); 4719 int nTokens = st.countTokens(); 4720 if (nTokens < 2) 4721 { 4722 throw new ConfigException(NOTE_ERR_FRACTIONAL_CONFIG_WRONG_FORMAT.get(fractCfgStr)); 4723 } 4724 // Get the class name 4725 String classNameLower = st.nextToken().toLowerCase(); 4726 boolean allClasses = "*".equals(classNameLower); 4727 // Get the attributes 4728 String attributes = st.nextToken(); 4729 st = new StringTokenizer(attributes, ","); 4730 while (st.hasMoreTokens()) 4731 { 4732 String attrNameLower = st.nextToken().toLowerCase(); 4733 // Store attribute in the appropriate variable 4734 if (allClasses) 4735 { 4736 fractionalAllClassesAttributes.add(attrNameLower); 4737 } 4738 else 4739 { 4740 Set<String> attrList = fractionalSpecificClassesAttributes.get(classNameLower); 4741 if (attrList == null) 4742 { 4743 attrList = new LinkedHashSet<>(); 4744 fractionalSpecificClassesAttributes.put(classNameLower, attrList); 4745 } 4746 attrList.add(attrNameLower); 4747 } 4748 } 4749 } 4750 return fractionalMode; 4751 } 4752 4753 /** Return type of the parseFractionalConfig method. */ 4754 private static final int NOT_FRACTIONAL = 0; 4755 private static final int EXCLUSIVE_FRACTIONAL = 1; 4756 private static final int INCLUSIVE_FRACTIONAL = 2; 4757 4758 /** 4759 * Get an integer representation of the domain fractional configuration. 4760 * @return An integer representation of the domain fractional configuration. 4761 */ 4762 private int fractionalConfigToInt() 4763 { 4764 if (!fractional) 4765 { 4766 return NOT_FRACTIONAL; 4767 } 4768 else if (fractionalExclusive) 4769 { 4770 return EXCLUSIVE_FRACTIONAL; 4771 } 4772 return INCLUSIVE_FRACTIONAL; 4773 } 4774 4775 /** 4776 * Compare 2 fractional replication configurations and returns true if they 4777 * are equivalent. 4778 * @param cfg1 First fractional configuration 4779 * @param cfg2 Second fractional configuration 4780 * @return True if both configurations are equivalent. 4781 * @throws ConfigException If some classes or attributes could not be 4782 * retrieved from the schema. 4783 */ 4784 private static boolean isFractionalConfigEquivalent(FractionalConfig cfg1, 4785 FractionalConfig cfg2) throws ConfigException 4786 { 4787 // Compare base DNs just to be consistent 4788 if (!cfg1.getBaseDn().equals(cfg2.getBaseDn())) 4789 { 4790 return false; 4791 } 4792 4793 // Compare modes 4794 if (cfg1.isFractional() != cfg2.isFractional() 4795 || cfg1.isFractionalExclusive() != cfg2.isFractionalExclusive()) 4796 { 4797 return false; 4798 } 4799 4800 // Compare all classes attributes 4801 Set<String> allClassesAttrs1 = cfg1.getFractionalAllClassesAttributes(); 4802 Set<String> allClassesAttrs2 = cfg2.getFractionalAllClassesAttributes(); 4803 if (!areAttributesEquivalent(allClassesAttrs1, allClassesAttrs2)) 4804 { 4805 return false; 4806 } 4807 4808 // Compare specific classes attributes 4809 Map<String, Set<String>> specificClassesAttrs1 = 4810 cfg1.getFractionalSpecificClassesAttributes(); 4811 Map<String, Set<String>> specificClassesAttrs2 = 4812 cfg2.getFractionalSpecificClassesAttributes(); 4813 if (specificClassesAttrs1.size() != specificClassesAttrs2.size()) 4814 { 4815 return false; 4816 } 4817 4818 /* 4819 * Check consistency of specific classes attributes 4820 * 4821 * For each class in specificClassesAttributes1, check that the attribute 4822 * list is equivalent to specificClassesAttributes2 attribute list 4823 */ 4824 Schema schema = DirectoryServer.getSchema(); 4825 for (String className1 : specificClassesAttrs1.keySet()) 4826 { 4827 // Get class from specificClassesAttributes1 4828 ObjectClass objectClass1 = schema.getObjectClass(className1); 4829 if (objectClass1 == null) 4830 { 4831 throw new ConfigException( 4832 NOTE_ERR_FRACTIONAL_CONFIG_UNKNOWN_OBJECT_CLASS.get(className1)); 4833 } 4834 4835 // Look for matching one in specificClassesAttributes2 4836 boolean foundClass = false; 4837 for (String className2 : specificClassesAttrs2.keySet()) 4838 { 4839 ObjectClass objectClass2 = schema.getObjectClass(className2); 4840 if (objectClass2 == null) 4841 { 4842 throw new ConfigException( 4843 NOTE_ERR_FRACTIONAL_CONFIG_UNKNOWN_OBJECT_CLASS.get(className2)); 4844 } 4845 if (objectClass1.equals(objectClass2)) 4846 { 4847 foundClass = true; 4848 // Now compare the 2 attribute lists 4849 Set<String> attributes1 = specificClassesAttrs1.get(className1); 4850 Set<String> attributes2 = specificClassesAttrs2.get(className2); 4851 if (!areAttributesEquivalent(attributes1, attributes2)) 4852 { 4853 return false; 4854 } 4855 break; 4856 } 4857 } 4858 // Found matching class ? 4859 if (!foundClass) 4860 { 4861 return false; 4862 } 4863 } 4864 4865 return true; 4866 } 4867 } 4868 4869 /** 4870 * Specifies whether this domain is enabled/disabled regarding the ECL. 4871 * @return enabled/disabled for the ECL. 4872 */ 4873 boolean isECLEnabled() 4874 { 4875 return this.eclDomain.isEnabled(); 4876 } 4877 4878 /** 4879 * Return the minimum time (in ms) that the domain keeps the historical 4880 * information necessary to solve conflicts. 4881 * 4882 * @return the purge delay. 4883 */ 4884 long getHistoricalPurgeDelay() 4885 { 4886 return config.getConflictsHistoricalPurgeDelay() * 60 * 1000; 4887 } 4888 4889 /** 4890 * Check and purge the historical attribute on all eligible entries under this domain. 4891 * 4892 * The purging logic is the same applied to individual entries during modify operations. This 4893 * task may be useful in scenarios where a large number of changes are made as a one-off occurrence. 4894 * Running a purge-historical after the 'ds-cfg-conflicts-historical-purge-delay' period has elapsed 4895 * would clear out obsolete historical data from all the modified entries reducing the overall 4896 * database size. 4897 * 4898 * @param task 4899 * the task raising this purge. 4900 * @param endDate 4901 * the date to stop this task whether the job is done or not. 4902 * @throws DirectoryException 4903 * when an exception happens. 4904 */ 4905 public void purgeConflictsHistorical(PurgeConflictsHistoricalTask task, 4906 long endDate) throws DirectoryException 4907 { 4908 logger.trace("[PURGE] purgeConflictsHistorical " 4909 + "on domain: " + getBaseDN() 4910 + "endDate:" + new Date(endDate) 4911 + "lastCSNPurgedFromHist: " 4912 + lastCSNPurgedFromHist.toStringUI()); 4913 4914 4915 // It would be nice to have an upper bound on this filter to eliminate results that don't have a purgeable 4916 // csn in them. However, historicalCsnOrderingMatch keys start with serverid rather than timestamp so this 4917 // isn't possible. 4918 String filter = "(" + HISTORICAL_ATTRIBUTE_NAME + ">=dummy:" + lastCSNPurgedFromHist + ")"; 4919 4920 int count = 0; 4921 boolean finished = false; 4922 ByteString pagingCookie = null; 4923 4924 while(!finished) 4925 { 4926 if (task != null) 4927 { 4928 task.setProgressStats(lastCSNPurgedFromHist, count); 4929 } 4930 4931 finished = true; 4932 4933 4934 SearchRequest request = Requests.newSearchRequest(getBaseDN(), SearchScope.WHOLE_SUBTREE, filter) 4935 .addAttribute(USER_AND_REPL_OPERATIONAL_ATTRS) 4936 .addControl(new PagedResultsControl(false, ConfigConstants.DEFAULT_SIZE_LIMIT, pagingCookie)) 4937 .setSizeLimit(ConfigConstants.DEFAULT_SIZE_LIMIT + 1); 4938 4939 InternalSearchOperation searchOp = conn.processSearch(request); 4940 4941 for (Control c : searchOp.getResponseControls()) 4942 { 4943 if (c.getOID().equals(OID_PAGED_RESULTS_CONTROL)) 4944 { 4945 ByteString newPagingCookie = ((PagedResultsControl)c).getCookie(); 4946 4947 if( newPagingCookie != null && 4948 newPagingCookie.length() > 0 && 4949 !newPagingCookie.equals(pagingCookie)) 4950 { 4951 pagingCookie = newPagingCookie; 4952 finished = false; 4953 } 4954 } 4955 } 4956 4957 for (SearchResultEntry entry : searchOp.getSearchEntries()) 4958 { 4959 long maxTimeToRun = endDate - TimeThread.getTime(); 4960 if (maxTimeToRun < 0) { 4961 throw new DirectoryException(ResultCode.ADMIN_LIMIT_EXCEEDED, 4962 LocalizableMessage.raw(" end date reached")); 4963 } 4964 4965 EntryHistorical entryHist = EntryHistorical.newInstanceFromEntry(entry); 4966 4967 CSN latestOldCSN = entryHist.getOldestCSN(); 4968 entryHist.setPurgeDelay(getHistoricalPurgeDelay()); 4969 Attribute attr = entryHist.encodeAndPurge(); 4970 4971 if(entryHist.getLastPurgedValuesCount() > 0) 4972 { 4973 lastCSNPurgedFromHist = latestOldCSN; 4974 List<Modification> mods = newArrayList(new Modification(ModificationType.REPLACE, attr)); 4975 count += entryHist.getLastPurgedValuesCount(); 4976 ModifyOperation newOp = new ModifyOperationBasis( 4977 conn, nextOperationID(), nextMessageID(), new ArrayList<Control>(0), 4978 entry.getName(), mods); 4979 runAsSynchronizedOperation(newOp); 4980 4981 if (newOp.getResultCode() != ResultCode.SUCCESS) 4982 { 4983 // Log information for the repair tool. 4984 logger.error(ERR_CANNOT_ADD_CONFLICT_ATTRIBUTE, newOp, newOp.getResultCode()); 4985 } 4986 else if (task != null) 4987 { 4988 task.setProgressStats(lastCSNPurgedFromHist, count); 4989 } 4990 } 4991 } 4992 } 4993 // If a full sweep was completed, the lastCSNPurgedFromHist must be reset so that the next 4994 // run-through starts from the beginning. Otherwise, subsequent runs of the task would only 4995 // pick up purgeable changes for the last server id. 4996 lastCSNPurgedFromHist = new CSN(0,0,0); 4997 } 4998}