From 1afbb2969f0f5bb254f2c777f57803635be23a9e Mon Sep 17 00:00:00 2001
From: gbellato <gbellato@localhost>
Date: Mon, 11 Jun 2007 10:08:13 +0000
Subject: [PATCH] Final part for issue 604 : replication needs to resolve naming conflicts

---
 opendj-sdk/opends/src/server/org/opends/server/messages/ReplicationMessages.java                            |   25 ++
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/UpdateOperationTest.java |  190 +++++++++++++--
 opendj-sdk/opends/src/server/org/opends/server/replication/protocol/AddMsg.java                             |   32 ++
 opendj-sdk/opends/src/server/org/opends/server/replication/plugin/ReplicationDomain.java                    |  458 +++++++++++++++++++++++++++----------
 opendj-sdk/opends/resource/schema/02-config.ldif                                                            |    3 
 5 files changed, 546 insertions(+), 162 deletions(-)

diff --git a/opendj-sdk/opends/resource/schema/02-config.ldif b/opendj-sdk/opends/resource/schema/02-config.ldif
index 21bb051..dd453eb 100644
--- a/opendj-sdk/opends/resource/schema/02-config.ldif
+++ b/opendj-sdk/opends/resource/schema/02-config.ldif
@@ -1276,6 +1276,9 @@
   NAME 'ds-cfg-file-size-limit'
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
   X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.380 NAME 'ds-sync-conflict'
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+  USAGE directoryOperation X-ORIGIN 'OpenDS Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
   NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL
   MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled )
diff --git a/opendj-sdk/opends/src/server/org/opends/server/messages/ReplicationMessages.java b/opendj-sdk/opends/src/server/org/opends/server/messages/ReplicationMessages.java
index fb4e3cf..85f2e53 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/messages/ReplicationMessages.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/messages/ReplicationMessages.java
@@ -401,6 +401,24 @@
   public static final int MSGID_BAD_HISTORICAL =
     CATEGORY_MASK_SYNC | SEVERITY_MASK_SEVERE_ERROR | 56;
 
+  /**
+   * Could not add the conflict attribute to an entry after a conflict was
+   * deteceted.
+   */
+  public static final int MSGID_CANNOT_ADD_CONFLICT_ATTRIBUTE =
+    CATEGORY_MASK_SYNC | SEVERITY_MASK_SEVERE_ERROR | 57;
+
+  /**
+   * Could not rename a conflicting entry.
+   */
+  public static final int MSGID_CANNOT_RENAME_CONFLICT_ENTRY =
+    CATEGORY_MASK_SYNC | SEVERITY_MASK_SEVERE_ERROR | 58;
+
+  /**
+   * Eception durin rename of a conflicting entry.
+   */
+  public static final int MSGID_EXCEPTION_RENAME_CONFLICT_ENTRY =
+    CATEGORY_MASK_SYNC | SEVERITY_MASK_SEVERE_ERROR | 59;
 
   /**
    * Register the messages from this class in the core server.
@@ -550,5 +568,12 @@
     registerMessage(MSGID_BAD_HISTORICAL,
         "Entry %s was containing some unknown historical information,"
         + " This may cause some inconsistency for this entry");
+    registerMessage(MSGID_CANNOT_ADD_CONFLICT_ATTRIBUTE,
+        "A conflict was detected but the conflict information could not be" +
+        "added. Operation : ");
+    registerMessage(MSGID_CANNOT_RENAME_CONFLICT_ENTRY,
+        "An error happened trying the rename a conflicting entry : ");
+    registerMessage(MSGID_EXCEPTION_RENAME_CONFLICT_ENTRY,
+        "An Exception happened when trying the rename a conflicting entry : ");
   }
 }
diff --git a/opendj-sdk/opends/src/server/org/opends/server/replication/plugin/ReplicationDomain.java b/opendj-sdk/opends/src/server/org/opends/server/replication/plugin/ReplicationDomain.java
index 4275f74..3d76049 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/replication/plugin/ReplicationDomain.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/replication/plugin/ReplicationDomain.java
@@ -75,6 +75,7 @@
 import org.opends.server.replication.common.ServerState;
 import org.opends.server.replication.protocol.AckMessage;
 import org.opends.server.replication.protocol.AddContext;
+import org.opends.server.replication.protocol.AddMsg;
 import org.opends.server.replication.protocol.DeleteContext;
 import org.opends.server.replication.protocol.DoneMessage;
 import org.opends.server.replication.protocol.EntryMessage;
@@ -91,6 +92,9 @@
 import org.opends.server.tasks.InitializeTargetTask;
 import org.opends.server.tasks.InitializeTask;
 import org.opends.server.tasks.TaskUtils;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.AttributeValue;
 import org.opends.server.types.ConfigChangeResult;
 import org.opends.server.types.DN;
 import org.opends.server.types.DereferencePolicy;
@@ -102,6 +106,7 @@
 import org.opends.server.types.LDIFExportConfig;
 import org.opends.server.types.LDIFImportConfig;
 import org.opends.server.types.Modification;
+import org.opends.server.types.ModificationType;
 import org.opends.server.types.Operation;
 import org.opends.server.types.RDN;
 import org.opends.server.types.ResultCode;
@@ -122,6 +127,13 @@
        implements ConfigurationChangeListener<MultimasterDomainCfg>
 {
   /**
+   * The attribute used to mark conflicting entries.
+   * The value of this attribute should be the dn that this entry was
+   * supposed to have when it was marked as conflicting.
+   */
+  public static final String DS_SYNC_CONFLICT = "ds-sync-conflict";
+
+  /**
    * The tracer object for the debug logger.
    */
   private static final DebugTracer TRACER = getTracer();
