diff --git a/lib/src/main/java/io/ably/lib/realtime/ChannelBase.java b/lib/src/main/java/io/ably/lib/realtime/ChannelBase.java index 2ef6a3718..b096933e8 100644 --- a/lib/src/main/java/io/ably/lib/realtime/ChannelBase.java +++ b/lib/src/main/java/io/ably/lib/realtime/ChannelBase.java @@ -843,6 +843,10 @@ private void onMessage(final ProtocolMessage protocolMessage) { if(msg.connectionId == null) msg.connectionId = protocolMessage.connectionId; if(msg.timestamp == 0) msg.timestamp = protocolMessage.timestamp; if(msg.id == null) msg.id = protocolMessage.id + ':' + i; + // (TM2p) + if(msg.version == null) msg.version = String.format("%s:%03d", protocolMessage.channelSerial, i); + // (TM2k) + if(msg.serial == null) msg.serial = msg.version; try { msg.decode(options, decodingContext); diff --git a/lib/src/main/java/io/ably/lib/types/BaseMessage.java b/lib/src/main/java/io/ably/lib/types/BaseMessage.java index a46b73b20..44b91d7e2 100644 --- a/lib/src/main/java/io/ably/lib/types/BaseMessage.java +++ b/lib/src/main/java/io/ably/lib/types/BaseMessage.java @@ -278,6 +278,20 @@ protected Long readLong(final JsonObject map, final String key) { return element.getAsLong(); } + /** + * Read an optional numerical value. + * @return The value, or null if the key was not present in the map. + * @throws ClassCastException if an element exists for that key and that element is not a {@link JsonPrimitive} + * or is not a valid int value. + */ + protected Integer readInt(final JsonObject map, final String key) { + final JsonElement element = map.get(key); + if (null == element || element instanceof JsonNull) { + return null; + } + return element.getAsInt(); + } + /* Msgpack processing */ boolean readField(MessageUnpacker unpacker, String fieldName, MessageFormat fieldType) throws IOException { boolean result = true; diff --git a/lib/src/main/java/io/ably/lib/types/Message.java b/lib/src/main/java/io/ably/lib/types/Message.java index 9551c0c26..53eb14a0c 100644 --- a/lib/src/main/java/io/ably/lib/types/Message.java +++ b/lib/src/main/java/io/ably/lib/types/Message.java @@ -46,9 +46,33 @@ public class Message extends BaseMessage { */ public String connectionKey; + /** + * (TM2k) serial string – an opaque string that uniquely identifies the message. If a message received from Ably + * (whether over realtime or REST, eg history) with an action of MESSAGE_CREATE does not contain a serial, + * the SDK must set it equal to its version. + */ + public String serial; + + /** + * (TM2p) version string – an opaque string that uniquely identifies the message, and is different for different versions. + * If a message received from Ably over a realtime transport does not contain a version, + * the SDK must set it to : from the channelSerial field of the enclosing ProtocolMessage, + * and padded_index is the index of the message inside the messages array of the ProtocolMessage, + * left-padded with 0s to three digits (for example, the second entry might be foo:001) + */ + public String version; + + /** + * (TM2j) action enum + */ + public MessageAction action; + private static final String NAME = "name"; private static final String EXTRAS = "extras"; private static final String CONNECTION_KEY = "connectionKey"; + private static final String SERIAL = "serial"; + private static final String VERSION = "version"; + private static final String ACTION = "action"; /** * Default constructor @@ -128,6 +152,9 @@ void writeMsgpack(MessagePacker packer) throws IOException { int fieldCount = super.countFields(); if(name != null) ++fieldCount; if(extras != null) ++fieldCount; + if(serial != null) ++fieldCount; + if(version != null) ++fieldCount; + if(action != null) ++fieldCount; packer.packMapHeader(fieldCount); super.writeFields(packer); if(name != null) { @@ -138,6 +165,18 @@ void writeMsgpack(MessagePacker packer) throws IOException { packer.packString(EXTRAS); extras.write(packer); } + if(serial != null) { + packer.packString(SERIAL); + packer.packString(serial); + } + if(version != null) { + packer.packString(VERSION); + packer.packString(version); + } + if(action != null) { + packer.packString(ACTION); + packer.packInt(action.ordinal()); + } } Message readMsgpack(MessageUnpacker unpacker) throws IOException { @@ -157,6 +196,12 @@ Message readMsgpack(MessageUnpacker unpacker) throws IOException { name = unpacker.unpackString(); } else if (fieldName.equals(EXTRAS)) { extras = MessageExtras.read(unpacker); + } else if (fieldName.equals(SERIAL)) { + serial = unpacker.unpackString(); + } else if (fieldName.equals(VERSION)) { + version = unpacker.unpackString(); + } else if (fieldName.equals(ACTION)) { + action = MessageAction.tryFindByOrdinal(unpacker.unpackInt()); } else { Log.v(TAG, "Unexpected field: " + fieldName); unpacker.skipValue(); @@ -313,6 +358,11 @@ protected void read(final JsonObject map) throws MessageDecodeException { } extras = MessageExtras.read((JsonObject) extrasElement); } + + serial = readString(map, SERIAL); + version = readString(map, VERSION); + Integer actionOrdinal = readInt(map, ACTION); + action = actionOrdinal == null ? null : MessageAction.tryFindByOrdinal(actionOrdinal); } public static class Serializer implements JsonSerializer, JsonDeserializer { @@ -328,6 +378,15 @@ public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializat if (message.connectionKey != null) { json.addProperty(CONNECTION_KEY, message.connectionKey); } + if (message.serial != null) { + json.addProperty(SERIAL, message.serial); + } + if (message.version != null) { + json.addProperty(VERSION, message.version); + } + if (message.action != null) { + json.addProperty(ACTION, message.action.ordinal()); + } return json; } diff --git a/lib/src/main/java/io/ably/lib/types/MessageAction.java b/lib/src/main/java/io/ably/lib/types/MessageAction.java new file mode 100644 index 000000000..8c80e914c --- /dev/null +++ b/lib/src/main/java/io/ably/lib/types/MessageAction.java @@ -0,0 +1,15 @@ +package io.ably.lib.types; + +public enum MessageAction { + MESSAGE_UNSET, // 0 + MESSAGE_CREATE, // 1 + MESSAGE_UPDATE, // 2 + MESSAGE_DELETE, // 3 + ANNOTATION_CREATE, // 4 + ANNOTATION_DELETE, // 5 + META_OCCUPANCY; // 6 + + static MessageAction tryFindByOrdinal(int ordinal) { + return values().length <= ordinal ? null: values()[ordinal]; + } +} diff --git a/lib/src/test/java/io/ably/lib/types/MessageTest.java b/lib/src/test/java/io/ably/lib/types/MessageTest.java index 3abeb9fe6..9e58d9c3b 100644 --- a/lib/src/test/java/io/ably/lib/types/MessageTest.java +++ b/lib/src/test/java/io/ably/lib/types/MessageTest.java @@ -46,4 +46,69 @@ public void serialize_message_with_name_and_data() { assertEquals("test-data", serializedObject.get("data").getAsString()); assertEquals("test-name", serializedObject.get("name").getAsString()); } + + @Test + public void serialize_message_with_serial() { + // Given + Message message = new Message("test-name", "test-data"); + message.clientId = "test-client-id"; + message.connectionKey = "test-key"; + message.action = MessageAction.MESSAGE_CREATE; + message.serial = "01826232498871-001@abcdefghij:001"; + + // When + JsonElement serializedElement = serializer.serialize(message, null, null); + + // Then + JsonObject serializedObject = serializedElement.getAsJsonObject(); + assertEquals("test-client-id", serializedObject.get("clientId").getAsString()); + assertEquals("test-key", serializedObject.get("connectionKey").getAsString()); + assertEquals("test-data", serializedObject.get("data").getAsString()); + assertEquals("test-name", serializedObject.get("name").getAsString()); + assertEquals(1, serializedObject.get("action").getAsInt()); + assertEquals("01826232498871-001@abcdefghij:001", serializedObject.get("serial").getAsString()); + } + + @Test + public void deserialize_message_with_serial() throws Exception { + // Given + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("clientId", "test-client-id"); + jsonObject.addProperty("data", "test-data"); + jsonObject.addProperty("name", "test-name"); + jsonObject.addProperty("action", 1); + jsonObject.addProperty("serial", "01826232498871-001@abcdefghij:001"); + + // When + Message message = Message.fromEncoded(jsonObject, new ChannelOptions()); + + // Then + assertEquals("test-client-id", message.clientId); + assertEquals("test-data", message.data); + assertEquals("test-name", message.name); + assertEquals(MessageAction.MESSAGE_CREATE, message.action); + assertEquals("01826232498871-001@abcdefghij:001", message.serial); + } + + + @Test + public void deserialize_message_with_unknown_action() throws Exception { + // Given + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("clientId", "test-client-id"); + jsonObject.addProperty("data", "test-data"); + jsonObject.addProperty("name", "test-name"); + jsonObject.addProperty("action", 10); + jsonObject.addProperty("serial", "01826232498871-001@abcdefghij:001"); + + // When + Message message = Message.fromEncoded(jsonObject, new ChannelOptions()); + + // Then + assertEquals("test-client-id", message.clientId); + assertEquals("test-data", message.data); + assertEquals("test-name", message.name); + assertNull(message.action); + assertEquals("01826232498871-001@abcdefghij:001", message.serial); + } }