Skip to content

Commit 4e25624

Browse files
authored
Merge pull request #1109 from ably/feature/liveobject-serialization
[ECO-5386] Liveobjects serialization
2 parents 80a655e + b5c1554 commit 4e25624

File tree

16 files changed

+1423
-52
lines changed

16 files changed

+1423
-52
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.ably.lib.objects;
2+
3+
import com.google.gson.JsonArray;
4+
import org.jetbrains.annotations.NotNull;
5+
import org.msgpack.core.MessagePacker;
6+
import org.msgpack.core.MessageUnpacker;
7+
8+
import java.io.IOException;
9+
10+
/**
11+
* Serializer interface for converting between LiveObject arrays and their
12+
* MessagePack or JSON representations.
13+
*/
14+
public interface LiveObjectSerializer {
15+
/**
16+
* Reads a MessagePack array from the given unpacker and deserializes it into an Object array.
17+
*
18+
* @param unpacker the MessageUnpacker to read from
19+
* @return the deserialized Object array
20+
* @throws IOException if an I/O error occurs during unpacking
21+
*/
22+
@NotNull
23+
Object[] readMsgpackArray(@NotNull MessageUnpacker unpacker) throws IOException;
24+
25+
/**
26+
* Serializes the given Object array as a MessagePack array using the provided packer.
27+
*
28+
* @param objects the Object array to serialize
29+
* @param packer the MessagePacker to write to
30+
* @throws IOException if an I/O error occurs during packing
31+
*/
32+
void writeMsgpackArray(@NotNull Object[] objects, @NotNull MessagePacker packer) throws IOException;
33+
34+
/**
35+
* Reads a JSON array from the given {@link JsonArray} and deserializes it into an Object array.
36+
*
37+
* @param json the {@link JsonArray} representing the array to deserialize
38+
* @return the deserialized Object array
39+
*/
40+
@NotNull
41+
Object[] readFromJsonArray(@NotNull JsonArray json);
42+
43+
/**
44+
* Serializes the given Object array as a JSON array.
45+
*
46+
* @param objects the Object array to serialize
47+
* @return the resulting JsonArray
48+
*/
49+
@NotNull
50+
JsonArray asJsonArray(@NotNull Object[] objects);
51+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.ably.lib.objects;
2+
3+
import io.ably.lib.realtime.AblyRealtime;
4+
import io.ably.lib.util.Log;
5+
6+
import java.lang.reflect.InvocationTargetException;
7+
8+
public class LiveObjectsHelper {
9+
10+
private static final String TAG = LiveObjectsHelper.class.getName();
11+
private static volatile LiveObjectSerializer liveObjectSerializer;
12+
13+
public static LiveObjectsPlugin tryInitializeLiveObjectsPlugin(AblyRealtime ablyRealtime) {
14+
try {
15+
Class<?> liveObjectsImplementation = Class.forName("io.ably.lib.objects.DefaultLiveObjectsPlugin");
16+
LiveObjectsAdapter adapter = new Adapter(ablyRealtime);
17+
return (LiveObjectsPlugin) liveObjectsImplementation
18+
.getDeclaredConstructor(LiveObjectsAdapter.class)
19+
.newInstance(adapter);
20+
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException |
21+
InvocationTargetException e) {
22+
Log.i(TAG, "LiveObjects plugin not found in classpath. LiveObjects functionality will not be available.", e);
23+
return null;
24+
}
25+
}
26+
27+
public static LiveObjectSerializer getLiveObjectSerializer() {
28+
if (liveObjectSerializer == null) {
29+
synchronized (LiveObjectsHelper.class) {
30+
if (liveObjectSerializer == null) { // Double-Checked Locking (DCL)
31+
try {
32+
Class<?> serializerClass = Class.forName("io.ably.lib.objects.serialization.DefaultLiveObjectSerializer");
33+
liveObjectSerializer = (LiveObjectSerializer) serializerClass.getDeclaredConstructor().newInstance();
34+
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException |
35+
NoSuchMethodException |
36+
InvocationTargetException e) {
37+
Log.e(TAG, "Failed to init LiveObjectSerializer, LiveObjects plugin not included in the classpath", e);
38+
return null;
39+
}
40+
}
41+
}
42+
}
43+
return liveObjectSerializer;
44+
}
45+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.ably.lib.objects;
2+
3+
import com.google.gson.JsonDeserializationContext;
4+
import com.google.gson.JsonDeserializer;
5+
import com.google.gson.JsonElement;
6+
import com.google.gson.JsonNull;
7+
import com.google.gson.JsonParseException;
8+
import com.google.gson.JsonSerializationContext;
9+
import com.google.gson.JsonSerializer;
10+
import io.ably.lib.util.Log;
11+
12+
import java.lang.reflect.Type;
13+
14+
public class LiveObjectsJsonSerializer implements JsonSerializer<Object[]>, JsonDeserializer<Object[]> {
15+
private static final String TAG = LiveObjectsJsonSerializer.class.getName();
16+
private final LiveObjectSerializer serializer = LiveObjectsHelper.getLiveObjectSerializer();
17+
18+
@Override
19+
public Object[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
20+
if (serializer == null) {
21+
Log.w(TAG, "Skipping 'state' field json deserialization because LiveObjectsSerializer not found.");
22+
return null;
23+
}
24+
if (!json.isJsonArray()) {
25+
throw new JsonParseException("Expected a JSON array for 'state' field, but got: " + json);
26+
}
27+
return serializer.readFromJsonArray(json.getAsJsonArray());
28+
}
29+
30+
@Override
31+
public JsonElement serialize(Object[] src, Type typeOfSrc, JsonSerializationContext context) {
32+
if (serializer == null) {
33+
Log.w(TAG, "Skipping 'state' field json serialization because LiveObjectsSerializer not found.");
34+
return JsonNull.INSTANCE;
35+
}
36+
return serializer.asJsonArray(src);
37+
}
38+
}

lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package io.ably.lib.realtime;
22

3-
import java.lang.reflect.InvocationTargetException;
43
import java.util.ArrayList;
54
import java.util.HashMap;
65
import java.util.List;
76
import java.util.Map;
87

9-
import io.ably.lib.objects.Adapter;
10-
import io.ably.lib.objects.LiveObjectsAdapter;
8+
import io.ably.lib.objects.LiveObjectsHelper;
119
import io.ably.lib.objects.LiveObjectsPlugin;
1210
import io.ably.lib.rest.AblyRest;
1311
import io.ably.lib.rest.Auth;
@@ -74,7 +72,7 @@ public AblyRealtime(ClientOptions options) throws AblyException {
7472
final InternalChannels channels = new InternalChannels();
7573
this.channels = channels;
7674

77-
liveObjectsPlugin = tryInitializeLiveObjectsPlugin();
75+
liveObjectsPlugin = LiveObjectsHelper.tryInitializeLiveObjectsPlugin(this);
7876

7977
connection = new Connection(this, channels, platformAgentProvider, liveObjectsPlugin);
8078

@@ -185,20 +183,6 @@ public interface Channels extends ReadOnlyMap<String, Channel> {
185183
void release(String channelName);
186184
}
187185

188-
private LiveObjectsPlugin tryInitializeLiveObjectsPlugin() {
189-
try {
190-
Class<?> liveObjectsImplementation = Class.forName("io.ably.lib.objects.DefaultLiveObjectsPlugin");
191-
LiveObjectsAdapter adapter = new Adapter(this);
192-
return (LiveObjectsPlugin) liveObjectsImplementation
193-
.getDeclaredConstructor(LiveObjectsAdapter.class)
194-
.newInstance(adapter);
195-
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException |
196-
InvocationTargetException e) {
197-
Log.i(TAG, "LiveObjects plugin not found in classpath. LiveObjects functionality will not be available.", e);
198-
return null;
199-
}
200-
}
201-
202186
private class InternalChannels extends InternalMap<String, Channel> implements Channels, ConnectionManager.Channels {
203187
/**
204188
* Get the named channel; if it does not already exist,

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

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

7+
import com.google.gson.annotations.JsonAdapter;
8+
import io.ably.lib.objects.LiveObjectSerializer;
9+
import io.ably.lib.objects.LiveObjectsHelper;
10+
import io.ably.lib.objects.LiveObjectsJsonSerializer;
11+
import org.jetbrains.annotations.Nullable;
712
import org.msgpack.core.MessageFormat;
813
import org.msgpack.core.MessagePacker;
914
import org.msgpack.core.MessageUnpacker;
@@ -123,6 +128,14 @@ public ProtocolMessage(Action action, String channel) {
123128
public AuthDetails auth;
124129
public Map<String, String> params;
125130
public Annotation[] annotations;
131+
/**
132+
* This will be null if we skipped decoding this property due to user not requesting Objects functionality
133+
* JsonAdapter annotation supports java version (1.8) mentioned in build.gradle
134+
* This is targeted and specific to the state field, so won't affect other fields
135+
*/
136+
@Nullable
137+
@JsonAdapter(LiveObjectsJsonSerializer.class)
138+
public Object[] state;
126139

127140
public boolean hasFlag(final Flag flag) {
128141
return (flags & flag.getMask()) == flag.getMask();
@@ -147,6 +160,7 @@ void writeMsgpack(MessagePacker packer) throws IOException {
147160
if(params != null) ++fieldCount;
148161
if(channelSerial != null) ++fieldCount;
149162
if(annotations != null) ++fieldCount;
163+
if(state != null && LiveObjectsHelper.getLiveObjectSerializer() != null) ++fieldCount;
150164
packer.packMapHeader(fieldCount);
151165
packer.packString("action");
152166
packer.packInt(action.getValue());
@@ -186,6 +200,15 @@ void writeMsgpack(MessagePacker packer) throws IOException {
186200
packer.packString("annotations");
187201
AnnotationSerializer.writeMsgpackArray(annotations, packer);
188202
}
203+
if(state != null) {
204+
LiveObjectSerializer liveObjectsSerializer = LiveObjectsHelper.getLiveObjectSerializer();
205+
if (liveObjectsSerializer != null) {
206+
packer.packString("state");
207+
liveObjectsSerializer.writeMsgpackArray(state, packer);
208+
} else {
209+
Log.w(TAG, "Skipping 'state' field msgpack serialization because LiveObjectsSerializer not found");
210+
}
211+
}
189212
}
190213

191214
ProtocolMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
@@ -248,6 +271,15 @@ ProtocolMessage readMsgpack(MessageUnpacker unpacker) throws IOException {
248271
case "annotations":
249272
annotations = AnnotationSerializer.readMsgpackArray(unpacker);
250273
break;
274+
case "state":
275+
LiveObjectSerializer liveObjectsSerializer = LiveObjectsHelper.getLiveObjectSerializer();
276+
if (liveObjectsSerializer != null) {
277+
state = liveObjectsSerializer.readMsgpackArray(unpacker);
278+
} else {
279+
Log.w(TAG, "Skipping 'state' field msgpack deserialization because LiveObjectsSerializer not found");
280+
unpacker.skipValue();
281+
}
282+
break;
251283
default:
252284
Log.v(TAG, "Unexpected field: " + fieldName);
253285
unpacker.skipValue();

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class ProtocolSerializer {
1414
/****************************************
1515
* Msgpack decode
1616
****************************************/
17-
17+
1818
public static ProtocolMessage readMsgpack(byte[] packed) throws AblyException {
1919
try {
2020
MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(packed);
@@ -27,30 +27,31 @@ public static ProtocolMessage readMsgpack(byte[] packed) throws AblyException {
2727
/****************************************
2828
* Msgpack encode
2929
****************************************/
30-
31-
public static byte[] writeMsgpack(ProtocolMessage message) {
30+
31+
public static byte[] writeMsgpack(ProtocolMessage message) throws AblyException {
3232
ByteArrayOutputStream out = new ByteArrayOutputStream();
3333
MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out);
3434
try {
3535
message.writeMsgpack(packer);
36-
3736
packer.flush();
3837
return out.toByteArray();
39-
} catch(IOException e) { return null; }
38+
} catch (IOException ioe) {
39+
throw AblyException.fromThrowable(ioe);
40+
}
4041
}
4142

4243
/****************************************
4344
* JSON decode
4445
****************************************/
45-
46+
4647
public static ProtocolMessage fromJSON(String packed) throws AblyException {
4748
return Serialisation.gson.fromJson(packed, ProtocolMessage.class);
4849
}
4950

5051
/****************************************
5152
* JSON encode
5253
****************************************/
53-
54+
5455
public static byte[] writeJSON(ProtocolMessage message) throws AblyException {
5556
return Serialisation.gson.toJson(message).getBytes(Charset.forName("UTF-8"));
5657
}

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
/**

0 commit comments

Comments
 (0)