Skip to content

Commit 1187b88

Browse files
committed
fix(mps-sync-plugin): removed last usages of originalId in ModelSynchronizer
1 parent a8b6fcc commit 1187b88

File tree

2 files changed

+72
-83
lines changed

2 files changed

+72
-83
lines changed

bulk-model-sync-lib/src/commonMain/kotlin/org/modelix/model/sync/bulk/ModelSynchronizer.kt

Lines changed: 69 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import org.modelix.model.api.IReferenceLinkReference
88
import org.modelix.model.api.IRoleReference
99
import org.modelix.model.api.IWritableNode
1010
import org.modelix.model.api.NewNodeSpec
11-
import org.modelix.model.api.PNodeAdapter
12-
import org.modelix.model.api.getOriginalOrCurrentReference
1311
import org.modelix.model.api.getOriginalReference
1412
import org.modelix.model.api.isOrdered
1513
import org.modelix.model.api.matches
@@ -171,16 +169,10 @@ class ModelSynchronizer(
171169
) {
172170
val sourceNodes = getFilteredSourceChildren(sourceParent, role)
173171
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)
181173

182174
// optimization that uses the bulk operation .syncNewChildren
183-
if (targetNodes.isEmpty() && allExpectedNodesDoNotExist) {
175+
if (associatedChildren.all { it.hasToCreate() }) {
184176
targetParent.syncNewChildren(role, -1, sourceNodes.map { NewNodeSpec(it) })
185177
.zip(sourceNodes)
186178
.forEach { (newChild, sourceChild) ->
@@ -190,74 +182,49 @@ class ModelSynchronizer(
190182
return
191183
}
192184

185+
val isOrdered = targetParent.isOrdered(role)
186+
193187
// 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+
}
199192
return
200193
}
201194

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
211216
} 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)
219218
}
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)
254219
}
255220

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+
}
259224
}
260225

226+
private class RecursiveSyncTask(val source: IReadableNode, val target: IWritableNode, val isNew: Boolean)
227+
261228
/**
262229
* In MPS and also in Modelix nodes internally are stored in a single list that is filtered when a specific role is
263230
* accessed. The information about this internal order is visible when using getAllChildren().
@@ -281,6 +248,40 @@ class ModelSynchronizer(
281248
}
282249
}
283250

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+
284285
inner class PendingReference(val sourceNode: IReadableNode, val targetNode: IWritableNode, val role: IReferenceLinkReference) {
285286
override fun toString(): String = "${sourceNode.getNodeReference()}[$role] : ${targetNode.getAllReferenceTargetRefs()}"
286287

@@ -371,18 +372,3 @@ class AndFilter(val filter1: IIncrementalUpdateInformation, val filter2: IIncrem
371372
return filter1.needsSynchronization(node) && filter2.needsSynchronization(node)
372373
}
373374
}
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-
}

mps-sync-plugin3/src/test/kotlin/org/modelix/mps/sync3/ProjectSnapshot.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ private fun normalizeXmlFile(content: String): String {
100100
"dev-kit" -> {
101101
node.childElements("exported-language").sortByAttribute("name")
102102
}
103+
"dependencies" -> {
104+
node.childElements("dependency").sortBy { it.textContent }
105+
}
103106
"sourceRoot" -> {
104107
val location = node.getAttribute("location")
105108
val path = node.getAttribute("path")

0 commit comments

Comments
 (0)