Skip to content

Commit cd92e05

Browse files
committed
fix(model-server): content explorer can now show models with string based node IDs
1 parent 1bed582 commit cd92e05

File tree

3 files changed

+79
-92
lines changed

3 files changed

+79
-92
lines changed

model-api/src/commonMain/kotlin/org/modelix/model/api/INodeReference.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ abstract class INodeReference {
3737
return serialize().hashCode()
3838
}
3939

40-
override fun toString(): String {
40+
final override fun toString(): String {
4141
return serialize()
4242
}
4343
}

model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/ContentExplorer.kt

Lines changed: 72 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package org.modelix.model.server.handlers.ui
22

33
import io.ktor.http.HttpStatusCode
44
import io.ktor.server.application.Application
5-
import io.ktor.server.application.call
65
import io.ktor.server.html.respondHtml
76
import io.ktor.server.html.respondHtmlTemplate
87
import io.ktor.server.request.receive
@@ -24,6 +23,7 @@ import kotlinx.html.div
2423
import kotlinx.html.getForm
2524
import kotlinx.html.h1
2625
import kotlinx.html.h3
26+
import kotlinx.html.i
2727
import kotlinx.html.id
2828
import kotlinx.html.label
2929
import kotlinx.html.li
@@ -45,13 +45,16 @@ import kotlinx.html.ul
4545
import kotlinx.html.unsafe
4646
import org.modelix.authorization.checkPermission
4747
import org.modelix.authorization.requiresLogin
48+
import org.modelix.kotlin.utils.urlDecode
49+
import org.modelix.kotlin.utils.urlEncode
4850
import org.modelix.model.api.BuiltinLanguages
49-
import org.modelix.model.api.INodeResolutionScope
50-
import org.modelix.model.api.ITree
51-
import org.modelix.model.api.PNodeAdapter
52-
import org.modelix.model.api.TreePointer
51+
import org.modelix.model.api.INodeReference
52+
import org.modelix.model.api.IReadableNode
53+
import org.modelix.model.api.NodeReference
54+
import org.modelix.model.api.ancestors
5355
import org.modelix.model.lazy.BranchReference
5456
import org.modelix.model.lazy.RepositoryId
57+
import org.modelix.model.mutable.asModelSingleThreaded
5558
import org.modelix.model.server.ModelServerPermissionSchema
5659
import org.modelix.model.server.handlers.IRepositoriesManager
5760
import org.modelix.model.server.handlers.NodeNotFoundException
@@ -110,7 +113,7 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) {
110113
// IMPORTANT Do not let `expandTo` be an arbitrary string to avoid code injection.
111114
// The value of `expandTo` is expanded into JavaScript.
112115
val expandTo = call.request.queryParameters["expandTo"]?.let {
113-
it.toLongOrNull() ?: return@get call.respondText("Invalid expandTo value. Provide a node id.", status = HttpStatusCode.BadRequest)
116+
NodeReference(it)
114117
}
115118

116119
val version = repoManager.getTransactionManager().runReadIO {
@@ -120,10 +123,21 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) {
120123
call.respondText("version $versionHash not found", status = HttpStatusCode.NotFound)
121124
return@get
122125
}
123-
val tree = version.getTree()
124-
val rootNode = PNodeAdapter(ITree.ROOT_ID, TreePointer(tree))
126+
val tree = version.getModelTree()
127+
val model = tree.asModelSingleThreaded()
128+
val rootNode = model.getRootNode()
125129

126-
val expandedNodes = expandTo?.let { nodeId -> getAncestorsAndSelf(nodeId, tree) }.orEmpty()
130+
val expandedNodes = expandTo?.let {
131+
try {
132+
model.tryResolveNode(it)
133+
} catch (ex: IllegalArgumentException) {
134+
return@get call.respondText("Invalid expandTo value. Provide a node id.", status = HttpStatusCode.BadRequest)
135+
} ?: throw NodeNotFoundException("Node not found: $it")
136+
}
137+
?.ancestors(true)
138+
.orEmpty()
139+
.map { it.getNodeReference() }
140+
.toSet()
127141

128142
call.respondHtmlTemplate(PageWithMenuBar("repos/", "../../../../..")) {
129143
headContent {
@@ -168,12 +182,12 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) {
168182
call.respondText("version $versionHash not found", status = HttpStatusCode.NotFound)
169183
return@post
170184
}
171-
val tree = version.getTree()
172-
val rootNode = PNodeAdapter(ITree.ROOT_ID, TreePointer(tree))
185+
val tree = version.getModelTree()
186+
val rootNode = tree.asModelSingleThreaded().getRootNode()
173187

174-
var expandedNodeIds = expandedNodes.expandedNodeIds
188+
var expandedNodeIds: List<INodeReference> = expandedNodes.expandedNodeIds.mapNotNull { it.urlDecode() }.map { NodeReference(it) }
175189
if (expandedNodes.expandAll) {
176-
expandedNodeIds = expandedNodeIds + collectExpandableChildNodes(rootNode, expandedNodes.expandedNodeIds.toSet())
190+
expandedNodeIds = expandedNodeIds + collectExpandableChildNodes(rootNode, expandedNodes.expandedNodeIds.map { NodeReference(it) }.toSet())
177191
}
178192

179193
call.respondText(
@@ -185,7 +199,7 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) {
185199
)
186200
}
187201
get("/content/repositories/{repository}/versions/{versionHash}/{nodeId}") {
188-
val id = call.parameters["nodeId"]?.toLongOrNull()
202+
val id = call.parameters["nodeId"]?.urlDecode()?.let { NodeReference(it) }
189203
?: return@get call.respondText("node id not found", status = HttpStatusCode.NotFound)
190204

191205
val versionHash = call.parameters["versionHash"]
@@ -203,7 +217,7 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) {
203217
call.respondText("version $versionHash not found", status = HttpStatusCode.NotFound)
204218
return@get
205219
}
206-
val node = PNodeAdapter(id, TreePointer(version.getTree())).takeIf { it.isValid }
220+
val node = version.getModelTree().asModelSingleThreaded().tryResolveNode(id)
207221

208222
if (node != null) {
209223
call.respondHtml { body { nodeInspector(node) } }
@@ -215,40 +229,29 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) {
215229
}
216230
}
217231

