Skip to content

Commit 280de6f

Browse files
authored
Merge branch 'main' into docs/nav-link-for-mps-model-server-plugin
2 parents 22aa1c9 + adfc7dd commit 280de6f

File tree

11 files changed

+684
-52
lines changed

11 files changed

+684
-52
lines changed

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

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.modelix.model.api.INodeReference
2323
import org.modelix.model.api.INodeResolutionScope
2424
import org.modelix.model.api.SerializedNodeReference
2525
import org.modelix.model.api.getDescendants
26+
import org.modelix.model.api.isChildRoleOrdered
2627
import org.modelix.model.api.remove
2728
import org.modelix.model.data.ModelData
2829
import org.modelix.model.data.NodeData
@@ -118,15 +119,15 @@ class ModelImporter(private val root: INode, private val continueOnError: Boolea
118119
}
119120
}
120121

121-
private fun syncChildren(node: INode, data: NodeData, progressReporter: ProgressReporter) {
122-
val allRoles = (data.children.map { it.role } + node.allChildren.map { it.roleInParent }).distinct()
122+
private fun syncChildren(existingParent: INode, expectedParent: NodeData, progressReporter: ProgressReporter) {
123+
val allRoles = (expectedParent.children.map { it.role } + existingParent.allChildren.map { it.roleInParent }).distinct()
123124
for (role in allRoles) {
124-
val expectedNodes = data.children.filter { it.role == role }
125-
val existingNodes = node.getChildren(role).toList()
125+
val expectedNodes = expectedParent.children.filter { it.role == role }
126+
val existingNodes = existingParent.getChildren(role).toList()
126127

127128
// optimization that uses the bulk operation .addNewChildren
128129
if (existingNodes.isEmpty() && expectedNodes.all { originalIdToExisting[it.originalId()] == null }) {
129-
node.addNewChildren(role, -1, expectedNodes.map { it.concept?.let { ConceptReference(it) } }).zip(expectedNodes).forEach { (newChild, expected) ->
130+
existingParent.addNewChildren(role, -1, expectedNodes.map { it.concept?.let { ConceptReference(it) } }).zip(expectedNodes).forEach { (newChild, expected) ->
130131
val expectedId = checkNotNull(expected.originalId()) { "Specified node '$expected' has no id" }
131132
newChild.setPropertyValue(NodeData.idPropertyKey, expectedId)
132133
originalIdToExisting[expectedId] = newChild
@@ -142,19 +143,41 @@ class ModelImporter(private val root: INode, private val continueOnError: Boolea
142143
continue
143144
}
144145

145-
expectedNodes.forEachIndexed { index, expected ->
146-
val nodeAtIndex = node.getChildren(role).toList().getOrNull(index)
146+
val isOrdered = existingParent.isChildRoleOrdered(role)
147+
148+
expectedNodes.forEachIndexed { indexInImport, expected ->
149+
val existingChildren = existingParent.getChildren(role).toList()
147150
val expectedId = checkNotNull(expected.originalId()) { "Specified node '$expected' has no id" }
151+
// newIndex is the index on which to import the expected child.
152+
// It might be -1 if the child does not exist and should be added at the end.
153+
val newIndex = if (isOrdered) {
154+
indexInImport
155+
} else {
156+
// The `existingChildren` are only searched once for the expected element before changing.
157+
// Therefore, indexing existing children will not be more efficient than iterating once.
158+
// (For the moment, this is fine because as we expect unordered children to be the exception,
159+
// Reusable indexing would be possible if we switch from
160+
// a depth-first import to a breadth-first import.)
161+
existingChildren
162+
.indexOfFirst { existingChild -> existingChild.originalId() == expected.originalId() }
163+
}
164+
// existingChildren.getOrNull handles `-1` as needed by returning `null`.
165+
val nodeAtIndex = existingChildren.getOrNull(newIndex)
148166
val expectedConcept = expected.concept?.let { s -> ConceptReference(s) }
149167
val childNode = if (nodeAtIndex?.originalId() != expectedId) {
150168
val existingNode = originalIdToExisting[expectedId]
151169
if (existingNode == null) {
152-
val newChild = node.addNewChild(role, index, expectedConcept)
170+
val newChild = existingParent.addNewChild(role, newIndex, expectedConcept)
153171
newChild.setPropertyValue(NodeData.idPropertyKey, expectedId)
154172
originalIdToExisting[expectedId] = newChild
155173
newChild
156174
} else {
157-
node.moveChild(role, index, existingNode)
175+
// The existing child node is not only moved to a new index,
176+
// it is potentially moved to a new parent and role.
177+
existingParent.moveChild(role, newIndex, existingNode)
178+
// If the old parent and old role synchronized before the move operation,
179+
// the existing child node would have been marked as to be deleted.
180+
// Now that it is used, it should not be deleted.
158181
nodesToRemove.remove(existingNode)
159182
existingNode
160183
}
@@ -166,7 +189,10 @@ class ModelImporter(private val root: INode, private val continueOnError: Boolea
166189
syncNode(childNode, expected, progressReporter)
167190
}
168191

169-
nodesToRemove += node.getChildren(role).drop(expectedNodes.size)
192+
val expectedNodesIds = expectedNodes.map(NodeData::originalId).toSet()
193+
// Do not use existingNodes, but call node.getChildren(role) because
194+
// the recursive synchronization in the meantime already removed some nodes from node.getChildren(role).
195+
nodesToRemove += existingParent.getChildren(role).filterNot { existingNode -> expectedNodesIds.contains(existingNode.originalId()) }
170196
}
171197
}
172198

0 commit comments

Comments
 (0)