Verified Commit ee230d6f authored by Konstantin Kopper's avatar Konstantin Kopper
Browse files

Replace gson with kotlinx serialization in JANI library

parent ed19120e
Pipeline #27145 passed with stages
in 6 minutes and 16 seconds
package com.pseuco.dataraces
import jani.JaniJSONAdapter
import jani.interaction.ConnectionState
import jani.interaction.JaniWebSocket
import jani.interaction.basic.AnyModel
import jani.interaction.basic.ModellingFormalism
import jani.interaction.basic.messages.Authenticate
import jani.interaction.basic.messages.Close
import jani.interaction.basic.messages.JaniMessage
import jani.interaction.tasks.ProvideTaskMessage
import jani.interaction.tasks.analyse.messages.QueryAnalysisEngines
import jani.interaction.tasks.analyse.messages.StartAnalysisTask
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.http4k.client.WebsocketClient
import org.http4k.core.Uri
import org.http4k.websocket.WsMessage
......@@ -180,7 +180,7 @@ internal object PseuCoDRD {
init {
socket = JaniWebSocket(WebsocketClient.nonBlocking(drdServer, listOf("Origin" to "https://pseuco.com")) {
it.send(WsMessage(JaniJSONAdapter.serialize(Authenticate(1))))
it.send(WsMessage(Json.encodeToString(Authenticate(1))))
state = ConnectionState.AUTHENTICATING
})
......@@ -258,8 +258,6 @@ internal object PseuCoDRD {
receivedTaskEnded.await()
}
// TODO more message types
}
/**
......@@ -314,7 +312,7 @@ internal object PseuCoDRD {
}
/**
* Sends [Close] message with [reason] and throws a [DRDException], which is also stored in [error].
* Sends [jani.interaction.basic.messages.Close] message with [reason] and throws a [DRDException], which is also stored in [error].
*
* @author Konstantin Kopper
* @since 2.0.0
......
package jani
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonParser
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import jani.interaction.basic.Extension
import jani.interaction.basic.ModellingFormalism
import jani.interaction.basic.ParameterDefinition
import jani.interaction.basic.ParameterType
import jani.interaction.basic.ParameterValue
import jani.interaction.basic.messages.Authenticate
import jani.interaction.basic.messages.Capabilities
import jani.interaction.basic.messages.Close
import jani.interaction.basic.messages.JaniMessage
import jani.interaction.basic.messages.ReplyUpdateServerParameters
import jani.interaction.basic.messages.RequestUpdateServerParameters
import jani.interaction.tasks.ProvideTaskMessage
import jani.interaction.tasks.ProvideTaskProgress
import jani.interaction.tasks.ProvideTaskStatus
import jani.interaction.tasks.StopTask
import jani.interaction.tasks.TaskEnded
import jani.interaction.tasks.analyse.messages.ProvideAnalysisResults
import jani.interaction.tasks.analyse.messages.QueryAnalysisEngines
import jani.interaction.tasks.analyse.messages.ReplyAnalysisEngines
import jani.interaction.tasks.analyse.messages.StartAnalysisTask
import jani.model.Identifier
import jani.model.models.ModelFeature
/**
* Provides methods to convert all JANI related objects to JSON and back.
* Uses specific [TypeAdapter]s.
*
* @author Konstantin Kopper
* @since 2.0.0
*/
object JaniJSONAdapter {
/**
* Create a JSON string from any object using [jsonBuilder],
* hence all type adapters are used.
*
* @author Konstantin Kopper
* @since 2.0.0
*/
fun serialize(obj: Any): String = jsonBuilder.toJson(obj)
/**
* Parse a JSON encoded type using [jsonBuilder],
* hence all type adapters are used.
*
* @author Konstantin Kopper
* @since 2.0.0
*/
fun <T> deserialize(text: String, type: java.lang.reflect.Type): T = jsonBuilder.fromJson(text, type)
/**
* Parse a JSON encoded [JaniMessage] to a concrete object.
* The resulting object has the actual type of the message,
* such that distinction by type information is possible.
*
* @author Konstantin Kopper
* @since 2.0.0
* @throws JsonParseException The given JSON does not describe a [JaniMessage].
*/
fun deserializeMessage(text: String): JaniMessage = JsonParser.parseString(text).let {
assert(it.isJsonObject)
jsonBuilder.fromJson(it, when (it.asJsonObject["type"]?.asString) {
// Basic messages
null -> Authenticate::class.java
"capabilities" -> Capabilities::class.java
"close" -> Close::class.java
"server-parameters" -> if ("parameters" in it.asJsonObject)
RequestUpdateServerParameters::class.java
else
ReplyUpdateServerParameters::class.java
// Tasks
"task-end" -> TaskEnded::class.java
"task-message" -> ProvideTaskMessage::class.java
"task-progress" -> ProvideTaskProgress::class.java
"task-status" -> ProvideTaskStatus::class.java
"task-stop" -> StopTask::class.java
// Analyse tasks
"analysis-engines" -> if ("engines" in it.asJsonObject)
ReplyAnalysisEngines::class.java
else
QueryAnalysisEngines::class.java
"analysis-start" -> StartAnalysisTask::class.java
"analysis-result" -> ProvideAnalysisResults::class.java
// Unknown (or unsupported)
else -> throw JsonParseException("Unknown message type.")
})
}
private val jsonBuilder: Gson = GsonBuilder()
.registerTypeHierarchyAdapter(Extension::class.java, ExtensionTypeAdapter)
.registerTypeAdapter(Identifier::class.java, IdentifierTypeAdapter)
.registerTypeHierarchyAdapter(ModelFeature::class.java, ModelFeatureTypeAdapter)
.registerTypeHierarchyAdapter(ModellingFormalism::class.java, ModellingFormalismTypeAdapter)
.registerTypeHierarchyAdapter(ParameterType::class.java, ParameterTypeTypeAdapter)
.registerTypeAdapter(ParameterValue.Values::class.java, ParameterValuesTypeAdapter)
.registerTypeAdapter(ParameterDefinition.DefaultValue::class.java, ParameterDefaultValuesTypeAdapter)
.create()
private object ExtensionTypeAdapter : TypeAdapter<Extension>() {
override fun write(out: JsonWriter, value: Extension?) {
when (value) {
null -> out.nullValue()
is Extension.PersistentState -> out.value("persistent-state")
}
}
override fun read(`in`: JsonReader): Extension? = when (`in`.nextString()) {
"persistent-state" -> Extension.PersistentState
else -> throw JsonSyntaxException("Unexpected extension.")
}
}
private object ModelFeatureTypeAdapter : TypeAdapter<ModelFeature>() {
override fun write(out: JsonWriter, value: ModelFeature?) {
when (value) {
null -> out.nullValue()
else -> out.value(value.asString)
}
}
override fun read(`in`: JsonReader): ModelFeature? =
if (!`in`.hasNext()) null else when (val str = `in`.nextString()) {
ModelFeature.Arrays.asString -> ModelFeature.Arrays
ModelFeature.Datatypes.asString -> ModelFeature.Datatypes
ModelFeature.DerivedOperators.asString -> ModelFeature.DerivedOperators
ModelFeature.EdgePriorities.asString -> ModelFeature.EdgePriorities
ModelFeature.Functions.asString -> ModelFeature.Functions
ModelFeature.HyperbolicFunctions.asString -> ModelFeature.HyperbolicFunctions
ModelFeature.NamedExpressions.asString -> ModelFeature.NamedExpressions
ModelFeature.NonDetSelection.asString -> ModelFeature.NonDetSelection
ModelFeature.TradeoffProperties.asString -> ModelFeature.TradeoffProperties
ModelFeature.TrigonometricFunctions.asString -> ModelFeature.TrigonometricFunctions
else -> try {
ModelFeature.new(str)
} catch (e: IllegalArgumentException) {
throw JsonParseException("Creating a custom model feature failed.", e)
}
}
}
private object ModellingFormalismTypeAdapter : TypeAdapter<ModellingFormalism>() {
override fun write(out: JsonWriter, value: ModellingFormalism?) {
when (value) {
null -> out.nullValue()
else -> out.value(value.asString)
}
}
override fun read(`in`: JsonReader): ModellingFormalism? =
if (!`in`.hasNext()) null else when (val str = `in`.nextString()) {
ModellingFormalism.IOSA.asString -> ModellingFormalism.IOSA
ModellingFormalism.Modest.asString -> ModellingFormalism.Modest
ModellingFormalism.pGCL.asString -> ModellingFormalism.pGCL
ModellingFormalism.PRISM.asString -> ModellingFormalism.PRISM
ModellingFormalism.xSADF.asString -> ModellingFormalism.xSADF
else -> try {
ModellingFormalism.new(str)
} catch (e: IllegalArgumentException) {
throw JsonParseException("Creating a custom modelling formalism failed.", e)
}
}
}
private object IdentifierTypeAdapter : TypeAdapter<Identifier>() {
override fun write(out: JsonWriter, value: Identifier?) {
when (value) {
null -> out.nullValue()
else -> out.value(value.plainString)
}
}
override fun read(`in`: JsonReader): Identifier? =
if (!`in`.hasNext()) null else Identifier(`in`.nextString())
}
private object ParameterTypeTypeAdapter : TypeAdapter<ParameterType>() {
override fun write(out: JsonWriter, value: ParameterType?) {
when (value) {
null -> out.nullValue()
ParameterType.Bool -> out.value("bool")
ParameterType.Int -> out.value("int")
ParameterType.Real -> out.value("real")
ParameterType.String -> out.value("string")
is ParameterType.Enum -> {
out.beginArray()
value.values.forEach { out.value(it) }
out.endArray()
}
is ParameterType.Option -> {
// TODO use default serialization
out.beginObject()
out.name("kind")
out.value("option")
out.name("type")
out.jsonValue(toJson(value.type))
out.endObject()
}
}
}
override fun read(`in`: JsonReader): ParameterType? = if (!`in`.hasNext()) null else when (`in`.peek()) {
JsonToken.BEGIN_ARRAY -> {
`in`.beginArray()
val list = mutableListOf<String>()
while (`in`.peek() != JsonToken.END_ARRAY)
list += `in`.nextString()
`in`.endArray()
ParameterType.Enum(list)
}
JsonToken.BEGIN_OBJECT -> {
`in`.beginObject()
var hasKind = false
var type: ParameterType? = null
while (`in`.peek() != JsonToken.END_OBJECT)
when (`in`.nextName()) {
"kind" -> {
hasKind = true
if (`in`.nextString() != "option")
throw JsonSyntaxException("Expected 'option' for key 'kind'.")
}
"type" -> {
type = read(`in`)
}
else -> throw JsonSyntaxException("Unknown key during object parsing.")
}
`in`.endObject()
if (!hasKind)
throw JsonSyntaxException("Missing 'kind' attribute.")
if (type == null)
throw JsonSyntaxException("Missing 'type' attribute.")
ParameterType.Option(type)
}
JsonToken.STRING -> when (`in`.nextString()) {
"bool" -> ParameterType.Bool
"int" -> ParameterType.Int
"real" -> ParameterType.Real
"string" -> ParameterType.String
else -> throw JsonSyntaxException("Unexpected string representation for parameter types.")
}
else -> null
}
}
private object ParameterValuesTypeAdapter : TypeAdapter<ParameterValue.Values>() {
override fun write(out: JsonWriter, value: ParameterValue.Values?) {
when (value) {
null -> out.nullValue()
is ParameterValue.Values.True -> out.value(true)
is ParameterValue.Values.False -> out.value(false)
is ParameterValue.Values.Number -> out.value(value.number)
is ParameterValue.Values.String -> out.value(value.string)
}
}
override fun read(`in`: JsonReader): ParameterValue.Values? =
if (!`in`.hasNext()) null else when (`in`.peek()) {
JsonToken.BOOLEAN -> if (`in`.nextBoolean()) ParameterValue.Values.True else ParameterValue.Values.False
JsonToken.NUMBER -> ParameterValue.Values.Number(`in`.nextInt())
JsonToken.STRING -> ParameterValue.Values.String(`in`.nextString())
else -> throw JsonSyntaxException("Unexpected token, expected a boolean value, a number or a string.")
}
}
private object ParameterDefaultValuesTypeAdapter : TypeAdapter<ParameterDefinition.DefaultValue>() {
override fun write(out: JsonWriter, value: ParameterDefinition.DefaultValue?) {
when (value) {
null -> out.nullValue()
is ParameterDefinition.DefaultValue.Null -> out.value("null")
is ParameterDefinition.DefaultValue.True -> out.value(true)
is ParameterDefinition.DefaultValue.False -> out.value(false)
is ParameterDefinition.DefaultValue.Number -> when (value) {
is ParameterDefinition.DefaultValue.Number.Integer -> out.value(value.n)
is ParameterDefinition.DefaultValue.Number.Real -> out.value(value.n)
}
}
}
override fun read(`in`: JsonReader): ParameterDefinition.DefaultValue? {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
}
/**
* Utility method. Allows to use the `in` operator of Kotlin on [JsonObject]s.
*
* @author Konstantin Kopper
* @since 2.0.0
*/
private operator fun JsonObject.contains(key: String): Boolean = this.has(key)
}
package jani.interaction
import jani.JaniJSONAdapter
import jani.interaction.basic.messages.Capabilities
import jani.interaction.basic.messages.Close
import jani.interaction.basic.messages.JaniMessage
......@@ -11,6 +10,9 @@ import jani.interaction.tasks.ProvideTaskStatus
import jani.interaction.tasks.TaskEnded
import jani.interaction.tasks.analyse.messages.ProvideAnalysisResults
import jani.interaction.tasks.analyse.messages.ReplyAnalysisEngines
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.http4k.websocket.Websocket
import org.http4k.websocket.WsMessage
......@@ -111,8 +113,7 @@ class JaniWebSocket(socket: Websocket) : Websocket by socket {
onError { it.printStackTrace() }
onMessage {
val msg = JaniJSONAdapter.deserializeMessage(it.bodyString())
when (msg) {
when (val msg = serverJson.decodeFromString<JaniMessage>(it.bodyString())) {
is Capabilities -> onCapabilities(msg)
is Close -> onJaniClose(msg)
is ReplyUpdateServerParameters -> onReplyUpdateServerParameters(msg)
......@@ -132,9 +133,8 @@ class JaniWebSocket(socket: Websocket) : Websocket by socket {
*
* @author Konstantin Kopper
* @since
* @see JaniJSONAdapter.serialize
*/
fun send(msg: JaniMessage) = send(WsMessage(JaniJSONAdapter.serialize(msg)))
fun send(msg: JaniMessage) = send(WsMessage(clientJson.encodeToString(msg)))
/**
* Sends a [Close] message with [msg] as body.
......@@ -243,4 +243,26 @@ class JaniWebSocket(socket: Websocket) : Websocket by socket {
fun onProvideAnalysisResults(fn: (ProvideAnalysisResults) -> Unit) {
onProvideAnalysisResults = fn
}
companion object {
/**
* [Json] instance capable of serializing client-to-server messages according to the jani-interaction protocol.
* Separation required as some query/reply pairs share the same type attribute,
* which does not allow to choose the appropriate serializer automatically.
*
* @author Konstantin Kopper
* @see JaniMessage.clientModule
*/
private val clientJson = Json { serializersModule = JaniMessage.clientModule }
/**
* [Json] instance capable of serializing server-to-client messages according to the jani-interaction protocol.
* Separation required as some query/reply pairs share the same type attribute,
* which does not allow to choose the appropriate serializer automatically.
*
* @author Konstantin Kopper
* @see JaniMessage.serverModule
*/
private val serverJson = Json { serializersModule = JaniMessage.serverModule }
}
}
package jani.interaction.basic
import jani.model.models.Model
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
@Serializable(with = AnyModel.Companion::class)
sealed class AnyModel {
// TODO check if best solution
@Serializable
data class JaniModel(private val m: Model) : AnyModel()
@Serializable
data class Textual(
val name: String,
val parts: List<Part>
) : AnyModel() {
constructor(name: String, vararg parts: Part) : this(name, parts.asList())
@Serializable
data class Part(
val name: String,
val role: String,
val part: String
)
}
companion object : JsonContentPolymorphicSerializer<AnyModel>(AnyModel::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out AnyModel> =
if (element.jsonObject.containsKey("parts")) Textual.serializer() else JaniModel.serializer()
}
}
package jani.interaction.basic
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@Serializable(with = Extension.Companion::class)
sealed class Extension {
object PersistentState : Extension()
companion object : KSerializer<Extension> {
override val descriptor = PrimitiveSerialDescriptor("jani.interaction.basic.Extension", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Extension = when (val s = decoder.decodeString()) {
"persistent-state" -> PersistentState
else -> throw SerializationException("Unexpected extension $s")
}
override fun serialize(encoder: Encoder, value: Extension) = when (value) {
is PersistentState -> encoder.encodeString("persistent-state")
}
}
}
package jani.interaction.basic
import kotlinx.serialization.Serializable
@Serializable
data class Metadata(
val name: String,
val version: Version,
val author: String?,
val description: String?,
val url: String?
val author: String? = null,
val description: String? = null,
val url: String? = null
)
package jani.interaction.basic
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@Suppress("ClassName")
@Serializable(ModellingFormalism.Companion::class)
sealed class ModellingFormalism(val string: String) {
val asString
......@@ -13,7 +23,7 @@ sealed class ModellingFormalism(val string: String) {
private class ModellingFormalismImpl(string: String) : ModellingFormalism(string) {
init {
require(string.startsWith("x-"), { "The name of custom modelling formalisms must start 'x-'." })
require(string.startsWith("x-")) { "The name of custom modelling formalisms must start with 'x-'." }
}
override fun toString(): String = asString
......@@ -23,7 +33,21 @@ sealed class ModellingFormalism(val string: String) {
override fun hashCode() = string.hashCode()
companion object {
companion object : KSerializer<ModellingFormalism> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("jani.interaction.basic.ModellingFormalism", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): ModellingFormalism = when (val s = decoder.decodeString()) {
"modest" -> Modest // Workaround: The value in the map below is somehow 'null'...
in stringMap -> stringMap[s]!!
else -> new(s)
}
override fun serialize(encoder: Encoder, value: ModellingFormalism) = encoder.encodeString(value.asString)
fun new(s: String): ModellingFormalism = ModellingFormalismImpl(s)
private val stringMap =
mapOf("iosa" to IOSA, "modest" to Modest, "pgcl" to pGCL, "prism" to PRISM, "xsadf" to xSADF)
}
}
package jani.interaction.basic
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.intOrNull
@Serializable
data class ParameterDefinition(
val id: Identifier,
val name: String,
val description: String?,
val category: String?,
@SerializedName("is-global") val isGlobal: Boolean,
@SerialName("is-global") val isGlobal: Boolean,
val type: ParameterType,
@SerializedName("default-value") val defaultValue: DefaultValue
@SerialName("default-value") val defaultValue: List<DefaultValue>
) {
init {
// TODO do some checks
}
@Serializable(with = DefaultValue.Companion::class)
sealed class DefaultValue {
object Null : DefaultValue()
object True : DefaultValue()
......@@ -23,6 +38,42 @@ data class ParameterDefinition(
data class Integer(val n: Int) : Number()
data class Real(val n: Double) : Number()
}
// TODO string and enums
data class String(val s: kotlin.String) : DefaultValue()
/**
* Serializer for [DefaultValue].