@@ -167,54 +167,86 @@ class ModelSynchronizer(
167
167
targetParent : IWritableNode ,
168
168
forceSyncDescendants : Boolean ,
169
169
) {
170
- val sourceNodes = getFilteredSourceChildren(sourceParent, role)
171
- val targetNodes = getFilteredTargetChildren(targetParent, role)
172
- val associatedChildren = associateChildren(sourceNodes, targetNodes)
170
+ var forceSyncDescendants = forceSyncDescendants
171
+ var associatedChildren = associateChildren(sourceParent, targetParent, role)
173
172
174
173
// optimization that uses the bulk operation .syncNewChildren
175
174
if (associatedChildren.all { it.hasToCreate() }) {
176
- targetParent.syncNewChildren(role, - 1 , sourceNodes.map { NewNodeSpec (it) })
177
- .zip(sourceNodes)
178
- .forEach { (newChild, sourceChild) ->
179
- nodeAssociation.associate(sourceChild, newChild)
180
- synchronizeNode(sourceChild, newChild, forceSyncDescendants = true )
181
- }
182
- return
175
+ forceSyncDescendants = true
176
+ runSafe {
177
+ val newChildren = targetParent
178
+ .syncNewChildren(role, - 1 , associatedChildren.map { NewNodeSpec (it.getSource()) })
179
+ newChildren
180
+ .zip(associatedChildren.map { it.getSource() })
181
+ .forEach { (newChild, sourceChild) ->
182
+ runSafe {
183
+ nodeAssociation.associate(sourceChild, newChild)
184
+ synchronizeNode(sourceChild, newChild, forceSyncDescendants = forceSyncDescendants)
185
+ }
186
+ }
187
+ }.onSuccess {
188
+ return
189
+ }.onFailure {
190
+ // Some children may have been created successfully.
191
+ associatedChildren = associateChildren(sourceParent, targetParent, role)
192
+ // Continue with trying to sync the remaining ones individually.
193
+ }
183
194
}
184
195
185
196
val isOrdered = targetParent.isOrdered(role)
186
197
187
198
// optimization for when there is no change in the child list
188
199
if (associatedChildren.all { it.alreadyMatches(isOrdered) }) {
189
200
associatedChildren.forEach {
190
- synchronizeNode(it.getSource(), it.getTarget(), forceSyncDescendants)
201
+ runSafe {
202
+ synchronizeNode(it.getSource(), it.getTarget(), forceSyncDescendants)
203
+ }
191
204
}
192
205
return
193
206
}
194
207
195
- val unusedTargetChildren: List <IWritableNode > = associatedChildren
196
- .asSequence()
197
- .filter { it.hasToDelete() }
198
- .map { it.getTarget() }
199
- .toList()
200
-
201
- nodesToRemove + = unusedTargetChildren
202
-
208
+ // Recursive sync is done at the end because they may apply move operations that mutate
209
+ // the currently iterated list.
203
210
val recursiveSyncTasks = ArrayList <RecursiveSyncTask >()
204
211
205
- for (associatedChild in associatedChildren) {
206
- if (associatedChild.hasToCreate()) {
207
- val newChild = targetParent.syncNewChild(role, associatedChild.sourceIndex, NewNodeSpec (associatedChild.getSource()))
208
- nodeAssociation.associate(associatedChild.getSource(), newChild)
209
- recursiveSyncTasks + = RecursiveSyncTask (associatedChild.getSource(), newChild, true )
210
- } else if (associatedChild.hasToMove(isOrdered)) {
211
- targetParent.moveChild(role, associatedChild.sourceIndex, associatedChild.getTarget())
212
- nodesToRemove.remove(associatedChild.getTarget())
213
- recursiveSyncTasks + = RecursiveSyncTask (associatedChild.getSource(), associatedChild.getTarget(), false )
214
- } else if (associatedChild.hasToDelete()) {
215
- // no sync between child nodes needed
216
- } else {
217
- recursiveSyncTasks + = RecursiveSyncTask (associatedChild.getSource(), associatedChild.getTarget(), false )
212
+ // Since nodes are removed at the end of the sync, we have to adjust the index for add/move operations.
213
+ // We could just use `sourceIndex`, but that would result in unnecessary move operations.
214
+ val orderBeforeRemove = associatedChildren.sortedWith(
215
+ compareBy({
216
+ when (it.getOperationType(isOrdered)) {
217
+ AssociatedChild .OperationType .REMOVE -> it.targetIndex!!
218
+ else -> it.sourceIndex!!
219
+ }
220
+ }, {
221
+ when (it.getOperationType(isOrdered)) {
222
+ AssociatedChild .OperationType .REMOVE -> 0
223
+ else -> 1
224
+ }
225
+ }),
226
+ )
227
+
228
+ for ((targetIndex, associatedChild) in orderBeforeRemove.withIndex()) {
229
+ when (associatedChild.getOperationType(isOrdered)) {
230
+ AssociatedChild .OperationType .CREATE -> {
231
+ val newChild = targetParent.syncNewChild(role, targetIndex, NewNodeSpec (associatedChild.getSource()))
232
+ nodeAssociation.associate(associatedChild.getSource(), newChild)
233
+ recursiveSyncTasks + = RecursiveSyncTask (associatedChild.getSource(), newChild, isNew = true )
234
+ }
235
+ AssociatedChild .OperationType .REMOVE -> {
236
+ nodesToRemove + = associatedChild.getTarget()
237
+ }
238
+ AssociatedChild .OperationType .MOVE_SAME_CONTAINMENT -> {
239
+ targetParent.moveChild(role, targetIndex, associatedChild.getTarget())
240
+ recursiveSyncTasks + = RecursiveSyncTask (associatedChild.getSource(), associatedChild.getTarget(), isNew = false )
241
+ }
242
+ AssociatedChild .OperationType .MOVE_DIFFERENT_CONTAINMENT -> {
243
+ targetParent.moveChild(role, targetIndex, associatedChild.getTarget())
244
+ recursiveSyncTasks + = RecursiveSyncTask (associatedChild.getSource(), associatedChild.getTarget(), isNew = false )
245
+ nodesToRemove.remove(associatedChild.getTarget())
246
+ }
247
+ AssociatedChild .OperationType .ALREADY_MATCHES -> {
248
+ recursiveSyncTasks + = RecursiveSyncTask (associatedChild.getSource(), associatedChild.getTarget(), isNew = false )
249
+ }
218
250
}
219
251
}
220
252
@@ -248,38 +280,73 @@ class ModelSynchronizer(
248
280
}
249
281
}
250
282
283
+ private fun associateChildren (sourceParent : IReadableNode , targetParent : IWritableNode , role : IChildLinkReference ): List <AssociatedChild > {
284
+ val sourceNodes = getFilteredSourceChildren(sourceParent, role)
285
+ val targetNodes = getFilteredTargetChildren(targetParent, role)
286
+ return associateChildren(sourceNodes, targetNodes)
287
+ }
288
+
251
289
private fun associateChildren (sourceChildren : List <IReadableNode >, targetChildren : List <IWritableNode >): List <AssociatedChild > {
252
290
val unassociatedTargetNodes = targetChildren.withIndex().toMutableList()
253
291
return sourceChildren.mapIndexed { sourceIndex, sourceChild ->
254
292
val foundAt = unassociatedTargetNodes.indexOfFirst { targetChild ->
255
293
nodeAssociation.matches(sourceChild, targetChild.value)
256
294
}
257
295
if (foundAt == - 1 ) {
258
- AssociatedChild (sourceIndex, - 1 , sourceChild, null , nodeAssociation.resolveTarget(sourceChild))
296
+ AssociatedChild (sourceIndex, null , sourceChild, null , nodeAssociation.resolveTarget(sourceChild))
259
297
} else {
260
298
val foundTarget = unassociatedTargetNodes.removeAt(foundAt)
261
299
AssociatedChild (sourceIndex, foundTarget.index, sourceChild, foundTarget.value, null )
262
300
}
263
- } + unassociatedTargetNodes.map { AssociatedChild (- 1 , it.index, null , it.value, null ) }
301
+ } + unassociatedTargetNodes.map { AssociatedChild (null , it.index, null , it.value, null ) }
264
302
}
265
303
266
304
private class AssociatedChild (
267
- val sourceIndex : Int ,
268
- private val targetIndex : Int ,
305
+ val sourceIndex : Int? ,
306
+ val targetIndex : Int? ,
269
307
private val source : IReadableNode ? ,
270
- private val existingTarget : IWritableNode ? ,
271
- private val resolvedTarget : IWritableNode ? ,
308
+ private val existingTargetInSameContainment : IWritableNode ? ,
309
+ private val existingTargetInDifferentContainment : IWritableNode ? ,
272
310
) {
273
- fun hasToCreate () = existingTarget == null && resolvedTarget == null
274
- fun hasToMoveFromDifferentContainment () = source != null && resolvedTarget != null
275
- fun hasToMoveWithinSameContainment () = source != null && existingTarget != null
276
- fun hasToMove (ordered : Boolean ) = hasToMoveFromDifferentContainment() || ordered && hasToMoveWithinSameContainment()
277
- fun hasToDelete () = source == null
278
- fun alreadyMatchesOrdered () = source != null && existingTarget != null && sourceIndex == targetIndex
279
- fun alreadyMatchesUnordered () = source != null && existingTarget != null
311
+ var currentTargetIndex: Int? = targetIndex
312
+
313
+ fun hasToCreate () = getOperationType(true ) == OperationType .CREATE
314
+ fun hasToMoveFromDifferentContainment () = getOperationType(true ) == OperationType .MOVE_DIFFERENT_CONTAINMENT
315
+ fun hasToMoveWithinSameContainment (ordered : Boolean ) = getOperationType(ordered) == OperationType .MOVE_SAME_CONTAINMENT
316
+ fun hasToMove (ordered : Boolean ) = hasToMoveFromDifferentContainment() || hasToMoveWithinSameContainment(ordered)
317
+ fun hasToDelete () = getOperationType(true ) == OperationType .REMOVE
318
+ fun alreadyMatchesOrdered () = alreadyMatchesUnordered() && sourceIndex == targetIndex
319
+ fun alreadyMatchesUnordered () = source != null && existingTargetInSameContainment != null
280
320
fun alreadyMatches (ordered : Boolean ) = if (ordered) alreadyMatchesOrdered() else alreadyMatchesUnordered()
281
- fun getTarget () = existingTarget ? : resolvedTarget!!
321
+
322
+ fun getTarget () = existingTargetInSameContainment ? : existingTargetInDifferentContainment!!
282
323
fun getSource () = source!!
324
+
325
+ fun getOperationType (ordered : Boolean ): OperationType {
326
+ return if (source == null ) {
327
+ OperationType .REMOVE
328
+ } else {
329
+ if (existingTargetInSameContainment != null ) {
330
+ if (ordered) {
331
+ OperationType .MOVE_SAME_CONTAINMENT
332
+ } else {
333
+ OperationType .ALREADY_MATCHES
334
+ }
335
+ } else if (existingTargetInDifferentContainment != null ) {
336
+ OperationType .MOVE_DIFFERENT_CONTAINMENT
337
+ } else {
338
+ OperationType .CREATE
339
+ }
340
+ }
341
+ }
342
+
343
+ enum class OperationType {
344
+ CREATE ,
345
+ REMOVE ,
346
+ MOVE_SAME_CONTAINMENT ,
347
+ MOVE_DIFFERENT_CONTAINMENT ,
348
+ ALREADY_MATCHES ,
349
+ }
283
350
}
284
351
285
352
inner class PendingReference (val sourceNode : IReadableNode , val targetNode : IWritableNode , val role : IReferenceLinkReference ) {
0 commit comments