diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..221e82d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +.gradle +build \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..759add5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +os: linux +dist: trusty +language: java +jdk: oraclejdk8 + +before_install: + - chmod +x gradlew + +branches: + only: + - master \ No newline at end of file diff --git a/Readme.md b/Readme.md index a3f7ad3..3688749 100644 --- a/Readme.md +++ b/Readme.md @@ -36,22 +36,159 @@ repositories { } ``` -## Use -The `VcdiffDecoder` class is the entry point to the public API. It provides a stateful and stateless way of applying `vcdiff` deltas. +## General Use -### Stateful Delta Application +The `VcdiffDecoder` class is an entry point to the public API. It provides a stateful way of applying a stream of `vcdiff` deltas. +`VcdiffDecoder` can do the necessary bookkeeping in the scenario where a number of successive deltas/patches have to be applied where each of them represents the difference to the previous one (e.g. a sequence of messages each of which represents a set of mutations to a given JavaScript object; i.e. sending only the mutations of an object instead the full object each time). +In order to benefit from the bookkeeping provided by the `VcdiffDecoder` class one has to first provide the base object that the first delta would be generated against. That could be done using the `setBase` method. The most simple flavor of `setBase` is: -### Stateless Delta Application +``` +VcdiffDecoder decoder = new VcdiffDecoder(); +decoder.setBase(baseObject /*the base object/message*/); +``` +Once the decoder is initialized like this it could be used to apply a stream of deltas/patches each one resulting in a new full payload. E.g. for binary objects/messages: +``` +byte[] result = decoder.applyDelta(vcdiffDelta).asByteArray(); +``` +or for string objects/messages: + +``` +string result = decoder.applyDelta(vcdiffDelta).asUtf8String(); +``` + +`applyDelta` could be called as many times as needed. The `VcdiffDecoder` will automatically retain the last delta application result and use it as a base for the next delta application. Thus it allows applying an infinite sequence of deltas. + +`applyDelta` return type is `DeltaApplicationResult`. That is a convenience class that allows interpreting the result in various data formats - string, array, etc. + +`CheckedVcdiffDecoder` is a flavor of `VcdiffDecoder` that could be used if deltas and objects against which deltas are generated have unique IDs. `CheckedVcdiffDecoder`'s `setBase` and `applyDelta` methods require these IDs and make sure the deltas are applied to the objects they were generated against. E.g. + +``` +DeltaApplicationResult result = checkedDecoder.applyDelta(vcdiffDelta, + deltaID,/*any unique identifier of the delta there might be*/ + baseID/*any unique identifier of the object this delta was generated against there might be */); +``` +There are `base64` flavors of `setBase` and `applyDelta` that would accept `base64` encoded input - `setBase64Base` and `applyBase64Delta`. These are convenience methods and they follow the same logic as `setBase` and `applyDelta`. +## Common Use Cases +### Ably Related +#### MQTT with Binary Payload + +This is a simple example of how one can utilize the `delta-codec-java` to handle delta messages received by Ably via MQTT + +``` +public class Main { + public static void main(String[] args) { + final String channelName = "sample-app-mqtt"; + final Mqtt3AsyncClient client = createClient(); + final VcdiffDecoder channelDecoder = new VcdiffDecoder(); + + connect(client, () -> { + subscribe(client, "[?delta=vcdiff]" + channelName, (payload) -> { + byte[] data; + try { + if (VcdiffDecoder.isDelta(payload)) { + data = channelDecoder.applyDelta(payload).asByteArray(); + } else { + data = payload; + channelDecoder.setBase(data); + } + } catch (Throwable error) { + /* Delta decoder error */ + System.out.println(error.getMessage()); + return; + } + + /* Process decoded data */ + System.out.println(Arrays.toString(data)); + }); + }); + } + + private static Mqtt3AsyncClient createClient() { + return Mqtt3Client.builder() + .serverHost("mqtt.ably.io") + .serverPort(8883) + .sslWithDefaultConfig() + .simpleAuth( + Mqtt3SimpleAuth.builder() + .username("FIRST_HALF_OF_API_KEY") + .password("SECOND_HALF_OF_API_KEY".getBytes(StandardCharsets.UTF_8)) + .build() + ) + .buildAsync(); + } + + private static void connect(Mqtt3AsyncClient client, Runnable callback) { + client.connect().whenComplete((mqtt3ConnAck, throwable) -> { + if (throwable != null) { + System.out.println("Connect failed - " + throwable.getMessage()); + return; + } + + callback.run(); + }); + } + + private static void subscribe(Mqtt3AsyncClient client, String channelName, Consumer callback) { + client.subscribeWith() + .topicFilter(channelName) + .qos(MqttQos.AT_MOST_ONCE) + .callback(mqtt3Publish -> callback.accept(mqtt3Publish.getPayloadAsBytes())) + .send(); + } +} + +``` + +### Non Ably Related + +#### Object Mutations Store/Retrieve + +VCDiff encoded deltas could be used to efficiently store the history of the mutations of a given instance of a class. Instead of preserving full copies of the instance state at various points of time in its existence one could preserve just the differences between two successive copies, i.e. the delta. The following method can then be used to restore the full copies of the object with the help of `delta-codec-java` lib: + +``` + Foo[] getObjectMutationsHistory(Foo initialState, + byte[][] objectMutationsDeltas /*array of vcdiff deltas computed between each two successive states of the base object with deltas being computed on JSON serialized object */) { + + final VcdiffDecoder stateDecoder = new DeltaCodec.VcdiffDecoder(); + + stateDecoder.setBase(initialState); + List objectMutations = new ArrayList(objectMutationsDeltas.length]); + + for(byte[] state : objectMutationsDeltas) { + try { + String jsonSerializedObject = stateDecoder.applyDelta(state).asUtf8String(); + objectMutations.add(JSON.parse(serialzedObject)); + } + catch (e) { + console.log(e); + /* Delta decoder error */ + } + } + + objectMutationsDeltas.forEach(state => { + try { + let serialzedObject = stateDecoder.applyDelta(state).asUtf8String(); + objectMutations.push(JSON.parse(serialzedObject)); + } catch (Throwable error) { + /* Delta decoder error */ + System.out.println(error.getMessage()); + return; + } + }); + + return objectMutations.toArray(); + } +``` ## Building ## diff --git a/delta-codec/build.gradle b/delta-codec/build.gradle new file mode 100644 index 0000000..021865a --- /dev/null +++ b/delta-codec/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' +} + +group 'io.ably' +version '1.0.0' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.davidehrmann.vcdiff:vcdiff-core:0.1.1' + implementation group: 'org.slf4j', name: 'slf4j-nop', version: '1.7.21' + testCompile group:'junit', name: 'junit', version: '4.12' +} diff --git a/delta-codec/src/main/java/io/ably/deltacodec/Base64Coder.java b/delta-codec/src/main/java/io/ably/deltacodec/Base64Coder.java new file mode 100644 index 0000000..f8f0703 --- /dev/null +++ b/delta-codec/src/main/java/io/ably/deltacodec/Base64Coder.java @@ -0,0 +1,239 @@ +package io.ably.deltacodec; + +import java.nio.charset.Charset; + +//Copyright 2003-2010 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland +//www.source-code.biz, www.inventec.ch/chdh +// +//This module is multi-licensed and may be used under the terms +//of any of the following licenses: +// +//EPL, Eclipse Public License, http://www.eclipse.org/legal +//LGPL, GNU Lesser General Public License, http://www.gnu.org/licenses/lgpl.html +//AL, Apache License, http://www.apache.org/licenses +//BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php +// +//Please contact the author if you need another license. +//This module is provided "as is", without warranties of any kind. + +/** +* A Base64 encoder/decoder. +* +*

+* This class is used to encode and decode data in Base64 format as described in RFC 1521. +* +*

+* Project home page: www.source-code.biz/base64coder/java
+* Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland
+* Multi-licensed: EPL / LGPL / AL / BSD. +*/ +public class Base64Coder { + +//The line separator string of the operating system. +private static final String systemLineSeparator = System.getProperty("line.separator"); + +//Mapping table from 6-bit nibbles to Base64 characters. +private static char[] map1 = new char[64]; +static { + int i=0; + for (char c='A'; c<='Z'; c++) map1[i++] = c; + for (char c='a'; c<='z'; c++) map1[i++] = c; + for (char c='0'; c<='9'; c++) map1[i++] = c; + map1[i++] = '+'; map1[i++] = '/'; } + +//Mapping table from Base64 characters to 6-bit nibbles. +private static byte[] map2 = new byte[128]; +static { + for (int i=0; isun.misc.BASE64Encoder.encodeBuffer(byte[]). +* @param in An array containing the data bytes to be encoded. +* @return A String containing the Base64 encoded data, broken into lines. +*/ +public static String encodeLines (byte[] in) { +return encodeLines(in, 0, in.length, 76, systemLineSeparator); } + +/** +* Encodes a byte array into Base 64 format and breaks the output into lines. +* @param in An array containing the data bytes to be encoded. +* @param iOff Offset of the first byte in in to be processed. +* @param iLen Number of bytes to be processed in in, starting at iOff. +* @param lineLen Line length for the output data. Should be a multiple of 4. +* @param lineSeparator The line separator to be used to separate the output lines. +* @return A String containing the Base64 encoded data, broken into lines. +*/ +public static String encodeLines (byte[] in, int iOff, int iLen, int lineLen, String lineSeparator) { +int blockLen = (lineLen*3) / 4; +if (blockLen <= 0) throw new IllegalArgumentException(); +int lines = (iLen+blockLen-1) / blockLen; +int bufLen = ((iLen+2)/3)*4 + lines*lineSeparator.length(); +StringBuilder buf = new StringBuilder(bufLen); +int ip = 0; +while (ip < iLen) { + int l = Math.min(iLen-ip, blockLen); + buf.append (encode(in, iOff+ip, l)); + buf.append (lineSeparator); + ip += l; } +return buf.toString(); } + +/** +* Encodes a byte array into Base64 format. +* No blanks or line breaks are inserted in the output. +* @param in An array containing the data bytes to be encoded. +* @return A character array containing the Base64 encoded data. +*/ +public static char[] encode (byte[] in) { +return encode(in, 0, in.length); } + +/** +* Encodes a byte array into Base64 format. +* No blanks or line breaks are inserted in the output. +* @param in An array containing the data bytes to be encoded. +* @return A String containing the Base64 encoded data. +*/ +public static String encodeToString (byte[] in) { +return new String(encode(in, 0, in.length)); } + +/** +* Encodes a byte array into Base64 format. +* No blanks or line breaks are inserted in the output. +* @param in An array containing the data bytes to be encoded. +* @param iLen Number of bytes to process in in. +* @return A character array containing the Base64 encoded data. +*/ +public static char[] encode (byte[] in, int iLen) { +return encode(in, 0, iLen); } + +/** +* Encodes a byte array into Base64 format. +* No blanks or line breaks are inserted in the output. +* @param in An array containing the data bytes to be encoded. +* @param iOff Offset of the first byte in in to be processed. +* @param iLen Number of bytes to process in in, starting at iOff. +* @return A character array containing the Base64 encoded data. +*/ +public static char[] encode (byte[] in, int iOff, int iLen) { +int oDataLen = (iLen*4+2)/3; // output length without padding +int oLen = ((iLen+2)/3)*4; // output length including padding +char[] out = new char[oLen]; +int ip = iOff; +int iEnd = iOff + iLen; +int op = 0; +while (ip < iEnd) { + int i0 = in[ip++] & 0xff; + int i1 = ip < iEnd ? in[ip++] & 0xff : 0; + int i2 = ip < iEnd ? in[ip++] & 0xff : 0; + int o0 = i0 >>> 2; + int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); + int o3 = i2 & 0x3F; + out[op++] = map1[o0]; + out[op++] = map1[o1]; + out[op] = op < oDataLen ? map1[o2] : '='; op++; + out[op] = op < oDataLen ? map1[o3] : '='; op++; } +return out; } + +/** +* Decodes a string from Base64 format. +* No blanks or line breaks are allowed within the Base64 encoded input data. +* @param s A Base64 String to be decoded. +* @return A String containing the decoded data. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static String decodeString (String s) { +return new String(decode(s)); } + +/** +* Decodes a byte array from Base64 format and ignores line separators, tabs and blanks. +* CR, LF, Tab and Space characters are ignored in the input data. +* This method is compatible with sun.misc.BASE64Decoder.decodeBuffer(String). +* @param s A Base64 String to be decoded. +* @return An array containing the decoded data bytes. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static byte[] decodeLines (String s) { +char[] buf = new char[s.length()]; +int p = 0; +for (int ip = 0; ip < s.length(); ip++) { + char c = s.charAt(ip); + if (c != ' ' && c != '\r' && c != '\n' && c != '\t') + buf[p++] = c; } +return decode(buf, 0, p); } + +/** +* Decodes a byte array from Base64 format. +* No blanks or line breaks are allowed within the Base64 encoded input data. +* @param s A Base64 String to be decoded. +* @return An array containing the decoded data bytes. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static byte[] decode (String s) { +return decode(s.toCharArray()); } + +/** +* Decodes a byte array from Base64 format. +* No blanks or line breaks are allowed within the Base64 encoded input data. +* @param in A character array containing the Base64 encoded data. +* @return An array containing the decoded data bytes. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static byte[] decode (char[] in) { +return decode(in, 0, in.length); } + +/** +* Decodes a byte array from Base64 format. +* No blanks or line breaks are allowed within the Base64 encoded input data. +* @param in A character array containing the Base64 encoded data. +* @param iOff Offset of the first character in in to be processed. +* @param iLen Number of characters to process in in, starting at iOff. +* @return An array containing the decoded data bytes. +* @throws IllegalArgumentException If the input is not valid Base64 encoded data. +*/ +public static byte[] decode (char[] in, int iOff, int iLen) { +if (iLen%4 != 0) throw new IllegalArgumentException ("Length of Base64 encoded input string is not a multiple of 4."); +while (iLen > 0 && in[iOff+iLen-1] == '=') iLen--; +int oLen = (iLen*3) / 4; +byte[] out = new byte[oLen]; +int ip = iOff; +int iEnd = iOff + iLen; +int op = 0; +while (ip < iEnd) { + int i0 = in[ip++]; + int i1 = in[ip++]; + int i2 = ip < iEnd ? in[ip++] : 'A'; + int i3 = ip < iEnd ? in[ip++] : 'A'; + if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) + throw new IllegalArgumentException ("Illegal character in Base64 encoded data."); + int b0 = map2[i0]; + int b1 = map2[i1]; + int b2 = map2[i2]; + int b3 = map2[i3]; + if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) + throw new IllegalArgumentException ("Illegal character in Base64 encoded data."); + int o0 = ( b0 <<2) | (b1>>>4); + int o1 = ((b1 & 0xf)<<4) | (b2>>>2); + int o2 = ((b2 & 3)<<6) | b3; + out[op++] = (byte)o0; + if (op \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..0f8d593 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sample-apps/mqtt-binary/build.gradle b/sample-apps/mqtt-binary/build.gradle new file mode 100644 index 0000000..a3a6bfd --- /dev/null +++ b/sample-apps/mqtt-binary/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' +} + +version '1.0.0' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':delta-codec') + implementation group: 'com.hivemq', name: 'hivemq-mqtt-client', version: '1.1.3' +} diff --git a/sample-apps/mqtt-binary/src/main/java/io/ably/mqtt_binary/Main.java b/sample-apps/mqtt-binary/src/main/java/io/ably/mqtt_binary/Main.java new file mode 100644 index 0000000..459cf08 --- /dev/null +++ b/sample-apps/mqtt-binary/src/main/java/io/ably/mqtt_binary/Main.java @@ -0,0 +1,85 @@ +package io.ably.mqtt_binary; + +import com.hivemq.client.mqtt.datatypes.MqttQos; +import com.hivemq.client.mqtt.mqtt3.Mqtt3AsyncClient; +import com.hivemq.client.mqtt.mqtt3.Mqtt3Client; +import com.hivemq.client.mqtt.mqtt3.message.auth.Mqtt3SimpleAuth; +import io.ably.deltacodec.VcdiffDecoder; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.function.Consumer; + +public class Main { + public static void main(String[] args) { + final String channelName = "sample-app-mqtt"; + final Mqtt3AsyncClient client = createClient(); + final VcdiffDecoder channelDecoder = new VcdiffDecoder(); + + connect(client, () -> { + subscribe(client, "[?delta=vcdiff]" + channelName, (payload) -> { + byte[] data; + try { + if (VcdiffDecoder.isDelta(payload)) { + data = channelDecoder.applyDelta(payload).asByteArray(); + } else { + data = payload; + channelDecoder.setBase(data); + } + } catch (Throwable error) { + /* Delta decoder error */ + System.out.println(error.getMessage()); + return; + } + + /* Process decoded data */ + System.out.println(Arrays.toString(data)); + }); + + publish(client, channelName, new byte[] { 76, 111, 114, 101, 109, 32, 105, 112, 115, 117, 109, 32, 100, 111, 108, 111, 114, 32, 115, 105, 116, 32, 97, 109, 101, 116 }); + publish(client, channelName, new byte[] { 76, 111, 114, 101, 109, 32, 105, 112, 115, 117, 109, 32, 100, 111, 108, 111, 114, 32, 115, 105, 116, 32, 97, 109, 101, 116, 44, 32, 99, 111, 110, 115, 101, 99, 116, 101, 116, 117, 114, 32, 97, 100, 105, 112, 105, 115, 99, 105, 110, 103, 32, 101, 108, 105, 116, 46 }); + publish(client, channelName, new byte[] { 76, 111, 114, 101, 109, 32, 105, 112, 115, 117, 109, 32, 100, 111, 108, 111, 114, 32, 115, 105, 116, 32, 97, 109, 101, 116, 44, 32, 99, 111, 110, 115, 101, 99, 116, 101, 116, 117, 114, 32, 97, 100, 105, 112, 105, 115, 99, 105, 110, 103, 32, 101, 108, 105, 116, 46, 32, 70, 117, 115, 99, 101, 32, 105, 100, 32, 110, 117, 108, 108, 97, 32, 108, 97, 99, 105, 110, 105, 97, 44, 32, 118, 111, 108, 117, 116, 112, 97, 116, 32, 111, 100, 105, 111, 32, 117, 116, 44, 32, 117, 108, 116, 114, 105, 99, 101, 115, 32, 108, 105, 103, 117, 108, 97, 46 }); + }); + } + + private static Mqtt3AsyncClient createClient() { + return Mqtt3Client.builder() + .serverHost("mqtt.ably.io") + .serverPort(8883) + .sslWithDefaultConfig() + .simpleAuth( + Mqtt3SimpleAuth.builder() + .username("FIRST_HALF_OF_API_KEY") + .password("SECOND_HALF_OF_API_KEY".getBytes(StandardCharsets.UTF_8)) + .build() + ) + .buildAsync(); + } + + private static void connect(Mqtt3AsyncClient client, Runnable callback) { + client.connect().whenComplete((mqtt3ConnAck, throwable) -> { + if (throwable != null) { + System.out.println("Connect failed - " + throwable.getMessage()); + return; + } + + callback.run(); + }); + } + + private static void subscribe(Mqtt3AsyncClient client, String channelName, Consumer callback) { + client.subscribeWith() + .topicFilter(channelName) + .qos(MqttQos.AT_MOST_ONCE) + .callback(mqtt3Publish -> callback.accept(mqtt3Publish.getPayloadAsBytes())) + .send(); + } + + private static void publish(Mqtt3AsyncClient client, String channelName, byte[] data) { + client.publishWith() + .topic(channelName) + .qos(MqttQos.AT_MOST_ONCE) + .payload(data) + .send(); + } +} diff --git a/sample-apps/mqtt-string/build.gradle b/sample-apps/mqtt-string/build.gradle new file mode 100644 index 0000000..a3a6bfd --- /dev/null +++ b/sample-apps/mqtt-string/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' +} + +version '1.0.0' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':delta-codec') + implementation group: 'com.hivemq', name: 'hivemq-mqtt-client', version: '1.1.3' +} diff --git a/sample-apps/mqtt-string/src/main/java/io/ably/mqtt_string/Main.java b/sample-apps/mqtt-string/src/main/java/io/ably/mqtt_string/Main.java new file mode 100644 index 0000000..759ec3f --- /dev/null +++ b/sample-apps/mqtt-string/src/main/java/io/ably/mqtt_string/Main.java @@ -0,0 +1,84 @@ +package io.ably.mqtt_string; + +import com.hivemq.client.mqtt.datatypes.MqttQos; +import com.hivemq.client.mqtt.mqtt3.Mqtt3AsyncClient; +import com.hivemq.client.mqtt.mqtt3.Mqtt3Client; +import com.hivemq.client.mqtt.mqtt3.message.auth.Mqtt3SimpleAuth; +import io.ably.deltacodec.VcdiffDecoder; + +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +public class Main { + public static void main(String[] args) { + final String channelName = "sample-app-mqtt"; + final Mqtt3AsyncClient client = createClient(); + final VcdiffDecoder channelDecoder = new VcdiffDecoder(); + + connect(client, () -> { + subscribe(client, "[?delta=vcdiff]" + channelName, (payload) -> { + String data; + try { + if (VcdiffDecoder.isDelta(payload)) { + data = channelDecoder.applyDelta(payload).asUtf8String(); + } else { + data = new String(payload); + channelDecoder.setBase(data); + } + } catch (Throwable error) { + /* Delta decoder error */ + System.out.println(error.getMessage()); + return; + } + + /* Process decoded data */ + System.out.println(data); + }); + + publish(client, channelName, "Lorem ipsum dolor sit amet"); + publish(client, channelName, "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); + publish(client, channelName, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a elit fermentum felis feugiat lacinia."); + }); + } + + private static Mqtt3AsyncClient createClient() { + return Mqtt3Client.builder() + .serverHost("mqtt.ably.io") + .serverPort(8883) + .sslWithDefaultConfig() + .simpleAuth( + Mqtt3SimpleAuth.builder() + .username("FIRST_HALF_OF_API_KEY") + .password("SECOND_HALF_OF_API_KEY".getBytes(StandardCharsets.UTF_8)) + .build() + ) + .buildAsync(); + } + + private static void connect(Mqtt3AsyncClient client, Runnable callback) { + client.connect().whenComplete((mqtt3ConnAck, throwable) -> { + if (throwable != null) { + System.out.println("Connect failed - " + throwable.getMessage()); + return; + } + + callback.run(); + }); + } + + private static void subscribe(Mqtt3AsyncClient client, String channelName, Consumer callback) { + client.subscribeWith() + .topicFilter(channelName) + .qos(MqttQos.AT_MOST_ONCE) + .callback(mqtt3Publish -> callback.accept(mqtt3Publish.getPayloadAsBytes())) + .send(); + } + + private static void publish(Mqtt3AsyncClient client, String channelName, String data) { + client.publishWith() + .topic(channelName) + .qos(MqttQos.AT_MOST_ONCE) + .payload(data.getBytes()) + .send(); + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..dae1888 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,7 @@ +rootProject.name = 'delta-codec-java' +include 'delta-codec' +include 'sample-apps:mqtt-string' +findProject(':sample-apps:mqtt-string')?.name = 'mqtt-string' +include 'sample-apps:mqtt-binary' +findProject(':sample-apps:mqtt-binary')?.name = 'mqtt-binary' +