@@ -1117,10 +1129,11 @@
           else if (op instanceof AddOperation)
           {
             AddOperation newOp = (AddOperation) op;
+            AddMsg addMsg = (AddMsg) msg;
             dependency = pendingChanges.checkDependencies(newOp);
             if (!dependency)
             {
-              done = solveNamingConflict(newOp, msg);
+              done = solveNamingConflict(newOp, addMsg);
             }
           }
           else if (op instanceof ModifyDNOperation)
@@ -1165,10 +1178,6 @@
 
         updateError(changeNumber);
       }
-      else
-      {
-        numResolvedNamingConflicts.incrementAndGet();
-      }
     }
     catch (ASN1Exception e)
     {
@@ -1353,26 +1362,38 @@
     if (result == ResultCode.NO_SUCH_OBJECT)
     {
       /*
-       * This error may happen the operation is a modification but
-       * the entry had been renamed on a different master in the same time.
+       * The operation is a modification but
+       * the entry has been renamed on a different master in the same time.
        * search if the entry has been renamed, and return the new dn
        * of the entry.
        */
       DN newdn = findEntryDN(entryUid);
       if (newdn != null)
       {
+        // There is an entry with the same unique id as this modify operation
+        // replay the modify using the current dn of this entry.
         msg.setDn(newdn.toString());
+        numResolvedNamingConflicts.incrementAndGet();
         return false;
       }
       else
+      {
+        // This entry does not exist anymore.
+        // It has probably been deleted, stop the processing of this operation
+        numResolvedNamingConflicts.incrementAndGet();
         return true;
+      }
     }
-
-    // TODO log a message for the repair tool.
-    return true;
+    else
+    {
+      // The other type of errors can not be caused by naming conflicts.
+      // TODO log a message for the repair tool.
+      return true;
+    }
   }
 
- /** Solve a conflict detected when replaying a delete operation.
+ /**
+  * Solve a conflict detected when replaying a delete operation.
   *
   * @param op The operation that triggered the conflict detection.
   * @param msg The operation that triggered the conflict detection.
@@ -1399,6 +1420,7 @@
         * has already done the job.
         * In any case, there is is nothing more to do.
         */
+       numResolvedNamingConflicts.incrementAndGet();
        return true;
      }
      else
@@ -1407,6 +1429,7 @@
         * This entry has been renamed, replay the delete using its new DN.
         */
        msg.setDn(currentDn.toString());
+       numResolvedNamingConflicts.incrementAndGet();
        return false;
      }
    }
@@ -1415,17 +1438,129 @@
      /*
       * This may happen when we replay a DELETE done on a master
       * but children of this entry have been added on another master.
+      *
+      * Rename all the children by adding entryuuid in dn and delete this entry.
+      *
+      * The action taken here must be consistent with the actions
+      * done in the solveNamingConflict(AddOperation) method
+      * when we are adding an entry whose parent entry has already been deleted.
       */
-
-     /*
-      * TODO : either delete all the childs or rename the child below
-      * the top suffix by adding entryuuid in dn and delete this entry.
-      */
+     findAndRenameChild(entryUid, op.getEntryDN(), op);
+     numUnresolvedNamingConflicts.incrementAndGet();
+     return false;
    }
