diff --git a/lib/src/main/java/io/ably/lib/types/ProtocolMessage.java b/lib/src/main/java/io/ably/lib/types/ProtocolMessage.java index 986a8628b..03002ba49 100644 --- a/lib/src/main/java/io/ably/lib/types/ProtocolMessage.java +++ b/lib/src/main/java/io/ably/lib/types/ProtocolMessage.java @@ -4,10 +4,7 @@ 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; @@ -15,9 +12,16 @@ 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 @@ -116,6 +120,11 @@ public ProtocolMessage(Action action, String channel) { public ConnectionDetails connectionDetails; public AuthDetails auth; public Map 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(); @@ -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()); @@ -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 { @@ -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(); diff --git a/lib/src/main/java/io/ably/lib/util/Base64Coder.java b/lib/src/main/java/io/ably/lib/util/Base64Coder.java index 9a68562c2..24804d54c 100644 --- a/lib/src/main/java/io/ably/lib/util/Base64Coder.java +++ b/lib/src/main/java/io/ably/lib/util/Base64Coder.java @@ -236,4 +236,4 @@ public static byte[] decode (char[] in, int iOff, int iLen) { //Dummy constructor. private Base64Coder() {} -} // end class Base64Coder \ No newline at end of file +} // end class Base64Coder diff --git a/lib/src/main/java/io/ably/lib/util/Serialisation.java b/lib/src/main/java/io/ably/lib/util/Serialisation.java index d2ae3baad..7957eac73 100644 --- a/lib/src/main/java/io/ably/lib/util/Serialisation.java +++ b/lib/src/main/java/io/ably/lib/util/Serialisation.java @@ -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; @@ -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; @@ -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") ? @@ -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) { @@ -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) { @@ -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) { @@ -286,4 +314,24 @@ public static JsonElement msgpackToGson(Value value) { return null; } } + + public static class BinaryJsonPrimitive extends JsonElement { + private final byte[] value; + + public BinaryJsonPrimitive(byte[] value) { + this.value = value; + } + + @Override + public JsonElement deepCopy() { + return null; + } + + public static class Serializer implements JsonSerializer { + @Override + public JsonElement serialize(BinaryJsonPrimitive src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(Base64Coder.encodeToString(src.value)); + } + } + } } diff --git a/live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt b/live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt index ea88c5e99..0e984af49 100644 --- a/live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt +++ b/live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt @@ -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 @@ -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() { diff --git a/live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt b/live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt index 85a4d25fa..6c63e6e80 100644 --- a/live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt +++ b/live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt @@ -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 + } +} diff --git a/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt b/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt index 2c2d825f6..32b6c48f9 100644 --- a/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt +++ b/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt @@ -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 @@ -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 ) /** diff --git a/live-objects/src/main/kotlin/io/ably/lib/objects/Serializers.kt b/live-objects/src/main/kotlin/io/ably/lib/objects/Serializers.kt new file mode 100644 index 000000000..537ab8d7a --- /dev/null +++ b/live-objects/src/main/kotlin/io/ably/lib/objects/Serializers.kt @@ -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, JsonDeserializer { + 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 + } +}