Skip to content

Commit

Permalink
feat(bulk-model-sync): migration to IReadableNode/IWritableNode
Browse files Browse the repository at this point in the history
  • Loading branch information
slisson committed Jan 9, 2025
1 parent c9fe5fd commit b74e985
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 131 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package org.modelix.model.sync.bulk

import org.modelix.model.api.INode
import org.modelix.model.api.IReadableNode
import org.modelix.model.api.IWritableNode

/**
* A node association is responsible for storing the mapping between a source node and the imported target node.
* Provides efficient lookup of the mapping from previous synchronization runs.
*/
interface INodeAssociation {
fun resolveTarget(sourceNode: INode): INode?
fun associate(sourceNode: INode, targetNode: INode)
fun resolveTarget(sourceNode: IReadableNode): IWritableNode?
fun associate(sourceNode: IReadableNode, targetNode: IWritableNode)
fun matches(sourceNode: IReadableNode, targetNode: IWritableNode) = resolveTarget(sourceNode) == targetNode
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import org.modelix.model.api.ConceptReference
import org.modelix.model.api.INode
import org.modelix.model.api.INodeReference
import org.modelix.model.api.INodeResolutionScope
import org.modelix.model.api.IPropertyReference
import org.modelix.model.api.IReadableNode
import org.modelix.model.api.IReplaceableNode
import org.modelix.model.api.SerializedNodeReference
import org.modelix.model.api.getDescendants
Expand Down Expand Up @@ -329,6 +331,9 @@ internal fun INode.originalId(): String? {
return this.getOriginalReference()
}

fun IReadableNode.originalId(): String? = getPropertyValue(NodeData.ID_PROPERTY_REF)
?: getPropertyValue(IPropertyReference.fromIdAndName("#mpsNodeId#", "#mpsNodeId#"))

internal fun NodeData.originalId(): String? {
return properties[NodeData.ID_PROPERTY_KEY] ?: id
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package org.modelix.model.sync.bulk

import mu.KotlinLogging
import org.modelix.model.api.ConceptReference
import org.modelix.model.api.INode
import org.modelix.model.api.INodeReference
import org.modelix.model.api.IReferenceLink
import org.modelix.model.api.IReplaceableNode
import org.modelix.model.api.IRole
import org.modelix.model.api.IReadableNode
import org.modelix.model.api.IReferenceLinkReference
import org.modelix.model.api.IRoleReference
import org.modelix.model.api.IRoleReferenceByName
import org.modelix.model.api.IRoleReferenceByUID
import org.modelix.model.api.IUnclassifiedRoleReference
import org.modelix.model.api.IWritableNode
import org.modelix.model.api.PNodeAdapter
import org.modelix.model.api.getOriginalReference
import org.modelix.model.api.isChildRoleOrdered
import org.modelix.model.api.matches
import org.modelix.model.api.remove
import org.modelix.model.data.NodeData

Expand All @@ -25,11 +29,11 @@ import org.modelix.model.data.NodeData
*/
class ModelSynchronizer(
val filter: IFilter,
val sourceRoot: INode,
val targetRoot: INode,
val sourceRoot: IReadableNode,
val targetRoot: IWritableNode,
val nodeAssociation: INodeAssociation,
) {
private val nodesToRemove: MutableSet<INode> = HashSet()
private val nodesToRemove: MutableSet<IWritableNode> = HashSet()
private val pendingReferences: MutableList<PendingReference> = ArrayList()
private val logger = KotlinLogging.logger {}

Expand All @@ -39,11 +43,11 @@ class ModelSynchronizer(
logger.info { "Synchronizing pending references..." }
pendingReferences.forEach { it.trySyncReference() }
logger.info { "Removing extra nodes..." }
nodesToRemove.filter { it.isValid }.forEach { it.remove() }
nodesToRemove.filter { it.isValid() }.forEach { it.remove() }
logger.info { "Synchronization finished." }
}

private fun synchronizeNode(sourceNode: INode, targetNode: INode) {
private fun synchronizeNode(sourceNode: IReadableNode, targetNode: IWritableNode) {
nodeAssociation.associate(sourceNode, targetNode)
if (filter.needsSynchronization(sourceNode)) {
logger.info { "Synchronizing changed node. sourceNode = $sourceNode" }
Expand All @@ -53,15 +57,15 @@ class ModelSynchronizer(
val sourceConcept = sourceNode.getConceptReference()
val targetConcept = targetNode.getConceptReference()

val conceptCorrectedTargetNode = if (sourceConcept != targetConcept && targetNode is IReplaceableNode) {
targetNode.replaceNode(sourceConcept?.getUID()?.let { ConceptReference(it) })
val conceptCorrectedTargetNode = if (sourceConcept != targetConcept) {
targetNode.changeConcept(sourceConcept)
} else {
targetNode
}

syncChildren(sourceNode, conceptCorrectedTargetNode)
} else if (filter.needsDescentIntoSubtree(sourceNode)) {
for (sourceChild in sourceNode.allChildren) {
for (sourceChild in sourceNode.getAllChildren()) {
val targetChild = nodeAssociation.resolveTarget(sourceChild) ?: error("Expected target node was not found. sourceChild=$sourceChild")
synchronizeNode(sourceChild, targetChild)
}
Expand All @@ -71,8 +75,8 @@ class ModelSynchronizer(
}

private fun synchronizeReferences(
sourceNode: INode,
targetNode: INode,
sourceNode: IReadableNode,
targetNode: IWritableNode,
) {
iterateMergedRoles(sourceNode.getReferenceLinks(), targetNode.getReferenceLinks()) { role ->
val pendingReference = PendingReference(sourceNode, targetNode, role)
Expand All @@ -86,23 +90,25 @@ class ModelSynchronizer(
}

private fun synchronizeProperties(
sourceNode: INode,
targetNode: INode,
sourceNode: IReadableNode,
targetNode: IWritableNode,
) {
iterateMergedRoles(sourceNode.getPropertyLinks(), targetNode.getPropertyLinks()) { role ->
val oldValue = targetNode.getPropertyValue(role.preferTarget())
val newValue = sourceNode.getPropertyValue(role.preferSource())
val oldValue = targetNode.getPropertyValue(role)
val newValue = sourceNode.getPropertyValue(role)
if (oldValue != newValue) {
targetNode.setPropertyValue(role.preferTarget(), newValue)
targetNode.setPropertyValue(role, newValue)
}
}
}

private fun syncChildren(sourceParent: INode, targetParent: INode) {
val allRoles = (sourceParent.allChildren.map { it.roleInParent } + targetParent.allChildren.map { it.roleInParent }).distinct()
for (role in allRoles) {
val sourceNodes = sourceParent.getChildren(role).toList()
val targetNodes = targetParent.getChildren(role).toList()
private fun syncChildren(sourceParent: IReadableNode, targetParent: IWritableNode) {
iterateMergedRoles(
sourceParent.getAllChildren().map { it.getContainmentLink() }.distinct(),
targetParent.getAllChildren().map { it.getContainmentLink() }.distinct(),
) { role ->
val sourceNodes = sourceParent.getChildren(role)
val targetNodes = targetParent.getChildren(role)

val allExpectedNodesDoNotExist by lazy {
sourceNodes.all { sourceNode ->
Expand All @@ -118,14 +124,14 @@ class ModelSynchronizer(
nodeAssociation.associate(sourceChild, newChild)
synchronizeNode(sourceChild, newChild)
}
continue
return@iterateMergedRoles
}

// optimization for when there is no change in the child list
// size check first to avoid querying the original ID
if (sourceNodes.size == targetNodes.size && sourceNodes.map { it.originalId() } == targetNodes.map { it.originalId() }) {
if (sourceNodes.size == targetNodes.size && sourceNodes.zip(targetNodes).all { nodeAssociation.matches(it.first, it.second) }) {
sourceNodes.zip(targetNodes).forEach { synchronizeNode(it.first, it.second) }
continue
return@iterateMergedRoles
}

val isOrdered = targetParent.isChildRoleOrdered(role)
Expand Down Expand Up @@ -156,7 +162,7 @@ class ModelSynchronizer(
if (existingNode == null) {
val newChild = targetParent.addNewChild(role, newIndex, expectedConcept)
if (newChild.originalId() == null) {
newChild.setPropertyValue(NodeData.idPropertyKey, expectedId)
newChild.setPropertyValue(NodeData.ID_PROPERTY_REF, expectedId)
}
newChild.originalId()?.let { newlyCreatedIds.add(it) }
nodeAssociation.associate(expected, newChild)
Expand Down Expand Up @@ -188,14 +194,14 @@ class ModelSynchronizer(
}
}

inner class PendingReference(val sourceNode: INode, val targetNode: INode, val role: MergedRole<IReferenceLink>) {
inner class PendingReference(val sourceNode: IReadableNode, val targetNode: IWritableNode, val role: IReferenceLinkReference) {
fun trySyncReference(): Boolean {
val expectedRef = sourceNode.getReferenceTargetRef(role.preferSource())
val expectedRef = sourceNode.getReferenceTargetRef(role)
if (expectedRef == null) {
targetNode.setReferenceTarget(role.preferTarget(), null as INodeReference?)
targetNode.setReferenceTargetRef(role, null)
return true
}
val actualRef = targetNode.getReferenceTargetRef(role.preferTarget())
val actualRef = targetNode.getReferenceTargetRef(role)

// Some reference targets may be excluded from the sync,
// in that case a serialized reference is stored and no lookup of the target is required.
Expand All @@ -204,48 +210,36 @@ class ModelSynchronizer(
return true
}

val referenceTargetInSource = sourceNode.getReferenceTarget(role.preferSource())
checkNotNull(referenceTargetInSource) { "Failed to resolve $expectedRef referenced by $sourceNode.${role.preferSource()}" }
val referenceTargetInSource = sourceNode.getReferenceTarget(role)
checkNotNull(referenceTargetInSource) { "Failed to resolve $expectedRef referenced by $sourceNode.$role" }

val referenceTargetInTarget = nodeAssociation.resolveTarget(referenceTargetInSource)
?: return false // Target cannot be resolved right now but might become resolvable later.

if (referenceTargetInTarget.reference.serialize() != actualRef?.serialize()) {
targetNode.setReferenceTarget(role.preferTarget(), referenceTargetInTarget)
if (referenceTargetInTarget.getNodeReference().serialize() != actualRef?.serialize()) {
targetNode.setReferenceTarget(role, referenceTargetInTarget)
}
return true
}
}

private fun <T : IRole> iterateMergedRoles(
private fun <T : IRoleReference> iterateMergedRoles(
sourceRoles: Iterable<T>,
targetRoles: Iterable<T>,
body: (role: MergedRole<T>) -> Unit,
body: (role: T) -> Unit,
) = iterateMergedRoles(sourceRoles.asSequence(), targetRoles.asSequence(), body)

private fun <T : IRole> iterateMergedRoles(
private fun <T : IRoleReference> iterateMergedRoles(
sourceRoles: Sequence<T>,
targetRoles: Sequence<T>,
body: (role: MergedRole<T>) -> Unit,
body: (role: T) -> Unit,
) {
val sourceRolesMap = sourceRoles.filter { it.getUID() != NodeData.ID_PROPERTY_KEY }.associateBy { it.getUID() }
val targetRolesMap = targetRoles.associateBy { it.getUID() }
val roleUIDs = (sourceRolesMap.keys + targetRolesMap.keys).toSet()
for (roleUID in roleUIDs) {
val sourceRole = sourceRolesMap[roleUID]
val targetRole = targetRolesMap[roleUID]
body(MergedRole(sourceRole, targetRole))
for (role in sourceRoles.mergeWith(targetRoles)) {
if (role.matches(NodeData.ID_PROPERTY_REF)) continue
body(role)
}
}

class MergedRole<E : IRole>(
private val source: E?,
private val target: E?,
) {
fun preferTarget(): E = (target ?: source)!!
fun preferSource() = (source ?: target)!!
}

/**
* Determines, which nodes need synchronization and which can be skipped.
*
Expand All @@ -256,17 +250,17 @@ class ModelSynchronizer(
* Checks if a subtree needs synchronization.
*
* @param subtreeRoot root of the subtree to be checked
* @return true iff the subtree must not be skipped
* @return true iff for any node in this subtree needsSynchronization returns true
*/
fun needsDescentIntoSubtree(subtreeRoot: INode): Boolean
fun needsDescentIntoSubtree(subtreeRoot: IReadableNode): Boolean

/**
* Checks if a single node needs synchronization.
*
* @param node node to be checked
* @return true iff the node must not be skipped
*/
fun needsSynchronization(node: INode): Boolean
fun needsSynchronization(node: IReadableNode): Boolean
}
}

Expand All @@ -277,3 +271,50 @@ private fun INode.originalIdOrFallback(): String? {
if (this is PNodeAdapter) return reference.serialize()
return null
}

private fun IReadableNode.originalIdOrFallback(): String? {
val originalRef = getOriginalReference()
if (originalRef != null) return originalRef

return getNodeReference().serialize()
}

fun <T : IRoleReference> Sequence<T>.mergeWith(others: Sequence<T>): List<T> {
val remaining = others.toMutableList()
val merged = ArrayList<T>()
outer@for (left in this) {
for (i in remaining.indices) {
val right = remaining[i]
if (right.matches(left)) {
val mostSpecific = left.merge(right)
remaining.removeAt(i)
merged.add(mostSpecific)
continue@outer
}
}
merged.add(left)
}
merged.addAll(remaining)
return merged
}

/**
* Choose the more specific one of two matching references.
*/
fun <T : IRoleReference> T.merge(other: T): T {
return when (this) {
is IRoleReferenceByUID -> when (other) {
is IRoleReferenceByUID -> if (other is IRoleReferenceByName) other else this
else -> this
}
is IRoleReferenceByName -> when (other) {
is IRoleReferenceByUID -> other
else -> this
}
is IUnclassifiedRoleReference -> when (other) {
is IUnclassifiedRoleReference -> this
else -> other
}
else -> this
}
}
Loading

0 comments on commit b74e985

Please sign in to comment.