-   return true;
+   else
+   {
+     // The other type of errors can not be caused by naming conflicts.
+     // TODO log a message for the repair tool.
+     return true;
+   }
  }
 
   /**
+ * Solve a conflict detected when replaying a Modify DN operation.
+ *
+ * @param op The operation that triggered the conflict detection.
+ * @param msg The operation that triggered the conflict detection.
+ * @return true if the process is completed, false if it must continue.
+ * @throws Exception When the operation is not valid.
+ */
+private boolean solveNamingConflict(ModifyDNOperation op,
+    UpdateMessage msg) throws Exception
+{
+  ResultCode result = op.getResultCode();
+  ModifyDnContext ctx = (ModifyDnContext) op.getAttachment(SYNCHROCONTEXT);
+  String entryUid = ctx.getEntryUid();
+  String newSuperiorID = ctx.getNewParentId();
+
+  /*
+   * four possible cases :
+   * - the modified entry has been renamed
+   * - the new parent has been renamed
+   * - the operation is replayed for the second time.
+   * - the entry has been deleted
+   * action :
+   *  - change the target dn and the new parent dn and
+   *        restart the operation,
+   *  - don't do anything if the operation is replayed.
+   */
+
+  // Construct the new DN to use for the entry.
+  DN entryDN = op.getEntryDN();
+  DN newSuperior = findEntryDN(newSuperiorID);
+  RDN newRDN = op.getNewRDN();
+  DN parentDN;
+
+  if (newSuperior == null)
+  {
+    parentDN = entryDN.getParent();
+  }
+  else
+  {
+    parentDN = newSuperior;
+  }
+
+  if ((parentDN == null) || parentDN.isNullDN())
+  {
+    /* this should never happen
+     * can't solve any conflict in this case.
+     */
+    throw new Exception("operation parameters are invalid");
+  }
+
+  DN newDN = parentDN.concat(newRDN);
+
+  // get the current DN of this entry in the database.
+  DN currentDN = findEntryDN(entryUid);
+
+  // if the newDN and the current DN match then the operation
+  // is a no-op (this was probably a second replay)
+  // don't do anything.
+  if (newDN.equals(currentDN))
+  {
+    numResolvedNamingConflicts.incrementAndGet();
+    return true;
+  }
+
+  if ((result == ResultCode.NO_SUCH_OBJECT) ||
+      (result == ResultCode.OBJECTCLASS_VIOLATION))
+  {
+    /*
+     * The entry or it's new parent has not been found
+     * reconstruct the operation with the DN we just built
+     */
+    ModifyDNMsg modifyDnMsg = (ModifyDNMsg) msg;
+    msg.setDn(currentDN.toString());
+    modifyDnMsg.setNewSuperior(newSuperior.toString());
+    numResolvedNamingConflicts.incrementAndGet();
+    return false;
+  }
+  else if (result == ResultCode.ENTRY_ALREADY_EXISTS)
+  {
+    /*
+     * This may happen when two modifyDn operation
+     * are done on different servers but with the same target DN
+     * add the conflict object class to the entry
+     * and rename it using its entryuuid.
+     */
+    ModifyDNMsg modifyDnMsg = (ModifyDNMsg) msg;
+    markConflictEntry(op, op.getEntryDN(), newDN);
+    modifyDnMsg.setNewRDN(generateConflictRDN(entryUid,
+                          modifyDnMsg.getNewRDN()));
+    modifyDnMsg.setNewSuperior(newSuperior.toString());
+    numUnresolvedNamingConflicts.incrementAndGet();
+    return false;
+  }
+  else
+  {
+    // The other type of errors can not be caused by naming conflicts.
+    // TODO log a message for the repair tool.
+    return true;
+  }
+}
+
+
+  /**
    * Solve a conflict detected when replaying a ADD operation.
    *
    * @param op The operation that triggered the conflict detection.
@@ -1434,7 +1569,7 @@
    * @throws Exception When the operation is not valid.
    */
   private boolean solveNamingConflict(AddOperation op,
-      UpdateMessage msg) throws Exception
+      AddMsg msg) throws Exception
   {
     ResultCode result = op.getResultCode();
     AddContext ctx = (AddContext) op.getAttachment(SYNCHROCONTEXT);
@@ -1451,7 +1586,7 @@
       {
         /*
          * This entry is the base dn of the backend.
-         * It is quite weird that the operation result be NO_SUCH_OBJECT.
+         * It is quite surprising that the operation result be NO_SUCH_OBJECT.
          * There is nothing more we can do except TODO log a
          * message for the repair tool to look at this problem.
          */
@@ -1461,15 +1596,28 @@
       if (parentDn == null)
       {
         /*
-         * The parent has been deleted, so this entry should not
-         * exist don't do the ADD.
+         * The parent has been deleted
+         * rename the entry as a conflicting entry.
+         * The action taken here must be consistent with the actions
+         * done when in the solveNamingConflict(DeleteOperation) method
+         * when we are deleting an entry that have some child entries.
          */
-        return true;
+        addConflict(msg);
+
+        msg.setDn(generateConflictRDN(entryUid,
+                    op.getEntryDN().getRDN().toString()) + ","
+                    + baseDN);
+        // reset the parent uid so that the check done is the handleConflict
+        // phase does not fail.
+        msg.setParentUid(null);
+        numUnresolvedNamingConflicts.incrementAndGet();
+        return false;
       }
       else
       {
         RDN entryRdn = DN.decode(msg.getDn()).getRDN();
         msg.setDn(entryRdn + "," + parentDn);
+        numResolvedNamingConflicts.incrementAndGet();
         return false;
       }
     }
@@ -1491,132 +1639,158 @@
       }
       else
       {
-        addConflict(op);
-        msg.setDn(generateConflictDn(entryUid, msg.getDn()));
+        addConflict(msg);
+        msg.setDn(generateConflictRDN(entryUid, msg.getDn()));
+        numUnresolvedNamingConflicts.incrementAndGet();
         return false;
       }
     }
