Skip to content

Added gson serializers to liveobjects module ( Option 2 ) #1091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@
import java.lang.reflect.Type;
import java.util.Map;

import org.msgpack.core.MessageFormat;
import org.msgpack.core.MessagePacker;
import org.msgpack.core.MessageUnpacker;

import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import org.jetbrains.annotations.Nullable;
import org.msgpack.core.MessageFormat;
import org.msgpack.core.MessagePacker;
import org.msgpack.core.MessageUnpacker;

import io.ably.lib.util.Log;

import static io.ably.lib.util.Serialisation.gsonToMsgpack;
import static io.ably.lib.util.Serialisation.msgpackToGson;

/**
* A message sent and received over the Realtime protocol.
* A ProtocolMessage always relates to a single channel only, but
Expand Down Expand Up @@ -116,6 +120,11 @@ public ProtocolMessage(Action action, String channel) {
public ConnectionDetails connectionDetails;
public AuthDetails auth;
public Map<String, String> params;
/**
* This will be null if we skipped decoding this property due to user not requesting Objects functionality
*/
public @Nullable JsonArray state;


public boolean hasFlag(final Flag flag) {
return (flags & flag.getMask()) == flag.getMask();
Expand All @@ -139,6 +148,7 @@ void writeMsgpack(MessagePacker packer) throws IOException {
if(flags != 0) ++fieldCount;
if(params != null) ++fieldCount;
if(channelSerial != null) ++fieldCount;
if(state != null) ++fieldCount;
packer.packMapHeader(fieldCount);
packer.packString("action");
packer.packInt(action.getValue());
Expand Down Expand Up @@ -174,6 +184,10 @@ void writeMsgpack(MessagePacker packer) throws IOException {
packer.packString("channelSerial");
packer.packString(channelSerial);
}
if(state != null) {
packer.packString("state");
gsonToMsgpack(state, packer);
}
}

ProtocolMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
Expand Down Expand Up @@ -233,6 +247,9 @@ ProtocolMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
case "params":
params = MessageSerializer.readStringMap(unpacker);
break;
case "state":
state = (JsonArray) msgpackToGson(unpacker.unpackValue());
break;
default:
Log.v(TAG, "Unexpected field: " + fieldName);
unpacker.skipValue();
Expand Down
2 changes: 1 addition & 1 deletion lib/src/main/java/io/ably/lib/util/Base64Coder.java
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,4 @@ public static byte[] decode (char[] in, int iOff, int iLen) {
//Dummy constructor.
private Base64Coder() {}

} // end class Base64Coder
} // end class Base64Coder
58 changes: 53 additions & 5 deletions lib/src/main/java/io/ably/lib/util/Serialisation.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import io.ably.lib.http.HttpCore;
import io.ably.lib.platform.Platform;
import io.ably.lib.types.AblyException;
Expand All @@ -27,12 +29,14 @@
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Map;
import java.util.Set;

public class Serialisation {
public static final String TAG = Serialisation.class.getName();
public static final JsonParser gsonParser;
public static final GsonBuilder gsonBuilder;
public static final Gson gson;
Expand All @@ -48,6 +52,7 @@ public class Serialisation {
gsonBuilder.registerTypeAdapter(PresenceMessage.class, new PresenceMessage.Serializer());
gsonBuilder.registerTypeAdapter(PresenceMessage.Action.class, new PresenceMessage.ActionSerializer());
gsonBuilder.registerTypeAdapter(ProtocolMessage.Action.class, new ProtocolMessage.ActionSerializer());
gsonBuilder.registerTypeAdapter(BinaryJsonPrimitive.class, new BinaryJsonPrimitive.Serializer());
gson = gsonBuilder.create();

msgpackPackerConfig = Platform.name.equals("android") ?
Expand Down Expand Up @@ -193,18 +198,35 @@ public static void gsonToMsgpack(JsonElement json, MessagePacker packer) {
gsonToMsgpack((JsonNull)json, packer);
} else if (json.isJsonPrimitive()) {
gsonToMsgpack((JsonPrimitive)json, packer);
} else {
} else if (json instanceof BinaryJsonPrimitive) {
gsonToMsgpack((BinaryJsonPrimitive)json, packer);
}
else {
Log.e(TAG, "Unsupported JsonElement type: " + json.getClass().getName());
throw new RuntimeException("unreachable");
}
}

public static void gsonToMsgpack(BinaryJsonPrimitive json, MessagePacker packer) {
try {
byte[] binaryData = json.value;
packer.packBinaryHeader(binaryData.length);
packer.writePayload(binaryData);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private static void gsonToMsgpack(JsonArray array, MessagePacker packer) {
try {
packer.packArrayHeader(array.size());
for (JsonElement elem : array) {
gsonToMsgpack(elem, packer);
}
} catch(IOException e) {}
} catch(IOException e) {
// Handle IOException, possibly log it or rethrow as a runtime exception
Log.e(TAG, "Error packing JsonArray to MsgPack", e);
}
}

private static void gsonToMsgpack(JsonObject object, MessagePacker packer) {
Expand All @@ -215,13 +237,17 @@ private static void gsonToMsgpack(JsonObject object, MessagePacker packer) {
packer.packString(entry.getKey());
gsonToMsgpack(entry.getValue(), packer);
}
} catch(IOException e) {}
} catch(IOException e) {
Log.e(TAG, "Error packing JsonObject to MsgPack", e);
}
}

