Skip to content

Commit 53116e8

Browse files
committed
[ECO-5386] Added impl. for LiveObjectSerializer interface
1. Implemented JsonSerializetion using gson library 2. Implemented MsgpackSerialization without external dependency 3. Annotated/updated ObjectMessage fields for gson serialization 4. Added msgpack dependency to liveobjects
1 parent 5c694c9 commit 53116e8

7 files changed

Lines changed: 884 additions & 21 deletions

File tree

live-objects/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ repositories {
1111

1212
dependencies {
1313
implementation(project(":java"))
14+
implementation(libs.bundles.common)
1415
implementation(libs.coroutine.core)
1516

1617
testImplementation(kotlin("test"))

live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,18 @@ internal enum class ProtocolMessageFormat(private val value: String) {
3939
override fun toString(): String = value
4040
}
4141

42-
internal class Binary(val data: ByteArray?) {
42+
internal class Binary(val data: ByteArray) {
4343
override fun equals(other: Any?): Boolean {
4444
if (this === other) return true
4545
if (other !is Binary) return false
46-
return data?.contentEquals(other.data) == true
46+
return data.contentEquals(other.data)
4747
}
4848

4949
override fun hashCode(): Int {
50-
return data?.contentHashCode() ?: 0
50+
return data.contentHashCode()
5151
}
5252
}
5353

5454
internal fun Binary.size(): Int {
55-
return data?.size ?: 0
55+
return data.size
5656
}

live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ package io.ably.lib.objects
33
import com.google.gson.JsonArray
44
import com.google.gson.JsonObject
55

6+
import com.google.gson.annotations.JsonAdapter
7+
import com.google.gson.annotations.SerializedName
8+
import io.ably.lib.objects.serialization.InitialValueJsonSerializer
9+
import io.ably.lib.objects.serialization.ObjectDataJsonSerializer
10+
import io.ably.lib.objects.serialization.gson
11+
612
/**
713
* An enum class representing the different actions that can be performed on an object.
814
* Spec: OOP2
@@ -28,19 +34,14 @@ internal enum class MapSemantics(val code: Int) {
2834
* An ObjectData represents a value in an object on a channel.
2935
* Spec: OD1
3036
*/
37+
@JsonAdapter(ObjectDataJsonSerializer::class)
3138
internal data class ObjectData(
3239
/**
3340
* A reference to another object, used to support composable object structures.
3441
* Spec: OD2a
3542
*/
3643
val objectId: String? = null,
3744

38-
/**
39-
* Can be set by the client to indicate that value in `string` or `bytes` field have an encoding.
40-
* Spec: OD2b
41-
*/
42-
val encoding: String? = null,
43-
4445
/**
4546
* String, number, boolean or binary - a concrete value of the object
4647
* Spec: OD2c
@@ -217,11 +218,13 @@ internal data class ObjectOperation(
217218
* the initialValue, nonce, and initialValueEncoding will be removed.
218219
* Spec: OOP3h
219220
*/
221+
@JsonAdapter(InitialValueJsonSerializer::class)
220222
val initialValue: Binary? = null,
221223

222224
/** The initial value encoding defines how the initialValue should be interpreted.
223225
* Spec: OOP3i
224226
*/
227+
@Deprecated("Will be removed in the future, initialValue will be json string")
225228
val initialValueEncoding: ProtocolMessageFormat? = null
226229
)
227230

@@ -312,7 +315,7 @@ internal data class ObjectMessage(
312315
* or validation of the @extras@ field itself, but should treat it opaquely, encoding it and passing it to realtime unaltered
313316
* Spec: OM2d
314317
*/
315-
val extras: Any? = null,
318+
val extras: JsonObject? = null,
316319

317320
/**
318321
* Describes an operation to be applied to an object.
@@ -328,6 +331,7 @@ internal data class ObjectMessage(
328331
* the `ProtocolMessage` encapsulating it is `OBJECT_SYNC`.
329332
* Spec: OM2g
330333
*/
334+
@SerializedName("object")
331335
val objectState: ObjectState? = null,
332336

333337
/**

live-objects/src/main/kotlin/io/ably/lib/objects/Serialization.kt

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@file:Suppress("UNCHECKED_CAST")
2+
3+
package io.ably.lib.objects.serialization
4+
5+
import com.google.gson.*
6+
import io.ably.lib.objects.*
7+
8+
import io.ably.lib.objects.ObjectMessage
9+
import org.msgpack.core.MessagePacker
10+
import org.msgpack.core.MessageUnpacker
11+
12+
/**
13+
* Default implementation of {@link LiveObjectSerializer} that handles serialization/deserialization
14+
* of ObjectMessage arrays for both JSON and MessagePack formats using Jackson and Gson.
15+
* Dynamically loaded by LiveObjectsHelper#getLiveObjectSerializer() to avoid hard dependencies.
16+
*/
17+
@Suppress("unused") // Used via reflection in LiveObjectsHelper
18+
internal class DefaultLiveObjectSerializer : LiveObjectSerializer {
19+
20+
override fun readMsgpackArray(unpacker: MessageUnpacker): Array<Any> {
21+
val objectMessagesCount = unpacker.unpackArrayHeader()
22+
return Array(objectMessagesCount) { readObjectMessage(unpacker) }
23+
}
24+
25+
override fun writeMsgpackArray(objects: Array<out Any>, packer: MessagePacker) {
26+
val objectMessages: Array<ObjectMessage> = objects as Array<ObjectMessage>
27+
packer.packArrayHeader(objectMessages.size)
28+
objectMessages.forEach { it.writeMsgpack(packer) }
29+
}
30+
31+
override fun readFromJsonArray(json: JsonArray): Array<Any> {
32+
return json.map { element ->
33+
if (element.isJsonObject) element.asJsonObject.toObjectMessage()
34+
else throw JsonParseException("Expected JsonObject, but found: $element")
35+
}.toTypedArray()
36+
}
37+
38+
override fun asJsonArray(objects: Array<out Any>): JsonArray {
39+
val objectMessages: Array<ObjectMessage> = objects as Array<ObjectMessage>
40+
val jsonArray = JsonArray()
41+
for (objectMessage in objectMessages) {
42+
jsonArray.add(objectMessage.toJsonObject())
43+
}
44+
return jsonArray
45+
}
46+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package io.ably.lib.objects.serialization
2+
3+
import com.google.gson.*
4+
import io.ably.lib.objects.Binary
5+
import io.ably.lib.objects.MapSemantics
6+
import io.ably.lib.objects.ObjectData
7+
import io.ably.lib.objects.ObjectMessage
8+
import io.ably.lib.objects.ObjectOperationAction
9+
import io.ably.lib.objects.ObjectValue
10+
import java.lang.reflect.Type
11+
import java.util.*
12+
import kotlin.enums.EnumEntries
13+
14+
// Gson instance for JSON serialization/deserialization
15+
internal val gson = GsonBuilder()
16+
.registerTypeAdapter(ObjectOperationAction::class.java, EnumCodeTypeAdapter({ it.code }, ObjectOperationAction.entries))
17+
.registerTypeAdapter(MapSemantics::class.java, EnumCodeTypeAdapter({ it.code }, MapSemantics.entries))
18+
.create()
19+
20+
internal fun ObjectMessage.toJsonObject(): JsonObject {
21+
return gson.toJsonTree(this).asJsonObject
22+
}
23+
24+
internal fun JsonObject.toObjectMessage(): ObjectMessage {
25+
return gson.fromJson(this, ObjectMessage::class.java)
26+
}
27+
28+
internal class EnumCodeTypeAdapter<T : Enum<T>>(
29+
private val getCode: (T) -> Int,
30+
private val enumValues: EnumEntries<T>
31+
) : JsonSerializer<T>, JsonDeserializer<T> {
32+
33+
override fun serialize(src: T, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
34+
return JsonPrimitive(getCode(src))
35+
}
36+
37+
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T {
38+
val code = json.asInt
39+
return enumValues.first { getCode(it) == code }
40+
}
41+
}
42+
43+
internal class ObjectDataJsonSerializer : JsonSerializer<ObjectData>, JsonDeserializer<ObjectData> {
44+
override fun serialize(src: ObjectData, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
45+
val obj = JsonObject()
46+
src.objectId?.let { obj.addProperty("objectId", it) }
47+
48+
src.value?.let { value ->
49+
when (val v = value.value) {
50+
is Boolean -> obj.addProperty("boolean", v)
51+
is String -> obj.addProperty("string", v)
52+
is Number -> obj.addProperty("number", v.toDouble())
53+
is Binary -> obj.addProperty("bytes", Base64.getEncoder().encodeToString(v.data))
54+
// Spec: OD4c5
55+
is JsonObject, is JsonArray -> {
56+
obj.addProperty("string", v.toString())
57+
obj.addProperty("encoding", "json")
58+
}
59+
}
60+
}
61+
return obj
62+
}
63+
64+
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): ObjectData {
65+
val obj = if (json.isJsonObject) json.asJsonObject else throw JsonParseException("Expected JsonObject")
66+
val objectId = if (obj.has("objectId")) obj.get("objectId").asString else null
67+
val encoding = if (obj.has("encoding")) obj.get("encoding").asString else null
68+
val value = when {
69+
obj.has("boolean") -> ObjectValue(obj.get("boolean").asBoolean)
70+
// Spec: OD5b3
71+
obj.has("string") && encoding == "json" -> {
72+
val jsonStr = obj.get("string").asString
73+
val parsed = JsonParser.parseString(jsonStr)
74+
ObjectValue(
75+
when {
76+
parsed.isJsonObject -> parsed.asJsonObject
77+
parsed.isJsonArray -> parsed.asJsonArray
78+
else -> throw JsonParseException("Invalid JSON string for encoding=json")
79+
}
80+
)
81+
}
82+
obj.has("string") -> ObjectValue(obj.get("string").asString)
83+
obj.has("number") -> ObjectValue(obj.get("number").asDouble)
84+
obj.has("bytes") -> ObjectValue(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))
85+
else -> throw JsonParseException("ObjectData must have one of the fields: boolean, string, number, or bytes")
86+
}
87+
return ObjectData(objectId, value)
88+
}
89+
}
90+
91+
internal class InitialValueJsonSerializer : JsonSerializer<Binary>, JsonDeserializer<Binary> {
92+
override fun serialize(src: Binary, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
93+
return JsonPrimitive(Base64.getEncoder().encodeToString(src.data))
94+
}
95+
96+
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): Binary {
97+
return Binary(Base64.getDecoder().decode(json.asString))
98+
}
99+
}

0 commit comments

Comments
 (0)