Skip to content

Commit 353ba9b

Browse files
committed
Added gson serializers to liveobjects module, declared common protomsg state
field between common to liveobjects and core java sdk
1 parent 375328e commit 353ba9b

7 files changed

Lines changed: 130 additions & 16 deletions

File tree

lib/src/main/java/io/ably/lib/types/ProtocolMessage.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@
44
import java.lang.reflect.Type;
55
import java.util.Map;
66

7-
import org.msgpack.core.MessageFormat;
8-
import org.msgpack.core.MessagePacker;
9-
import org.msgpack.core.MessageUnpacker;
10-
7+
import com.google.gson.JsonArray;
118
import com.google.gson.JsonDeserializationContext;
129
import com.google.gson.JsonDeserializer;
1310
import com.google.gson.JsonElement;
1411
import com.google.gson.JsonParseException;
1512
import com.google.gson.JsonPrimitive;
1613
import com.google.gson.JsonSerializationContext;
1714
import com.google.gson.JsonSerializer;
15+
import org.jetbrains.annotations.Nullable;
16+
import org.msgpack.core.MessageFormat;
17+
import org.msgpack.core.MessagePacker;
18+
import org.msgpack.core.MessageUnpacker;
1819

1920
import io.ably.lib.util.Log;
2021

22+
import static io.ably.lib.util.Serialisation.gsonToMsgpack;
23+
import static io.ably.lib.util.Serialisation.msgpackToGson;
24+
2125
/**
2226
* A message sent and received over the Realtime protocol.
2327
* A ProtocolMessage always relates to a single channel only, but
@@ -116,6 +120,11 @@ public ProtocolMessage(Action action, String channel) {
116120
public ConnectionDetails connectionDetails;
117121
public AuthDetails auth;
118122
public Map<String, String> params;
123+
/**
124+
* This will be null if we skipped decoding this property due to user not requesting Objects functionality
125+
*/
126+
public @Nullable JsonArray state;
127+
119128

120129
public boolean hasFlag(final Flag flag) {
121130
return (flags & flag.getMask()) == flag.getMask();
@@ -139,6 +148,7 @@ void writeMsgpack(MessagePacker packer) throws IOException {
139148
if(flags != 0) ++fieldCount;
140149
if(params != null) ++fieldCount;
141150
if(channelSerial != null) ++fieldCount;
151+
if(state != null) ++fieldCount;
142152
packer.packMapHeader(fieldCount);
143153
packer.packString("action");
144154
packer.packInt(action.getValue());
@@ -174,6 +184,10 @@ void writeMsgpack(MessagePacker packer) throws IOException {
174184
packer.packString("channelSerial");
175185
packer.packString(channelSerial);
176186
}
187+
if(state != null) {
188+
packer.packString("state");
189+
gsonToMsgpack(state, packer);
190+
}
177191
}
178192

179193
ProtocolMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
@@ -233,6 +247,9 @@ ProtocolMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
233247
case "params":
234248
params = MessageSerializer.readStringMap(unpacker);
235249
break;
250+
case "state":
251+
state = (JsonArray) msgpackToGson(unpacker.unpackValue());
252+
break;
236253
default:
237254
Log.v(TAG, "Unexpected field: " + fieldName);
238255
unpacker.skipValue();

lib/src/main/java/io/ably/lib/util/Base64Coder.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,14 @@ public static byte[] decode (char[] in, int iOff, int iLen) {
233233
if (op<oLen) out[op++] = (byte)o2; }
234234
return out; }
235235

236-
//Dummy constructor.
236+
public static boolean isBase64(String s) {
237+
if (s == null || s.isEmpty()) {
238+
return false;
239+
}
240+
// Check if the string matches the Base64 pattern
241+
return s.matches("^[A-Za-z0-9+/]*={0,2}$") && (s.length() % 4 == 0);
242+
}
243+
//Dummy constructor.
237244
private Base64Coder() {}
238245

239-
} // end class Base64Coder
246+
} // end class Base64Coder

lib/src/main/java/io/ably/lib/util/Serialisation.java

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Set;
3434

3535
public class Serialisation {
36+
public static final String TAG = Serialisation.class.getName();
3637
public static final JsonParser gsonParser;
3738
public static final GsonBuilder gsonBuilder;
3839
public static final Gson gson;
@@ -194,6 +195,7 @@ public static void gsonToMsgpack(JsonElement json, MessagePacker packer) {
194195
} else if (json.isJsonPrimitive()) {
195196
gsonToMsgpack((JsonPrimitive)json, packer);
196197
} else {
198+
Log.e(TAG, "Unsupported JsonElement type: " + json.getClass().getName());
197199
throw new RuntimeException("unreachable");
198200
}
199201
}
@@ -204,7 +206,10 @@ private static void gsonToMsgpack(JsonArray array, MessagePacker packer) {
204206
for (JsonElement elem : array) {
205207
gsonToMsgpack(elem, packer);
206208
}
207-
} catch(IOException e) {}
209+
} catch(IOException e) {
210+
// Handle IOException, possibly log it or rethrow as a runtime exception
211+
Log.e(TAG, "Error packing JsonArray to MsgPack", e);
212+
}
208213
}
209214

210215
private static void gsonToMsgpack(JsonObject object, MessagePacker packer) {
@@ -215,13 +220,17 @@ private static void gsonToMsgpack(JsonObject object, MessagePacker packer) {
215220
packer.packString(entry.getKey());
216221
gsonToMsgpack(entry.getValue(), packer);
217222
}
218-
} catch(IOException e) {}
223+
} catch(IOException e) {
224+
Log.e(TAG, "Error packing JsonObject to MsgPack", e);
225+
}
219226
}
220227