private static void gsonToMsgpack(JsonNull n, MessagePacker packer) {
try {
packer.packNil();
} catch(IOException e) {}
} catch(IOException e) {
Log.e(TAG, "Error packing JsonNull to MsgPack", e);
}
}

private static void gsonToMsgpack(JsonPrimitive primitive, MessagePacker packer) {
Expand All @@ -248,7 +274,9 @@ private static void gsonToMsgpack(JsonPrimitive primitive, MessagePacker packer)
} else {
packer.packString(primitive.getAsString());
}
} catch(IOException e) {}
} catch(Exception e) {
Log.e(TAG, "Error packing JsonPrimitive to MsgPack", e);
}
}

public static JsonElement msgpackToGson(Value value) {
Expand Down Expand Up @@ -286,4 +314,24 @@ public static JsonElement msgpackToGson(Value value) {
return null;
}
}

public static class BinaryJsonPrimitive extends JsonElement {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand why we need this BinaryJsonPrimitive class

private final byte[] value;

public BinaryJsonPrimitive(byte[] value) {
this.value = value;
}

@Override
public JsonElement deepCopy() {
return null;
}

public static class Serializer implements JsonSerializer<BinaryJsonPrimitive> {
@Override
public JsonElement serialize(BinaryJsonPrimitive src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(Base64Coder.encodeToString(src.value));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.ably.lib.objects

import com.google.gson.JsonArray
import io.ably.lib.types.Callback
import io.ably.lib.types.ProtocolMessage
import io.ably.lib.util.Log
Expand Down Expand Up @@ -55,6 +56,19 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
adapter.setChannelSerial(channelName, msg.channelSerial)
}
}
val objectMessages = msg.state?.map { it.toObjectMessage() } ?: emptyList()
Log.v(tag, "Received ${objectMessages.size} object messages for channelName: $channelName")
objectMessages.forEach { Log.v(tag, "Object message: $it") }
}

suspend fun send(message: ObjectMessage) {
Log.v(tag, "Sending message for channelName: $channelName, message: $message")
val protocolMsg = ProtocolMessage().apply {
state = JsonArray().apply {
add(message.toJsonObject())
}
}
adapter.sendAsync(protocolMsg)
}

fun dispose() {
Expand Down
14 changes: 13 additions & 1 deletion live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,21 @@ internal suspend fun LiveObjectsAdapter.sendAsync(message: ProtocolMessage) {
deferred.await()
}

internal enum class MessageFormat(private val value: String) {
internal enum class ProtocolMessageFormat(private val value: String) {
MSGPACK("msgpack"),
JSON("json");

override fun toString(): String = value
}

internal class Binary(val data: ByteArray?) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Binary) return false
return data?.contentEquals(other.data) == true
}

override fun hashCode(): Int {
return data?.contentHashCode() ?: 0
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.ably.lib.objects

import java.nio.ByteBuffer

/**
* An enum class representing the different actions that can be performed on an object.
* Spec: OOP2
Expand Down Expand Up @@ -190,12 +188,12 @@ internal data class ObjectOperation(
* the initialValue, nonce, and initialValueEncoding will be removed.
* Spec: OOP3h
*/
val initialValue: ByteBuffer? = null,
val initialValue: Binary? = null,

/** The initial value encoding defines how the initialValue should be interpreted.
* Spec: OOP3i
*/
val initialValueEncoding: MessageFormat? = null
val initialValueEncoding: ProtocolMessageFormat? = null
)

/**
Expand Down
45 changes: 45 additions & 0 deletions live-objects/src/main/kotlin/io/ably/lib/objects/Serializers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.ably.lib.objects

import com.google.gson.*
import io.ably.lib.util.Base64Coder
import io.ably.lib.util.Serialisation.BinaryJsonPrimitive
import java.lang.reflect.Type

/**
* Creates a Gson instance with a custom serializer for live objects.
* Omits null values during serialization.
*/

internal fun ObjectMessage.toJsonObject(): JsonObject {
return gson.toJsonTree(this).asJsonObject
}

internal fun JsonElement.toObjectMessage(): ObjectMessage {
return gson.fromJson(this, ObjectMessage::class.java)
}

private val gson: Gson = createGsonSerializer()

private fun createGsonSerializer(): Gson {
return GsonBuilder()
.registerTypeAdapter(Binary::class.java, BinarySerializer())
.create() // Do not call serializeNulls() to omit null values
}

// Custom serializer for Binary type
internal class BinarySerializer : JsonSerializer<Binary>, JsonDeserializer<Binary> {
override fun serialize(src: Binary?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement? {
src?.data?.let {
return BinaryJsonPrimitive(it)
}
return null // Omit null values
}

override fun deserialize(json: JsonElement?, typeOfT: Type, context: JsonDeserializationContext): Binary? {
if (json != null && json.isJsonPrimitive) {
val decodedData = Base64Coder.decode(json.asString)
return Binary(decodedData)
}
return null // Return null if the JSON element is not valid
}
}
Loading