Skip to content

Commit b21dd8a

Browse files
author
Oleksandr Dzhychko
committed
feat(bulk-model-sync): do not reorder unordered children during import
Child links that are unordered need to be handled specially when imported. Such unordered children will never be moved in between other children. Existing unordered children will not be reordered, and new children will only be appended to the existing children.
1 parent 6fcc489 commit b21dd8a

File tree

2 files changed

+397
-13
lines changed

2 files changed

+397
-13
lines changed

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

Lines changed: 31 additions & 13 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,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

Comments
 (0)