Skip to content

Commit dc44995

Browse files
author
Oleksandr Dzhychko
committed
feat(bulk-model-sync): workaround removed and outdated resolveInfo after import into MPS
In projects, loading all libraries and plugins can significant time. This workaround enables to sync projects better that do not want to load all libraries and plugins. With this workaround, the `name` or `resolveInfo` property of a target is used as the `resoleInfo` property on a reference even if the concept of the target could not be fully loaded.
1 parent 5e8a62b commit dc44995

File tree

9 files changed

+284
-30
lines changed

9 files changed

+284
-30
lines changed

bulk-model-sync-lib/mps-test/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ plugins {
3030

3131
dependencies {
3232
testImplementation(project(":bulk-model-sync-lib"))
33+
testImplementation(project(":bulk-model-sync-mps"))
3334
testImplementation(project(":mps-model-adapters"))
3435
testImplementation(libs.kotlin.serialization.json)
36+
testImplementation(libs.xmlunit.matchers)
3537
}
3638

3739
intellij {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright (c) 2024.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.modelix.model.sync.bulk.lib.test
18+
import kotlinx.serialization.json.Json
19+
import org.hamcrest.CoreMatchers.equalTo
20+
import org.junit.Assert.assertThat
21+
import org.modelix.model.api.BuiltinLanguages
22+
import org.modelix.model.api.INode
23+
import org.modelix.model.data.ModelData
24+
import org.modelix.model.mpsadapters.MPSRepositoryAsNode
25+
import org.modelix.model.sync.bulk.ExistingAndExpectedNode
26+
import org.modelix.model.sync.bulk.asExported
27+
import org.modelix.mps.model.sync.bulk.MPSBulkSynchronizer
28+
import org.xmlunit.builder.Input
29+
import org.xmlunit.matchers.EvaluateXPathMatcher.hasXPath
30+
31+
class ResolveInfoUpdateTest : MPSTestBase() {
32+
33+
fun `test resolve info is updated with name from INamedConcept (testdata ResolveInfoUpdateTest)`() {
34+
val exportedModuleJson = exportModuleJson()
35+
val modifiedModuleJson = exportedModuleJson.replace("referencedNodeA", "referencedNodeANewName")
36+
val modifiedModule: ModelData = Json.decodeFromString(modifiedModuleJson)
37+
38+
val getModulesToImport = { sequenceOf(ExistingAndExpectedNode(getTestModule(), modifiedModule)) }
39+
MPSBulkSynchronizer.importModelsIntoRepository(mpsProject.repository, getTestModule(), false, getModulesToImport)
40+
41+
assertReferenceHasResolveInfo("3vHUMVfa0RY", "referencedNodeANewName")
42+
}
43+
44+
fun `test resolve info is updated with resolveInfo from IResolveInfo (testdata ResolveInfoUpdateTest)`() {
45+
val exportedModuleJson = exportModuleJson()
46+
val modifiedModuleJson = exportedModuleJson.replace("referencedNodeC", "referencedNodeCNewName")
47+
val modifiedModule: ModelData = Json.decodeFromString(modifiedModuleJson)
48+
49+
val getModulesToImport = { sequenceOf(ExistingAndExpectedNode(getTestModule(), modifiedModule)) }
50+
MPSBulkSynchronizer.importModelsIntoRepository(mpsProject.repository, getTestModule(), false, getModulesToImport)
51+
52+
assertReferenceHasResolveInfo("3vHUMVfa0RZ", "referencedNodeCNewName")
53+
}
54+
55+
private fun assertReferenceHasResolveInfo(referencedNode: String, expectedResolveInfo: String) {
56+
val testModelPath = projectDir.resolve("solutions/NewSolution/models/NewSolution.a_model.mps")
57+
val testModelXml = Input.fromPath(testModelPath).build()
58+
assertThat(testModelXml, hasXPath("model/node/node/ref[@node='$referencedNode']/@resolve", equalTo(expectedResolveInfo)))
59+
}
60+
61+
private fun getTestModule(): INode {
62+
var result: INode? = null
63+
mpsProject.repository.modelAccess.runReadAction {
64+
val repository = mpsProject.repository
65+
val repositoryNode = MPSRepositoryAsNode(repository)
66+
result = repositoryNode.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules)
67+
.single { it.getPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name) == "NewSolution" }
68+
}
69+
return checkNotNull(result)
70+
}
71+
72+
private fun exportModuleJson(): String {
73+
var result: String? = null
74+
mpsProject.repository.modelAccess.runReadAction {
75+
val module = getTestModule()
76+
val modelData = ModelData(root = module.asExported())
77+
result = modelData.toJson()
78+
}
79+
return checkNotNull(result)
80+
}
81+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Default ignored files
2+
/shelf/
3+
/workspace.xml
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project version="4">
3+
<component name="MigrationProperties">
4+
<entry key="project.baseline.version" value="211" />
5+
</component>
6+
</project>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project version="4">
3+
<component name="MPSProject">
4+
<projectModules>
5+
<modulePath path="$PROJECT_DIR$/solutions/NewSolution/NewSolution.msd" folder="" />
6+
</projectModules>
7+
</component>
8+
</project>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<solution name="NewSolution" uuid="471b29cb-3253-460b-9743-1e1443884a6b" moduleVersion="0" compileInMPS="true">
3+
<models>
4+
<modelRoot contentPath="${module}" type="default">
5+
<sourceRoot location="models" />
6+
</modelRoot>
7+
</models>
8+
<facets>
9+
<facet type="java">
10+
<classes generated="true" path="${module}/classes_gen" />
11+
</facet>
12+
</facets>
13+
<sourcePath />
14+
<languageVersions />
15+
<dependencyVersions>
16+
<module reference="471b29cb-3253-460b-9743-1e1443884a6b(NewSolution)" version="0" />
17+
</dependencyVersions>
18+
</solution>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<model ref="r:cd78e6ac-0e34-490a-9b49-e5643f948d6d(NewSolution.a_model)">
3+
<persistence version="9" />
4+
<languages>
5+
<use id="e2840528-cf1a-4707-9968-32c55e0e5b6c" name="NewLanguage" version="0" />
6+
</languages>
7+
<imports />
8+
<registry>
9+
<language id="ceab5195-25ea-4f22-9b92-103b95ca8c0c" name="jetbrains.mps.lang.core">
10+
<concept id="1196978630214" name="jetbrains.mps.lang.core.structure.IResolveInfo" flags="ng" index="2Lv6Xg">
11+
<property id="1196978656277" name="resolveInfo" index="2Lvdk3" />
12+
</concept>
13+
<concept id="1169194658468" name="jetbrains.mps.lang.core.structure.INamedConcept" flags="ng" index="TrEIO">
14+
<property id="1169194664001" name="name" index="TrG5h" />
15+
</concept>
16+
</language>
17+
<language id="e2840528-cf1a-4707-9968-32c55e0e5b6c" name="NewLanguage">
18+
<concept id="4030135827843012252" name="NewLanguage.structure.RootNode" flags="ng" index="3SLrQM">
19+
<child id="4030135827843012255" name="referencedNode" index="3SLrQL" />
20+
<child id="4030135827843012253" name="referencingNodes" index="3SLrQN" />
21+
</concept>
22+
<concept id="4030135827842946229" name="NewLanguage.structure.ReferencingNode" flags="ng" index="3SMFYr">
23+
<reference id="4030135827843004992" name="aReference" index="3SLt5I" />
24+
</concept>
25+
<concept id="4030135827842946260" name="NewLanguage.structure.ReferencedNodeWithResolveInfo" flags="ng" index="3SMFZU" />
26+
<concept id="4030135827842946256" name="NewLanguage.structure.ReferencedNodeWithName" flags="ng" index="3SMFZY" />
27+
</language>
28+
</registry>
29+
<node concept="3SLrQM" id="3vHUMVfa5C_">
30+
<node concept="3SMFYr" id="3vHUMVfa0RX" role="3SLrQN" >
31+
<ref role="3SLt5I" node="3vHUMVfa0RY" resolve="referencedNodeA" />
32+
</node>
33+
<node concept="3SMFZY" id="3vHUMVfa0RY" role="3SLrQL">
34+
<property role="TrG5h" value="referencedNodeA" />
35+
</node>
36+
<node concept="3SMFZU" id="3vHUMVfa0RZ" role="3SLrQL">
37+
<property role="2Lvdk3" value="referencedNodeC" />
38+
</node>
39+
<node concept="3SMFYr" id="3vHUMVfa4pM" role="3SLrQN">
40+
<ref role="3SLt5I" node="3vHUMVfa0RZ" resolve="referencedNodeC" />
41+
</node>
42+
</node>
43+
</model>

bulk-model-sync-mps/src/main/kotlin/org/modelix/mps/model/sync/bulk/MPSBulkSynchronizer.kt

Lines changed: 120 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
package org.modelix.mps.model.sync.bulk
1818

19-
import com.intellij.openapi.application.ApplicationManager
2019
import com.intellij.openapi.project.ProjectManager
20+
import jetbrains.mps.ide.ThreadUtils
2121
import jetbrains.mps.ide.project.ProjectHelper
2222
import jetbrains.mps.smodel.SNodeUtil
23+
import jetbrains.mps.smodel.StaticReference
2324
import jetbrains.mps.smodel.adapter.ids.MetaIdHelper
2425
import jetbrains.mps.smodel.adapter.ids.SConceptId
26+
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory
2527
import jetbrains.mps.smodel.adapter.structure.concept.SConceptAdapterById
2628
import jetbrains.mps.smodel.language.ConceptRegistry
2729
import jetbrains.mps.smodel.language.StructureRegistry
@@ -33,6 +35,7 @@ import kotlinx.serialization.json.decodeFromStream
3335
import org.jetbrains.mps.openapi.model.EditableSModel
3436
import org.jetbrains.mps.openapi.module.SModule
3537
import org.jetbrains.mps.openapi.module.SRepository
38+
import org.modelix.model.api.INode
3639
import org.modelix.model.data.ModelData
3740
import org.modelix.model.mpsadapters.MPSModuleAsNode
3841
import org.modelix.model.mpsadapters.MPSRepositoryAsNode
@@ -43,6 +46,32 @@ import org.modelix.model.sync.bulk.isModuleIncluded
4346
import java.io.File
4447
import java.util.concurrent.atomic.AtomicInteger
4548

49+
/**
50+
* Identifier of the `name` property in the `INamedConcept` concept.
51+
* See https://github.com/JetBrains/MPS/blob/5bb20b8a104c08206490e0f3fad70304fa0e0151/core/kernel/kernelSolution/source_gen/jetbrains/mps/util/SNodeOperations.java#L355
52+
*/
53+
@Suppress("MagicNumber")
54+
private val namePropertyOfINamedConceptConcept = MetaAdapterFactory.getProperty(
55+
-0x3154ae6ada15b0deL,
56+
-0x646defc46a3573f4L,
57+
0x110396eaaa4L,
58+
0x110396ec041L,
59+
"name",
60+
)
61+
62+
/**
63+
* Identifier of the `resolveInfo` property in the `IResolveInfoConcept` concept.
64+
* See https://github.com/JetBrains/MPS/blob/5bb20b8a104c08206490e0f3fad70304fa0e0151/core/kernel/kernelSolution/source_gen/jetbrains/mps/util/SNodeOperations.java#L355
65+
*/
66+
@Suppress("MagicNumber")
67+
private val resolveInfoPropertyOfIResolveInfoConcept = MetaAdapterFactory.getProperty(
68+
-0x3154ae6ada15b0deL,
69+
-0x646defc46a3573f4L,
70+
0x116b17c6e46L,
71+
0x116b17cd415L,
72+
"resolveInfo",
73+
)
74+
4675
object MPSBulkSynchronizer {
4776

4877
@JvmStatic
@@ -94,56 +123,118 @@ object MPSBulkSynchronizer {
94123
if (jsonFiles.isNullOrEmpty()) error("no json files found for included modules")
95124

96125
println("Found ${jsonFiles.size} modules to be imported")
97-
val access = repository.modelAccess
98-
access.executeCommandInEDT {
126+
val getModulesToImport = {
99127
val allModules = repository.modules
100128
val includedModules: Iterable<SModule> = allModules.filter {
101129
isModuleIncluded(it.moduleName!!, includedModuleNames, includedModulePrefixes)
102130
}
103131
val numIncludedModules = includedModules.count()
104-
val repoAsNode = MPSRepositoryAsNode(repository)
105-
println("Importing modules...")
106-
try {
132+
val modulesToImport = includedModules.asSequence().flatMapIndexed { index, module ->
133+
println("Importing module ${index + 1} of $numIncludedModules: '${module.moduleName}'")
134+
val fileName = inputPath + File.separator + module.moduleName + ".json"
135+
val moduleFile = File(fileName)
136+
if (moduleFile.exists()) {
137+
val expectedData: ModelData = moduleFile.inputStream().use(Json::decodeFromStream)
138+
sequenceOf(ExistingAndExpectedNode(MPSModuleAsNode(module), expectedData))
139+
} else {
140+
println("Skip importing ${module.moduleName}} because $fileName does not exist.")
141+
sequenceOf()
142+
}
143+
}
144+
modulesToImport
145+
}
146+
importModelsIntoRepository(repository, MPSRepositoryAsNode(repository), continueOnError, getModulesToImport)
147+
}
148+
149+
/**
150+
* Import specified models into the repository.
151+
* [getModulesToImport] is a lambda to be executed with read access in MPS.
152+
*/
153+
@JvmStatic
154+
fun importModelsIntoRepository(
155+
repository: SRepository,
156+
rootOfImport: INode,
157+
continueOnError: Boolean,
158+
getModulesToImport: () -> Sequence<ExistingAndExpectedNode>,
159+
) {
160+
val access = repository.modelAccess
161+
ThreadUtils.runInUIThreadAndWait {
162+
access.executeCommand {
107163
println("Importing modules...")
108-
// `modulesToImport` lazily produces modules to import
109-
// so that loaded model data can be garbage collected.
110-
val modulesToImport = includedModules.asSequence().flatMapIndexed { index, module ->
111-
println("Importing module ${index + 1} of $numIncludedModules: '${module.moduleName}'")
112-
val fileName = inputPath + File.separator + module.moduleName + ".json"
113-
val moduleFile = File(fileName)
114-
if (moduleFile.exists()) {
115-
val expectedData: ModelData = moduleFile.inputStream().use(Json::decodeFromStream)
116-
sequenceOf(ExistingAndExpectedNode(MPSModuleAsNode(module), expectedData))
117-
} else {
118-
println("Skip importing ${module.moduleName}} because $fileName does not exist.")
119-
sequenceOf()
120-
}
164+
try {
165+
println("Importing modules...")
166+
// `modulesToImport` lazily produces modules to import
167+
// so that loaded model data can be garbage collected.
168+
val modulesToImport = getModulesToImport()
169+
ModelImporter(rootOfImport, continueOnError).importIntoNodes(modulesToImport)
170+
println("Import finished.")
171+
} catch (ex: Exception) {
172+
// Exceptions are only visible in the MPS log file by default
173+
ex.printStackTrace()
121174
}
122-
ModelImporter(repoAsNode, continueOnError).importIntoNodes(modulesToImport)
123175
println("Import finished.")
124-
} catch (ex: Exception) {
125-
// Exceptions are only visible in the MPS log file by default
126-
ex.printStackTrace()
127176
}
128-
println("Import finished.")
129177
}
130178

131-
ApplicationManager.getApplication().invokeAndWait {
179+
ThreadUtils.runInUIThreadAndWait {
132180
println("Persisting changes...")
133-
access.executeCommandInEDT {
181+
access.executeCommand {
134182
enableWorkaroundForFilePerRootPersistence(repository)
183+
updateUnsetResolveInfo(repository)
135184
repository.saveAll()
136185
}
137186
println("Changes persisted.")
138187
}
139188
}
140189

190+
/**
191+
* Workaround for MPS not being able to set the `resolveInfo` property on a reference.
192+
* This is the case when the concept of the target node cannot be loaded/is not valid.
193+
* Without this workaround, the `resolve` attribute in serialized references
194+
* (e.g. <ref role="3SLt5I" node="3vHUMVfa0RY" resolve="referencedNodeA" />)
195+
* will not be set, updated or even removed.
196+
*
197+
* The `resolve` is for example removed when the node that contains the reference is moved.
198+
*
199+
* The workaround follows the logic of MPS but without relying on the concept being loaded/valid.
200+
* Without this workaround a bulk sync can remove the `resolve` info unintentionally
201+
* and produce unwanted file changes.
202+
*/
203+
private fun updateUnsetResolveInfo(repository: SRepository) {
204+
val changedModels = repository.modules.asSequence()
205+
.flatMap { it.models }
206+
.mapNotNull { it as? EditableSModel }
207+
.filter { it.isChanged }
208+
val references = changedModels
209+
.flatMap { org.jetbrains.mps.openapi.model.SNodeUtil.getDescendants(it) }
210+
.flatMap { it.references }
211+
.mapNotNull { it as? StaticReference }
212+
213+
references.forEach { reference ->
214+
val target = reference.targetNode ?: return@forEach
215+
// A concept is not valid, for example, when the language could not be loaded.
216+
if (target.concept.isValid) {
217+
return@forEach
218+
}
219+
// Try guessing the resolve info following the logic of MPS.
220+
// Use the logic like in MPS but without relying on the concept being loaded.
221+
// https://github.com/JetBrains/MPS/blob/5bb20b8a104c08206490e0f3fad70304fa0e0151/core/kernel/kernelSolution/source_gen/jetbrains/mps/util/SNodeOperations.java#L230
222+
val newResolveInfo = target.getProperty(resolveInfoPropertyOfIResolveInfoConcept)
223+
?: target.getProperty(namePropertyOfINamedConceptConcept)
224+
if (newResolveInfo != reference.resolveInfo) {
225+
// This workaround works with different persistence because it sets the `resolveInfo`
226+
// using `jetbrains.mps.smodel.SReference` which is not specific to any persistence.
227+
reference.resolveInfo = newResolveInfo
228+
}
229+
}
230+
}
231+
141232
/**
142233
* Workaround for MPS not being able to read the name property of the node during the save process
143234
* in case FilePerRootPersistence is used.
144-
* This is because the concept is not properly loaded and in the MPS code it checks if the concept is a subconcept
145-
* of INamedConcept.
146-
* Without this workaround the id of the root node will be used instead of the name, resulting in renamed files.
235+
* This is because the concept is not properly loaded,
236+
* and in the MPS code it checks if the concept is a subconcept of INamedConcept.
237+
* Without this workaround, the id of the root node will be used instead of the name, resulting in renamed files.
147238
*/
148239
@JvmStatic
149240
private fun enableWorkaroundForFilePerRootPersistence(repository: SRepository) {

gradle/libs.versions.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ openapi = "7.6.0"
3838
micrometer = "1.13.1"
3939
dokka = "1.9.20"
4040
detekt = "1.23.6"
41+
xmlunit = "2.10.0"
4142

4243
[libraries]
4344

@@ -104,7 +105,8 @@ postgresql = { group = "org.postgresql", name = "postgresql", version = "42.7.3"
104105
jcommander = { group = "com.beust", name = "jcommander", version = "1.82" }
105106
cucumber-java = { group = "io.cucumber", name = "cucumber-java", version = "7.18.0" }
106107
junit = { group = "junit", name = "junit", version = "4.13.2" }
107-
xmlunit-core = { group = "org.xmlunit", name = "xmlunit-core", version = "2.10.0"}
108+
xmlunit-core = { group = "org.xmlunit", name = "xmlunit-core", version.ref="xmlunit"}
109+
xmlunit-matchers = { group = "org.xmlunit", name = "xmlunit-matchers", version.ref="xmlunit"}
108110
jsoup = { group = "org.jsoup", name = "jsoup", version = "1.17.2" }
109111

110112
apache-cxf-sse = { group = "org.apache.cxf", name = "cxf-rt-rs-sse", version.ref = "apacheCxf" }

0 commit comments

Comments
 (0)