Skip to content

Commit

Permalink
Merge pull request #581 from ably/feature/580-message-extras
Browse files Browse the repository at this point in the history
Support outbound message extras
  • Loading branch information
Quintin committed Jun 15, 2020
2 parents b53dc12 + 5101238 commit 77a261c
Show file tree
Hide file tree
Showing 14 changed files with 494 additions and 160 deletions.
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);
}
}

0 comments on commit 77a261c

Please sign in to comment.