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}