-    return true;
-  }
-
-  /**
-   * Solve a conflict detected when replaying a Modify DN operation.
-   *
-   * @param op The operation that triggered the conflict detection.
-   * @param msg The operation that triggered the conflict detection.
-   * @return true if the process is completed, false if it must continue.
-   * @throws Exception When the operation is not valid.
-   */
-  private boolean solveNamingConflict(ModifyDNOperation op,
-      UpdateMessage msg) throws Exception
-  {
-    ResultCode result = op.getResultCode();
-    ModifyDnContext ctx = (ModifyDnContext) op.getAttachment(SYNCHROCONTEXT);
-    String entryUid = ctx.getEntryUid();
-    String newSuperiorID = ctx.getNewParentId();
-
-    /*
-     * four possible cases :
-     * - the modified entry has been renamed
-     * - the new parent has been renamed
-     * - the operation is replayed for the second time.
-     * - the entry has been deleted
-     * action :
-     *  - change the target dn and the new parent dn and
-     *        restart the operation,
-     *  - don't do anything if the operation is replayed.
-     */
-
-    // Construct the new DN to use for the entry.
-    DN entryDN = op.getEntryDN();
-    DN newSuperior = findEntryDN(newSuperiorID);
-    RDN newRDN = op.getNewRDN();
-    DN parentDN;
-
-    if (newSuperior == null)
-    {
-      parentDN = entryDN.getParent();
-    }
     else
     {
-      parentDN = newSuperior;
-    }
-
-    if ((parentDN == null) || parentDN.isNullDN())
-    {
-      /* this should never happen
-       * can't solve any conflict in this case.
-       */
-      throw new Exception("operation parameters are invalid");
-    }
-
-    DN newDN = parentDN.concat(newRDN);
-
-    // get the current DN of this entry in the database.
-    DN currentDN = findEntryDN(entryUid);
-
-    // if the newDN and the current DN match then the operation
-    // is a no-op (this was probably a second replay)
-    // don't do anything.
-    if (newDN.equals(currentDN))
-    {
+      // The other type of errors can not be caused by naming conflicts.
+      // TODO log a message for the repair tool.
       return true;
     }
-
-    if ((result == ResultCode.NO_SUCH_OBJECT) ||
-        (result == ResultCode.OBJECTCLASS_VIOLATION))
-    {
-      /*
-       * The entry or it's new parent has not been found
-       * reconstruct the operation with the DN we just built
-       */
-      ModifyDNMsg modifyDnMsg = (ModifyDNMsg) msg;
-      msg.setDn(currentDN.toString());
-      modifyDnMsg.setNewSuperior(newSuperior.toString());
-      numUnresolvedNamingConflicts.incrementAndGet();
-      return false;
-    }
-    else if (result == ResultCode.ENTRY_ALREADY_EXISTS)
-    {
-      /*
-       * This may happen when two modifyDn operation
-       * are done on different servers but with the same target DN
-       * add the conflict object class to the entry
-       * and rename it using its entryuuid.
-       */
-      ModifyDNMsg modifyDnMsg = (ModifyDNMsg) msg;
-      generateAddConflictOp(op);
-      modifyDnMsg.setNewRDN(generateConflictDn(entryUid,
-                            modifyDnMsg.getNewRDN()));
-      modifyDnMsg.setNewSuperior(newSuperior.toString());
-      numUnresolvedNamingConflicts.incrementAndGet();
-      return false;
-    }
-    return true;
   }
 
   /**
-   * Generate a modification to add the conflict ObjectClass to an entry
+   * Find all the entries below the provided DN and rename them
+   * so that they stay below the baseDn of this replicationDomain and
+   * use the conflicting name and attribute.
+   *
+   * @param entryUid   The unique ID of the entry whose child must be renamed.
+   * @param entryDN    The DN of the entry whose child must be renamed.
+   * @param conflictOp The Operation that generated the conflict.
+   */
+  private void findAndRenameChild(
+      String entryUid, DN entryDN, Operation conflictOp)
+  {
+    // Find an rename child entries.
+    InternalClientConnection conn =
+      InternalClientConnection.getRootConnection();
+
+    try
+    {
+      LinkedHashSet<String> attrs = new LinkedHashSet<String>(1);
+      attrs.add(ENTRYUIDNAME);
+
+      SearchFilter ALLMATCH;
+      ALLMATCH = SearchFilter.createFilterFromString("(objectClass=*)");
+      InternalSearchOperation op =
+          conn.processSearch(entryDN, SearchScope.SINGLE_LEVEL,
+              DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false, ALLMATCH,
+              attrs);
+
+      if (op.getResultCode() == ResultCode.SUCCESS)
+      {
+        LinkedList<SearchResultEntry> entries = op.getSearchEntries();
+        if (entries != null)
+        {
+          for (SearchResultEntry entry : entries)
+          {
+            markConflictEntry(conflictOp, entry.getDN(), entryDN);
+            renameConflictEntry(conflictOp, entry.getDN(),
+                                Historical.getEntryUuid(entry));
+          }
+        }
+      }
+      else
+      {
+        int    msgID   = MSGID_CANNOT_RENAME_CONFLICT_ENTRY;
+        String message = getMessage(msgID) + entryDN + " " + conflictOp + " "
+                         + op.getResultCode();
+        logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.SEVERE_ERROR,
+            message, msgID);
+        // TODO : log error and information for the REPAIR tool.
+      }
+    } catch (DirectoryException e)
+    {
+      int    msgID   = MSGID_EXCEPTION_RENAME_CONFLICT_ENTRY;
+      String message = getMessage(msgID) + entryDN + " " + conflictOp + " " + e;
+      logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.SEVERE_ERROR,
+          message, msgID);
+      // TODO log errror and information for the REPAIR tool.
+    }
+  }
+
+
+  /**
+   * Rename an entry that was conflicting so that it stays below the
+   * baseDN of the replicationDomain.
+   *
+   * @param conflictOp The Operation that caused the conflict.
+   * @param dn         The DN of the entry to be renamed.
+   * @param uid        The uniqueID of the entry to be renamed.
+   */
+  private void renameConflictEntry(Operation conflictOp, DN dn, String uid)
+  {
+    InternalClientConnection conn =
+      InternalClientConnection.getRootConnection();
+
+    ModifyDNOperation newOp = conn.processModifyDN(
+        dn, generateDeleteConflictDn(uid, dn),false, baseDN);
+
+    if (newOp.getResultCode() != ResultCode.SUCCESS)
+    {
+      int    msgID   = MSGID_CANNOT_RENAME_CONFLICT_ENTRY;
+      String message = getMessage(msgID) + dn + " " + conflictOp + " "
+                       + newOp.getResultCode();
+      logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.SEVERE_ERROR,
+          message, msgID);
+      /*
+       * TODO : REPAIR should log information for the repair tool.
+       */
+    }
+  }
+
+
+  /**
+   * Generate a modification to add the conflict attribute to an entry
    * whose Dn is now conflicting with another entry.
    *
-   * @param op The operation causing the conflict.
+   * @param op        The operation causing the conflict.
+   * @param currentDN The current DN of the operation to mark as conflicting.
+   * @param conflictDN     The newDn on which the conflict happened.
    */
