Skip to content

Commit cd81c99

Browse files
author
Oleksandr Dzhychko
authored
Merge pull request #830 from modelix/MODELIX-813-set-resolveInfo-in-updated-modules-for-targets-without-concept
feat(bulk-model-sync): workaround removed and outdated resolveInfo after import into MPS
2 parents 5e8a62b + dc44995 commit cd81c99

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)