diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 858be3a..7afa26e 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -14,7 +14,6 @@
-
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 6d0ee1c..c224ad5 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 1ecc78d..c87f9bb 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -60,16 +60,6 @@ services:
environment:
- MODELIX_CLIENT_SERVER_URI=http://model-server:28101
- # This use case is currently being reworked
- # rest-api-model-ql:
- # image: modelix/rest-api-model-ql:latest
- # container_name: rest-api-model-ql
- # profiles:
- # - useCase3b
- # ports:
- # # The dashboard expects our service to be running under 8090.
- # - 8090:8080
-
spa-dashboard-angular:
container_name: ${COMPOSE_PROJECT_NAME}-spa-dashboard-angular
image: nginx
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a780379..151ce6e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -31,8 +31,6 @@ quarkusPlugin = "2.16.12.Final"
openapi = "7.5.0"
kotlin = "2.0.21"
antJunit = "1.10.15"
-junit = "4.13.2"
-logback = "1.5.11"
nodeGradle = "7.1.0"
node = "16.18.0"
@@ -46,24 +44,13 @@ itemis-mps-gradle-plugin = { group = "de.itemis.mps", name="mps-gradle-plugin",
# modelix
modelix-syncPlugin = { group = "org.modelix.mps", name = "model-server-sync-plugin", version.ref = "mpsModelServerSyncPlugin" }
modelix-model-server = { group = "org.modelix", name = "model-server", version.ref = "modelixCore" }
-modelix-model-server-api = { group = "org.modelix", name = "model-server-api", version.ref = "modelixCore" }
modelix-model-client = { group = "org.modelix", name = "model-client", version.ref = "modelixCore" }
-modelix-model-api = { group = "org.modelix", name = "model-api", version.ref = "modelixCore" }
modelix-model-api-gen-runtime = { group = "org.modelix", name = "model-api-gen-runtime", version.ref = "modelixCore" }
-modelix-light-model-client = { group = "org.modelix", name = "light-model-client", version.ref = "modelixCore" }
# quarkus
quarkus-bom = { group = "io.quarkus.platform", name = "quarkus-bom", version.ref = "quarkusPlatform" }
# kotlin/ktor
-ktor-server-default-headers = { group = "io.ktor", name = "ktor-server-default-headers", version.ref = "ktor" }
-ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" }
-ktor-server-auto-head-response = { group = "io.ktor", name = "ktor-server-auto-head-response", version.ref = "ktor" }
-ktor-serialization-gson = { group = "io.ktor", name = "ktor-serialization-gson", version.ref = "ktor" }
-ktor-server-locations = { group = "io.ktor", name = "ktor-server-locations", version.ref = "ktor" }
-ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
-ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors", version.ref = "ktor" }
-ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" }
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
@@ -72,13 +59,10 @@ ktor-serialization-json = { group = "io.ktor", name = "ktor-serialization-kotlin
# other
ant-junit = { group = "org.apache.ant", name = "ant-junit", version.ref = "antJunit" }
-junit-junit = { group = "junit", name = "junit", version.ref = "junit" }
-logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
[plugins]
# MPS/modelix
-itemis-mps-gradle-common = { id = "de.itemis.mps.gradle.common", version.ref = "mpsBase" }
modelix-model-api-gen = { id = "org.modelix.model-api-gen", version.ref = "modelixCore" }
# kotlin
diff --git a/rest-api-model-ql/build.gradle.kts b/rest-api-model-ql/build.gradle.kts
deleted file mode 100644
index 2800044..0000000
--- a/rest-api-model-ql/build.gradle.kts
+++ /dev/null
@@ -1,79 +0,0 @@
-plugins {
- application
- alias(libs.plugins.kotlin.jvm)
- alias(libs.plugins.openapi.generator)
- id("io.ktor.plugin") version "2.3.12"
-}
-
-val openApiFile = layout.projectDirectory.file("../openapi/openapi.yaml")
-
-dependencies {
- // api-gen v2
- implementation(project(":mps:metamodel-api-kts"))
-
- // kotlin
- implementation(libs.logback.classic)
- implementation(libs.ktor.server.default.headers)
- implementation(libs.ktor.server.content.negotiation)
- implementation(libs.ktor.server.auto.head.response)
- implementation(libs.ktor.serialization.gson)
- implementation(libs.ktor.server.locations)
- implementation(libs.ktor.server.netty)
- implementation(libs.ktor.server.cors)
- implementation(libs.ktor.server.websockets)
-
- testImplementation(libs.junit.junit)
-
- api(libs.modelix.model.api)
- implementation(libs.modelix.light.model.client)
- implementation(libs.modelix.model.server.api)
- implementation(libs.modelix.model.client)
- implementation(libs.ktor.client.cio)
-}
-
-val basePackage = "org.modelix.sample.restapimodelql"
-
-// Use the OpenAPI generator to generate data classes representing the REST response data types.
-// Unfortunately, other generated artifacts cannot be used, because the generator still assumes
-// ktor 1.x and doesn't have an interfaceOnly mode such as in the Quarkus example.
-openApiGenerate {
- generatorName.set("kotlin-server")
- inputSpec.set(openApiFile.toString())
- outputDir.set("$buildDir/openapi-generator")
- packageName.set(basePackage)
- // The OpenAPI generator gradle plugin is strange. According to the documentation of the generator itself,
- // one has to specify `true` to generate all models. However, the plugin does something such that `true`
- // is assumed to be the name of a specific model to generate only then. Strangely, specifying an empty
- // string configures the plugin in such a way that all available models are generated and nothing else.
- globalProperties.set(
- mapOf(
- "models" to "",
- )
- )
- configOptions.set(
- mapOf(
- "library" to "ktor",
- )
- )
-}
-
-// Ensure that the OpenAPI generator runs before starting to compile
-tasks.named("processResources") {
- dependsOn("openApiGenerate")
-}
-tasks.named("compileKotlin") {
- dependsOn("openApiGenerate")
-}
-
-java.sourceSets.getByName("main").java.srcDir(file("$buildDir/openapi-generator/src/main/kotlin"))
-
-application {
- mainClass.set("org.modelix.sample.restapimodelql.ApplicationKt")
-}
-
-ktor {
- docker {
- localImageName.set("modelix/rest-api-model-ql")
- imageTag.set("latest")
- }
-}
diff --git a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/Application.kt b/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/Application.kt
deleted file mode 100644
index 10b6370..0000000
--- a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/Application.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.modelix.sample.restapimodelql
-
-import com.typesafe.config.ConfigFactory
-import io.ktor.serialization.gson.*
-import io.ktor.server.application.*
-import io.ktor.server.engine.*
-import io.ktor.server.locations.*
-import io.ktor.server.netty.*
-import io.ktor.server.plugins.autohead.*
-import io.ktor.server.plugins.contentnegotiation.*
-import io.ktor.server.plugins.cors.routing.*
-import io.ktor.server.plugins.defaultheaders.*
-import io.ktor.server.routing.*
-import io.ktor.server.websocket.*
-import org.modelix.sample.restapimodelql.models.apis.ModelQLAPI
-
-suspend fun main() {
- val config = ConfigFactory.load()
- val host = config.getString("mps.model-ql.host")
- val port = config.getInt("mps.model-ql.port")
- val models = config.getStringList("mps.model-ql.models")
-
- // initialize the client which handles the connection to the
- // lightModelServer plugin running in our MPS instance
- val lightModelClientWrapper = LightModelClientWrapper(host, port, models.first())
- lightModelClientWrapper.createConnection()
-
-
- // start the embedded server to serve the API
- val deploymentPort = config.getInt("ktor.deployment.port")
- embeddedServer(Netty, port = deploymentPort) {
- install(CORS) {
- anyHost()
- }
- install(DefaultHeaders)
- install(ContentNegotiation) { gson() }
- install(AutoHeadResponse)
- install(Locations)
- install(WebSockets)
-
- install(Routing) {
- // the BulkApi provides the routs defined in our OpenAPI specification
- ModelQLAPI(lightModelClientWrapper)
- UpdateSocketRoute(lightModelClientWrapper)
- }
- }.start(wait = true)
-}
\ No newline at end of file
diff --git a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/LightModelClientWrapper.kt b/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/LightModelClientWrapper.kt
deleted file mode 100644
index d0581cb..0000000
--- a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/LightModelClientWrapper.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-package org.modelix.sample.restapimodelql
-
-import University.Schedule.*
-import io.ktor.utils.io.charsets.*
-import jetbrains.mps.lang.core.N_BaseConcept
-import org.modelix.client.light.LightModelClient
-import org.modelix.metamodel.ITypedNode
-import org.modelix.metamodel.typed
-import org.modelix.model.api.INodeReferenceSerializer
-import org.modelix.model.repositoryconcepts.N_Module
-import org.modelix.model.repositoryconcepts.N_Repository
-import org.modelix.model.repositoryconcepts.models
-import org.modelix.model.repositoryconcepts.rootNodes
-import org.modelix.model.server.api.buildModelQuery
-import org.modelix.sample.restapimodelql.models.Lecture
-import org.modelix.sample.restapimodelql.models.LectureList
-import org.modelix.sample.restapimodelql.models.Room
-import org.modelix.sample.restapimodelql.models.RoomList
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import java.net.URLDecoder
-
-private val logger: Logger = LoggerFactory.getLogger("LightModelClientWrapper")
-
-/**
- * A wrapper for the LightModelClient which holds the model query executed on the model storage.
- */
-class LightModelClientWrapper(
- private val host: String = "localhost",
- private val port: Int = 48302,
- private var modelNameSubstring: String) {
-
- private lateinit var lightModelClient: LightModelClient
-
- init {
- GeneratedLanguages.registerAll()
- }
-
- fun createConnection() {
-
- // we require a http client with WS support for the connection
- logger.info("Connecting to light model-server at ws://$host:$port/ws")
- this.lightModelClient = LightModelClient.builder().
- host(host).port(port).
- autoFilterNonLoadedNodes().build()
-
- // the modelQL query
- this.lightModelClient.changeQuery(buildModelQuery {
- // we traverse from the root (a repository)
- root {
- // to all modules
- children("modules") {
- // selecting the ones that contain our desired model name
- whereProperty("name").contains(modelNameSubstring)
- children("models") {
- // and extract all root nodes, so Rooms and Courses, and their descendants
- children("rootNodes") {
- descendants { }
- }
- }
- }
- }
- })
- logger.info("Connection successful")
- }
-
- suspend fun getAllLectures(): List {
- // TODO: can we avoid this cast by using generics in runAsyncRead?
- logger.info("Loading all lectures")
- return runAsyncRead(loadLectures) as List
- }
-
- suspend fun getAllRooms(): List {
- logger.info("Loading all rooms")
- return runAsyncRead(loadRooms) as List
- }
-
- private suspend fun runAsyncRead(givenFunction: (node: N_Repository) -> List?): List? {
- var result: List? = null
- val root = lightModelClient.waitForRootNode()
- if (root != null) {
- lightModelClient.runRead {
- result = givenFunction(root.typed())
- }
- }
- return result
- }
-
- private var loadRooms: (N_Repository) -> List? = Any@{ node: N_Repository ->
- return@Any getAllRootNodesOfTypeInRepository(node).rooms
- }
-
- private var loadLectures: (N_Repository) -> List? = Any@{ node: N_Repository ->
- return@Any getAllRootNodesOfTypeInRepository(node).lectures
- }
-
- private inline fun getAllRootNodesOfTypeInRepository(node: N_Repository) =
- node.unwrap().allChildren.filter { it.isValid }.map { it.typed() }.models.rootNodes.filterIsInstance()
-
- val resolveNodeIdToConcept: suspend (String) -> ITypedNode? = Any@{ ref: String ->
- logger.info("Resolving node $ref")
- return@Any lightModelClient.runRead {
- INodeReferenceSerializer.deserialize(ref).resolveNode(lightModelClient.getRootNode()?.getArea())!!.typed()
- }
- }
-
- fun runRead(body: () -> T): T {
- return lightModelClient.runRead {
- body()
- }
- }
-
- fun getArea(): LightModelClient.Area {
- return this.lightModelClient.Area()
- }
-
-
- suspend fun updateRooms(newRoomList: RoomList){
- newRoomList.rooms?.forEach { updateRoom(it) }
- }
-
- /**
- * Update a given room by writing to the storage using the LightModelClient.
- */
- suspend fun updateRoom(newRoom: Room){
- val decodedReference = URLDecoder.decode(newRoom.roomRef, Charset.defaultCharset())
- val result = resolveNodeIdToConcept(decodedReference) as N_Room
-
- // TODO: test ways to resolve concept
- // "the right way"
- //INodeReferenceSerializer.deserialize(actualRef).resolveNode(lightModelClient.getRootNode()?.getArea())
-
- // warning this is not performant!
- // val root = lightModelClient.waitForRootNode() ?: return
- // root.typed().descendants(true).ofType()
-
- lightModelClient.runWrite {
- result.name = newRoom.name
-
- result.maximumCapacity = newRoom.maxPlaces
- // todo: handle new equipment
-// result.hasRemoteEquipment = newRoom.hasRemoteEquipment!!
- logger.info("Updated Room '${newRoom.roomRef}'")
- }
- }
-
- suspend fun updateLectures(newLectureList: LectureList){
- newLectureList.lectures?.forEach { updateLecture(it) }
- }
-
- /**
- * Update a given lecture by writing to the storage using the LightModelClient.
- */
- suspend fun updateLecture(newLecture: Lecture){
-
- val decodedReference = URLDecoder.decode(newLecture.lectureRef, Charset.defaultCharset())
- val result = resolveNodeIdToConcept(decodedReference) as N_Lecture
-
- val decodedRoomReference = URLDecoder.decode(newLecture.room, Charset.defaultCharset())
- val roomRefConcept = resolveNodeIdToConcept(decodedRoomReference) as N_Room
-
- lightModelClient.runWrite {
- result.name = newLecture.name
- result.description = newLecture.description
- result.isInRoom = roomRefConcept
- result.maximumCapacity = newLecture.maxParticipants
- logger.info("Updated Lecture '${newLecture.lectureRef}'")
- }
- }
-}
diff --git a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/ModelQLAPI.kt b/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/ModelQLAPI.kt
deleted file mode 100644
index 56f9f97..0000000
--- a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/ModelQLAPI.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package org.modelix.sample.restapimodelql.models.apis
-
-import University.Schedule.N_Lecture
-import University.Schedule.N_Room
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.locations.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import org.modelix.metamodel.untypedReference
-import org.modelix.model.api.serialize
-import org.modelix.sample.restapimodelql.LightModelClientWrapper
-import org.modelix.sample.restapimodelql.Paths
-import org.modelix.sample.restapimodelql.models.*
-import java.net.URLEncoder
-import java.nio.charset.Charset
-
-object RouteHelper {
- @JvmStatic
- public fun urlEncode(input: String): String {
- return URLEncoder.encode(input, Charset.defaultCharset())
- }
-}
-
-fun Route.ModelQLAPI(lightModelClientWrapper: LightModelClientWrapper) {
- get {
- val allLectures: List = lightModelClientWrapper.getAllLectures()
-
- val lectureList: LectureList = lightModelClientWrapper.runRead {
- LectureList(lectures = allLectures.map { lectureInstance ->
- Lecture(name = lectureInstance.name,
- description = lectureInstance.description,
- lectureRef = RouteHelper.urlEncode(lectureInstance.untypedReference().serialize()),
- room = RouteHelper.urlEncode(lectureInstance.isInRoom?.untypedReference()?.serialize() ?: "UNSET"),
- maxParticipants = lectureInstance.maximumCapacity,
- requiredEquipment = lectureInstance.requiredEquipment.map { it.equipment.name })
- })
- }
- call.respond(lectureList)
- }
-
- get {
- try {
- val resolvedLecture: N_Lecture = lightModelClientWrapper.resolveNodeIdToConcept(call.parameters["lectureRef"]!!.decodeURLPart())!! as N_Lecture
- val lecture: Lecture = lightModelClientWrapper.runRead {
- Lecture(name = resolvedLecture.name,
- maxParticipants = resolvedLecture.maximumCapacity,
- lectureRef = RouteHelper.urlEncode(resolvedLecture.untypedReference().serialize()),
- room = RouteHelper.urlEncode(resolvedLecture.isInRoom?.untypedReference()?.serialize() ?: "UNSET"),
- description = resolvedLecture.description,
- requiredEquipment = resolvedLecture.requiredEquipment.map { it.equipment.name }
- )
- }
- call.respond(lecture)
- } catch (e: RuntimeException) {
- call.respond(HttpStatusCode.NotFound, "Cannot load Lecture: " + e.message)
- }
- }
-
- get {
- val allRooms: List = lightModelClientWrapper.getAllRooms()
-
- val roomList: RoomList = lightModelClientWrapper.runRead {
- RoomList(rooms = allRooms.map { roomInstance ->
- Room(name = roomInstance.name,
- maxPlaces = roomInstance.maximumCapacity,
- roomRef = RouteHelper.urlEncode(roomInstance.untypedReference().serialize()),
- equipment = roomInstance.equipment.map { it.equipment.name })
- })
- }
- call.respond(roomList)
- }
-
- get {
- try {
- val resolvedRoom: N_Room = lightModelClientWrapper.resolveNodeIdToConcept(call.parameters["roomRef"]!!.decodeURLPart())!! as N_Room
-
- val room: Room = lightModelClientWrapper.runRead {
- Room(name = resolvedRoom.name,
- roomRef = RouteHelper.urlEncode(resolvedRoom.untypedReference().serialize()),
- maxPlaces = resolvedRoom.maximumCapacity,
- equipment = resolvedRoom.equipment.map { it.equipment.name })
- }
- call.respond(room)
- } catch (e: RuntimeException) {
- call.respond(HttpStatusCode.NotFound, "Can not load Room: " + e.message)
- }
- }
-}
diff --git a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/Paths.kt b/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/Paths.kt
deleted file mode 100644
index 0b14c66..0000000
--- a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/Paths.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * University.Schedule.api
- * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
- *
- * The version of the OpenAPI document: 1.0
- *
- *
- * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
- * https://openapi-generator.tech
- * Do not edit the class manually.
- */
-package org.modelix.sample.restapimodelql
-
-import io.ktor.server.locations.*
-
-object Paths {
- /**
- * Provides access to all known lectures
- *
- */
- @Location("/lectures")
- object getLectures
-
- /**
- * Provides access to a single lecture
- *
- * @param lectureRef
- */
- @Location("/lectures/{lectureRef}")
- class getLecturesLectureRef(val lectureRef: kotlin.String)
-
- /**
- * Provides access to all known rooms
- *
- */
- @Location("/rooms")
- object getRooms
-
- /**
- * Provides access to a single room
- *
- * @param roomRef
- */
- @Location("/rooms/{roomRef}")
- class getRoomsRoomID(val roomRef: kotlin.String)
-
-}
diff --git a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/Serialization.kt b/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/Serialization.kt
deleted file mode 100644
index bd382be..0000000
--- a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/Serialization.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-package org.modelix.sample.restapimodelql
-
-import University.Schedule.N_Lecture
-import University.Schedule.N_Room
-import com.google.gson.*
-import org.modelix.metamodel.untypedReference
-import org.modelix.model.api.serialize
-import org.modelix.sample.restapimodelql.models.*
-import org.slf4j.LoggerFactory
-import java.lang.reflect.Type
-import java.util.EnumMap
-
-private val logger = LoggerFactory.getLogger("Serialization")
-
-/**
- * A Kotlin extension function to convert a model lecture to its JSON representation enforced by the generated
- * data class [Lecture].
- */
-fun N_Lecture.toJson() = Lecture(
- lectureRef = this.untypedReference().serialize(),
- name = this.name,
- description = this.description,
- maxParticipants = this.maximumCapacity,
- room = this.isInRoom?.untypedReference()?.serialize() ?: "",
- requiredEquipment = this.requiredEquipment.map { it.equipment.name }
-)
-
-/**
- * A Kotlin extension function to convert a model room to its JSON representation enforced by the generated
- * data class [Room].
- */
-fun N_Room.toJson() = Room(
- roomRef = this.untypedReference().serialize(),
- name = this.name,
- maxPlaces = this.maximumCapacity,
- equipment = this.equipment.map { it.equipment.name }
-)
-
-fun List.toJson() = RoomList(this.mapNotNull {
- try {
- it.toJson()
- } catch (e: RuntimeException){
- logger.warn("Ignoring Room with invalid content: ${e.message}")
- return@mapNotNull null
- }
-})
-
-fun List.toJson() = LectureList(this.mapNotNull {
- try {
- it.toJson()
- } catch (e: RuntimeException){
- logger.warn("Ignoring Lecture with invalid content: ${e.message}")
- return@mapNotNull null
- }
-})
-
-/**
- * A data class to represent the type of change that was sent/received by the websocket
- */
-enum class WhatChanged {
- ROOM,
- ROOM_LIST,
- LECTURE,
- LECTURE_LIST
-}
-/**
- * A data class to represent the change notification sent/received by the update websocket
- */
-data class ChangeNotification(
- val whatChanged: WhatChanged,
- val change: Any
-)
-/**
- * A Gson deserializer which can distinguish between the different WhatChanged types.
- * Will return the deserialized ChangeNotification
- */
-class ChangeNotificationDeserializer : JsonDeserializer {
-
- private val gson = Gson()
- private val changeTypeRegistry: MutableMap> = EnumMap(WhatChanged::class.java)
- private val changeTypeElementName = "whatChanged"
-
- fun registerChangeType(changeTypeName: WhatChanged, changeType: Class) {
- changeTypeRegistry[changeTypeName] = changeType
- }
-
- override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): ChangeNotification {
- val changeNotificationObject = json!!.asJsonObject
-
- return when (val whatChanged = WhatChanged.valueOf(changeNotificationObject[changeTypeElementName].asString)) {
- WhatChanged.ROOM -> ChangeNotification(whatChanged, gson.fromJson(changeNotificationObject["change"], Room::class.java))
- WhatChanged.ROOM_LIST -> ChangeNotification(whatChanged, gson.fromJson(changeNotificationObject["change"], RoomList::class.java))
- WhatChanged.LECTURE -> ChangeNotification(whatChanged, gson.fromJson(changeNotificationObject["change"], Lecture::class.java))
- WhatChanged.LECTURE_LIST -> ChangeNotification(whatChanged, gson.fromJson(changeNotificationObject["change"], LectureList::class.java))
- }
- }
-}
diff --git a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/UpdateSocket.kt b/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/UpdateSocket.kt
deleted file mode 100644
index 931a36d..0000000
--- a/rest-api-model-ql/src/main/kotlin/org/modelix/sample/restapimodelql/UpdateSocket.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-package org.modelix.sample.restapimodelql
-
-
-import University.Schedule.N_Lecture
-import University.Schedule.N_LectureList
-import University.Schedule.N_Room
-import University.Schedule.N_RoomList
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
-import io.ktor.server.routing.*
-import io.ktor.server.websocket.*
-import io.ktor.websocket.*
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.launch
-import org.modelix.metamodel.typed
-import org.modelix.model.api.INode
-import org.modelix.model.area.IAreaChangeList
-import org.modelix.model.area.IAreaListener
-import org.modelix.sample.restapimodelql.models.Lecture
-import org.modelix.sample.restapimodelql.models.LectureList
-import org.modelix.sample.restapimodelql.models.Room
-import org.modelix.sample.restapimodelql.models.RoomList
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import java.util.*
-import java.util.concurrent.atomic.AtomicInteger
-
-private val logger: Logger = LoggerFactory.getLogger("UpdateSocket")
-
-class Connection(val session: DefaultWebSocketSession) {
- companion object {
- val lastId = AtomicInteger(0)
- }
- val id = "${lastId.getAndIncrement()}"
-}
-
-fun Route.UpdateSocketRoute(lightModelClientWrapper: LightModelClientWrapper) {
-
- // the list of connections to the update socket
- val connections = Collections.synchronizedSet(LinkedHashSet())
-
- // we register our custom deserializers for the various ChangeNotifications
- val deserializer = ChangeNotificationDeserializer()
- deserializer.registerChangeType(WhatChanged.ROOM, Room::class.java)
- deserializer.registerChangeType(WhatChanged.ROOM_LIST, RoomList::class.java)
- deserializer.registerChangeType(WhatChanged.LECTURE, Lecture::class.java)
- deserializer.registerChangeType(WhatChanged.LECTURE_LIST, LectureList::class.java)
-
- val gson: Gson = GsonBuilder().registerTypeAdapter(ChangeNotification::class.java, deserializer).create()
-
- suspend fun broadcast(data: String) {
- connections.forEach {
- try {
- it.session.send(data)
- } catch (e: Throwable) {
- logger.debug("Error broadcasting to connection. [connection={}]", it.id)
- }
- }
- }
-
- suspend fun handleChange(node: INode) {
- when (node.typed()) {
- is N_Room -> broadcast(gson.toJson(ChangeNotification(WhatChanged.ROOM, node.typed().toJson())))
- is N_RoomList -> broadcast(gson.toJson(ChangeNotification(WhatChanged.ROOM_LIST, node.typed().rooms.toList().toJson())))
- is N_Lecture -> broadcast(gson.toJson(ChangeNotification(WhatChanged.LECTURE, node.typed().toJson())))
- is N_LectureList -> broadcast(gson.toJson(ChangeNotification(WhatChanged.LECTURE_LIST, node.typed().lectures.toList().toJson())))
- else -> logger.warn("Could not handle change")
- }
- }
-
- // add the listener to the light model client
- lightModelClientWrapper.getArea().addListener(object : IAreaListener {
- override fun areaChanged(changes: IAreaChangeList) {
- changes.visitChanges {
- application.launch {
- // todo: implement once available in API
- // todo: get all nodes that were changed and call handleChange for each
- // handleChange(...)
- }
- true// ???
- }
-
- }
- })
-
- // the updates websocket is able to send and receive changes to the model
- // whatever is transmitted is expected to be a ChangeNotification
- webSocket("/updates") {
-
- // adding a new connection when a new client shows up
- val thisConnection = Connection(this)
- logger.debug("Opening new update socket connection. [connection={}]", thisConnection.id)
- connections += thisConnection
-
- try {
- while (true) {
- when(val frame = incoming.receive()){
- is Frame.Text -> {
- // we iterate over all incoming messages and handle the supported change notifications
- val changeNotification: ChangeNotification = gson.fromJson(frame.readText(), ChangeNotification::class.java)
- when (changeNotification.whatChanged){
- WhatChanged.ROOM -> lightModelClientWrapper.updateRoom(changeNotification.change as Room)
- WhatChanged.ROOM_LIST -> lightModelClientWrapper.updateRooms(changeNotification.change as RoomList)
- WhatChanged.LECTURE -> lightModelClientWrapper.updateLecture(changeNotification.change as Lecture)
- WhatChanged.LECTURE_LIST -> lightModelClientWrapper.updateLectures(changeNotification.change as LectureList)
- else -> logger.debug("Got unknown change, ignoring. [whatChanged={}, change={}]", changeNotification.whatChanged, changeNotification.change)
- }
-
- // once we processed the changes, we broadcast them to all registered clients to keep them in sync
- broadcast(frame.readText())
- }
- else -> logger.debug("Got unknown frame on WS, ignoring. [frame={}]", frame)
- }
- // TODO: investigate if a delay is required
- // delay(500)
- }
- } catch (e: ClosedReceiveChannelException) {
- // closing a connection regularly
- logger.debug("Closing connection regularly {} [connection={}]", closeReason.await(), thisConnection.id)
- } catch (e: Throwable) {
- // closing a connection on error
- logger.warn("Closing connection after error. [connection={}, throwable={}]", thisConnection.id, e, e)
- e.printStackTrace()
- } finally {
- thisConnection.session.close()
- connections -= thisConnection
- }
- }
-}
diff --git a/rest-api-model-ql/src/main/resources/application.conf b/rest-api-model-ql/src/main/resources/application.conf
deleted file mode 100644
index efb4197..0000000
--- a/rest-api-model-ql/src/main/resources/application.conf
+++ /dev/null
@@ -1,13 +0,0 @@
-ktor {
- deployment {
- environment = development
- port = 8090
- }
-}
-mps{
- model-ql {
- host = "localhost"
- port = 48302
- models = [ "backend.sandbox" ]
- }
-}
diff --git a/rest-api-model-ql/src/main/resources/logback.xml b/rest-api-model-ql/src/main/resources/logback.xml
deleted file mode 100644
index d360053..0000000
--- a/rest-api-model-ql/src/main/resources/logback.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
-
-
-
-
-
-
-
-
-
-
-
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 00bb08a..7554c04 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -31,8 +31,6 @@ include("model-server")
//
// APIs
//
-// API using ktor and a light model-server client (light)
-include("rest-api-model-ql")
// API using quarkus and a direct model-server connection (advanced)
include("rest-api-model-server")
@@ -43,4 +41,4 @@ include("rest-api-model-server")
include("spa-dashboard-angular")
// a single page application which can write on the model through a model client
include("spa-management-vue")
-include("spa-overview-angular")
\ No newline at end of file
+include("spa-overview-angular")