-  private void generateAddConflictOp(ModifyDNOperation op)
+  private void markConflictEntry(Operation op, DN currentDN, DN conflictDN)
   {
-    // TODO
+    // create new internal modify operation and run it.
+    InternalClientConnection conn =
+      InternalClientConnection.getRootConnection();
+
+    AttributeType attrType =
+      DirectoryServer.getAttributeType(DS_SYNC_CONFLICT, true);
+    LinkedHashSet<AttributeValue> values = new LinkedHashSet<AttributeValue>();
+    values.add(new AttributeValue(attrType, conflictDN.toString()));
+    Attribute attr = new Attribute(attrType, DS_SYNC_CONFLICT, values);
+    List<Modification> mods = new ArrayList<Modification>();
+    Modification mod = new Modification(ModificationType.REPLACE, attr);
+    mods.add(mod);
+    ModifyOperation newOp = conn.processModify(currentDN, mods);
+    if (newOp.getResultCode() != ResultCode.SUCCESS)
+    {
+      int    msgID   = MSGID_CANNOT_ADD_CONFLICT_ATTRIBUTE;
+      String message = getMessage(msgID) + op + " " + newOp.getResultCode();
+      logError(ErrorLogCategory.BACKEND, ErrorLogSeverity.SEVERE_ERROR,
+          message, msgID);
+      /*
+       * TODO : REPAIR should log information for the repair tool.
+       */
+    }
   }
 
   /**
    * Add the conflict object class to an entry that could
    * not be added because it is conflicting with another entry.
    *
-   * @param addOp The conflicting Add Operation.
+   * @param addOp          The conflicting Add Operation.
+   *
+   * @throws ASN1Exception When an encoding error happenned manipulating the
+   *                       msg.
    */
-  private void addConflict(AddOperation addOp)
+  private void addConflict(AddMsg msg) throws ASN1Exception
   {
-    /*
-     * TODO
-     */
+    msg.addAttribute(DS_SYNC_CONFLICT, msg.getDn());
   }
 
   /**
@@ -1624,12 +1798,36 @@
    *
    * @param entryUid The unique identifier of the entry involved in the
    * conflict.
-   * @param dn Original dn.
-   * @return The generated Dn for a conflicting entry.
+   * @param rdn Original rdn.
+   * @return The generated RDN for a conflicting entry.
    */
-  private String generateConflictDn(String entryUid, String dn)
+  private String generateConflictRDN(String entryUid, String rdn)
   {
-    return "entryuuid=" + entryUid + "+" + dn;
+    return "entryuuid=" + entryUid + "+" + rdn;
+  }
+
+  /**
+   * Generate the RDN to use for a conflicting entry whose father was deleted.
+   *
+   * @param entryUid The unique identifier of the entry involved in the
+   *                 conflict.
+   * @param dn       The original DN of the entry.
+   *
+   * @return The generated RDN for a conflicting entry.
+   * @throws DirectoryException
+   */
+  private RDN generateDeleteConflictDn(String entryUid, DN dn)
+  {
+    String newRDN =  "entryuuid=" + entryUid + "+" + dn.getRDN();
+    RDN rdn = null;
+    try
+    {
+      rdn = RDN.decode(newRDN);
+    } catch (DirectoryException e)
+    {
+      // cannot happen
+    }
+    return rdn;
   }
 
   /**
diff --git a/opendj-sdk/opends/src/server/org/opends/server/replication/protocol/AddMsg.java b/opendj-sdk/opends/src/server/org/opends/server/replication/protocol/AddMsg.java
index f93bb98..5979842 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/replication/protocol/AddMsg.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/replication/protocol/AddMsg.java
@@ -57,8 +57,8 @@
 public class AddMsg extends UpdateMessage
 {
   private static final long serialVersionUID = -4905520652801395185L;
-  private final byte[] encodedAttributes;
-  private final String parentUniqueId;
+  private byte[] encodedAttributes;
+  private String parentUniqueId;
 
   /**
    * Creates a new AddMessage.
@@ -263,4 +263,32 @@
   {
     return ("ADD " + getDn() + " " + getChangeNumber());
   }
+
+  /**
+   * Add the specified attribute/attribute value in the entry contained
+   * in this AddMsg.
+   *
+   * @param name  The name of the attribute to add.
+   * @param value The value of the attribute to add.
+   * @throws ASN1Exception When this Msg is not valid.
+   */
+  public void addAttribute(String name, String value)
+         throws ASN1Exception
+  {
+    RawAttribute newAttr = new LDAPAttribute(name, value);
+    ArrayList<ASN1Element> elems;
+    elems = ASN1Element.decodeElements(encodedAttributes);
+    elems.add(newAttr.encode());
+    encodedAttributes = ASN1Element.encodeValue(elems);
+  }
+
+  /**
+   * Set the parent unique id of this add msg.
+   *
+   * @param uid the parent unique id.
+   */
+  public void setParentUid(String uid)
+  {
+    parentUniqueId = uid;
+  }
 }
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/UpdateOperationTest.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/UpdateOperationTest.java
index a3d33e4..3b63fc5 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/UpdateOperationTest.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/UpdateOperationTest.java
@@ -41,6 +41,7 @@
 import org.opends.server.replication.common.ChangeNumber;
 import org.opends.server.replication.common.ChangeNumberGenerator;
 import org.opends.server.replication.plugin.ReplicationBroker;