221228
private static void gsonToMsgpack(JsonNull n, MessagePacker packer) {
222229
try {
223230
packer.packNil();
224-
} catch(IOException e) {}
231+
} catch(IOException e) {
232+
Log.e(TAG, "Error packing JsonNull to MsgPack", e);
233+
}
225234
}
226235

227236
private static void gsonToMsgpack(JsonPrimitive primitive, MessagePacker packer) {
@@ -244,11 +253,24 @@ private static void gsonToMsgpack(JsonPrimitive primitive, MessagePacker packer)
244253
packer.packByte(number.byteValue());
245254
} else {
246255
packer.packString(primitive.getAsString());
256+
Log.e(TAG, "Unsupported number type: " + number.getClass().getName());
247257
}
248258
} else {
249-
packer.packString(primitive.getAsString());
259+
String value = primitive.getAsString();
260+
if (Base64Coder.isBase64(value)) {
261+
byte[] decodedData = Base64Coder.decode(value);
262+
packer.packBinaryHeader(decodedData.length);
263+
packer.writePayload(decodedData);
264+
} else {
265+
packer.packString(value);
266+
}
267+
if (!primitive.isString()) {
268+
Log.e(TAG, "Unsupported JsonPrimitive type: " + primitive.getClass().getName());
269+
}
250270
}
251-
} catch(IOException e) {}
271+
} catch(Exception e) {
272+
Log.e(TAG, "Error packing JsonPrimitive to MsgPack", e);
273+
}
252274
}
253275

254276
public static JsonElement msgpackToGson(Value value) {

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.ably.lib.objects
22

3+
import com.google.gson.JsonArray
34
import io.ably.lib.types.Callback
45
import io.ably.lib.types.ProtocolMessage
56
import io.ably.lib.util.Log
@@ -55,6 +56,19 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
5556
adapter.setChannelSerial(channelName, msg.channelSerial)
5657
}
5758
}
59+
val objectMessages = msg.state?.map { it.toObjectMessage() } ?: emptyList()
60+
Log.v(tag, "Received ${objectMessages.size} object messages for channelName: $channelName")
61+
objectMessages.forEach { Log.v(tag, "Object message: $it") }
62+
}
63+
64+
suspend fun send(message: ObjectMessage) {
65+
Log.v(tag, "Sending message for channelName: $channelName, message: $message")
66+
val protocolMsg = ProtocolMessage().apply {
67+
state = JsonArray().apply {
68+
add(message.toJsonObject())
69+
}
70+
}
71+
adapter.sendAsync(protocolMsg)
5872
}
5973

6074
fun dispose() {

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,21 @@ internal suspend fun LiveObjectsAdapter.sendAsync(message: ProtocolMessage) {
2323
deferred.await()
2424
}
2525

26-
internal enum class MessageFormat(private val value: String) {
26+
internal enum class ProtocolMessageFormat(private val value: String) {
2727
MSGPACK("msgpack"),
2828
JSON("json");
2929

3030
override fun toString(): String = value
3131
}
32+
33+
internal class Binary(val data: ByteArray?) {
34+
override fun equals(other: Any?): Boolean {
35+
if (this === other) return true
36+
if (other !is Binary) return false
37+
return data?.contentEquals(other.data) == true
38+
}
39+
40+
override fun hashCode(): Int {
41+
return data?.contentHashCode() ?: 0
42+
}
43+
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package io.ably.lib.objects
22

3-
import java.nio.ByteBuffer
4-
53
/**
64
* An enum class representing the different actions that can be performed on an object.
75
* Spec: OOP2
@@ -190,12 +188,12 @@ internal data class ObjectOperation(
190188
* the initialValue, nonce, and initialValueEncoding will be removed.
191189
* Spec: OOP3h
192190
*/
193-
val initialValue: ByteBuffer? = null,
191+
val initialValue: Binary? = null,
194192

195193
/** The initial value encoding defines how the initialValue should be interpreted.
196194
* Spec: OOP3i
197195
*/
198-
val initialValueEncoding: MessageFormat? = null
196+
val initialValueEncoding: ProtocolMessageFormat? = null
199197
)
200198

201199
/**
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.ably.lib.objects
2+
3+
import com.google.gson.*
4+
import io.ably.lib.util.Base64Coder
5+
import java.lang.reflect.Type
6+
7+
/**
8+
* Creates a Gson instance with a custom serializer for live objects.
9+
* Omits null values during serialization.
10+
*/
11+
12+
internal fun ObjectMessage.toJsonObject(): JsonObject {
13+
return gson.toJsonTree(this).asJsonObject
14+
}
15+
16+
internal fun JsonElement.toObjectMessage(): ObjectMessage {
17+
return gson.fromJson(this, ObjectMessage::class.java)
18+
}
19+
20+
private val gson: Gson = createGsonSerializer()
21+
22+
private fun createGsonSerializer(): Gson {
23+
return GsonBuilder()
24+
.registerTypeAdapter(Binary::class.java, BinarySerializer())
25+
.create() // Do not call serializeNulls() to omit null values
26+
}
27+
28+
// Custom serializer for Binary type
29+
internal class BinarySerializer : JsonSerializer<Binary>, JsonDeserializer<Binary> {
30+
override fun serialize(src: Binary?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement? {
31+
src?.data?.let {
32+
return JsonPrimitive(Base64Coder.encodeToString(it))
33+
}
34+
return null // Omit null values
35+
}
36+
37+
override fun deserialize(json: JsonElement?, typeOfT: Type, context: JsonDeserializationContext): Binary? {
38+
if (json != null && json.isJsonPrimitive) {
39+
val decodedData = Base64Coder.decode(json.asString)
40+
return Binary(decodedData)
41+
}
42+
return null // Return null if the JSON element is not valid
43+
}
44+
}

0 commit comments

Comments
 (0)