Reg Exp
Web Design
Notes Client
Turn Replication Conflict Loser Into Winner
Replication/Save conflicts are a necessary evil in a distrubuted environment like Lotus Notes. A developer can do all he/she can to minimize conflicts through things like setting the form properties to merge conflicts if possible and using document locking in Notes 6. But maybe document locking is something that can't be implemented, because your users aren't on Notes 6 yet or because the application is really distributed (users with local replicas and no network access can edit existing documents). In that case, you're bound to have conflicts and will need to deal with them.

This isn't the right place to go into all the details about how Notes handles conflicts, but let's just say that an algorithm based on the time of last editing and the number of edits is used to determine a conflict "winner" and a conflict "loser". These are two distinct documents in the database. When evaluating the two documents, there are three actions that you can take:
  1. Decide that the "winner" has all the information it needs and there is nothing that the "loser" can add.
  2. Decide that the "loser" has all the information it needs and there is nothing that the "winner" can add.
  3. Decide that each document has information that needs to be kept.
In case #1 above, the replication conflict loser can simply be deleted from the database. In case #3 above, the information in the loser that needs to be in the winner can be copied/pasted into the winner, and then the loser can be deleted from the database. Those cases are relatively easy to handle.

In case #2, things become a bit more complicated. You can treat it like case #3 and copy information from the loser into the winner and move on. But you can't treat it like case #1 because if you delete the replication conflict winner then the loser becomes an orphan and won't appear anywhere. Using the code below will turn a replication conflict loser into a replication conflict winner. The original winner is then deleted without causing an orphan.

To implement this code, you have to understand a bit about conflicts. Not necessarily how Notes determines a conflict or how Notes determines a winner and a loser, but what Notes does to the loser. Two fields are added to the document deemed to be the loser. The first field is called $Conflict and is a text field with an empty string. The fact that this field exists is an indicator to Notes to show the familiar [Replication or Save Conflict] indicator in the view, as seen in figure 1. (You can verify this for yourself by creating a LotusScript agent that takes the unprocessed documents - ones seleted in a view - taking the first document, putting a text field called $Conflict with a value of an empty string on the document and then saving it. Your view will change).

The other thing that Notes does to the conflict loser is make it a response to the conflict winner by creating a $Ref field. This is done so it will appear along with the conflict winner in views. But if you want to turn the loser back into a regular document, you need to remove this assocation. Simply removing the $Ref field may or may not be sufficient. If the original winner was a parent document, then removing $Ref on the loser (along with $Conflict) will do the trick. But if the winner was itself a response document, to keep the loser you need to remove $Conflict and retain $Ref, but have it point to the correct parent.

To build a completely generic agent to turn conflict losers into conflict winners, we have to account for the possibility that the original winner was itself a response document. We also want the user running the agent to not have to worry about what document was selected (they should have to know to select the original winner or the original loser - it should be sufficient for them to select either, or even both). The agent should also be able to run on multiple documents all at once. So let's take a look at the code:

Create an agent. The agent should be run from the actions menu. (You can have it run from the agent list and have some button somewhere that kicks it off if you want). The agent should run on selected documents. First, let's establish some variables. This is fairly common code, so it won't be discussed.

Sub Initialize
    Dim session As New NotesSession
    Dim db As NotesDatabase
    Dim coll As NotesDocumentCollection
    Dim doc As NotesDocument
    Dim nextDoc As NotesDocument

    Set db = session.CurrentDatabase
    Set coll = db.UnprocessedDocuments
    If Not coll Is Nothing Then
        Set doc = coll.GetFirstDocument
        While Not doc Is Nothing
            Set nextDoc = coll.GetNextDocument(doc)
            Call ProcessDocument(db, doc)
            Set doc = nextDoc
    End If
End Sub

The main subroutine will go through all documents in the collection (all selected documents) and process them one by one. Since the user might have selected an original winner (which will be deleted), get the handle to the next document before calling the "ProcessDocument" subroutine. All the work happens in that other subroutine:

Sub ProcessDocument(db As NotesDatabase, doc As NotesDocument)
    Dim winner As NotesDocument
    Dim loser As NotesDocument
    Dim children As NotesDocumentCollection
    Dim child As NotesDocument
    Dim parent As NotesDocument

    If doc Is Nothing Then Exit Sub
    If doc.IsDeleted Then Exit Sub
    If doc.UniversalID = "" Then Exit Sub

    Set winner = Nothing
    Set loser = Nothing

    If doc.HasItem("$Conflict") Then
        Set loser = doc
        On Error Resume Next
        Set winner = db.GetDocumentByUNID(loser.ParentDocumentUNID)
        On Error Goto 0
        If Err <> 0 Then
            Err = 0
            Set winner = Nothing
        End If
        Set winner = doc
        Set children = winner.Responses
        If Not children Is Nothing Then
            Set child = children.GetFirstDocument
            While Not child Is Nothing
                If child.HasItem("$Conflict") Then Set loser = child
                Set child = children.GetNextDocument(child)
        End If
    End If

    If winner Is Nothing Or loser Is Nothing Then Exit Sub

    Set parent = Nothing
    If winner.HasItem("$Ref") Then
        On Error Resume Next
        Set parent = db.GetDocumentByUNID(winner.ParentDocumentUNID)
        On Error Goto 0
        If Err <> 0 Then
            Err = 0
            Exit Sub
        End If
    End If

    Call loser.RemoveItem("$Conflict")
    Call loser.RemoveItem("$Ref")
    If Not parent Is Nothing Then Call loser.MakeResponse(parent)
    On Error Resume Next
    Call loser.Save(True, False)
    Call winner.Remove(True)
    On Error Goto 0
    If Err <> 0 Then
        Msgbox "Error removing original winner: " & Error$, 16, "Error"
        Err = 0
    End If
End Sub

The first thing we do is make sure the passed-in document is valid. If the user selected both the original winner and the original loser, on the first pass the original winner will be deleted from the database so there's a chance of this function being called with a deleted document. So check to make sure the passed-in document is valid.

Next, find out which document is the original winner and which is the original loser. If the passed-in document has a field called $Conflict, it is the original loser and its parent is the original winner. If the passed-in document does not have that field, it is the original winner and search its responses for the original loser. Make sure to trap for any errors like orphan documents or the passed-in document not having any responses.

If either the original winner or the original loser were not found, then exit the subroutine.

Next, find out if the original winner is itself a response document. If it has a field called $Ref, then it should be a response document and attempt to find the winner. If the original winner is an orphan (it says it's a response, but the parent can't be found) then exit the subroutine.

If we haven't exited at this point, then we have an original winner, original loser, and the parent of the original winner if applicable. To turn the loser into the winner, remove the $Conflict and $Ref fields, then reset $Ref if the original winner was itself a response document. Save the changes in the loser document and remove the winner document. Trap for any errors and give a message if there were errors. Possible errors would be that the user is an Author and doesn't have access to the update the loser document, or they don't have the ability to delete the winner document from the database.

Now, you should have a generic agent for turning a replication conflict loser into a winner. Remember that the winner is deleted from the database. One possible change to this agent is to simply make the conflict loser into a winner and not delete the winner. There would end up with two similar documents in the database. The two documents would be on the same response level and neither would be marked as a conflict document. Then you could manually determine which one to delete, but at least nothing else would have to happen.