@@ -23,6 +23,7 @@ import org.modelix.model.api.INodeReference
2323import org.modelix.model.api.INodeResolutionScope
2424import org.modelix.model.api.SerializedNodeReference
2525import org.modelix.model.api.getDescendants
26+ import org.modelix.model.api.isChildRoleOrdered
2627import org.modelix.model.api.remove
2728import org.modelix.model.data.ModelData
2829import 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,21 +143,38 @@ 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 {
157175 // The existing child node is not only moved to a new index,
158176 // it is potentially moved to a new parent and role.
159- node .moveChild(role, index , existingNode)
177+ existingParent .moveChild(role, newIndex , existingNode)
160178 // If the old parent and old role synchronized before the move operation,
161179 // the existing child node would have been marked as to be deleted.
162180 // Now that it is used, it should not be deleted.
@@ -171,10 +189,10 @@ class ModelImporter(private val root: INode, private val continueOnError: Boolea
171189 syncNode(childNode, expected, progressReporter)
172190 }
173191
174- // At this point, all n expected children for this role are created and correctly sorted.
175- // This means the first n children are correct.
176- // Any child beyond that is an unexpected child has to be marked for removal .
177- 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()) }
178196 }
179197 }
180198
0 commit comments