From 5cec0a7171d136a86b70dcb18ddbfe02b5b1915b Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Mon, 24 Aug 2020 02:22:29 +0530 Subject: [PATCH 1/4] Refactored MessageExtras, Added annotations library to have precompilation null checks on data, updated tests --- dependencies.gradle | 1 + .../java/io/ably/lib/types/BaseMessage.java | 12 +++--- .../java/io/ably/lib/types/DeltaExtras.java | 27 +++++-------- .../main/java/io/ably/lib/types/Message.java | 11 ++++-- .../java/io/ably/lib/types/MessageExtras.java | 39 +++++++------------ .../io/ably/lib/types/MessageExtrasTest.java | 6 --- 6 files changed, 37 insertions(+), 59 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index f413b2081..92bb64855 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -13,4 +13,5 @@ dependencies { testImplementation 'org.mockito:mockito-core:1.10.19' testImplementation 'net.jodah:concurrentunit:0.4.2' testImplementation 'org.slf4j:slf4j-simple:1.7.30' + compileOnly 'org.jetbrains:annotations:20.0.0' } 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 29c0f003f..bcf417c18 100644 --- a/lib/src/main/java/io/ably/lib/types/BaseMessage.java +++ b/lib/src/main/java/io/ably/lib/types/BaseMessage.java @@ -205,16 +205,16 @@ public static JsonObject toJsonObject(final BaseMessage message) { if(data != null) { if(data instanceof byte[]) { byte[] dataBytes = (byte[])data; - json.addProperty("data", new String(Base64Coder.encode(dataBytes))); + json.addProperty(DATA, new String(Base64Coder.encode(dataBytes))); encoding = (encoding == null) ? "base64" : encoding + "/base64"; } else { - json.addProperty("data", data.toString()); + json.addProperty(DATA, data.toString()); } - if(encoding != null) json.addProperty("encoding", encoding); + if(encoding != null) json.addProperty(ENCODING, encoding); } - if(message.id != null) json.addProperty("id", message.id); - if(message.clientId != null) json.addProperty("clientId", message.clientId); - if(message.connectionId != null) json.addProperty("connectionId", message.connectionId); + if(message.id != null) json.addProperty(ID, message.id); + if(message.clientId != null) json.addProperty(CLIENT_ID, message.clientId); + if(message.connectionId != null) json.addProperty(CONNECTION_ID, message.connectionId); return json; } diff --git a/lib/src/main/java/io/ably/lib/types/DeltaExtras.java b/lib/src/main/java/io/ably/lib/types/DeltaExtras.java index f3615d0da..78876399c 100644 --- a/lib/src/main/java/io/ably/lib/types/DeltaExtras.java +++ b/lib/src/main/java/io/ably/lib/types/DeltaExtras.java @@ -1,17 +1,12 @@ package io.ably.lib.types; -import com.google.gson.Gson; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import io.ably.lib.util.Serialisation; -import org.msgpack.core.MessagePacker; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.msgpack.value.Value; import org.msgpack.value.ValueFactory; import java.io.IOException; -import java.lang.reflect.Type; import java.util.Map; import java.util.Objects; @@ -23,10 +18,14 @@ public final class DeltaExtras { private static final String FROM = "from"; private static final String FORMAT = "format"; + @NotNull private final String format; + + @NotNull private final String from; - private DeltaExtras(final String format, final String from) { + private DeltaExtras(@Nullable final String format, @Nullable final String from) { + if (null == format) { throw new IllegalArgumentException("format cannot be null."); } @@ -42,6 +41,7 @@ private DeltaExtras(final String format, final String from) { * The delta format. As at API version 1.2, only {@link DeltaExtras.FORMAT_VCDIFF} is supported. * Will never return null. */ + @NotNull public String getFormat() { return format; } @@ -50,20 +50,11 @@ public String getFormat() { * The id of the message the delta was generated from. * Will never return null. */ + @NotNull public String getFrom() { return from; } - /* package private */ void write(MessagePacker packer) throws IOException { - packer.packMapHeader(2); - - packer.packString(FORMAT); - packer.packString(format); - - packer.packString(FROM); - packer.packString(from); - } - /* package private */ static DeltaExtras read(final Map map) throws IOException { final Value format = map.get(ValueFactory.newString(FORMAT)); final Value from = map.get(ValueFactory.newString(FROM)); 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 f76304459..9270ddc48 100644 --- a/lib/src/main/java/io/ably/lib/types/Message.java +++ b/lib/src/main/java/io/ably/lib/types/Message.java @@ -6,6 +6,7 @@ import com.google.gson.*; import io.ably.lib.util.Serialisation; +import org.jetbrains.annotations.Nullable; import org.msgpack.core.MessageFormat; import org.msgpack.core.MessagePacker; import org.msgpack.core.MessageUnpacker; @@ -21,11 +22,13 @@ public class Message extends BaseMessage { /** * The event name, if available */ + @Nullable public String name; /** * Extras, if available */ + @Nullable public MessageExtras extras; private static final String NAME = "name"; @@ -62,7 +65,7 @@ public Message(String name, Object data, MessageExtras extras) { * @param clientId * @param extras */ - public Message(String name, Object data, String clientId, MessageExtras extras) { + public Message(@Nullable String name, Object data, String clientId, @Nullable MessageExtras extras) { this.name = name; this.clientId = clientId; this.data = data; @@ -89,11 +92,11 @@ void writeMsgpack(MessagePacker packer) throws IOException { packer.packMapHeader(fieldCount); super.writeFields(packer); if(name != null) { - packer.packString("name"); + packer.packString(NAME); packer.packString(name); } if(extras != null) { - packer.packString("extras"); + packer.packString(EXTRAS); extras.write(packer); } } @@ -146,7 +149,7 @@ public Batch(String[] channels, Message[] messages) { } public Batch(Collection channels, Collection messages) { - this(channels.toArray(new String[channels.size()]), messages.toArray(new Message[messages.size()])); + this(channels.toArray(new String[0]), messages.toArray(new Message[0])); } public void writeMsgpack(MessagePacker packer) throws IOException { diff --git a/lib/src/main/java/io/ably/lib/types/MessageExtras.java b/lib/src/main/java/io/ably/lib/types/MessageExtras.java index 8a921a8b6..45f73f716 100644 --- a/lib/src/main/java/io/ably/lib/types/MessageExtras.java +++ b/lib/src/main/java/io/ably/lib/types/MessageExtras.java @@ -5,6 +5,8 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import io.ably.lib.util.Serialisation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.msgpack.core.MessagePacker; import org.msgpack.core.MessageUnpacker; import org.msgpack.value.ImmutableMapValue; @@ -22,7 +24,10 @@ public final class MessageExtras { private static final String DELTA = "delta"; + @Nullable private final DeltaExtras delta; // may be null + + @NotNull private final JsonObject jsonObject; // never null /** @@ -32,11 +37,13 @@ public final class MessageExtras { * * @since 1.2.1 */ - public MessageExtras(final JsonObject jsonObject) { + public MessageExtras(@NotNull final JsonObject jsonObject) { this(jsonObject, null); } - private MessageExtras(final JsonObject jsonObject, final DeltaExtras delta) { + private MessageExtras(@NotNull final JsonObject jsonObject, @Nullable final DeltaExtras delta) { + // Necessary to add null check where error is possible to be introduced either by developer or server data + // noinspection ConstantConditions - Nullable/NoNull inspections doen't enforce error at precompilation level if (null == jsonObject) { throw new NullPointerException("jsonObject cannot be null."); } @@ -45,24 +52,18 @@ private MessageExtras(final JsonObject jsonObject, final DeltaExtras delta) { this.delta = delta; } + @Nullable public DeltaExtras getDelta() { return delta; } + @NotNull public JsonObject asJsonObject() { return jsonObject; } /* package private */ void write(MessagePacker packer) throws IOException { - if (null == jsonObject) { - // raw is null, so delta is not null - packer.packMapHeader(1); - packer.packString(DELTA); - delta.write(packer); - } else { - // raw is not null, so delta can be ignored Serialisation.gsonToMsgpack(jsonObject, packer); - } } /* package private */ static MessageExtras read(MessageUnpacker unpacker) throws IOException { @@ -112,14 +113,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MessageExtras that = (MessageExtras) o; - return (null == jsonObject) ? - Objects.equals(delta, that.delta) : - Objects.equals(jsonObject, that.jsonObject); + return Objects.equals(jsonObject, that.jsonObject); } @Override public int hashCode() { - return (null == jsonObject) ? Objects.hashCode(delta) : Objects.hashCode(jsonObject); + return Objects.hashCode(jsonObject); } @Override @@ -133,17 +132,7 @@ public String toString() { public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final MessageExtras src, final Type typeOfSrc, final JsonSerializationContext context) { - return (null != src.jsonObject) ? src.jsonObject : wrapDelta(src.getDelta()); - } - - public static JsonObject wrapDelta(final DeltaExtras delta) { - if (null == delta) { - throw new NullPointerException("delta cannot be null."); - } - - final JsonObject json = new JsonObject(); - json.add(DELTA, Serialisation.gson.toJsonTree(delta)); - return json; + return src.jsonObject; } } } diff --git a/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java b/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java index df5328e67..163952cc1 100644 --- a/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java +++ b/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java @@ -11,7 +11,6 @@ import java.io.IOException; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; public class MessageExtrasTest { @@ -24,9 +23,6 @@ public void raw() { final JsonObject objectA = new JsonObject(); objectA.addProperty("someKey", "someValue"); - final JsonObject objectB = new JsonObject(); - objectB.addProperty("someOtherKey", "someValue"); - final MessageExtras messageExtras = new MessageExtras(objectA); assertNull(messageExtras.getDelta()); @@ -34,8 +30,6 @@ public void raw() { final JsonElement serialised = serializer.serialize(messageExtras, null, null); assertEquals(objectA, serialised); - assertNotEquals(objectB, serialised); - assertNotEquals(objectB, objectA); } @Test From 1f9da19ddb786858b343334f084f23b6129996f1 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sun, 4 Oct 2020 19:30:10 +0530 Subject: [PATCH 2/4] Removed jetbrains annotations lib, removed null annotations from the code --- dependencies.gradle | 1 - lib/src/main/java/io/ably/lib/types/DeltaExtras.java | 8 +------- lib/src/main/java/io/ably/lib/types/Message.java | 5 +---- lib/src/main/java/io/ably/lib/types/MessageExtras.java | 10 ++-------- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 92bb64855..f413b2081 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -13,5 +13,4 @@ dependencies { testImplementation 'org.mockito:mockito-core:1.10.19' testImplementation 'net.jodah:concurrentunit:0.4.2' testImplementation 'org.slf4j:slf4j-simple:1.7.30' - compileOnly 'org.jetbrains:annotations:20.0.0' } diff --git a/lib/src/main/java/io/ably/lib/types/DeltaExtras.java b/lib/src/main/java/io/ably/lib/types/DeltaExtras.java index 78876399c..3e93eee33 100644 --- a/lib/src/main/java/io/ably/lib/types/DeltaExtras.java +++ b/lib/src/main/java/io/ably/lib/types/DeltaExtras.java @@ -1,8 +1,6 @@ package io.ably.lib.types; import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.msgpack.value.Value; import org.msgpack.value.ValueFactory; @@ -18,13 +16,11 @@ public final class DeltaExtras { private static final String FROM = "from"; private static final String FORMAT = "format"; - @NotNull private final String format; - @NotNull private final String from; - private DeltaExtras(@Nullable final String format, @Nullable final String from) { + private DeltaExtras(final String format, final String from) { if (null == format) { throw new IllegalArgumentException("format cannot be null."); @@ -41,7 +37,6 @@ private DeltaExtras(@Nullable final String format, @Nullable final String from) * The delta format. As at API version 1.2, only {@link DeltaExtras.FORMAT_VCDIFF} is supported. * Will never return null. */ - @NotNull public String getFormat() { return format; } @@ -50,7 +45,6 @@ public String getFormat() { * The id of the message the delta was generated from. * Will never return null. */ - @NotNull public String getFrom() { return from; } 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 9270ddc48..f8f521d1b 100644 --- a/lib/src/main/java/io/ably/lib/types/Message.java +++ b/lib/src/main/java/io/ably/lib/types/Message.java @@ -6,7 +6,6 @@ import com.google.gson.*; import io.ably.lib.util.Serialisation; -import org.jetbrains.annotations.Nullable; import org.msgpack.core.MessageFormat; import org.msgpack.core.MessagePacker; import org.msgpack.core.MessageUnpacker; @@ -22,13 +21,11 @@ public class Message extends BaseMessage { /** * The event name, if available */ - @Nullable public String name; /** * Extras, if available */ - @Nullable public MessageExtras extras; private static final String NAME = "name"; @@ -65,7 +62,7 @@ public Message(String name, Object data, MessageExtras extras) { * @param clientId * @param extras */ - public Message(@Nullable String name, Object data, String clientId, @Nullable MessageExtras extras) { + public Message(String name, Object data, String clientId, MessageExtras extras) { this.name = name; this.clientId = clientId; this.data = data; diff --git a/lib/src/main/java/io/ably/lib/types/MessageExtras.java b/lib/src/main/java/io/ably/lib/types/MessageExtras.java index 45f73f716..c9d6b85ec 100644 --- a/lib/src/main/java/io/ably/lib/types/MessageExtras.java +++ b/lib/src/main/java/io/ably/lib/types/MessageExtras.java @@ -5,8 +5,6 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import io.ably.lib.util.Serialisation; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.msgpack.core.MessagePacker; import org.msgpack.core.MessageUnpacker; import org.msgpack.value.ImmutableMapValue; @@ -24,10 +22,8 @@ public final class MessageExtras { private static final String DELTA = "delta"; - @Nullable private final DeltaExtras delta; // may be null - @NotNull private final JsonObject jsonObject; // never null /** @@ -37,11 +33,11 @@ public final class MessageExtras { * * @since 1.2.1 */ - public MessageExtras(@NotNull final JsonObject jsonObject) { + public MessageExtras(final JsonObject jsonObject) { this(jsonObject, null); } - private MessageExtras(@NotNull final JsonObject jsonObject, @Nullable final DeltaExtras delta) { + private MessageExtras(final JsonObject jsonObject, final DeltaExtras delta) { // Necessary to add null check where error is possible to be introduced either by developer or server data // noinspection ConstantConditions - Nullable/NoNull inspections doen't enforce error at precompilation level if (null == jsonObject) { @@ -52,12 +48,10 @@ private MessageExtras(@NotNull final JsonObject jsonObject, @Nullable final Delt this.delta = delta; } - @Nullable public DeltaExtras getDelta() { return delta; } - @NotNull public JsonObject asJsonObject() { return jsonObject; } From 4481cb0cbf87ca3aff56fde3297564e1653b3fd6 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Sun, 4 Oct 2020 19:51:56 +0530 Subject: [PATCH 3/4] Added checkstyle formatting to the code --- .../java/io/ably/lib/types/BaseMessage.java | 630 +++++++++--------- .../java/io/ably/lib/types/DeltaExtras.java | 124 ++-- .../main/java/io/ably/lib/types/Message.java | 550 +++++++-------- .../java/io/ably/lib/types/MessageExtras.java | 219 +++--- .../io/ably/lib/types/MessageExtrasTest.java | 85 +-- 5 files changed, 805 insertions(+), 803 deletions(-) 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 bcf417c18..912c37e6d 100644 --- a/lib/src/main/java/io/ably/lib/types/BaseMessage.java +++ b/lib/src/main/java/io/ably/lib/types/BaseMessage.java @@ -22,319 +22,319 @@ import java.util.regex.Pattern; public class BaseMessage implements Cloneable { - /** - * A unique id for this message - */ - public String id; - - /** - * The timestamp for this message - */ - public long timestamp; - - /** - * The id of the publisher of this message - */ - public String clientId; - - /** - * The connection id of the publisher of this message - */ - public String connectionId; - - /** - * Any transformation applied to the data for this message - */ - public String encoding; - - /** - * The message payload. - */ - public Object data; - - private static final String TIMESTAMP = "timestamp"; - private static final String ID = "id"; - private static final String CLIENT_ID = "clientId"; - private static final String CONNECTION_ID = "connectionId"; - private static final String ENCODING = "encoding"; - private static final String DATA = "data"; - - /** - * Generate a String summary of this BaseMessage - * @return string - */ - public void getDetails(StringBuilder builder) { - if(clientId != null) - builder.append(" clientId=").append(clientId); - if(connectionId != null) - builder.append(" connectionId=").append(connectionId); - if(data != null) - builder.append(" data=").append(data); - if(encoding != null) - builder.append(" encoding=").append(encoding); - if(id != null) - builder.append(" id=").append(id); - } - - public void decode(ChannelOptions opts) throws MessageDecodeException { - - this.decode(opts, new DecodingContext()); - } - - private final static VCDiffDecoder vcdiffDecoder = VCDiffDecoderBuilder.builder().buildSimple(); - - private static byte[] vcdiffApply(byte[] delta, byte[] base) throws MessageDecodeException { - try { - ByteArrayOutputStream decoded = new ByteArrayOutputStream(); - vcdiffDecoder.decode(base, delta, decoded); - return decoded.toByteArray(); - } catch (Throwable t) { - throw MessageDecodeException.fromThrowableAndErrorInfo(t, new ErrorInfo("VCDIFF delta decode failed", 400, 40018)); - } - } - - public void decode(ChannelOptions opts, DecodingContext context) throws MessageDecodeException { - - Object lastPayload = data; - - if(encoding != null) { - String[] xforms = encoding.split("\\/"); - int lastProcessedEncodingIndex = 0, encodingsToProcess = xforms.length; - try { - while((lastProcessedEncodingIndex = encodingsToProcess ) > 0) { - Matcher match = xformPattern.matcher(xforms[--encodingsToProcess ]); - if(!match.matches()) break; - switch(match.group(1)) { - case "base64": - try { - data = Base64Coder.decode((String) data); - } catch (IllegalArgumentException e) { - throw MessageDecodeException.fromDescription("Invalid base64 data received"); - } - if(lastProcessedEncodingIndex == xforms.length) { - lastPayload = data; - } - continue; - - case "utf-8": - try { data = new String((byte[])data, "UTF-8"); } catch(UnsupportedEncodingException|ClassCastException e) {} - continue; - - case "json": - try { - String jsonText = ((String)data).trim(); - data = Serialisation.gsonParser.parse(jsonText); - } catch(JsonParseException e) { - throw MessageDecodeException.fromDescription("Invalid JSON data received"); - } - continue; - - case "cipher": - if(opts != null && opts.encrypted) { - try { - data = opts.getCipher().decrypt((byte[]) data); - } catch(AblyException e) { - throw MessageDecodeException.fromDescription(e.errorInfo.message); - } - continue; - } - else { - throw MessageDecodeException.fromDescription("Encrypted message received but encryption is not set up"); - } - case "vcdiff": - data = vcdiffApply((byte[]) data, context.getLastMessageData()); - lastPayload = data; - - continue; - } - break; - } - } finally { - encoding = (lastProcessedEncodingIndex <= 0) ? null : join(xforms, '/', 0, lastProcessedEncodingIndex ); - } - } - - //last message bookkeping - if(lastPayload instanceof String) - context.setLastMessageData((String)lastPayload); - else if (lastPayload instanceof byte[]) - context.setLastMessageData((byte[])lastPayload); - else - throw MessageDecodeException.fromDescription("Message data neither String nor byte[]. Unsupported message data type."); - } - - public void encode(ChannelOptions opts) throws AblyException { - if(data != null) { - if(data instanceof JsonElement) { - data = Serialisation.gson.toJson((JsonElement)data); - encoding = ((encoding == null) ? "" : encoding + "/") + "json"; - } - if(data instanceof String) { - if (opts != null && opts.encrypted) { - try { data = ((String)data).getBytes("UTF-8"); } catch(UnsupportedEncodingException e) {} - encoding = ((encoding == null) ? "" : encoding + "/") + "utf-8"; - } - } else if(!(data instanceof byte[])) { - Log.d(TAG, "Message data must be either `byte[]`, `String` or `JSONElement`; implicit coercion of other types to String is deprecated"); - throw AblyException.fromErrorInfo(new ErrorInfo("Invalid message data or encoding", 400, 40013)); - } - } - if (opts != null && opts.encrypted) { - ChannelCipher cipher = opts.getCipher(); - data = cipher.encrypt((byte[]) data); - encoding = ((encoding == null) ? "" : encoding + "/") + "cipher+" + cipher.getAlgorithm(); - } - } - - /* trivial utilities for processing encoding string */ - private static Pattern xformPattern = Pattern.compile("([\\-\\w]+)(\\+([\\-\\w]+))?"); - private String join(String[] elements, char separator, int start, int end) { - StringBuilder result = new StringBuilder(elements[start++]); - for(int i = start; i < end; i++) - result.append(separator).append(elements[i]); - return result.toString(); - } - - /** - * Base for gson serialisers. - */ - public static JsonObject toJsonObject(final BaseMessage message) { - JsonObject json = new JsonObject(); - Object data = message.data; - String encoding = message.encoding; - if(data != null) { - if(data instanceof byte[]) { - byte[] dataBytes = (byte[])data; - json.addProperty(DATA, new String(Base64Coder.encode(dataBytes))); - encoding = (encoding == null) ? "base64" : encoding + "/base64"; - } else { - json.addProperty(DATA, data.toString()); - } - if(encoding != null) json.addProperty(ENCODING, encoding); - } - if(message.id != null) json.addProperty(ID, message.id); - if(message.clientId != null) json.addProperty(CLIENT_ID, message.clientId); - if(message.connectionId != null) json.addProperty(CONNECTION_ID, message.connectionId); - return json; - } - - /** - * Populate fields from JSON. - */ - protected void read(final JsonObject map) throws MessageDecodeException { - final Long optionalTimestamp = readLong(map, TIMESTAMP); - if (null != optionalTimestamp) { - timestamp = optionalTimestamp; // unbox - } - - id = readString(map, ID); - clientId = readString(map, CLIENT_ID); - connectionId = readString(map, CONNECTION_ID); - encoding = readString(map, ENCODING); - data = readString(map, DATA); - } - - /** - * Read an optional textual 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 string value. - */ - protected String readString(final JsonObject map, final String key) { - final JsonElement element = map.get(key); - if (null == element || element instanceof JsonNull) { - return null; - } - return element.getAsString(); - } - - /** - * 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 long value. - */ - protected Long readLong(final JsonObject map, final String key) { - final JsonElement element = map.get(key); - if (null == element || element instanceof JsonNull) { - return null; - } - return element.getAsLong(); - } - - /* Msgpack processing */ - boolean readField(MessageUnpacker unpacker, String fieldName, MessageFormat fieldType) throws IOException { - boolean result = true; - switch (fieldName) { - case TIMESTAMP: - timestamp = unpacker.unpackLong(); break; - case ID: - id = unpacker.unpackString(); break; - case CLIENT_ID: - clientId = unpacker.unpackString(); break; - case CONNECTION_ID: - connectionId = unpacker.unpackString(); break; - case ENCODING: - encoding = unpacker.unpackString(); break; - case DATA: - if(fieldType.getValueType().isBinaryType()) { - byte[] byteData = new byte[unpacker.unpackBinaryHeader()]; - unpacker.readPayload(byteData); - data = byteData; - } else { - data = unpacker.unpackString(); - } - break; - default: - result = false; - break; - } - return result; - } - - protected int countFields() { - int fieldCount = 0; - if(timestamp > 0) ++fieldCount; - if(id != null) ++fieldCount; - if(clientId != null) ++fieldCount; - if(connectionId != null) ++fieldCount; - if(encoding != null) ++fieldCount; - if(data != null) ++fieldCount; - return fieldCount; - } - - void writeFields(MessagePacker packer) throws IOException { - if(timestamp > 0) { - packer.packString(TIMESTAMP); - packer.packLong(timestamp); - } - if(id != null) { - packer.packString(ID); - packer.packString(id); - } - if(clientId != null) { - packer.packString(CLIENT_ID); - packer.packString(clientId); - } - if(connectionId != null) { - packer.packString(CONNECTION_ID); - packer.packString(connectionId); - } - if(encoding != null) { - packer.packString(ENCODING); - packer.packString(encoding); - } - if(data != null) { - packer.packString(DATA); - if(data instanceof byte[]) { - byte[] byteData = (byte[])data; - packer.packBinaryHeader(byteData.length); - packer.writePayload(byteData); - } else { - packer.packString(data.toString()); - } - } - } - - private static final String TAG = BaseMessage.class.getName(); + /** + * A unique id for this message + */ + public String id; + + /** + * The timestamp for this message + */ + public long timestamp; + + /** + * The id of the publisher of this message + */ + public String clientId; + + /** + * The connection id of the publisher of this message + */ + public String connectionId; + + /** + * Any transformation applied to the data for this message + */ + public String encoding; + + /** + * The message payload. + */ + public Object data; + + private static final String TIMESTAMP = "timestamp"; + private static final String ID = "id"; + private static final String CLIENT_ID = "clientId"; + private static final String CONNECTION_ID = "connectionId"; + private static final String ENCODING = "encoding"; + private static final String DATA = "data"; + + /** + * Generate a String summary of this BaseMessage + * @return string + */ + public void getDetails(StringBuilder builder) { + if(clientId != null) + builder.append(" clientId=").append(clientId); + if(connectionId != null) + builder.append(" connectionId=").append(connectionId); + if(data != null) + builder.append(" data=").append(data); + if(encoding != null) + builder.append(" encoding=").append(encoding); + if(id != null) + builder.append(" id=").append(id); + } + + public void decode(ChannelOptions opts) throws MessageDecodeException { + + this.decode(opts, new DecodingContext()); + } + + private final static VCDiffDecoder vcdiffDecoder = VCDiffDecoderBuilder.builder().buildSimple(); + + private static byte[] vcdiffApply(byte[] delta, byte[] base) throws MessageDecodeException { + try { + ByteArrayOutputStream decoded = new ByteArrayOutputStream(); + vcdiffDecoder.decode(base, delta, decoded); + return decoded.toByteArray(); + } catch (Throwable t) { + throw MessageDecodeException.fromThrowableAndErrorInfo(t, new ErrorInfo("VCDIFF delta decode failed", 400, 40018)); + } + } + + public void decode(ChannelOptions opts, DecodingContext context) throws MessageDecodeException { + + Object lastPayload = data; + + if(encoding != null) { + String[] xforms = encoding.split("\\/"); + int lastProcessedEncodingIndex = 0, encodingsToProcess = xforms.length; + try { + while((lastProcessedEncodingIndex = encodingsToProcess ) > 0) { + Matcher match = xformPattern.matcher(xforms[--encodingsToProcess ]); + if(!match.matches()) break; + switch(match.group(1)) { + case "base64": + try { + data = Base64Coder.decode((String) data); + } catch (IllegalArgumentException e) { + throw MessageDecodeException.fromDescription("Invalid base64 data received"); + } + if(lastProcessedEncodingIndex == xforms.length) { + lastPayload = data; + } + continue; + + case "utf-8": + try { data = new String((byte[])data, "UTF-8"); } catch(UnsupportedEncodingException|ClassCastException e) {} + continue; + + case "json": + try { + String jsonText = ((String)data).trim(); + data = Serialisation.gsonParser.parse(jsonText); + } catch(JsonParseException e) { + throw MessageDecodeException.fromDescription("Invalid JSON data received"); + } + continue; + + case "cipher": + if(opts != null && opts.encrypted) { + try { + data = opts.getCipher().decrypt((byte[]) data); + } catch(AblyException e) { + throw MessageDecodeException.fromDescription(e.errorInfo.message); + } + continue; + } + else { + throw MessageDecodeException.fromDescription("Encrypted message received but encryption is not set up"); + } + case "vcdiff": + data = vcdiffApply((byte[]) data, context.getLastMessageData()); + lastPayload = data; + + continue; + } + break; + } + } finally { + encoding = (lastProcessedEncodingIndex <= 0) ? null : join(xforms, '/', 0, lastProcessedEncodingIndex ); + } + } + + //last message bookkeping + if(lastPayload instanceof String) + context.setLastMessageData((String)lastPayload); + else if (lastPayload instanceof byte[]) + context.setLastMessageData((byte[])lastPayload); + else + throw MessageDecodeException.fromDescription("Message data neither String nor byte[]. Unsupported message data type."); + } + + public void encode(ChannelOptions opts) throws AblyException { + if(data != null) { + if(data instanceof JsonElement) { + data = Serialisation.gson.toJson((JsonElement)data); + encoding = ((encoding == null) ? "" : encoding + "/") + "json"; + } + if(data instanceof String) { + if (opts != null && opts.encrypted) { + try { data = ((String)data).getBytes("UTF-8"); } catch(UnsupportedEncodingException e) {} + encoding = ((encoding == null) ? "" : encoding + "/") + "utf-8"; + } + } else if(!(data instanceof byte[])) { + Log.d(TAG, "Message data must be either `byte[]`, `String` or `JSONElement`; implicit coercion of other types to String is deprecated"); + throw AblyException.fromErrorInfo(new ErrorInfo("Invalid message data or encoding", 400, 40013)); + } + } + if (opts != null && opts.encrypted) { + ChannelCipher cipher = opts.getCipher(); + data = cipher.encrypt((byte[]) data); + encoding = ((encoding == null) ? "" : encoding + "/") + "cipher+" + cipher.getAlgorithm(); + } + } + + /* trivial utilities for processing encoding string */ + private static Pattern xformPattern = Pattern.compile("([\\-\\w]+)(\\+([\\-\\w]+))?"); + private String join(String[] elements, char separator, int start, int end) { + StringBuilder result = new StringBuilder(elements[start++]); + for(int i = start; i < end; i++) + result.append(separator).append(elements[i]); + return result.toString(); + } + + /** + * Base for gson serialisers. + */ + public static JsonObject toJsonObject(final BaseMessage message) { + JsonObject json = new JsonObject(); + Object data = message.data; + String encoding = message.encoding; + if(data != null) { + if(data instanceof byte[]) { + byte[] dataBytes = (byte[])data; + json.addProperty(DATA, new String(Base64Coder.encode(dataBytes))); + encoding = (encoding == null) ? "base64" : encoding + "/base64"; + } else { + json.addProperty(DATA, data.toString()); + } + if(encoding != null) json.addProperty(ENCODING, encoding); + } + if(message.id != null) json.addProperty(ID, message.id); + if(message.clientId != null) json.addProperty(CLIENT_ID, message.clientId); + if(message.connectionId != null) json.addProperty(CONNECTION_ID, message.connectionId); + return json; + } + + /** + * Populate fields from JSON. + */ + protected void read(final JsonObject map) throws MessageDecodeException { + final Long optionalTimestamp = readLong(map, TIMESTAMP); + if (null != optionalTimestamp) { + timestamp = optionalTimestamp; // unbox + } + + id = readString(map, ID); + clientId = readString(map, CLIENT_ID); + connectionId = readString(map, CONNECTION_ID); + encoding = readString(map, ENCODING); + data = readString(map, DATA); + } + + /** + * Read an optional textual 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 string value. + */ + protected String readString(final JsonObject map, final String key) { + final JsonElement element = map.get(key); + if (null == element || element instanceof JsonNull) { + return null; + } + return element.getAsString(); + } + + /** + * 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 long value. + */ + protected Long readLong(final JsonObject map, final String key) { + final JsonElement element = map.get(key); + if (null == element || element instanceof JsonNull) { + return null; + } + return element.getAsLong(); + } + + /* Msgpack processing */ + boolean readField(MessageUnpacker unpacker, String fieldName, MessageFormat fieldType) throws IOException { + boolean result = true; + switch (fieldName) { + case TIMESTAMP: + timestamp = unpacker.unpackLong(); break; + case ID: + id = unpacker.unpackString(); break; + case CLIENT_ID: + clientId = unpacker.unpackString(); break; + case CONNECTION_ID: + connectionId = unpacker.unpackString(); break; + case ENCODING: + encoding = unpacker.unpackString(); break; + case DATA: + if(fieldType.getValueType().isBinaryType()) { + byte[] byteData = new byte[unpacker.unpackBinaryHeader()]; + unpacker.readPayload(byteData); + data = byteData; + } else { + data = unpacker.unpackString(); + } + break; + default: + result = false; + break; + } + return result; + } + + protected int countFields() { + int fieldCount = 0; + if(timestamp > 0) ++fieldCount; + if(id != null) ++fieldCount; + if(clientId != null) ++fieldCount; + if(connectionId != null) ++fieldCount; + if(encoding != null) ++fieldCount; + if(data != null) ++fieldCount; + return fieldCount; + } + + void writeFields(MessagePacker packer) throws IOException { + if(timestamp > 0) { + packer.packString(TIMESTAMP); + packer.packLong(timestamp); + } + if(id != null) { + packer.packString(ID); + packer.packString(id); + } + if(clientId != null) { + packer.packString(CLIENT_ID); + packer.packString(clientId); + } + if(connectionId != null) { + packer.packString(CONNECTION_ID); + packer.packString(connectionId); + } + if(encoding != null) { + packer.packString(ENCODING); + packer.packString(encoding); + } + if(data != null) { + packer.packString(DATA); + if(data instanceof byte[]) { + byte[] byteData = (byte[])data; + packer.packBinaryHeader(byteData.length); + packer.writePayload(byteData); + } else { + packer.packString(data.toString()); + } + } + } + + private static final String TAG = BaseMessage.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/DeltaExtras.java b/lib/src/main/java/io/ably/lib/types/DeltaExtras.java index 3e93eee33..afcd8b095 100644 --- a/lib/src/main/java/io/ably/lib/types/DeltaExtras.java +++ b/lib/src/main/java/io/ably/lib/types/DeltaExtras.java @@ -9,67 +9,65 @@ import java.util.Objects; public final class DeltaExtras { - private static final String TAG = DeltaExtras.class.getName(); - - public static final String FORMAT_VCDIFF = "vcdiff"; - - private static final String FROM = "from"; - private static final String FORMAT = "format"; - - private final String format; - - private final String from; - - private DeltaExtras(final String format, final String from) { - - if (null == format) { - throw new IllegalArgumentException("format cannot be null."); - } - if (null == from) { - throw new IllegalArgumentException("from cannot be null."); - } - - this.format = format; - this.from = from; - } - - /** - * The delta format. As at API version 1.2, only {@link DeltaExtras.FORMAT_VCDIFF} is supported. - * Will never return null. - */ - public String getFormat() { - return format; - } - - /** - * The id of the message the delta was generated from. - * Will never return null. - */ - public String getFrom() { - return from; - } - - /* package private */ static DeltaExtras read(final Map map) throws IOException { - final Value format = map.get(ValueFactory.newString(FORMAT)); - final Value from = map.get(ValueFactory.newString(FROM)); - return new DeltaExtras(format.asStringValue().asString(), from.asStringValue().asString()); - } - - /* package private */ static DeltaExtras read(final JsonObject map) { - return new DeltaExtras(map.get(FORMAT).getAsString(), map.get(FROM).getAsString()); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DeltaExtras that = (DeltaExtras) o; - return format.equals(that.format) && - from.equals(that.from); - } - - @Override - public int hashCode() { - return Objects.hash(format, from); - } + private static final String TAG = DeltaExtras.class.getName(); + + public static final String FORMAT_VCDIFF = "vcdiff"; + + private static final String FROM = "from"; + private static final String FORMAT = "format"; + + private final String format; + private final String from; + + private DeltaExtras(final String format, final String from) { + if (null == format) { + throw new IllegalArgumentException("format cannot be null."); + } + if (null == from) { + throw new IllegalArgumentException("from cannot be null."); + } + + this.format = format; + this.from = from; + } + + /** + * The delta format. As at API version 1.2, only {@link DeltaExtras.FORMAT_VCDIFF} is supported. + * Will never return null. + */ + public String getFormat() { + return format; + } + + /** + * The id of the message the delta was generated from. + * Will never return null. + */ + public String getFrom() { + return from; + } + + /* package private */ static DeltaExtras read(final Map map) throws IOException { + final Value format = map.get(ValueFactory.newString(FORMAT)); + final Value from = map.get(ValueFactory.newString(FROM)); + return new DeltaExtras(format.asStringValue().asString(), from.asStringValue().asString()); + } + + /* package private */ static DeltaExtras read(final JsonObject map) { + return new DeltaExtras(map.get(FORMAT).getAsString(), map.get(FROM).getAsString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeltaExtras that = (DeltaExtras) o; + return format.equals(that.format) && + from.equals(that.from); + } + + @Override + public int hashCode() { + return Objects.hash(format, from); + } } 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 f8f521d1b..47bf105ce 100644 --- a/lib/src/main/java/io/ably/lib/types/Message.java +++ b/lib/src/main/java/io/ably/lib/types/Message.java @@ -3,8 +3,14 @@ import java.io.IOException; import java.lang.reflect.Type; import java.util.Collection; - -import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; import io.ably.lib.util.Serialisation; import org.msgpack.core.MessageFormat; import org.msgpack.core.MessagePacker; @@ -18,274 +24,274 @@ */ public class Message extends BaseMessage { - /** - * The event name, if available - */ - public String name; - - /** - * Extras, if available - */ - public MessageExtras extras; - - private static final String NAME = "name"; - private static final String EXTRAS = "extras"; - - /** - * Default constructor - */ - public Message() { - } - - /** - * Construct a message from event name and data - * @param name - * @param data - */ - public Message(String name, Object data) { - this(name, data, null, null); - } - - - public Message(String name, Object data, String clientId) { - this(name, data, clientId, null); - } - - public Message(String name, Object data, MessageExtras extras) { - this(name, data, null, extras); - } - - /** - * Generic constructor - * @param name - * @param data - * @param clientId - * @param extras - */ - public Message(String name, Object data, String clientId, MessageExtras extras) { - this.name = name; - this.clientId = clientId; - this.data = data; - this.extras = extras; - } - - /** - * Generate a String summary of this Message - * @return string - */ - public String toString() { - StringBuilder result = new StringBuilder("[Message"); - super.getDetails(result); - if(name != null) - result.append(" name=").append(name); - result.append(']'); - return result.toString(); - } - - void writeMsgpack(MessagePacker packer) throws IOException { - int fieldCount = super.countFields(); - if(name != null) ++fieldCount; - if(extras != null) ++fieldCount; - packer.packMapHeader(fieldCount); - super.writeFields(packer); - if(name != null) { - packer.packString(NAME); - packer.packString(name); - } - if(extras != null) { - packer.packString(EXTRAS); - extras.write(packer); - } - } - - Message readMsgpack(MessageUnpacker unpacker) throws IOException { - int fieldCount = unpacker.unpackMapHeader(); - for(int i = 0; i < fieldCount; i++) { - String fieldName = unpacker.unpackString().intern(); - MessageFormat fieldFormat = unpacker.getNextFormat(); - if(fieldFormat.equals(MessageFormat.NIL)) { - unpacker.unpackNil(); - continue; - } - - if(super.readField(unpacker, fieldName, fieldFormat)) { - continue; - } - if(fieldName.equals(NAME)) { - name = unpacker.unpackString(); - } else if (fieldName.equals(EXTRAS)) { - extras = MessageExtras.read(unpacker); - } else { - Log.v(TAG, "Unexpected field: " + fieldName); - unpacker.skipValue(); - } - } - return this; - } - - /** - * A specification for a collection of messages to be sent using the batch API - * @author paddy - */ - public static class Batch { - public String[] channels; - public Message[] messages; - - public Batch(String channel, Message[] messages) { - if(channel == null || channel.isEmpty()) throw new IllegalArgumentException("A Batch spec cannot have an empty set of channels"); - if(messages == null || messages.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of messages"); - this.channels = new String[]{channel}; - this.messages = messages; - } - - public Batch(String[] channels, Message[] messages) { - if(channels == null || channels.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of channels"); - if(messages == null || messages.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of messages"); - this.channels = channels; - this.messages = messages; - } - - public Batch(Collection channels, Collection messages) { - this(channels.toArray(new String[0]), messages.toArray(new Message[0])); - } - - public void writeMsgpack(MessagePacker packer) throws IOException { - packer.packMapHeader(2); - packer.packString("channels"); - packer.packArrayHeader(channels.length); - for(String ch : channels) packer.packString(ch); - packer.packString("messages"); - MessageSerializer.writeMsgpackArray(messages, packer); - } - } - - static Message fromMsgpack(MessageUnpacker unpacker) throws IOException { - return (new Message()).readMsgpack(unpacker); - } - - /** - * Refer Spec TM3
- * An alternative constructor that take an Message-JSON object and a channelOptions (optional), and return a Message - * @param messageJson - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static Message fromEncoded(JsonObject messageJson, ChannelOptions channelOptions) throws MessageDecodeException { - try { - Message message = Serialisation.gson.fromJson(messageJson, Message.class); - message.decode(channelOptions); - return message; - } catch(Exception e) { - Log.e(Message.class.getName(), e.getMessage(), e); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - /** - * Refer Spec TM3
- * An alternative constructor that takes a Stringified Message-JSON and a channelOptions (optional), and return a Message - * @param messageJson - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static Message fromEncoded(String messageJson, ChannelOptions channelOptions) throws MessageDecodeException { - try { - JsonObject jsonObject = Serialisation.gson.fromJson(messageJson, JsonObject.class); - return fromEncoded(jsonObject.getAsJsonObject(), channelOptions); - } catch(Exception e) { - Log.e(Message.class.getName(), e.getMessage(), e); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - /** - * Refer Spec TM3
- * An alternative constructor that takes a Messages JsonArray and a channelOptions (optional), and return array of Messages. - * @param messageArray - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static Message[] fromEncodedArray(JsonArray messageArray, ChannelOptions channelOptions) throws MessageDecodeException { - try { - Message[] messages = new Message[messageArray.size()]; - for(int index = 0; index < messageArray.size(); index++) { - JsonElement jsonElement = messageArray.get(index); - if(!jsonElement.isJsonObject()) { - throw new JsonParseException("Not all JSON elements are of type JSON Object."); - } - messages[index] = fromEncoded(jsonElement.getAsJsonObject(), channelOptions); - } - return messages; - } catch(Exception e) { - e.printStackTrace(); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - /** - * - * @param messagesArray - * @param channelOptions - * @return - * @throws MessageDecodeException - */ - public static Message[] fromEncodedArray(String messagesArray, ChannelOptions channelOptions) throws MessageDecodeException { - try { - JsonArray jsonArray = Serialisation.gson.fromJson(messagesArray, JsonArray.class); - return fromEncodedArray(jsonArray, channelOptions); - } catch(Exception e) { - e.printStackTrace(); - throw MessageDecodeException.fromDescription(e.getMessage()); - } - } - - @Override - protected void read(final JsonObject map) throws MessageDecodeException { - super.read(map); - - name = readString(map, NAME); - - final JsonElement extrasElement = map.get(EXTRAS); - if (null != extrasElement) { - if (!(extrasElement instanceof JsonObject)) { - throw MessageDecodeException.fromDescription("Message extras is of type \"" + extrasElement.getClass() + "\" when expected a JSON object."); - } - extras = MessageExtras.read((JsonObject) extrasElement); - } - } - - public static class Serializer implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializationContext ctx) { - final JsonObject json = BaseMessage.toJsonObject(message); - if (message.name != null) { - json.addProperty(NAME, message.name); - } - if (message.extras != null) { - json.add(EXTRAS, Serialisation.gson.toJsonTree(message.extras)); - } - return json; - } - - @Override - public Message deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (!(json instanceof JsonObject)) { - throw new JsonParseException("Expected an object but got \"" + json.getClass() + "\"."); - } - - final Message message = new Message(); - try { - message.read((JsonObject)json); - } catch (MessageDecodeException e) { - e.printStackTrace(); - throw new JsonParseException("Failed to deserialize Message from JSON.", e); - } - return message; - } - } - - private static final String TAG = Message.class.getName(); + /** + * The event name, if available + */ + public String name; + + /** + * Extras, if available + */ + public MessageExtras extras; + + private static final String NAME = "name"; + private static final String EXTRAS = "extras"; + + /** + * Default constructor + */ + public Message() { + } + + /** + * Construct a message from event name and data + * @param name + * @param data + */ + public Message(String name, Object data) { + this(name, data, null, null); + } + + + public Message(String name, Object data, String clientId) { + this(name, data, clientId, null); + } + + public Message(String name, Object data, MessageExtras extras) { + this(name, data, null, extras); + } + + /** + * Generic constructor + * @param name + * @param data + * @param clientId + * @param extras + */ + public Message(String name, Object data, String clientId, MessageExtras extras) { + this.name = name; + this.clientId = clientId; + this.data = data; + this.extras = extras; + } + + /** + * Generate a String summary of this Message + * @return string + */ + public String toString() { + StringBuilder result = new StringBuilder("[Message"); + super.getDetails(result); + if(name != null) + result.append(" name=").append(name); + result.append(']'); + return result.toString(); + } + + void writeMsgpack(MessagePacker packer) throws IOException { + int fieldCount = super.countFields(); + if(name != null) ++fieldCount; + if(extras != null) ++fieldCount; + packer.packMapHeader(fieldCount); + super.writeFields(packer); + if(name != null) { + packer.packString(NAME); + packer.packString(name); + } + if(extras != null) { + packer.packString(EXTRAS); + extras.write(packer); + } + } + + Message readMsgpack(MessageUnpacker unpacker) throws IOException { + int fieldCount = unpacker.unpackMapHeader(); + for(int i = 0; i < fieldCount; i++) { + String fieldName = unpacker.unpackString().intern(); + MessageFormat fieldFormat = unpacker.getNextFormat(); + if(fieldFormat.equals(MessageFormat.NIL)) { + unpacker.unpackNil(); + continue; + } + + if(super.readField(unpacker, fieldName, fieldFormat)) { + continue; + } + if(fieldName.equals(NAME)) { + name = unpacker.unpackString(); + } else if (fieldName.equals(EXTRAS)) { + extras = MessageExtras.read(unpacker); + } else { + Log.v(TAG, "Unexpected field: " + fieldName); + unpacker.skipValue(); + } + } + return this; + } + + /** + * A specification for a collection of messages to be sent using the batch API + * @author paddy + */ + public static class Batch { + public String[] channels; + public Message[] messages; + + public Batch(String channel, Message[] messages) { + if(channel == null || channel.isEmpty()) throw new IllegalArgumentException("A Batch spec cannot have an empty set of channels"); + if(messages == null || messages.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of messages"); + this.channels = new String[]{channel}; + this.messages = messages; + } + + public Batch(String[] channels, Message[] messages) { + if(channels == null || channels.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of channels"); + if(messages == null || messages.length == 0) throw new IllegalArgumentException("A Batch spec cannot have an empty set of messages"); + this.channels = channels; + this.messages = messages; + } + + public Batch(Collection channels, Collection messages) { + this(channels.toArray(new String[0]), messages.toArray(new Message[0])); + } + + public void writeMsgpack(MessagePacker packer) throws IOException { + packer.packMapHeader(2); + packer.packString("channels"); + packer.packArrayHeader(channels.length); + for(String ch : channels) packer.packString(ch); + packer.packString("messages"); + MessageSerializer.writeMsgpackArray(messages, packer); + } + } + + static Message fromMsgpack(MessageUnpacker unpacker) throws IOException { + return (new Message()).readMsgpack(unpacker); + } + + /** + * Refer Spec TM3
+ * An alternative constructor that take an Message-JSON object and a channelOptions (optional), and return a Message + * @param messageJson + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static Message fromEncoded(JsonObject messageJson, ChannelOptions channelOptions) throws MessageDecodeException { + try { + Message message = Serialisation.gson.fromJson(messageJson, Message.class); + message.decode(channelOptions); + return message; + } catch(Exception e) { + Log.e(Message.class.getName(), e.getMessage(), e); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + /** + * Refer Spec TM3
+ * An alternative constructor that takes a Stringified Message-JSON and a channelOptions (optional), and return a Message + * @param messageJson + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static Message fromEncoded(String messageJson, ChannelOptions channelOptions) throws MessageDecodeException { + try { + JsonObject jsonObject = Serialisation.gson.fromJson(messageJson, JsonObject.class); + return fromEncoded(jsonObject.getAsJsonObject(), channelOptions); + } catch(Exception e) { + Log.e(Message.class.getName(), e.getMessage(), e); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + /** + * Refer Spec TM3
+ * An alternative constructor that takes a Messages JsonArray and a channelOptions (optional), and return array of Messages. + * @param messageArray + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static Message[] fromEncodedArray(JsonArray messageArray, ChannelOptions channelOptions) throws MessageDecodeException { + try { + Message[] messages = new Message[messageArray.size()]; + for(int index = 0; index < messageArray.size(); index++) { + JsonElement jsonElement = messageArray.get(index); + if(!jsonElement.isJsonObject()) { + throw new JsonParseException("Not all JSON elements are of type JSON Object."); + } + messages[index] = fromEncoded(jsonElement.getAsJsonObject(), channelOptions); + } + return messages; + } catch(Exception e) { + e.printStackTrace(); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + /** + * + * @param messagesArray + * @param channelOptions + * @return + * @throws MessageDecodeException + */ + public static Message[] fromEncodedArray(String messagesArray, ChannelOptions channelOptions) throws MessageDecodeException { + try { + JsonArray jsonArray = Serialisation.gson.fromJson(messagesArray, JsonArray.class); + return fromEncodedArray(jsonArray, channelOptions); + } catch(Exception e) { + e.printStackTrace(); + throw MessageDecodeException.fromDescription(e.getMessage()); + } + } + + @Override + protected void read(final JsonObject map) throws MessageDecodeException { + super.read(map); + + name = readString(map, NAME); + + final JsonElement extrasElement = map.get(EXTRAS); + if (null != extrasElement) { + if (!(extrasElement instanceof JsonObject)) { + throw MessageDecodeException.fromDescription("Message extras is of type \"" + extrasElement.getClass() + "\" when expected a JSON object."); + } + extras = MessageExtras.read((JsonObject) extrasElement); + } + } + + public static class Serializer implements JsonSerializer, JsonDeserializer { + @Override + public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializationContext ctx) { + final JsonObject json = BaseMessage.toJsonObject(message); + if (message.name != null) { + json.addProperty(NAME, message.name); + } + if (message.extras != null) { + json.add(EXTRAS, Serialisation.gson.toJsonTree(message.extras)); + } + return json; + } + + @Override + public Message deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (!(json instanceof JsonObject)) { + throw new JsonParseException("Expected an object but got \"" + json.getClass() + "\"."); + } + + final Message message = new Message(); + try { + message.read((JsonObject)json); + } catch (MessageDecodeException e) { + e.printStackTrace(); + throw new JsonParseException("Failed to deserialize Message from JSON.", e); + } + return message; + } + } + + private static final String TAG = Message.class.getName(); } diff --git a/lib/src/main/java/io/ably/lib/types/MessageExtras.java b/lib/src/main/java/io/ably/lib/types/MessageExtras.java index c9d6b85ec..5a8e7d576 100644 --- a/lib/src/main/java/io/ably/lib/types/MessageExtras.java +++ b/lib/src/main/java/io/ably/lib/types/MessageExtras.java @@ -18,115 +18,112 @@ import java.util.Objects; public final class MessageExtras { - private static final String TAG = MessageExtras.class.getName(); - - private static final String DELTA = "delta"; - - private final DeltaExtras delta; // may be null - - private final JsonObject jsonObject; // never null - - /** - * Creates a MessageExtras instance to be sent as extra with a Message to Ably's servers. - * - * @see Channel-based push notification example - * - * @since 1.2.1 - */ - public MessageExtras(final JsonObject jsonObject) { - this(jsonObject, null); - } - - private MessageExtras(final JsonObject jsonObject, final DeltaExtras delta) { - // Necessary to add null check where error is possible to be introduced either by developer or server data - // noinspection ConstantConditions - Nullable/NoNull inspections doen't enforce error at precompilation level - if (null == jsonObject) { - throw new NullPointerException("jsonObject cannot be null."); - } - - this.jsonObject = jsonObject; - this.delta = delta; - } - - public DeltaExtras getDelta() { - return delta; - } - - public JsonObject asJsonObject() { - return jsonObject; - } - - /* package private */ void write(MessagePacker packer) throws IOException { - Serialisation.gsonToMsgpack(jsonObject, packer); - } - - /* package private */ static MessageExtras read(MessageUnpacker unpacker) throws IOException { - DeltaExtras delta = null; - - final ImmutableValue value = unpacker.unpackValue(); - if (value instanceof ImmutableMapValue) { - final Map map = ((ImmutableMapValue) value).map(); - final Value deltaValue = map.get(ValueFactory.newString(DELTA)); - if (null != deltaValue) { - if (!(deltaValue instanceof ImmutableMapValue)) { - // There's a delta key but the value at that key is not a map. - throw new IOException("The delta extras unpacked to the wrong type \"" + deltaValue.getClass() + "\" when expected a map."); - } - final Map deltaMap = ((ImmutableMapValue)deltaValue).map(); - delta = DeltaExtras.read(deltaMap); - } - } - - final JsonElement element = Serialisation.msgpackToGson(value); - if (!(element instanceof JsonObject)) { - // The root thing that we unpacked was not a map. - throw new IOException("The extras unpacked to the wrong type \"" + element.getClass() + "\" when expected a JsonObject."); - } - final JsonObject raw = (JsonObject)element; - - return new MessageExtras(raw, delta); - } - - /* package private */ static MessageExtras read(final JsonObject raw) throws MessageDecodeException { - DeltaExtras delta = null; - - final JsonElement deltaElement = raw.get(DELTA); - if (deltaElement instanceof JsonObject) { - delta = DeltaExtras.read((JsonObject)deltaElement); - } else { - if (null != deltaElement) { - throw MessageDecodeException.fromDescription("The value under the delta key is of the wrong type \"" + deltaElement.getClass() + "\" when expected a map."); - } - } - - return new MessageExtras(raw, delta); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MessageExtras that = (MessageExtras) o; - return Objects.equals(jsonObject, that.jsonObject); - } - - @Override - public int hashCode() { - return Objects.hashCode(jsonObject); - } - - @Override - public String toString() { - return "MessageExtras{" + - DELTA + "=" + delta + - ", raw=" + jsonObject + - '}'; - } - - public static class Serializer implements JsonSerializer { - @Override - public JsonElement serialize(final MessageExtras src, final Type typeOfSrc, final JsonSerializationContext context) { - return src.jsonObject; - } - } + private static final String TAG = MessageExtras.class.getName(); + + private static final String DELTA = "delta"; + + private final DeltaExtras delta; // may be null + private final JsonObject jsonObject; // never null + + /** + * Creates a MessageExtras instance to be sent as extra with a Message to Ably's servers. + * + * @see Channel-based push notification example + * + * @since 1.2.1 + */ + public MessageExtras(final JsonObject jsonObject) { + this(jsonObject, null); + } + + private MessageExtras(final JsonObject jsonObject, final DeltaExtras delta) { + if (null == jsonObject) { + throw new NullPointerException("jsonObject cannot be null."); + } + + this.jsonObject = jsonObject; + this.delta = delta; + } + + public DeltaExtras getDelta() { + return delta; + } + + public JsonObject asJsonObject() { + return jsonObject; + } + + /* package private */ void write(MessagePacker packer) { + Serialisation.gsonToMsgpack(jsonObject, packer); + } + + /* package private */ static MessageExtras read(MessageUnpacker unpacker) throws IOException { + DeltaExtras delta = null; + + final ImmutableValue value = unpacker.unpackValue(); + if (value instanceof ImmutableMapValue) { + final Map map = ((ImmutableMapValue) value).map(); + final Value deltaValue = map.get(ValueFactory.newString(DELTA)); + if (null != deltaValue) { + if (!(deltaValue instanceof ImmutableMapValue)) { + // There's a delta key but the value at that key is not a map. + throw new IOException("The delta extras unpacked to the wrong type \"" + deltaValue.getClass() + "\" when expected a map."); + } + final Map deltaMap = ((ImmutableMapValue)deltaValue).map(); + delta = DeltaExtras.read(deltaMap); + } + } + + final JsonElement element = Serialisation.msgpackToGson(value); + if (!(element instanceof JsonObject)) { + // The root thing that we unpacked was not a map. + throw new IOException("The extras unpacked to the wrong type \"" + element.getClass() + "\" when expected a JsonObject."); + } + final JsonObject raw = (JsonObject)element; + + return new MessageExtras(raw, delta); + } + + /* package private */ static MessageExtras read(final JsonObject raw) throws MessageDecodeException { + DeltaExtras delta = null; + + final JsonElement deltaElement = raw.get(DELTA); + if (deltaElement instanceof JsonObject) { + delta = DeltaExtras.read((JsonObject)deltaElement); + } else { + if (null != deltaElement) { + throw MessageDecodeException.fromDescription("The value under the delta key is of the wrong type \"" + deltaElement.getClass() + "\" when expected a map."); + } + } + + return new MessageExtras(raw, delta); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MessageExtras that = (MessageExtras) o; + return Objects.equals(jsonObject, that.jsonObject); + } + + @Override + public int hashCode() { + return Objects.hashCode(jsonObject); + } + + @Override + public String toString() { + return "MessageExtras{" + + DELTA + "=" + delta + + ", raw=" + jsonObject + + '}'; + } + + public static class Serializer implements JsonSerializer { + @Override + public JsonElement serialize(final MessageExtras src, final Type typeOfSrc, final JsonSerializationContext context) { + return src.jsonObject; + } + } } diff --git a/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java b/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java index 163952cc1..71c5ef373 100644 --- a/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java +++ b/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java @@ -11,49 +11,50 @@ import java.io.IOException; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; public class MessageExtrasTest { - /** - * Construct an instance from a JSON source and validate that the - * serialised JSON is the same. - */ - @Test - public void raw() { - final JsonObject objectA = new JsonObject(); - objectA.addProperty("someKey", "someValue"); - - final MessageExtras messageExtras = new MessageExtras(objectA); - assertNull(messageExtras.getDelta()); - - final MessageExtras.Serializer serializer = new MessageExtras.Serializer(); - final JsonElement serialised = serializer.serialize(messageExtras, null, null); - - assertEquals(objectA, serialised); - } - - @Test - public void rawViaMessagePack() throws IOException { - final JsonObject object = new JsonObject(); - object.addProperty("foo", "bar"); - object.addProperty("cliché", "cache"); - final MessageExtras messageExtras = new MessageExtras(object); - - // Encode to MessagePack - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - final MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); - messageExtras.write(packer); - packer.flush(); - - // Decode from MessagePack - MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(out.toByteArray()); - final MessageExtras unpacked = MessageExtras.read(unpacker); - - assertEquals(messageExtras, unpacked); - } - - @Test(expected = NullPointerException.class) - public void rawNullArgument() { - new MessageExtras((JsonObject)null); - } + /** + * Construct an instance from a JSON source and validate that the + * serialised JSON is the same. + */ + @Test + public void raw() { + final JsonObject objectA = new JsonObject(); + objectA.addProperty("someKey", "someValue"); + + final MessageExtras messageExtras = new MessageExtras(objectA); + assertNull(messageExtras.getDelta()); + + final MessageExtras.Serializer serializer = new MessageExtras.Serializer(); + final JsonElement serialised = serializer.serialize(messageExtras, null, null); + + assertEquals(objectA, serialised); + } + + @Test + public void rawViaMessagePack() throws IOException { + final JsonObject object = new JsonObject(); + object.addProperty("foo", "bar"); + object.addProperty("cliché", "cache"); + final MessageExtras messageExtras = new MessageExtras(object); + + // Encode to MessagePack + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final MessagePacker packer = Serialisation.msgpackPackerConfig.newPacker(out); + messageExtras.write(packer); + packer.flush(); + + // Decode from MessagePack + MessageUnpacker unpacker = Serialisation.msgpackUnpackerConfig.newUnpacker(out.toByteArray()); + final MessageExtras unpacked = MessageExtras.read(unpacker); + + assertEquals(messageExtras, unpacked); + } + + @Test(expected = NullPointerException.class) + public void rawNullArgument() { + new MessageExtras((JsonObject)null); + } } From 72a166ab1dce81981ce51878bc165c6ace67efe6 Mon Sep 17 00:00:00 2001 From: Quintin Willison Date: Tue, 3 Nov 2020 10:35:04 +0000 Subject: [PATCH 4/4] Reinstate test. --- lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java b/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java index 4f1027a42..5472d9f61 100644 --- a/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java +++ b/lib/src/test/java/io/ably/lib/types/MessageExtrasTest.java @@ -11,6 +11,7 @@ import java.io.IOException; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; public class MessageExtrasTest { @@ -23,6 +24,9 @@ public void raw() { final JsonObject objectA = new JsonObject(); objectA.addProperty("someKey", "someValue"); + final JsonObject objectB = new JsonObject(); + objectB.addProperty("someOtherKey", "someValue"); + final MessageExtras messageExtras = new MessageExtras(objectA); assertNull(messageExtras.getDelta()); @@ -30,6 +34,8 @@ public void raw() { final JsonElement serialised = serializer.serialize(messageExtras, null, null); assertEquals(objectA, serialised); + assertNotEquals(objectB, serialised); + assertNotEquals(objectB, objectA); } @Test