Skip to content

Commit 77a261c

Browse files
author
Quintin
authored
Merge pull request ably#581 from ably/feature/580-message-extras
Support outbound message extras
2 parents b53dc12 + 5101238 commit 77a261c

14 files changed

Lines changed: 494 additions & 160 deletions

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,13 +508,18 @@ To run tests against a specific host, specify in the environment:
508508

509509
env ABLY_ENV=staging ./gradlew testRealtimeSuite
510510

511-
Tests will run against sandbox by default.
511+
Tests will run against the sandbox environment by default.
512512

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

516516
./gradlew android:connectedAndroidTest
517517

518+
We also have a small, fledgling set of unit tests which do not communicate with Ably's servers.
519+
The plan is to expand this collection of tests in due course:
520+
521+
./gradlew java:runUnitTests
522+
518523
### Interactive push tests
519524

520525
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).

dependencies.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
dependencies {
44
implementation 'org.msgpack:msgpack-core:0.8.11'
55
implementation 'org.java-websocket:Java-WebSocket:1.4.0'
6-
implementation 'com.google.code.gson:gson:2.5'
6+
implementation 'com.google.code.gson:gson:2.8.6'
77
implementation 'com.davidehrmann.vcdiff:vcdiff-core:0.1.1'
88
testImplementation 'org.hamcrest:hamcrest-all:1.3'
99
testImplementation 'junit:junit:4.12'
@@ -12,4 +12,5 @@ dependencies {
1212
testImplementation 'org.nanohttpd:nanohttpd-websocket:2.3.0'
1313
testImplementation 'org.mockito:mockito-core:1.10.19'
1414
testImplementation 'net.jodah:concurrentunit:0.4.2'
15+
testImplementation 'org.slf4j:slf4j-simple:1.7.30'
1516
}

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

Lines changed: 94 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
package io.ably.lib.types;
22

3-
import java.io.ByteArrayOutputStream;
4-
import java.io.IOException;
5-
import java.io.UnsupportedEncodingException;
6-
import java.lang.reflect.Type;
7-
import java.util.regex.Matcher;
8-
import java.util.regex.Pattern;
9-
10-
import org.msgpack.core.MessageFormat;
11-
import org.msgpack.core.MessagePacker;
12-
import org.msgpack.core.MessageUnpacker;
13-
143
import com.davidehrmann.vcdiff.VCDiffDecoder;
154
import com.davidehrmann.vcdiff.VCDiffDecoderBuilder;
165
import com.google.gson.JsonElement;
6+
import com.google.gson.JsonNull;
177
import com.google.gson.JsonObject;
188
import com.google.gson.JsonParseException;
19-
import com.google.gson.JsonSerializationContext;
20-
9+
import com.google.gson.JsonPrimitive;
2110
import io.ably.lib.util.Base64Coder;
2211
import io.ably.lib.util.Crypto.ChannelCipher;
2312
import io.ably.lib.util.Log;
2413
import io.ably.lib.util.Serialisation;
14+
import org.msgpack.core.MessageFormat;
15+
import org.msgpack.core.MessagePacker;
16+
import org.msgpack.core.MessageUnpacker;
17+
18+
import java.io.ByteArrayOutputStream;
19+
import java.io.IOException;
20+
import java.io.UnsupportedEncodingException;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
2523

2624
public class BaseMessage implements Cloneable {
2725
/**
@@ -54,6 +52,13 @@ public class BaseMessage implements Cloneable {
5452
*/
5553
public Object data;
5654

55+
private static final String TIMESTAMP = "timestamp";
56+
private static final String ID = "id";
57+
private static final String CLIENT_ID = "clientId";
58+
private static final String CONNECTION_ID = "connectionId";
59+
private static final String ENCODING = "encoding";
60+
private static final String DATA = "data";
61+
5762
/**
5863
* Generate a String summary of this BaseMessage
5964
* @return string
@@ -75,7 +80,7 @@ public void decode(ChannelOptions opts) throws MessageDecodeException {
7580

7681
this.decode(opts, new DecodingContext());
7782
}
78-
83+
7984
private final static VCDiffDecoder vcdiffDecoder = VCDiffDecoderBuilder.builder().buildSimple();
8085

8186
private static byte[] vcdiffApply(byte[] delta, byte[] base) throws MessageDecodeException {
@@ -190,44 +195,88 @@ private String join(String[] elements, char separator, int start, int end) {
190195
return result.toString();
191196
}
192197

193-
/* Gson Serializer */
194-
public static class Serializer {
195-
public JsonElement serialize(BaseMessage message, Type typeOfMessage, JsonSerializationContext ctx) {
196-
JsonObject json = new JsonObject();
197-
Object data = message.data;
198-
String encoding = message.encoding;
199-
if(data != null) {
200-
if(data instanceof byte[]) {
201-
byte[] dataBytes = (byte[])data;
202-
json.addProperty("data", new String(Base64Coder.encode(dataBytes)));
203-
encoding = (encoding == null) ? "base64" : encoding + "/base64";
204-
} else {
205-
json.addProperty("data", data.toString());
206-
}
207-
if(encoding != null) json.addProperty("encoding", encoding);
198+
/**
199+
* Base for gson serialisers.
200+
*/
201+
public static JsonObject toJsonObject(final BaseMessage message) {
202+
JsonObject json = new JsonObject();
203+
Object data = message.data;
204+
String encoding = message.encoding;
205+
if(data != null) {
206+
if(data instanceof byte[]) {
207+
byte[] dataBytes = (byte[])data;
208+
json.addProperty("data", new String(Base64Coder.encode(dataBytes)));
209+
encoding = (encoding == null) ? "base64" : encoding + "/base64";
210+
} else {
211+
json.addProperty("data", data.toString());
208212
}
209-
if(message.id != null) json.addProperty("id", message.id);
210-
if(message.clientId != null) json.addProperty("clientId", message.clientId);
211-
if(message.connectionId != null) json.addProperty("connectionId", message.connectionId);
212-
return json;
213+
if(encoding != null) json.addProperty("encoding", encoding);
214+
}
215+
if(message.id != null) json.addProperty("id", message.id);
216+
if(message.clientId != null) json.addProperty("clientId", message.clientId);
217+
if(message.connectionId != null) json.addProperty("connectionId", message.connectionId);
218+
return json;
219+
}
220+
221+
/**
222+
* Populate fields from JSON.
223+
*/
224+
protected void read(final JsonObject map) throws MessageDecodeException {
225+
final Long optionalTimestamp = readLong(map, TIMESTAMP);
226+
if (null != optionalTimestamp) {
227+
timestamp = optionalTimestamp; // unbox
228+
}
229+
230+
id = readString(map, ID);
231+
clientId = readString(map, CLIENT_ID);
232+
connectionId = readString(map, CONNECTION_ID);
233+
encoding = readString(map, ENCODING);
234+
data = readString(map, DATA);
235+
}
236+
237+
/**
238+
* Read an optional textual value.
239+
* @return The value, or null if the key was not present in the map.
240+
* @throws ClassCastException if an element exists for that key and that element is not a {@link JsonPrimitive}
241+
* or is not a valid string value.
242+
*/
243+
protected String readString(final JsonObject map, final String key) {
244+
final JsonElement element = map.get(key);
245+
if (null == element || element instanceof JsonNull) {
246+
return null;
247+
}
248+
return element.getAsString();
249+
}
250+
251+
/**
252+
* Read an optional numerical value.
253+
* @return The value, or null if the key was not present in the map.
254+
* @throws ClassCastException if an element exists for that key and that element is not a {@link JsonPrimitive}
255+
* or is not a valid long value.
256+
*/
257+
protected Long readLong(final JsonObject map, final String key) {
258+
final JsonElement element = map.get(key);
259+
if (null == element || element instanceof JsonNull) {
260+
return null;
213261
}
262+
return element.getAsLong();
214263
}
215264

216265
/* Msgpack processing */
217266
boolean readField(MessageUnpacker unpacker, String fieldName, MessageFormat fieldType) throws IOException {
218267
boolean result = true;
219268
switch (fieldName) {
220-
case "timestamp":
269+
case TIMESTAMP:
221270
timestamp = unpacker.unpackLong(); break;
222-
case "id":
271+
case ID:
223272
id = unpacker.unpackString(); break;
224-
case "clientId":
273+
case CLIENT_ID:
225274
clientId = unpacker.unpackString(); break;
226-
case "connectionId":
275+
case CONNECTION_ID:
227276
connectionId = unpacker.unpackString(); break;
228-
case "encoding":
277+
case ENCODING:
229278
encoding = unpacker.unpackString(); break;
230-
case "data":
279+
case DATA:
231280
if(fieldType.getValueType().isBinaryType()) {
232281
byte[] byteData = new byte[unpacker.unpackBinaryHeader()];
233282
unpacker.readPayload(byteData);
@@ -256,27 +305,27 @@ protected int countFields() {
256305

257306
void writeFields(MessagePacker packer) throws IOException {
258307
if(timestamp > 0) {
259-
packer.packString("timestamp");
308+
packer.packString(TIMESTAMP);
260309
packer.packLong(timestamp);
261310
}
262311
if(id != null) {
263-
packer.packString("id");
312+
packer.packString(ID);
264313
packer.packString(id);
265314
}
266315
if(clientId != null) {
267-
packer.packString("clientId");
316+
packer.packString(CLIENT_ID);
268317
packer.packString(clientId);
269318
}
270319
if(connectionId != null) {
271-
packer.packString("connectionId");
320+
packer.packString(CONNECTION_ID);
272321
packer.packString(connectionId);
273322
}
274323
if(encoding != null) {
275-
packer.packString("encoding");
324+
packer.packString(ENCODING);
276325
packer.packString(encoding);
277326
}
278327
if(data != null) {
279-
packer.packString("data");
328+
packer.packString(DATA);
280329
if(data instanceof byte[]) {
281330
byte[] byteData = (byte[])data;
282331
packer.packBinaryHeader(byteData.length);
Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,51 @@
11
package io.ably.lib.types;
22

3-
import java.io.IOException;
4-
5-
import org.msgpack.core.MessageFormat;
3+
import com.google.gson.Gson;
4+
import com.google.gson.JsonElement;
5+
import com.google.gson.JsonObject;
6+
import com.google.gson.JsonSerializationContext;
7+
import com.google.gson.JsonSerializer;
8+
import io.ably.lib.util.Serialisation;
69
import org.msgpack.core.MessagePacker;
7-
import org.msgpack.core.MessageUnpacker;
10+
import org.msgpack.value.Value;
11+
import org.msgpack.value.ValueFactory;
812

9-
import io.ably.lib.util.Log;
13+
import java.io.IOException;
14+
import java.lang.reflect.Type;
15+
import java.util.Map;
16+
import java.util.Objects;
1017

1118
public final class DeltaExtras {
1219
private static final String TAG = DeltaExtras.class.getName();
13-
20+
1421
public static final String FORMAT_VCDIFF = "vcdiff";
1522

23+
private static final String FROM = "from";
24+
private static final String FORMAT = "format";
25+
1626
private final String format;
1727
private final String from;
18-
19-
public DeltaExtras(final String format, final String from) {
28+
29+
private DeltaExtras(final String format, final String from) {
2030
if (null == format) {
2131
throw new IllegalArgumentException("format cannot be null.");
2232
}
2333
if (null == from) {
2434
throw new IllegalArgumentException("from cannot be null.");
2535
}
26-
36+
2737
this.format = format;
2838
this.from = from;
2939
}
30-
40+
3141
/**
3242
* The delta format. As at API version 1.2, only {@link DeltaExtras.FORMAT_VCDIFF} is supported.
3343
* Will never return null.
3444
*/
3545
public String getFormat() {
3646
return format;
3747
}
38-
48+
3949
/**
4050
* The id of the message the delta was generated from.
4151
* Will never return null.
@@ -44,38 +54,37 @@ public String getFrom() {
4454
return from;
4555
}
4656

47-
/* package private */ void writeMsgpack(MessagePacker packer) throws IOException {
57+
/* package private */ void write(MessagePacker packer) throws IOException {
4858
packer.packMapHeader(2);
4959

50-
packer.packString("format");
60+
packer.packString(FORMAT);
5161
packer.packString(format);
5262

53-
packer.packString("from");
63+
packer.packString(FROM);
5464
packer.packString(from);
5565
}
5666

57-
/* package private */ static DeltaExtras fromMsgpack(final MessageUnpacker unpacker) throws IOException {
58-
final int fieldCount = unpacker.unpackMapHeader();
59-
String format = null;
60-
String from = null;
61-
for(int i = 0; i < fieldCount; i++) {
62-
String fieldName = unpacker.unpackString();
63-
MessageFormat fieldFormat = unpacker.getNextFormat();
64-
if(fieldFormat.equals(MessageFormat.NIL)) {
65-
unpacker.unpackNil();
66-
continue;
67-
}
68-
69-
if(fieldName.equals("format")) {
70-
format = unpacker.unpackString();
71-
} else if (fieldName.equals("from")) {
72-
from = unpacker.unpackString();
73-
} else {
74-
Log.w(TAG, "Unexpected field: " + fieldName);
75-
unpacker.skipValue();
76-
}
77-
}
78-
79-
return new DeltaExtras(format, from);
67+
/* package private */ static DeltaExtras read(final Map<Value, Value> map) throws IOException {
68+
final Value format = map.get(ValueFactory.newString(FORMAT));
69+
final Value from = map.get(ValueFactory.newString(FROM));
70+
return new DeltaExtras(format.asStringValue().asString(), from.asStringValue().asString());
71+
}
72+
73+
/* package private */ static DeltaExtras read(final JsonObject map) {
74+
return new DeltaExtras(map.get(FORMAT).getAsString(), map.get(FROM).getAsString());
75+
}
76+
77+
@Override
78+
public boolean equals(Object o) {
79+
if (this == o) return true;
80+
if (o == null || getClass() != o.getClass()) return false;
81+
DeltaExtras that = (DeltaExtras) o;
82+
return format.equals(that.format) &&
83+
from.equals(that.from);
84+
}
85+
86+
@Override
87+
public int hashCode() {
88+
return Objects.hash(format, from);
8089
}
8190
}

0 commit comments

Comments
 (0)