Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support outbound message extras #581

Merged
merged 24 commits into from
Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a050229
Add constructor to MessageExtras allowing it to be instantiated from …
Jun 10, 2020
0f8ee1d
Add instructions for running pure unit tests to the read me.
Jun 10, 2020
e8d9335
Add unit test as basic smoke test for the new constructor.
Jun 10, 2020
13dd407
Implement equals and hashCode on MessageExtras and DeltaExtras.
Jun 10, 2020
00ad728
Add unit test for the MessageExtras constructor that takes a DeltaExt…
Jun 10, 2020
f2cc321
Refine existing unit test to validate that a MessageExtras instance c…
Jun 10, 2020
ef99b5b
Add unit test for MessagePack encode and decode of DeltaExtras within…
Jun 10, 2020
f3dfaf9
Add commentary.
Jun 10, 2020
0752b58
Refactor the message pack support within the MessageExtras to support…
Jun 10, 2020
c56c859
Remove misleading commentary.
Jun 11, 2020
8a65f7e
Improve message extras hash and equality implementations so that, if …
Jun 11, 2020
cb6bc7c
Add integration test for RSL6a2.
Jun 11, 2020
41d8857
Fix test now that MessageExtras quality check has been refactored to …
Jun 11, 2020
65b2b67
Update gson dependency to latest release (October 2019), replacing th…
Jun 11, 2020
d41635b
Simplify method names.
Jun 11, 2020
3cbb7d9
Add SLF4J binding when running tests to help debugging.
Jun 11, 2020
deb0f59
Add integration test for RSL6a2, covering the opaque case (raw JSON i…
Jun 11, 2020
db2a17f
Revert some of the manual JSON deserialisation code I injected as it …
Jun 11, 2020
e41c806
Make the API to obtain a JsonObject from a MessageExtras instance pub…
Jun 12, 2020
99e3407
Add unit tests for constructor null arguments on MessageExtras.
Jun 12, 2020
d6ff228
Remove the unnecessary DeltaExtras constructor on MessageExtras.
Jun 12, 2020
00f1167
Remove DeltaExtras JSON serialisation support as it's not needed outb…
Jun 12, 2020
7fbc7b1
Make the DeltaExtras constructor private as it's not needed in the pu…
Jun 12, 2020
5101238
Fix oversight in my base message deserialisation code, covering the c…
Jun 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,13 +508,18 @@ To run tests against a specific host, specify in the environment:

env ABLY_ENV=staging ./gradlew testRealtimeSuite

Tests will run against sandbox by default.
Tests will run against the sandbox environment by default.

Tests can be run on the Android-specific library. An Android device must be connected,
either a real device or the Android emulator.

./gradlew android:connectedAndroidTest

We also have a small, fledgling set of unit tests which do not communicate with Ably's servers.
The plan is to expand this collection of tests in due course:

./gradlew java:runUnitTests

### Interactive push tests

End-to-end tests for push notifications (ie where the Android client is the target) can be tested interactively via a [separate app](https://github.com/ably/push-example-android).
Expand Down
3 changes: 2 additions & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
dependencies {
implementation 'org.msgpack:msgpack-core:0.8.11'
implementation 'org.java-websocket:Java-WebSocket:1.4.0'
implementation 'com.google.code.gson:gson:2.5'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.davidehrmann.vcdiff:vcdiff-core:0.1.1'
testImplementation 'org.hamcrest:hamcrest-all:1.3'
testImplementation 'junit:junit:4.12'
Expand All @@ -12,4 +12,5 @@ dependencies {
testImplementation 'org.nanohttpd:nanohttpd-websocket:2.3.0'
testImplementation 'org.mockito:mockito-core:1.10.19'
testImplementation 'net.jodah:concurrentunit:0.4.2'
testImplementation 'org.slf4j:slf4j-simple:1.7.30'
}
139 changes: 94 additions & 45 deletions lib/src/main/java/io/ably/lib/types/BaseMessage.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
package io.ably.lib.types;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.msgpack.core.MessageFormat;
import org.msgpack.core.MessagePacker;
import org.msgpack.core.MessageUnpacker;

import com.davidehrmann.vcdiff.VCDiffDecoder;
import com.davidehrmann.vcdiff.VCDiffDecoderBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;

import com.google.gson.JsonPrimitive;
import io.ably.lib.util.Base64Coder;
import io.ably.lib.util.Crypto.ChannelCipher;
import io.ably.lib.util.Log;
import io.ably.lib.util.Serialisation;
import org.msgpack.core.MessageFormat;
import org.msgpack.core.MessagePacker;
import org.msgpack.core.MessageUnpacker;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class BaseMessage implements Cloneable {
/**
Expand Down Expand Up @@ -54,6 +52,13 @@ public class BaseMessage implements Cloneable {
*/
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
Expand All @@ -75,7 +80,7 @@ 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 {
Expand Down Expand Up @@ -190,44 +195,88 @@ private String join(String[] elements, char separator, int start, int end) {
return result.toString();
}

/* Gson Serializer */
public static class Serializer {
public JsonElement serialize(BaseMessage message, Type typeOfMessage, JsonSerializationContext ctx) {
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);
/**
* 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(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);
return json;
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);
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":
case TIMESTAMP:
timestamp = unpacker.unpackLong(); break;
case "id":
case ID:
id = unpacker.unpackString(); break;
case "clientId":
case CLIENT_ID:
clientId = unpacker.unpackString(); break;
case "connectionId":
case CONNECTION_ID:
connectionId = unpacker.unpackString(); break;
case "encoding":
case ENCODING:
encoding = unpacker.unpackString(); break;
case "data":
case DATA:
if(fieldType.getValueType().isBinaryType()) {
byte[] byteData = new byte[unpacker.unpackBinaryHeader()];
unpacker.readPayload(byteData);
Expand Down Expand Up @@ -256,27 +305,27 @@ protected int countFields() {

void writeFields(MessagePacker packer) throws IOException {
if(timestamp > 0) {
packer.packString("timestamp");
packer.packString(TIMESTAMP);
packer.packLong(timestamp);
}
if(id != null) {
packer.packString("id");
packer.packString(ID);
packer.packString(id);
}
if(clientId != null) {
packer.packString("clientId");
packer.packString(CLIENT_ID);
packer.packString(clientId);
}
if(connectionId != null) {
packer.packString("connectionId");
packer.packString(CONNECTION_ID);
packer.packString(connectionId);
}
if(encoding != null) {
packer.packString("encoding");
packer.packString(ENCODING);
packer.packString(encoding);
}
if(data != null) {
packer.packString("data");
packer.packString(DATA);
if(data instanceof byte[]) {
byte[] byteData = (byte[])data;
packer.packBinaryHeader(byteData.length);
Expand Down
83 changes: 46 additions & 37 deletions lib/src/main/java/io/ably/lib/types/DeltaExtras.java
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
package io.ably.lib.types;

import java.io.IOException;

import org.msgpack.core.MessageFormat;
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.msgpack.core.MessageUnpacker;
import org.msgpack.value.Value;
import org.msgpack.value.ValueFactory;

import io.ably.lib.util.Log;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Map;
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;
public DeltaExtras(final String format, 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.
Expand All @@ -44,38 +54,37 @@ public String getFrom() {
return from;
}

/* package private */ void writeMsgpack(MessagePacker packer) throws IOException {
/* package private */ void write(MessagePacker packer) throws IOException {
packer.packMapHeader(2);

packer.packString("format");
packer.packString(FORMAT);
packer.packString(format);

packer.packString("from");
packer.packString(FROM);
packer.packString(from);
}

/* package private */ static DeltaExtras fromMsgpack(final MessageUnpacker unpacker) throws IOException {
final int fieldCount = unpacker.unpackMapHeader();
String format = null;
String from = null;
for(int i = 0; i < fieldCount; i++) {
String fieldName = unpacker.unpackString();
MessageFormat fieldFormat = unpacker.getNextFormat();
if(fieldFormat.equals(MessageFormat.NIL)) {
unpacker.unpackNil();
continue;
}

if(fieldName.equals("format")) {
format = unpacker.unpackString();
} else if (fieldName.equals("from")) {
from = unpacker.unpackString();
} else {
Log.w(TAG, "Unexpected field: " + fieldName);
unpacker.skipValue();
}
}

return new DeltaExtras(format, from);
/* package private */ static DeltaExtras read(final Map<Value, Value> 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);
}
}