Skip to content

Commit 7aee76e

Browse files
committed
fix(model-sync-lib): fixed various bugs by rewriting importer
1 parent ee7b6d2 commit 7aee76e

File tree

3 files changed

+186
-85
lines changed

3 files changed

+186
-85
lines changed

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

Lines changed: 164 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ import java.io.File
77

88
class ModelImporter(private val root: INode, val stats: ImportStats? = null) {
99

10-
private lateinit var originalIdToSpec: MutableMap<String, NodeData>
11-
private lateinit var originalIdToExisting: MutableMap<String, INode>
12-
private lateinit var originalIdToRef: MutableMap<String, INodeReference>
13-
1410
fun import(jsonFile: File) {
1511
require(jsonFile.exists())
1612
require(jsonFile.extension == "json")
@@ -20,60 +16,81 @@ class ModelImporter(private val root: INode, val stats: ImportStats? = null) {
2016
}
2117

2218
fun import(data: ModelData) {
23-
originalIdToExisting = mutableMapOf()
24-
buildExistingIndex(root)
19+
syncProperties(root, data.root) // root original id is required for following operations
20+
val allExistingNodes = root.getDescendants(true).toList()
21+
val originalIdToSpec: MutableMap<String, NodeData> = buildSpecIndex(data.root)
22+
23+
syncAllProperties(allExistingNodes, originalIdToSpec)
24+
val originalIdToExisting = buildExistingIndex(allExistingNodes)
2525

26-
originalIdToSpec = mutableMapOf()
27-
buildSpecIndex(data.root)
26+
sortAllExistingChildren(allExistingNodes, originalIdToExisting, originalIdToSpec)
27+
val addedNodes = addAllMissingChildren(root, originalIdToExisting, originalIdToSpec)
28+
syncAllProperties(addedNodes, originalIdToSpec)
2829

29-
syncNode(root, data.root)
30+
handleAllMovesAcrossParents(allExistingNodes, originalIdToExisting, originalIdToSpec)
3031

31-
originalIdToRef = mutableMapOf()
32-
buildRefIndex(root)
32+
val allNodes = allExistingNodes + addedNodes
33+
val originalIdToRef: MutableMap<String, INodeReference> = buildRefIndex(allNodes)
34+
syncAllReferences(allNodes, originalIdToSpec, originalIdToRef)
35+
deleteAllExtraChildren(allNodes, originalIdToSpec)
3336

34-
syncAllReferences(root, data.root)
3537
}
3638

37-
private fun buildExistingIndex(root: INode) {
38-
root.originalId()?.let { originalIdToExisting[it] = root }
39-
root.allChildren.forEach { buildExistingIndex(it) }
39+
private fun buildExistingIndex(allNodes: List<INode>): MutableMap<String, INode> {
40+
val originalIdToExisting: MutableMap<String, INode> = mutableMapOf()
41+
allNodes.forEach {node ->
42+
node.originalId()?.let { originalIdToExisting[it] = node }
43+
}
44+
return originalIdToExisting
4045
}
4146

42-
private fun buildRefIndex(node: INode) {
43-
node.originalId()?.let { originalIdToRef[it] = node.reference }
44-
node.allChildren.forEach { buildRefIndex(it) }
47+
private fun buildRefIndex(allNodes: List<INode>): MutableMap<String, INodeReference> {
48+
val originalIdToRef: MutableMap<String, INodeReference> = mutableMapOf()
49+
allNodes.forEach {node ->
50+
node.originalId()?.let { originalIdToRef[it] = node.reference }
51+
}
52+
return originalIdToRef
4553
}
4654

47-
private fun buildSpecIndex(nodeData: NodeData) {
55+
private fun buildSpecIndex(
56+
nodeData: NodeData,
57+
originalIdToSpec: MutableMap<String, NodeData> = mutableMapOf()
58+
): MutableMap<String, NodeData> {
4859
nodeData.originalId()?.let { originalIdToSpec[it] = nodeData }
49-
nodeData.children.forEach { buildSpecIndex(it) }
60+
nodeData.children.forEach { buildSpecIndex(it, originalIdToSpec) }
61+
return originalIdToSpec
5062
}
5163

52-
private fun syncNode(node: INode, nodeData: NodeData) {
53-
syncProperties(node, nodeData)
54-
syncChildNodes(node, nodeData)
64+
private fun syncAllProperties(allNodes: List<INode>, originalIdToSpec: MutableMap<String, NodeData>) {
65+
allNodes.forEach {node ->
66+
originalIdToSpec[node.originalId()]?.let { spec -> syncProperties(node, spec) }
67+
}
5568
}
5669

5770
private fun syncProperties(node: INode, nodeData: NodeData) {
71+
if (node.getPropertyValue(NodeData.idPropertyKey) == null) {
72+
node.setPropertyValue(NodeData.idPropertyKey, nodeData.originalId())
73+
}
74+
5875
nodeData.properties.forEach {
5976
if (node.getPropertyValue(it.key) != it.value) {
6077
node.setPropertyValueWithStats(it.key, it.value)
6178
}
6279
}
80+
6381
val toBeRemoved = node.getPropertyRoles().toSet()
6482
.subtract(nodeData.properties.keys)
6583
.filter { it != NodeData.idPropertyKey }
6684
toBeRemoved.forEach { node.setPropertyValueWithStats(it, null) }
6785
}
6886

69-
private fun syncAllReferences(root: INode, rootData: NodeData) {
70-
syncReferences(root, rootData)
71-
for ((node, data) in root.allChildren.zip(rootData.children)) {
72-
syncAllReferences(node, data)
87+
private fun syncAllReferences(allNodes: List<INode>, originalIdToSpec: MutableMap<String, NodeData>, originalIdToRef: Map<String, INodeReference>) {
88+
allNodes.forEach {node ->
89+
originalIdToSpec[node.originalId()]?.let { spec -> syncReferences(node, spec, originalIdToRef) }
7390
}
7491
}
7592

76-
private fun syncReferences(node: INode, nodeData: NodeData) {
93+
private fun syncReferences(node: INode, nodeData: NodeData, originalIdToRef: Map<String, INodeReference>) {
7794
nodeData.references.forEach {
7895
if (node.getReferenceTargetRef(it.key) != originalIdToRef[it.value]) {
7996
node.setReferenceTargetWithStats(it.key, originalIdToRef[it.value])
@@ -83,77 +100,143 @@ class ModelImporter(private val root: INode, val stats: ImportStats? = null) {
83100
toBeRemoved.forEach { node.setReferenceTargetWithStats(it, null) }
84101
}
85102

86-
private fun syncChildNodes(node: INode, nodeData: NodeData) {
87-
val specifiedNodes = nodeData.children.toSet()
88-
val existingIds = node.allChildren.map { it.originalId() }.toSet()
89-
val missingNodes = specifiedNodes.filter { !existingIds.contains(it.originalId()) }
90-
91-
val toBeRemoved = node.allChildren.filter { !originalIdToSpec.contains(it.originalId()) }
92-
toBeRemoved.forEach { node.removeChildWithStats(it) }
93-
94-
val toBeMovedAwayFromHere = existingIds.subtract(specifiedNodes.map { it.originalId() }.toSet())
95-
96-
syncExistingChildOrder(node, nodeData, existingIds, toBeMovedAwayFromHere)
97-
98-
val toBeMovedHere = missingNodes.filter { originalIdToExisting.containsKey(it.originalId()) }.toSet()
99-
toBeMovedHere.forEach {
100-
val actualNode = originalIdToExisting[it.originalId()]
101-
val targetIndex = it.getIndexWithinRole(nodeData,existingIds.size - 1)
102-
if (actualNode != null) {
103-
node.moveChildWithStats(it.role, targetIndex, actualNode)
104-
}
103+
private fun addAllMissingChildren(
104+
node: INode,
105+
originalIdToExisting: MutableMap<String, INode>,
106+
originalIdToSpec: MutableMap<String, NodeData>
107+
): MutableList<INode> {
108+
val addedNodes = mutableListOf<INode>()
109+
originalIdToSpec[node.originalId()]?.let {
110+
addedNodes.addAll(
111+
addMissingChildren(node, it, originalIdToExisting, originalIdToSpec)
112+
)
105113
}
106-
107-
val toBeAdded = missingNodes.subtract(toBeMovedHere)
108-
toBeAdded.forEach {
109-
val index = it.getIndexWithinRole(nodeData, existingIds.size - 1)
110-
node.addNewChildWithStats(it, index)
114+
node.allChildren.forEach {
115+
addedNodes.addAll(addAllMissingChildren(it, originalIdToExisting, originalIdToSpec))
111116
}
117+
return addedNodes
118+
}
112119

113-
node.allChildren.forEach {
114-
val childData = originalIdToSpec[it.originalId()]
115-
if (childData != null) {
116-
syncNode(it, childData)
120+
private fun addMissingChildren(
121+
node: INode,
122+
nodeData: NodeData,
123+
originalIdToExisting: MutableMap<String, INode>,
124+
originalIdToSpec: MutableMap<String, NodeData>
125+
): List<INode> {
126+
val specifiedChildren = nodeData.children.toList()
127+
val toBeAdded = specifiedChildren.filter { !originalIdToExisting.contains(it.originalId()) }
128+
129+
return toBeAdded.map { nodeToBeAdded ->
130+
val childrenInRole = node.allChildren.filter { it.roleInParent == nodeToBeAdded.role }
131+
val existingIds = childrenInRole.map { it.originalId() }
132+
val baseIndex = nodeToBeAdded.getIndexWithinRole(nodeData, childrenInRole.size)
133+
var offset = 0
134+
offset += childrenInRole.slice(0..minOf(baseIndex, childrenInRole.lastIndex)).count {
135+
!originalIdToSpec.containsKey(it.originalId()) // node will be deleted
136+
}
137+
offset -= specifiedChildren.filter { it.role == nodeToBeAdded.role }.slice(0 until baseIndex).count {
138+
!existingIds.contains(it.originalId()) // node will be moved here
117139
}
140+
node.addNewChildWithStats(nodeToBeAdded, baseIndex + offset)
118141
}
119142
}
120143

121-
private fun syncExistingChildOrder(
144+
private fun sortAllExistingChildren(
145+
allNodes: List<INode>,
146+
originalIdToExisting: MutableMap<String, INode>,
147+
originalIdToSpec: MutableMap<String, NodeData>
148+
) {
149+
allNodes.forEach { node ->
150+
originalIdToSpec[node.originalId()]?.let { sortExistingChildren(node, it, originalIdToExisting, originalIdToSpec) }
151+
}
152+
}
153+
154+
private fun sortExistingChildren(
122155
node: INode,
123156
nodeData: NodeData,
124-
existingIds: Set<String?>,
125-
toBeMovedAwayFromHere: Set<String?>
157+
originalIdToExisting: MutableMap<String, INode>,
158+
originalIdToSpec: MutableMap<String, NodeData>
126159
) {
127160
val existingChildren = node.allChildren.toList()
161+
val existingIds = existingChildren.map { it.originalId() }
128162
val specifiedChildren = nodeData.children
129-
require(existingChildren.size <= specifiedChildren.size)
130-
131-
val filteredSpecifiedChildren = specifiedChildren.filter { existingIds.contains(it.originalId()) }
163+
val toBeSortedSpec = specifiedChildren.filter { originalIdToExisting.containsKey(it.originalId()) }
132164

133165
val targetIndices = HashMap<String?, Int>(nodeData.children.size)
134-
for (specifiedChild in filteredSpecifiedChildren) {
135-
val index = filteredSpecifiedChildren.filter { it.role == node.roleInParent }.indexOf(specifiedChild)
136-
targetIndices[specifiedChild.originalId()] = minOf(index, existingChildren.size - 1)
166+
for (child in toBeSortedSpec) {
167+
val baseIndex = toBeSortedSpec.filter { it.role == node.roleInParent }.indexOf(child)
168+
var offset = 0
169+
offset += existingChildren.filter { it.roleInParent == child.role }.slice(0..baseIndex).count {
170+
!originalIdToSpec.containsKey(it.originalId()) // node will be deleted
171+
}
172+
offset -= specifiedChildren.filter { it.role == child.role }.slice(0..baseIndex).count {
173+
!existingIds.contains(it.originalId()) // node will be moved here
174+
}
175+
val index = baseIndex + offset
176+
targetIndices[child.originalId()] = minOf(index, existingChildren.size - 1)
137177
}
138178

139179
for ((index, child) in existingChildren.withIndex()) {
140-
val targetIndex = targetIndices[child.originalId()] ?: -1
141-
if (targetIndex != index && !toBeMovedAwayFromHere.contains(child.originalId())) {
180+
val targetIndex = targetIndices[child.originalId()]
181+
if (targetIndex != null && targetIndex != index) {
142182
node.moveChildWithStats(child.roleInParent, targetIndex, child)
143183
}
144184
}
145185
}
146186

147-
private fun NodeData.getIndexWithinRole(parent: NodeData, maxIndex: Int) : Int {
148-
return minOf(parent.children.filter { it.role == this.role }.indexOf(this), maxIndex)
187+
private fun handleAllMovesAcrossParents(
188+
allNodes: List<INode>,
189+
originalIdToExisting: MutableMap<String, INode>,
190+
originalIdToSpec: MutableMap<String, NodeData>
191+
) {
192+
allNodes.forEach {
193+
originalIdToSpec[it.originalId()]?.let { spec -> handleMoveAcrossParents(it, spec, originalIdToExisting, originalIdToSpec) }
194+
}
149195
}
150196

151-
private fun INode.originalId(): String? {
152-
return this.getPropertyValue(NodeData.idPropertyKey)
197+
private fun handleMoveAcrossParents(
198+
node: INode,
199+
nodeData: NodeData,
200+
originalIdToExisting: MutableMap<String, INode>,
201+
originalIdToSpec: MutableMap<String, NodeData>
202+
) {
203+
204+
val existingChildren = node.allChildren.toList()
205+
val missingIds = nodeData.children.map { it.originalId() }.toSet()
206+
.subtract(node.allChildren.map { it.originalId() }.toSet())
207+
val toBeMovedHere = missingIds
208+
.filter { originalIdToSpec.containsKey(it) }
209+
.mapNotNull { originalIdToExisting[it] }
210+
211+
toBeMovedHere.forEach {nodeToBeMoved ->
212+
val spec = originalIdToSpec[nodeToBeMoved.originalId()]!!
213+
214+
val baseTargetIndex = spec.getIndexWithinRole(nodeData, existingChildren.size - 1)
215+
val offset = existingChildren.slice(0..baseTargetIndex).count {
216+
!originalIdToSpec.containsKey(it.originalId()) // node will be deleted
217+
}
218+
val targetIndex = baseTargetIndex + offset
219+
220+
node.moveChildWithStats(spec.role, targetIndex, nodeToBeMoved)
221+
}
222+
}
223+
224+
private fun deleteAllExtraChildren(allNodes: List<INode>, originalIdToSpec: MutableMap<String, NodeData>) {
225+
val toBeRemoved = mutableListOf<INode>()
226+
allNodes.forEach { node ->
227+
node.allChildren.forEach {
228+
if (!originalIdToSpec.containsKey(it.originalId())) {
229+
toBeRemoved.add(it)
230+
}
231+
}
232+
}
233+
toBeRemoved.asReversed().forEach {// delete bottom-up
234+
it.parent?.removeChildWithStats(it)
235+
}
153236
}
154237

155-
private fun NodeData.originalId(): String? {
156-
return properties[NodeData.idPropertyKey] ?: id
238+
private fun NodeData.getIndexWithinRole(parent: NodeData, maxIndex: Int) : Int {
239+
return minOf(parent.children.filter { it.role == this.role }.indexOf(this), maxIndex)
157240
}
158241

159242
private fun INode.addNewChildWithStats(spec: NodeData, index: Int) : INode {
@@ -208,4 +291,12 @@ class ModelImporter(private val root: INode, val stats: ImportStats? = null) {
208291
setReferenceTarget(role, target)
209292
}
210293

294+
}
295+
296+
internal fun INode.originalId(): String? {
297+
return this.getPropertyValue(NodeData.idPropertyKey)
298+
}
299+
300+
internal fun NodeData.originalId(): String? {
301+
return properties[NodeData.idPropertyKey] ?: id
211302
}

model-sync-lib/src/test/kotlin/org/modelix/model/sync/ModelImporterTest.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.assertDoesNotThrow
66
import org.modelix.model.api.IBranch
77
import org.modelix.model.api.PBranch
88
import org.modelix.model.api.getRootNode
9+
import org.modelix.model.api.serialize
910
import org.modelix.model.client.IdGenerator
1011
import org.modelix.model.data.ModelData
1112
import org.modelix.model.data.NodeData
@@ -118,11 +119,7 @@ class ModelImporterTest {
118119
assertDoesNotThrow {
119120
ModelImporter(tempBranch.getRootNode()).import(newModel)
120121
}
121-
val children = tempBranch.getRootNode().allChildren.toList()
122-
assertEquals(newModel.root.children.size, children.size)
123-
for ((expected, actual) in newModel.root.children zip children) {
124-
assertEquals(expected.children.size, actual.allChildren.toList().size)
125-
}
122+
assertAllNodesConformToSpec(newModel.root, tempBranch.getRootNode())
126123
}
127124
}
128125

@@ -131,16 +128,17 @@ class ModelImporterTest {
131128
val tree0 = CLTree(ObjectStoreCache(MapBaseStore()))
132129
val branch0 = PBranch(tree0, IdGenerator.getInstance(1))
133130

134-
val seed = Random.nextInt()
131+
val seed = 1896439714 //Random.nextInt()
135132
println("Seed for random change test: $seed")
136133
lateinit var initialState: NodeData
137134
lateinit var specification: NodeData
138135
val numChanges = 50
139136

140137
branch0.runWrite {
141138
val rootNode = branch0.getRootNode()
139+
rootNode.setPropertyValue(NodeData.idPropertyKey, rootNode.reference.serialize())
142140
val grower = RandomModelChangeGenerator(rootNode, Random(seed)).growingOperationsOnly()
143-
for (i in 1..50) {
141+
for (i in 1..20) {
144142
grower.applyRandomChange()
145143
}
146144
initialState = rootNode.asExported()
@@ -160,6 +158,15 @@ class ModelImporterTest {
160158
importer.import(ModelData(root = initialState))
161159
importer.import(ModelData(root = specification))
162160

161+
println("INITIAL")
162+
println(initialState.toJson())
163+
164+
println("SPEC")
165+
println(specification.toJson())
166+
167+
println("ACTUAL")
168+
println(branch1.getRootNode().toJson())
169+
163170
assertAllNodesConformToSpec(specification, branch1.getRootNode())
164171
assert(importer.stats!!.getTotal() <= numChanges)
165172
}

0 commit comments

Comments
 (0)