218-
private fun getAncestorsAndSelf(expandTo: Long, tree: ITree): Set<String> {
219-
val seq = generateSequence(expandTo) { id ->
220-
try {
221-
tree.getParent(id).takeIf { it != 0L } // getParent returns 0L for root node
222-
} catch (e: org.modelix.datastructures.model.NodeNotFoundException) {
223-
throw NodeNotFoundException(id, e)
224-
}
225-
}
226-
return seq.map { it.toString() }.toSet()
227-
}
228-
229232
// The method traverses the expanded tree based on the alreadyExpandedNodeIds and
230233
// collects the expandable (not empty) nodes which are not expanded yet
231-
private fun collectExpandableChildNodes(under: PNodeAdapter, alreadyExpandedNodeIds: Set<String>): Set<String> {
232-
if (alreadyExpandedNodeIds.contains(under.nodeId.toString())) {
233-
val expandableIds = mutableSetOf<String>()
234-
for (child in under.allChildren) {
235-
expandableIds.addAll(collectExpandableChildNodes(child as PNodeAdapter, alreadyExpandedNodeIds))
234+
private fun collectExpandableChildNodes(under: IReadableNode, alreadyExpandedNodeIds: Set<INodeReference>): Set<INodeReference> {
235+
if (alreadyExpandedNodeIds.contains(under.getNodeReference())) {
236+
val expandableIds = mutableSetOf<INodeReference>()
237+
for (child in under.getAllChildren()) {
238+
expandableIds.addAll(collectExpandableChildNodes(child, alreadyExpandedNodeIds))
236239
}
237240
return expandableIds
238241
}
239242

240-
if (under.allChildren.toList().isNotEmpty()) {
243+
if (under.getAllChildren().isNotEmpty()) {
241244
// Node is collected if it is expandable
242-
return setOf(under.nodeId.toString())
245+
return setOf(under.getNodeReference())
243246
}
244247
return emptySet()
245248
}
246249

247250
private fun FlowContent.contentPageBody(
248-
rootNode: PNodeAdapter,
251+
rootNode: IReadableNode,
249252
versionHash: String,
250-
expandedNodeIds: Set<String>,
251-
expandTo: Long?,
253+
expandedNodeIds: Set<INodeReference>,
254+
expandTo: INodeReference?,
252255
) {
253256
h1 { +"Model Server Content" }
254257
small {
@@ -291,62 +294,54 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) {
291294
}
292295
}
293296

294-
private fun UL.nodeItem(node: PNodeAdapter, expandedNodeIds: Set<String>, expandTo: Long? = null) {
297+
private fun UL.nodeItem(node: IReadableNode, expandedNodeIds: Set<INodeReference>, expandTo: INodeReference? = null) {
295298
li("nodeItem") {
296-
id = node.nodeId.toString()
297-
val expanded = expandedNodeIds.contains(node.nodeId.toString())
298-
if (node.allChildren.toList().isNotEmpty()) {
299+
id = node.getNodeReference().serialize()
300+
val expanded = expandedNodeIds.contains(node.getNodeReference())
301+
if (node.getAllChildren().toList().isNotEmpty()) {
299302
div(if (expanded) "expander expander-expanded" else "expander") { unsafe { +"&#x25B6;" } }
300303
}
301304
div("nameField") {
302-
if (expandTo == node.nodeId) {
305+
if (expandTo == node.getNodeReference()) {
303306
classes += "expandedToNameField"
304307
}
305-
attributes["data-nodeid"] = node.nodeId.toString()
306-
b {
307-
val namePropertyUID = BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.getUID()
308-
val namedConceptName = node.getPropertyValue(namePropertyUID)
309-
if (namedConceptName != null) {
310-
+namedConceptName
311-
} else if (node.getPropertyRoles().contains("name")) {
312-
+"${node.getPropertyValue("name")}"
313-
} else {
314-
+"Unnamed Node"
315-
}
308+
attributes["data-nodeid"] = node.getNodeReference().serialize().urlEncode()
309+
val namePropertyUID = BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference()
310+
val nodeName = node.getPropertyValue(namePropertyUID)
311+
if (nodeName != null) {
312+
b { +nodeName }
313+
} else {
314+
i { +"<no name>" }
316315
}
317-
small { +" | ${node.nodeId} | $node" }
316+
small { +" | ${node.getNodeReference().serialize()}" }
318317
br { }
319-
val conceptRef = node.getConceptReference()
320318
small {
321-
if (conceptRef != null) {
322-
+conceptRef.getUID()
323-
} else {
324-
+"No concept reference"
325-
}
319+
+"Concept: "
320+
+node.getConceptReference().getUID()
326321
}
327322
}
328323
div(if (expanded) "nested active" else "nested") {
329324
if (expanded) {
330325
ul("nodeTree") {
331-
for (child in node.allChildren) {
332-
nodeItem(child as PNodeAdapter, expandedNodeIds, expandTo)
326+
for (child in node.getAllChildren()) {
327+
nodeItem(child, expandedNodeIds, expandTo)
333328
}
334329
}
335330
}
336331
}
337332
}
338333
}
339334

340-
private fun BODY.nodeInspector(node: PNodeAdapter) {
335+
private fun BODY.nodeInspector(node: IReadableNode) {
341336
div {
342337
h3 { +"Node Details" }
343338
}
344-
val nodeEmpty = node.getReferenceRoles().isEmpty() && node.getPropertyRoles().isEmpty()
339+
val nodeEmpty = node.getAllReferenceTargetRefs().isEmpty() && node.getAllProperties().isEmpty()
345340
if (nodeEmpty) {
346341
div { +"No roles." }
347342
return
348343
}
349-
if (node.getPropertyRoles().isEmpty()) {
344+
if (node.getAllProperties().isEmpty()) {
350345
div { +"No properties." }
351346
} else {
352347
table {
@@ -357,44 +352,37 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) {
357352
}
358353
}
359354
tbody {
360-
for (propertyRole in node.getPropertyRoles()) {
355+
for (property in node.getAllProperties()) {
361356
tr {
362-
td { +propertyRole }
363-
td { +"${node.getPropertyValue(propertyRole)}" }
357+
td { +property.first.getNameOrId() }
358+
td { +property.second }
364359
}
365360
}
366361
}
367362
}
368363
}
369-
if (node.getReferenceRoles().isEmpty()) {
364+
if (node.getAllReferenceTargetRefs().isEmpty()) {
370365
div { +"No references." }
371366
} else {
372367
table {
373368
thead {
374369
tr {
375370
th { +"ReferenceRole" }
376-
th { +"Target NodeId" }
377371
th { +"Target Reference" }
378372
}
379373
}
380374
tbody {
381-
INodeResolutionScope.runWithAdditionalScope(node.getArea()) {
382-
for (referenceRole in node.getReferenceRoles()) {
383-
tr {
384-
td { +referenceRole }
385-
td {
386-
val nodeId = (node.getReferenceTarget(referenceRole) as? PNodeAdapter)?.nodeId
387-
if (nodeId != null) {
388-
a("?expandTo=$nodeId") {
389-
+"$nodeId"
390-
}
391-
} else {
392-
+"null"
375+
for ((referenceRole, targetId) in node.getAllReferenceTargetRefs()) {
376+
tr {
377+
td { +referenceRole.getNameOrId() }
378+
td {
379+
if (node.getModel().tryResolveNode(targetId) == null) {
380+
+targetId.serialize()
381+
} else {
382+
a("?expandTo=${targetId.serialize().urlEncode()}") {
383+
+targetId.serialize()
393384
}
394385
}
395-
td {
396-
+"${node.getReferenceTargetRef(referenceRole)?.serialize()}"
397-
}
398386
}
399387
}
400388
}

model-server/src/test/kotlin/org/modelix/model/server/handlers/ui/ContentExplorerTest.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import io.ktor.server.testing.testApplication
1717
import org.jsoup.Jsoup
1818
import org.jsoup.nodes.Element
1919
import org.jsoup.select.Evaluator
20+
import org.modelix.kotlin.utils.urlEncode
2021
import org.modelix.model.api.IReferenceLink
2122
import org.modelix.model.api.ITree
2223
import org.modelix.model.api.NodeReference
23-
import org.modelix.model.api.PNodeAdapter
2424
import org.modelix.model.api.addNewChild
2525
import org.modelix.model.client.successful
2626
import org.modelix.model.client2.ModelClientV2
@@ -81,23 +81,22 @@ class ContentExplorerTest {
8181
val refLinkName = "myUnresolvableRef"
8282
val refLinkTargetRef = NodeReference("notAResolvableId")
8383

84-
modelClient.initRepository(repoId)
84+
val initialVersion = modelClient.initRepository(repoId)
85+
val rootNodeId = initialVersion.getModelTree().getRootNodeId()
8586

8687
modelClient.runWrite(branchRef) { root ->
8788
root.setReferenceTarget(IReferenceLink.fromName(refLinkName), refLinkTargetRef)
8889
}
8990

9091
val versionHash = modelClient.pullHash(branchRef)
9192

92-
val response = client.get("/content/repositories/${repoId.id}/versions/$versionHash/${ITree.ROOT_ID}/")
93+
val response = client.get("/content/repositories/${repoId.id}/versions/$versionHash/${rootNodeId.serialize().urlEncode()}/")
9394
val html = Jsoup.parse(response.bodyAsText())
9495
val nameCell = html.selectXpath("""//td[text()="$refLinkName"]""").first() ?: error("table cell not found")
9596
val row = checkNotNull(nameCell.parent()) { "table row not found" }
96-
val targetNodeIdCell = row.allElements[2] // index 0 is the row itself and 1 the nameCell
97-
val targetRefCell = row.allElements[3]
97+
val targetRefCell = row.allElements[2] // index 0 is the row itself and 1 the nameCell
9898

9999
assertTrue(response.successful)
100-
assertEquals("null", targetNodeIdCell.text())
101100
assertEquals(refLinkTargetRef.serialize(), targetRefCell.text())
102101
}
103102

@@ -145,7 +144,7 @@ class ContentExplorerTest {
145144
.addNewChild(null)
146145
}
147146
val versionHash = modelClient.pullHash(branchRef)
148-
val expandToId = (newestChild as PNodeAdapter).nodeId
147+
val expandToId = newestChild.reference.serialize()
149148

150149
val response = client.get("/content/repositories/${branchRef.repositoryId}/versions/$versionHash/") {
151150
parameter("expandTo", expandToId)

0 commit comments

Comments
 (0)