+import org.opends.server.replication.plugin.ReplicationDomain;
 import org.opends.server.replication.protocol.AddMsg;
 import org.opends.server.replication.protocol.DeleteMsg;
 import org.opends.server.replication.protocol.HeartbeatThread;
@@ -87,6 +88,15 @@
    */
   protected Entry personEntry;
   private int replServerPort;
+  private String domain1uid;
+  private String domain2uid;
+  private String domain3uid;
+  private String domain1dn;
+  private String domain2dn;
+  private String domain3dn;
+  private Entry domain1;
+  private Entry domain2;
+  private Entry domain3;
 
   /**
    * Set up the environment for performing the tests in this Class.
@@ -111,16 +121,9 @@
     topEntries[1] = "dn: ou=People,dc=example,dc=com\n" + "objectClass: top\n"
         + "objectClass: organizationalUnit\n"
         + "entryUUID: 11111111-1111-1111-1111-111111111111\n";
-    Entry entry;
     for (String entryStr : topEntries)
     {
-      entry = TestCaseUtils.entryFromLdifString(entryStr);
-      AddOperation addOp = new AddOperation(connection,
-          InternalClientConnection.nextOperationID(), InternalClientConnection
-              .nextMessageID(), null, entry.getDN(), entry.getObjectClasses(),
-          entry.getUserAttributes(), entry.getOperationalAttributes());
-      addOp.setInternalOperation(true);
-      addOp.run();
+      addEntry(TestCaseUtils.entryFromLdifString(entryStr));
     }
 
     baseUUID = getEntryUUID(DN.decode("ou=People,dc=example,dc=com"));
@@ -216,11 +219,53 @@
       + "entryUUID: "+ user1entrysecondUUID + "\n";
     personWithSecondUniqueID =
       TestCaseUtils.entryFromLdifString(entryWithSecondUUID);
+    
+    
+    domain1dn = "dc=domain1,ou=People,dc=example,dc=com";
+    domain2dn = "dc=domain2,dc=domain1,ou=People,dc=example,dc=com";
+    domain3dn = "dc=domain3,dc=domain1,ou=People,dc=example,dc=com";
+    domain1 = TestCaseUtils.entryFromLdifString(
+        "dn:" + domain1dn + "\n" 
+        + "objectClass:domain\n"
+        + "dc:domain1");
+    domain2 = TestCaseUtils.entryFromLdifString(
+        "dn:" + domain2dn + "\n" 
+        + "objectClass:domain\n"
+        + "dc:domain2");
+    domain3 = TestCaseUtils.entryFromLdifString(
+        "dn:" + domain3dn + "\n" 
+        + "objectClass:domain\n"
+        + "dc:domain3");
 
     configureReplication();
   }
 
   /**
+   * Add an entry in the datatbase
+   *
+   */
+  private void addEntry(Entry entry) throws Exception
+  {
+    AddOperation addOp = new AddOperation(connection,
+        InternalClientConnection.nextOperationID(), InternalClientConnection
+            .nextMessageID(), null, entry.getDN(), entry.getObjectClasses(),
+        entry.getUserAttributes(), entry.getOperationalAttributes());
+    addOp.setInternalOperation(true);
+    addOp.run();
+    assertNotNull(getEntry(entry.getDN(), 1000, true));
+  }
+  
+  /**
+   * Delete an entry in the datatbase
+   *
+   */
+  private void delEntry(DN dn) throws Exception
+  {
+    connection.processDelete(dn);
+    assertNull(getEntry(dn, 1000, true));
+  }
+
+  /**
    * Tests whether the synchronization provider receive status can be disabled
    * then re-enabled.
    * FIXME Enable this test when broker suspend/resume receive are implemented.
@@ -454,7 +499,7 @@
 
     // Simulate the ordering t2:replace:B followed by t1:add:A that
     updateMonitorCount(baseDn, monitorAttr);
-    
+
     // Replay a replace of a value B at time t2 on a second server.
     Attribute attr = new Attribute(attrType.getNormalizedPrimaryName(), "B");
     Modification mod = new Modification(ModificationType.REPLACE, attr);
@@ -484,7 +529,7 @@
     // the value should be the last (time t2) value added
     assertEquals(attrValue1, "B");
     assertEquals(getMonitorDelta(), 1);
-    
+
     // Simulate the ordering t2:delete:displayname followed by
     // t1:replace:displayname
     // A change on a first server.
@@ -495,7 +540,7 @@
 
     // Simulate the ordering t2:delete:displayname followed by t1:replace:A
     updateMonitorCount(baseDn, monitorAttr);
-    
+
     // Replay an delete of attribute displayname at time t2 on a second server.
     attr = new Attribute(attrType);
     mod = new Modification(ModificationType.DELETE, attr);
@@ -521,9 +566,9 @@
     attrs = entry.getAttribute(attrType);
 
     // there should not be a value (delete at time t1)
-    assertNull(attrs);    
+    assertNull(attrs);
     assertEquals(getMonitorDelta(), 1);
-    
+
     broker.stop();
   }
 
@@ -545,7 +590,8 @@
         "Starting replication test : namingConflicts" , 1);
 
     final DN baseDn = DN.decode("ou=People,dc=example,dc=com");
-    String monitorAttr = "resolved-naming-conflicts";
+    String resolvedMonitorAttr = "resolved-naming-conflicts";
+    String unresolvedMonitorAttr = "unresolved-naming-conflicts";
 
     /*
      * Open a session to the replicationServer using the ReplicationServer broker API.
@@ -588,7 +634,7 @@
     ModifyMsg modMsg = new ModifyMsg(gen.newChangeNumber(),
         DN.decode("cn=something,ou=People,dc=example,dc=com"), mods,
         user1entryUUID);
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, resolvedMonitorAttr);
     broker.publish(modMsg);
 
     // check that the modify has been applied as if the entry had been renamed.
@@ -596,7 +642,7 @@
                            "telephonenumber", "01 02 45", 10000, true);
     if (found == false)
      fail("The modification has not been correctly replayed.");
-    assertEquals(getMonitorDelta(), 1);    
+    assertEquals(getMonitorDelta(), 1);
 
     /*
      * Test that the conflict resolution code is able to detect
@@ -626,7 +672,7 @@
     mods = generatemods("telephonenumber", "02 01 03 05");
     modMsg = new ModifyMsg(gen.newChangeNumber(),
         DN.decode(user1dn), mods, "10000000-9abc-def0-1234-1234567890ab");
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, resolvedMonitorAttr);
     broker.publish(modMsg);
 
     // check that the modify has not been applied
@@ -649,7 +695,7 @@
     DeleteMsg delMsg =
       new DeleteMsg("cn=anotherdn,ou=People,dc=example,dc=com",
           gen.newChangeNumber(), user1entryUUID);
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, resolvedMonitorAttr);
     broker.publish(delMsg);
 
     // check that the delete operation has been applied
@@ -684,7 +730,7 @@
         user1entrysecondUUID, baseUUID,
         personWithSecondUniqueID.getObjectClassAttribute(),
         personWithSecondUniqueID.getAttributes(), new ArrayList<Attribute>());
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, unresolvedMonitorAttr);
     broker.publish(addMsg);
 
     //  Check that the entry has been renamed and created in the local DS.
@@ -693,7 +739,8 @@
         10000, true);
     assertNotNull(resultEntry,
         "The ADD replication message was not applied");
-    assertEquals(getMonitorDelta(), 1);    
+    assertEquals(getMonitorDelta(), 1);
+    assertConflictAttribute(resultEntry);
 
     //  delete the entries to clean the database.
     delMsg =
@@ -722,7 +769,7 @@
         baseUUID,
         personWithUUIDEntry.getObjectClassAttribute(),
         personWithUUIDEntry.getAttributes(), new ArrayList<Attribute>());
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, resolvedMonitorAttr);
     broker.publish(addMsg);
 
     //  Check that the entry has been renamed and created in the local DS.
@@ -744,7 +791,7 @@
     delMsg =
       new DeleteMsg("uid=new person,ou=People,dc=example,dc=com",
           gen.newChangeNumber(), "11111111-9abc-def0-1234-1234567890ab");
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, resolvedMonitorAttr);
     broker.publish(delMsg);
     resultEntry = getEntry(
           DN.decode("uid=new person,ou=People,dc=example,dc=com"), 10000, true);
@@ -769,7 +816,7 @@
         user1entryUUID, baseUUID, false,
         "uid=wrong, ou=people,dc=example,dc=com",
         "uid=newrdn");
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, resolvedMonitorAttr);
     broker.publish(modDnMsg);
 
     resultEntry = getEntry(
@@ -787,7 +834,7 @@
      modDnMsg = new ModifyDNMsg(
         "uid=wrong,ou=People,dc=example,dc=com", gen.newChangeNumber(),
         user1entryUUID, baseUUID, false, null, "uid=reallynewrdn");
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, resolvedMonitorAttr);
     broker.publish(modDnMsg);
 
     resultEntry = getEntry(
@@ -810,7 +857,6 @@
         baseUUID,
         personWithSecondUniqueID.getObjectClassAttribute(),
         personWithSecondUniqueID.getAttributes(), new ArrayList<Attribute>());
-    updateMonitorCount(baseDn, monitorAttr);
     broker.publish(addMsg);
 
     //  check that the second entry has been added
@@ -818,23 +864,21 @@
 
     // check that the add operation has been applied
     assertNotNull(resultEntry, "The add operation was not replayed");
-    assertEquals(getMonitorDelta(), 1);
 
     // try to rename the first entry
     modDnMsg = new ModifyDNMsg(user1dn, gen.newChangeNumber(),
                                user1entrysecondUUID, baseUUID, false,
                                baseDn.toString(), "uid=reallynewrdn");
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, unresolvedMonitorAttr);
     broker.publish(modDnMsg);
 
    // check that the second entry has been renamed
     resultEntry = getEntry(
         DN.decode("entryUUID = " + user1entrysecondUUID + "+uid=reallynewrdn," +
             "ou=People,dc=example,dc=com"), 10000, true);
-
-    // check that the delete operation has been applied
     assertNotNull(resultEntry, "The modifyDN was not or incorrectly replayed");
     assertEquals(getMonitorDelta(), 1);
+    assertConflictAttribute(resultEntry);
 
     // delete the entries to clean the database
     delMsg =
@@ -940,7 +984,7 @@
 
 
     // - publish msg
-    updateMonitorCount(baseDn, monitorAttr);
+    updateMonitorCount(baseDn, resolvedMonitorAttr);
     broker.publish(addMsg);
 
     // - check that the Dn has been changed to baseDn2
@@ -955,11 +999,97 @@
         "The ADD replication message was NOT applied under ou=baseDn2,"+baseDn);
     entryList.add(resultEntry.getDN());
     assertEquals(getMonitorDelta(), 1);
+    
+    //
+    // Check that when a delete is conflicting with Add of some entries
+    // below the deleted entries, the child entry that have been added
+    // before the deleted is replayed gets renamed correctly.
+    //
+    
+    // add domain1 entry with 2 childs domain2 and domain3
+    addEntry(domain1);
+    domain1uid = getEntryUUID(DN.decode(domain1dn));
+    addEntry(domain2);
+    domain2uid = getEntryUUID(DN.decode(domain2dn));
+    addEntry(domain3);
+    domain3uid = getEntryUUID(DN.decode(domain3dn));
+    DN conflictDomain2dn = DN.decode(
+        "entryUUID = " + domain2uid + "+dc=domain2,ou=people,dc=example,dc=com");
+    DN conflictDomain3dn = DN.decode(
+        "entryUUID = " + domain3uid + "+dc=domain3,ou=people,dc=example,dc=com");
+ 
+    updateMonitorCount(baseDn, unresolvedMonitorAttr);
+    
+    // delete domain1
+    delMsg = new DeleteMsg(domain1dn, gen.newChangeNumber(), domain1uid);
+    broker.publish(delMsg);
 
+    // check that the domain1 has correctly been deleted
+    assertNull(getEntry(DN.decode(domain1dn), 10000, false),
+        "The DELETE replication message was not replayed");
+    
+    // check that domain2 and domain3 have been renamed
+    assertNotNull(getEntry(conflictDomain2dn, 1000, true),
+        "The conflicting entries were not created");
+    assertNotNull(getEntry(conflictDomain3dn, 1000, true),
+        "The conflicting entries were not created");
 
+    // check that the 2 conflicting entries have been correctly marked
+    assertTrue(checkEntryHasAttribute(conflictDomain2dn,
+        ReplicationDomain.DS_SYNC_CONFLICT, domain1dn, 1000, true));
+    assertTrue(checkEntryHasAttribute(conflictDomain3dn,
+        ReplicationDomain.DS_SYNC_CONFLICT, domain1dn, 1000, true));
+    
+    // check that unresolved conflict count has been incremented
+    assertEquals(getMonitorDelta(), 1);
+    
+    // delete the resulting entries for the next test
+    delEntry(conflictDomain2dn);
+    delEntry(conflictDomain3dn);
+    
+    //
+    // Check that when an entry is added on one master below an entry
+    // that is currently deleted on another master, the replay of the
+    // add on the second master cause the added entry to be renamed
+    //
+    addMsg = new AddMsg(gen.newChangeNumber(), domain2dn, domain2uid,
+        domain1uid,
+        domain2.getObjectClassAttribute(),
+        domain2.getAttributes(), new ArrayList<Attribute>());
+    broker.publish(addMsg);
+    
+    // check that conflict entry was created
+    assertNotNull(getEntry(conflictDomain2dn, 1000, true),
+      "The conflicting entries were not created");
+    
+    // check that the entry have been correctly marked as conflicting.
+    assertTrue(checkEntryHasAttribute(conflictDomain2dn,
+        ReplicationDomain.DS_SYNC_CONFLICT, domain2dn, 1000, true));
+    
+    // check that unresolved conflict count has been incremented
+    assertEquals(getMonitorDelta(), 1);
+    
     broker.stop();
   }
 
+  /**
+   * Check that the given entry does contain the attribute that mark the
+   * entry as conflicting.
+   *
+   * @param entry The entry that needs to be asserted.
+   *
+   * @return A boolean indicating if the entry is correctly marked.
+   */
+  private boolean assertConflictAttribute(Entry entry)
+  {
+    List<Attribute> attrs = entry.getAttribute("ds-sync-confict");
+
+    if (attrs == null)
+      return false;
+    else
+      return true;
+  }
+
   @DataProvider(name="assured")
   public Object[][] getAssuredFlag()
   {

--
Gitblit v1.10.0