@@ -8,8 +8,6 @@ import org.modelix.model.api.IReferenceLinkReference
8
8
import org.modelix.model.api.IRoleReference
9
9
import org.modelix.model.api.IWritableNode
10
10
import org.modelix.model.api.NewNodeSpec
11
- import org.modelix.model.api.PNodeAdapter
12
- import org.modelix.model.api.getOriginalOrCurrentReference
13
11
import org.modelix.model.api.getOriginalReference
14
12
import org.modelix.model.api.isOrdered
15
13
import org.modelix.model.api.matches
@@ -171,16 +169,10 @@ class ModelSynchronizer(
171
169
) {
172
170
val sourceNodes = getFilteredSourceChildren(sourceParent, role)
173
171
val targetNodes = getFilteredTargetChildren(targetParent, role)
174
- val unusedTargetChildren = targetNodes.toMutableSet()
175
-
176
- val allExpectedNodesDoNotExist by lazy {
177
- sourceNodes.all { sourceNode ->
178
- nodeAssociation.resolveTarget(sourceNode) == null
179
- }
180
- }
172
+ val associatedChildren = associateChildren(sourceNodes, targetNodes)
181
173
182
174
// optimization that uses the bulk operation .syncNewChildren
183
- if (targetNodes.isEmpty () && allExpectedNodesDoNotExist ) {
175
+ if (associatedChildren.all { it.hasToCreate () } ) {
184
176
targetParent.syncNewChildren(role, - 1 , sourceNodes.map { NewNodeSpec (it) })
185
177
.zip(sourceNodes)
186
178
.forEach { (newChild, sourceChild) ->
@@ -190,74 +182,49 @@ class ModelSynchronizer(
190
182
return
191
183
}
192
184
185
+ val isOrdered = targetParent.isOrdered(role)
186
+
193
187
// optimization for when there is no change in the child list
194
- // size check first to avoid querying the original ID
195
- if (sourceNodes.size == targetNodes.size && sourceNodes.zip(targetNodes)
196
- .all { nodeAssociation.matches(it.first, it.second) }
197
- ) {
198
- sourceNodes.zip(targetNodes).forEach { synchronizeNode(it.first, it.second, forceSyncDescendants) }
188
+ if (associatedChildren.all { it.alreadyMatches(isOrdered) }) {
189
+ associatedChildren.forEach {
190
+ synchronizeNode(it.getSource(), it.getTarget(), forceSyncDescendants)
191
+ }
199
192
return
200
193
}
201
194
202
- val isOrdered = targetParent.isOrdered(role)
203
-
204
- sourceNodes.forEachIndexed { indexInImport, expected ->
205
- val existingChildren = getFilteredTargetChildren(targetParent, role)
206
- val expectedId = checkNotNull(expected.originalIdOrFallback()) { " Specified node '$expected ' has no id" }
207
- // newIndex is the index on which to import the expected child.
208
- // It might be -1 if the child does not exist and should be added at the end.
209
- val newIndex = if (isOrdered) {
210
- indexInImport
195
+ val unusedTargetChildren: List <IWritableNode > = associatedChildren
196
+ .asSequence()
197
+ .filter { it.hasToDelete() }
198
+ .map { it.getTarget() }
199
+ .toList()
200
+
201
+ nodesToRemove + = unusedTargetChildren
202
+
203
+ val recursiveSyncTasks = ArrayList <RecursiveSyncTask >()
204
+
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
211
216
} else {
212
- // The `existingChildren` are only searched once for the expected element before changing.
213
- // Therefore, indexing existing children will not be more efficient than iterating once.
214
- // (For the moment, this is fine because as we expect unordered children to be the exception,
215
- // Reusable indexing would be possible if we switch from
216
- // a depth-first import to a breadth-first import.)
217
- existingChildren
218
- .indexOfFirst { existingChild -> existingChild.getOriginalOrCurrentReference() == expectedId }
217
+ recursiveSyncTasks + = RecursiveSyncTask (associatedChild.getSource(), associatedChild.getTarget(), false )
219
218
}
220
- // existingChildren.getOrNull handles `-1` as needed by returning `null`.
221
- val nodeAtIndex = existingChildren.getOrNull(newIndex)
222
- val expectedConcept = expected.getConceptReference()
223
- var isNewChild = false
224
- val childNode = if (nodeAtIndex?.getOriginalOrCurrentReference() != expectedId) {
225
- val existingNode = nodeAssociation.resolveTarget(expected)
226
- if (existingNode == null ) {
227
- val newChild = targetParent.syncNewChild(role, newIndex, NewNodeSpec (expected))
228
- nodeAssociation.associate(expected, newChild)
229
- isNewChild = true
230
- newChild
231
- } else {
232
- // The existing child node is not only moved to a new index,
233
- // it is potentially moved to a new parent and role.
234
- if (existingNode.getParent() != targetParent ||
235
- ! existingNode.getContainmentLink().matches(role) ||
236
- targetParent.isOrdered(role)
237
- ) {
238
- targetParent.moveChild(role, newIndex, existingNode)
239
- }
240
- // If the old parent and old role synchronized before the move operation,
241
- // the existing child node would have been marked as to be deleted.
242
- // Now that it is used, it should not be deleted.
243
- unusedTargetChildren.remove(existingNode)
244
- nodesToRemove.remove(existingNode)
245
- existingNode
246
- }
247
- } else {
248
- unusedTargetChildren.remove(nodeAtIndex)
249
- nodesToRemove.remove(nodeAtIndex)
250
- nodeAtIndex
251
- }
252
-
253
- synchronizeNode(expected, childNode, forceSyncDescendants || isNewChild)
254
219
}
255
220
256
- // Do not use existingNodes, but call node.getChildren(role) because
257
- // the recursive synchronization in the meantime already removed some nodes from node.getChildren(role).
258
- nodesToRemove + = getFilteredTargetChildren(targetParent, role).intersect(unusedTargetChildren)
221
+ for (task in recursiveSyncTasks) {
222
+ synchronizeNode(task.source, task.target, forceSyncDescendants || task.isNew)
223
+ }
259
224
}
260
225
226
+ private class RecursiveSyncTask (val source : IReadableNode , val target : IWritableNode , val isNew : Boolean )
227
+
261
228
/* *
262
229
* In MPS and also in Modelix nodes internally are stored in a single list that is filtered when a specific role is
263
230
* accessed. The information about this internal order is visible when using getAllChildren().
@@ -281,6 +248,40 @@ class ModelSynchronizer(
281
248
}
282
249
}
283
250
251
+ private fun associateChildren (sourceChildren : List <IReadableNode >, targetChildren : List <IWritableNode >): List <AssociatedChild > {
252
+ val unassociatedTargetNodes = targetChildren.withIndex().toMutableList()
253
+ return sourceChildren.mapIndexed { sourceIndex, sourceChild ->
254
+ val foundAt = unassociatedTargetNodes.indexOfFirst { targetChild ->
255
+ nodeAssociation.matches(sourceChild, targetChild.value)
256
+ }
257
+ if (foundAt == - 1 ) {
258
+ AssociatedChild (sourceIndex, - 1 , sourceChild, null , nodeAssociation.resolveTarget(sourceChild))
259
+ } else {
260
+ val foundTarget = unassociatedTargetNodes.removeAt(foundAt)
261
+ AssociatedChild (sourceIndex, foundTarget.index, sourceChild, foundTarget.value, null )
262
+ }
263
+ } + unassociatedTargetNodes.map { AssociatedChild (- 1 , it.index, null , it.value, null ) }
264
+ }
265
+
266
+ private class AssociatedChild (
267
+ val sourceIndex : Int ,
268
+ private val targetIndex : Int ,
269
+ private val source : IReadableNode ? ,
270
+ private val existingTarget : IWritableNode ? ,
271
+ private val resolvedTarget : IWritableNode ? ,
272
+ ) {
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
280
+ fun alreadyMatches (ordered : Boolean ) = if (ordered) alreadyMatchesOrdered() else alreadyMatchesUnordered()
281
+ fun getTarget () = existingTarget ? : resolvedTarget!!
282
+ fun getSource () = source!!
283
+ }
284
+
284
285
inner class PendingReference (val sourceNode : IReadableNode , val targetNode : IWritableNode , val role : IReferenceLinkReference ) {
285
286
override fun toString (): String = " ${sourceNode.getNodeReference()} [$role ] : ${targetNode.getAllReferenceTargetRefs()} "
286
287
@@ -371,18 +372,3 @@ class AndFilter(val filter1: IIncrementalUpdateInformation, val filter2: IIncrem
371
372
return filter1.needsSynchronization(node) && filter2.needsSynchronization(node)
372
373
}
373
374
}
374
-
375
- private fun INode.originalIdOrFallback (): String? {
376
- val originalRef = getOriginalReference()
377
- if (originalRef != null ) return originalRef
378
-
379
- if (this is PNodeAdapter ) return reference.serialize()
380
- return null
381
- }
382
-
383
- private fun IReadableNode.originalIdOrFallback (): String? {
384
- val originalRef = getOriginalReference()
385
- if (originalRef != null ) return originalRef
386
-
387
- return getNodeReference().serialize()
388
- }
0 commit comments