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:
- Decide that the "winner" has all the information it needs and there is nothing that the "loser" can add.
- Decide that the "loser" has all the information it needs and there is nothing that the "winner" can add.
- Decide that each document has information that needs to be kept.
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
Wend
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
Else
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)
Wend
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.