@@ -23,6 +23,7 @@ import org.modelix.model.api.INodeReference
23
23
import org.modelix.model.api.INodeResolutionScope
24
24
import org.modelix.model.api.SerializedNodeReference
25
25
import org.modelix.model.api.getDescendants
26
+ import org.modelix.model.api.isChildRoleOrdered
26
27
import org.modelix.model.api.remove
27
28
import org.modelix.model.data.ModelData
28
29
import org.modelix.model.data.NodeData
@@ -118,15 +119,15 @@ class ModelImporter(private val root: INode, private val continueOnError: Boolea
118
119
}
119
120
}
120
121
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()
123
124
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()
126
127
127
128
// optimization that uses the bulk operation .addNewChildren
128
129
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) ->
130
131
val expectedId = checkNotNull(expected.originalId()) { " Specified node '$expected ' has no id" }
131
132
newChild.setPropertyValue(NodeData .idPropertyKey, expectedId)
132
133
originalIdToExisting[expectedId] = newChild
@@ -142,21 +143,38 @@ class ModelImporter(private val root: INode, private val continueOnError: Boolea
142
143
continue
143
144
}
144
145
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()
147
150
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)
148
166
val expectedConcept = expected.concept?.let { s -> ConceptReference (s) }
149
167
val childNode = if (nodeAtIndex?.originalId() != expectedId) {
150
168
val existingNode = originalIdToExisting[expectedId]
151
169
if (existingNode == null ) {
152
- val newChild = node .addNewChild(role, index , expectedConcept)
170
+ val newChild = existingParent .addNewChild(role, newIndex , expectedConcept)
153
171
newChild.setPropertyValue(NodeData .idPropertyKey, expectedId)
154
172
originalIdToExisting[expectedId] = newChild
155
173
newChild
156
174
} else {
157
175
// The existing child node is not only moved to a new index,
158
176
// it is potentially moved to a new parent and role.
159
- node .moveChild(role, index , existingNode)
177
+ existingParent .moveChild(role, newIndex , existingNode)
160
178
// If the old parent and old role synchronized before the move operation,
161
179
// the existing child node would have been marked as to be deleted.
162
180
// Now that it is used, it should not be deleted.
@@ -171,10 +189,10 @@ class ModelImporter(private val root: INode, private val continueOnError: Boolea
171
189
syncNode(childNode, expected, progressReporter)
172
190
}
173
191
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()) }
178
196
}
179
197
}
180
198
0 commit comments