From aab957a355001ad853966735a9305c15c96cbdb1 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Mon, 16 Sep 2019 12:09:46 +0300 Subject: [PATCH 01/14] Base codec functionality --- .gitignore | 3 + delta-codec/build.gradle | 20 ++ .../java/io/ably/deltacodec/Base64Coder.java | 239 ++++++++++++++++++ .../deltacodec/DeltaApplicationResult.java | 40 +++ .../java/io/ably/deltacodec/JsonHelper.java | 22 ++ .../SequenceContinuityException.java | 10 + .../io/ably/deltacodec/VcdiffDecoder.java | 129 ++++++++++ delta-sample-app/build.gradle | 19 ++ .../java/io/ably/deltasampleapp/Main.java | 78 ++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55190 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 +++++++++++++ gradlew.bat | 84 ++++++ settings.gradle | 3 + 14 files changed, 824 insertions(+) create mode 100644 .gitignore create mode 100644 delta-codec/build.gradle create mode 100644 delta-codec/src/main/java/io/ably/deltacodec/Base64Coder.java create mode 100644 delta-codec/src/main/java/io/ably/deltacodec/DeltaApplicationResult.java create mode 100644 delta-codec/src/main/java/io/ably/deltacodec/JsonHelper.java create mode 100644 delta-codec/src/main/java/io/ably/deltacodec/SequenceContinuityException.java create mode 100644 delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java create mode 100644 delta-sample-app/build.gradle create mode 100644 delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle 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/delta-codec/build.gradle b/delta-codec/build.gradle new file mode 100644 index 0000000..d7868c1 --- /dev/null +++ b/delta-codec/build.gradle @@ -0,0 +1,20 @@ +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' + implementation 'com.google.code.gson:gson:2.8.5' + 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/settings.gradle b/settings.gradle new file mode 100644 index 0000000..403d1e4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'delta-codec-java' +include 'delta-codec' +include 'delta-sample-app' From 0cb9c91fe05b2291e80a205bcab1ce81750fb7a1 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Tue, 10 Dec 2019 16:11:49 +0200 Subject: [PATCH 02/14] Adding refactorings and tests --- delta-codec/build.gradle | 1 - .../deltacodec/DeltaApplicationResult.java | 9 -- .../java/io/ably/deltacodec/JsonHelper.java | 22 ---- .../io/ably/deltacodec/VcdiffDecoder.java | 90 +++++++++++---- .../io/ably/deltacodec/VcdiffDecoderTest.java | 105 ++++++++++++++++++ 5 files changed, 173 insertions(+), 54 deletions(-) delete mode 100644 delta-codec/src/main/java/io/ably/deltacodec/JsonHelper.java create mode 100644 delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java diff --git a/delta-codec/build.gradle b/delta-codec/build.gradle index d7868c1..021865a 100644 --- a/delta-codec/build.gradle +++ b/delta-codec/build.gradle @@ -15,6 +15,5 @@ repositories { dependencies { implementation 'com.davidehrmann.vcdiff:vcdiff-core:0.1.1' implementation group: 'org.slf4j', name: 'slf4j-nop', version: '1.7.21' - implementation 'com.google.code.gson:gson:2.8.5' testCompile group:'junit', name: 'junit', version: '4.12' } diff --git a/delta-codec/src/main/java/io/ably/deltacodec/DeltaApplicationResult.java b/delta-codec/src/main/java/io/ably/deltacodec/DeltaApplicationResult.java index 0a882b7..5bf65ab 100644 --- a/delta-codec/src/main/java/io/ably/deltacodec/DeltaApplicationResult.java +++ b/delta-codec/src/main/java/io/ably/deltacodec/DeltaApplicationResult.java @@ -28,13 +28,4 @@ public byte[] asByteArray() { public String asUtf8String() { return new String(this.data, StandardCharsets.UTF_8); } - - /** - * Exports the delta application result as object assuming the bytes in - * the result represent an UTF-8 encoded JSON string - * @return The object representation of this delta application result - */ - public Object asObject() { - return JsonHelper.getInstance().deserialize(this.asUtf8String()); - } } diff --git a/delta-codec/src/main/java/io/ably/deltacodec/JsonHelper.java b/delta-codec/src/main/java/io/ably/deltacodec/JsonHelper.java deleted file mode 100644 index ebba75d..0000000 --- a/delta-codec/src/main/java/io/ably/deltacodec/JsonHelper.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.ably.deltacodec; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; - -class JsonHelper { - private static final JsonHelper instance = new JsonHelper(); - - private final Gson gson = new Gson(); - - static JsonHelper getInstance() { - return instance; - } - - String serialize(Object obj) { - return this.gson.toJson(obj); - } - - Object deserialize(String str) { - return this.gson.fromJson(str, JsonElement.class); - } -} diff --git a/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java b/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java index 487f7cd..3d3d950 100644 --- a/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java +++ b/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java @@ -27,10 +27,36 @@ public class VcdiffDecoder { * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF */ public DeltaApplicationResult applyDelta(Object delta) throws IllegalStateException, IllegalArgumentException, IOException { + return this.applyDelta(delta, false); + } + + /** + * Applies the {@code delta} to the result of applying the previous delta or to the base data + * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(Object)} + * before calling this method for the first time. + * @param delta The delta to be applied + * @param isBase64Encoded Whether the delta is base64 encoded + * @return {@link DeltaApplicationResult} instance + * @throws IOException Delta application failed + * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(Object)} + * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF + */ + public DeltaApplicationResult applyDelta(Object delta, boolean isBase64Encoded) throws IllegalStateException, IllegalArgumentException, IOException { if (this.base == null) { throw new IllegalStateException("Uninitialized decoder - setBase() should be called first"); } - byte[] deltaAsByteArray = tryConvertToDeltaByteArray(delta); + + byte[] deltaAsByteArray; + + if (isBase64Encoded) { + deltaAsByteArray = tryConvertFromBase64String((String)delta); + } else { + if (!(delta instanceof byte[])) { + throw new IllegalStateException("The provided delta does not represent binary data"); + } + + deltaAsByteArray = (byte[])delta; + } if (deltaAsByteArray == null || !hasVcdiffHeader(deltaAsByteArray)) { throw new IllegalArgumentException("The provided delta is not a valid VCDIFF delta"); } @@ -56,7 +82,28 @@ public DeltaApplicationResult applyDelta(Object delta, String deltaId, String ba if (!Objects.equals(this.baseId, baseId)) { throw new SequenceContinuityException(baseId, this.baseId); } - DeltaApplicationResult result = this.applyDelta(delta); + DeltaApplicationResult result = this.applyDelta(delta, false); + this.baseId = deltaId; + return result; + } + + /** + * Applies the {@code delta} to the result of applying the previous delta or to the base data + * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(Object, String)} + * before calling this method for the first time. + * @param delta The delta to be applied + * @param isBase64Encoded Whether the delta is base64 encoded + * @return {@link DeltaApplicationResult} instance + * @throws IOException Delta application failed + * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(Object, String)} + * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF + * @throws SequenceContinuityException The provided {@code baseId} does not match the last preserved sequence ID + */ + public DeltaApplicationResult applyDelta(Object delta, String deltaId, String baseId, boolean isBase64Encoded) throws SequenceContinuityException, IllegalStateException, IllegalArgumentException, IOException { + if (!Objects.equals(this.baseId, baseId)) { + throw new SequenceContinuityException(baseId, this.baseId); + } + DeltaApplicationResult result = this.applyDelta(delta, isBase64Encoded); this.baseId = deltaId; return result; } @@ -67,11 +114,21 @@ public DeltaApplicationResult applyDelta(Object delta, String deltaId, String ba * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ public void setBase(Object newBase) throws IllegalArgumentException { + this.setBase(newBase, false); + } + + /** + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(Object)}). + * @param newBase The base object to be set + * @param isBase64Encoded Whether the base is base64 encoded + * @throws IllegalArgumentException The provided {@code newBase} parameter is null + */ + public void setBase(Object newBase, boolean isBase64Encoded) throws IllegalArgumentException { if (newBase == null) { throw new IllegalArgumentException("newBase cannot be null"); } - this.base = convertToByteArray(newBase); + this.base = isBase64Encoded ? convertFromBase64String((String)newBase) : convertToByteArray(newBase); } /** @@ -82,7 +139,7 @@ public void setBase(Object newBase) throws IllegalArgumentException { * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ public void setBase(Object newBase, String newBaseId) { - this.setBase(newBase); + this.setBase(newBase, false); this.baseId = newBaseId; } @@ -98,32 +155,21 @@ private static byte[] convertToByteArray(Object data) { return (byte[])data; } else if (data instanceof String) { String dataAsString = (String)data; - byte[] base64DecodeResult = tryConvertFromBase64String(dataAsString); - if (base64DecodeResult != null) { - return base64DecodeResult; - } else { - return dataAsString.getBytes(StandardCharsets.UTF_8); - } - } else { - return JsonHelper.getInstance().serialize(data).getBytes(StandardCharsets.UTF_8); - } - } - - private static byte[] tryConvertToDeltaByteArray(Object obj) { - if (obj instanceof byte[]) { - return (byte[])obj; - } else if (obj instanceof String) { - return tryConvertFromBase64String((String)obj); + return dataAsString.getBytes(StandardCharsets.UTF_8); } else { - return null; + throw new IllegalArgumentException("Unsupported data type. Supported types: String, byte[]."); } } private static byte[] tryConvertFromBase64String(String str) { try { - return Base64Coder.decode(str); + return convertFromBase64String(str); } catch (IllegalArgumentException e) { return null; } } + + private static byte[] convertFromBase64String(String str) { + return Base64Coder.decode(str); + } } diff --git a/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java b/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java new file mode 100644 index 0000000..ab2faa5 --- /dev/null +++ b/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java @@ -0,0 +1,105 @@ +package io.ably.deltacodec; + +import org.junit.Test; +import org.junit.After; +import org.junit.Before; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; + +import java.io.IOException; + +public class VcdiffDecoderTest { + private VcdiffDecoder decoder; + + @Before + public void createVcdiffDecoderInstance() { + decoder = new VcdiffDecoder(); + } + + @After + public void disposeVcdiffDecoderInstance() { + decoder = null; + } + + @Test + public void setBaseThrowsIllegalArgumentExceptionWhenNewBaseIsNull() { + try { + this.decoder.setBase(null); + } catch (IllegalArgumentException ex) { + assertEquals("newBase cannot be null", ex.getMessage()); + } + } + + @Test + public void applyDeltaThrowsIllegalStateExceptionWhenBaseIsNull() throws IOException { + try{ + this.decoder.applyDelta(null); + } catch (IllegalStateException ex) { + assertEquals("Uninitialized decoder - setBase() should be called first", ex.getMessage()); + } + } + + @Test + public void applyDeltaThrowsIllegalStateExceptionWhenDeltaIsNotInBinaryFormatAndIsBase64EncodedArgumentIsFalse() throws IOException { + try{ + this.decoder.setBase("baseContent"); + this.decoder.applyDelta("deltaContent"); + } catch (IllegalStateException ex) { + assertEquals("The provided delta does not represent binary data", ex.getMessage()); + } + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainVcdiffHeaderAndIsBase64EncodedArgumentIsFalse() throws IOException { + try{ + this.decoder.setBase("baseContent"); + this.decoder.applyDelta(new byte[1]); + } catch (IllegalArgumentException ex) { + assertEquals("The provided delta is not a valid VCDIFF delta", ex.getMessage()); + } + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64EncodedAndIsBase64EncodedArgumentIsTrue() throws IOException { + try{ + this.decoder.setBase("baseContent"); + this.decoder.applyDelta("nonBase64Content", true); + } catch (IllegalArgumentException ex) { + assertEquals("The provided delta is not a valid VCDIFF delta", ex.getMessage()); + } + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainsVcdiffHeaderAndIsBase64EncodedArgumentIsTrue() throws IOException { + try{ + this.decoder.setBase("baseContent"); + this.decoder.applyDelta("YmFzZTY0Q29udGVudA==", true); + } catch (IllegalArgumentException ex) { + assertEquals("The provided delta is not a valid VCDIFF delta", ex.getMessage()); + } + } + + @Test + public void applyDeltaReturnsDeltaResultWhenDeltaIsValidAndIsBase64EncodedArgumentIsTrue() throws IOException { + String base = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ="; + String delta = "1sPEAAABGgAoOAAeBAEsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4TGgEeAA=="; + String expectedResult = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + + this.decoder.setBase(base, true); + DeltaApplicationResult deltaResult = this.decoder.applyDelta(delta, true); + + assertEquals(expectedResult, deltaResult.asUtf8String()); + } + + @Test + public void applyDeltaReturnsDeltaResultWhenDeltaIsValidAndIsBase64EncodedArgumentIsFalse() throws IOException { + byte[] base = 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 }; + byte[] delta = new byte[] { (byte)214, (byte)195, (byte)196, 0, 0, 1, 26, 0, 40, 56, 0, 30, 4, 1, 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, 19, 26, 1, 30, 0 }; + byte[] expectedResult = 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 }; + + this.decoder.setBase(base); + DeltaApplicationResult deltaResult = this.decoder.applyDelta(delta); + + assertArrayEquals(deltaResult.asByteArray(), expectedResult); + } +} From b7ce3ed5867e5950d2b4050434c7710f733c180f Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Wed, 11 Dec 2019 22:17:16 +0200 Subject: [PATCH 03/14] Made API generic and completely independent of Ably Realtime --- .../io/ably/deltacodec/VcdiffDecoder.java | 136 ++++++++++++------ .../io/ably/deltacodec/VcdiffDecoderTest.java | 68 ++++----- .../java/io/ably/deltasampleapp/Main.java | 2 +- 3 files changed, 124 insertions(+), 82 deletions(-) diff --git a/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java b/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java index 3d3d950..34bc324 100644 --- a/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java +++ b/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java @@ -16,52 +16,61 @@ public class VcdiffDecoder { private byte[] base; private String baseId; + /** * Applies the {@code delta} to the result of applying the previous delta or to the base data - * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(Object)} + * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase64Base(String)} * before calling this method for the first time. - * @param delta The delta to be applied + * @param delta The delta to be applied as base64 string * @return {@link DeltaApplicationResult} instance * @throws IOException Delta application failed - * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(Object)} + * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase64Base(String)} * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF */ - public DeltaApplicationResult applyDelta(Object delta) throws IllegalStateException, IllegalArgumentException, IOException { - return this.applyDelta(delta, false); + public DeltaApplicationResult applyDelta(String delta) throws IllegalStateException, IllegalArgumentException, IOException { + if (this.base == null) { + throw new IllegalStateException("Uninitialized decoder - setBase() should be called first"); + } + + byte[] deltaAsByteArray; + + deltaAsByteArray = tryConvertFromBase64String(delta); + + if(deltaAsByteArray == null) { + throw new IllegalStateException("The provided delta does not represent binary data"); + } + + if (!hasVcdiffHeader(deltaAsByteArray)) { + throw new IllegalArgumentException("The provided delta is not a valid VCDIFF delta"); + } + + ByteArrayOutputStream decoded = new ByteArrayOutputStream(); + this.decoder.decode(this.base, deltaAsByteArray, decoded); + this.base = decoded.toByteArray(); + // Return a copy to avoid future delta application failures if the returned array is modified + return new DeltaApplicationResult(decoded.toByteArray()); } /** * Applies the {@code delta} to the result of applying the previous delta or to the base data - * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(Object)} + * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(byte[])} * before calling this method for the first time. * @param delta The delta to be applied - * @param isBase64Encoded Whether the delta is base64 encoded * @return {@link DeltaApplicationResult} instance * @throws IOException Delta application failed - * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(Object)} + * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(byte[])} * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF */ - public DeltaApplicationResult applyDelta(Object delta, boolean isBase64Encoded) throws IllegalStateException, IllegalArgumentException, IOException { + public DeltaApplicationResult applyDelta(byte[] delta) throws IllegalStateException, IllegalArgumentException, IOException { if (this.base == null) { throw new IllegalStateException("Uninitialized decoder - setBase() should be called first"); } - byte[] deltaAsByteArray; - - if (isBase64Encoded) { - deltaAsByteArray = tryConvertFromBase64String((String)delta); - } else { - if (!(delta instanceof byte[])) { - throw new IllegalStateException("The provided delta does not represent binary data"); - } - - deltaAsByteArray = (byte[])delta; - } - if (deltaAsByteArray == null || !hasVcdiffHeader(deltaAsByteArray)) { + if (delta == null || !hasVcdiffHeader(delta)) { throw new IllegalArgumentException("The provided delta is not a valid VCDIFF delta"); } ByteArrayOutputStream decoded = new ByteArrayOutputStream(); - this.decoder.decode(this.base, deltaAsByteArray, decoded); + this.decoder.decode(this.base, delta, decoded); this.base = decoded.toByteArray(); // Return a copy to avoid future delta application failures if the returned array is modified return new DeltaApplicationResult(decoded.toByteArray()); @@ -69,77 +78,116 @@ public DeltaApplicationResult applyDelta(Object delta, boolean isBase64Encoded) /** * Applies the {@code delta} to the result of applying the previous delta or to the base data - * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(Object, String)} + * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(String, String)} * before calling this method for the first time. - * @param delta The delta to be applied + * @param delta The delta to be applied as base64 string * @return {@link DeltaApplicationResult} instance * @throws IOException Delta application failed - * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(Object, String)} + * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(String, String)} * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF * @throws SequenceContinuityException The provided {@code baseId} does not match the last preserved sequence ID */ - public DeltaApplicationResult applyDelta(Object delta, String deltaId, String baseId) throws SequenceContinuityException, IllegalStateException, IllegalArgumentException, IOException { + public DeltaApplicationResult applyDelta(String delta, String deltaId, String baseId) throws SequenceContinuityException, IllegalStateException, IllegalArgumentException, IOException { if (!Objects.equals(this.baseId, baseId)) { throw new SequenceContinuityException(baseId, this.baseId); } - DeltaApplicationResult result = this.applyDelta(delta, false); + DeltaApplicationResult result = this.applyDelta(delta); this.baseId = deltaId; return result; } /** * Applies the {@code delta} to the result of applying the previous delta or to the base data - * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(Object, String)} + * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(byte[], String)} * before calling this method for the first time. * @param delta The delta to be applied - * @param isBase64Encoded Whether the delta is base64 encoded * @return {@link DeltaApplicationResult} instance * @throws IOException Delta application failed - * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(Object, String)} + * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(byte[], String)} * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF * @throws SequenceContinuityException The provided {@code baseId} does not match the last preserved sequence ID */ - public DeltaApplicationResult applyDelta(Object delta, String deltaId, String baseId, boolean isBase64Encoded) throws SequenceContinuityException, IllegalStateException, IllegalArgumentException, IOException { + public DeltaApplicationResult applyDelta(byte[] delta, String deltaId, String baseId) throws SequenceContinuityException, IllegalStateException, IllegalArgumentException, IOException { if (!Objects.equals(this.baseId, baseId)) { throw new SequenceContinuityException(baseId, this.baseId); } - DeltaApplicationResult result = this.applyDelta(delta, isBase64Encoded); + DeltaApplicationResult result = this.applyDelta(delta); this.baseId = deltaId; return result; } /** - * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(Object)}). + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(byte[])}). + * @param newBase The base object to be set + * @throws IllegalArgumentException The provided {@code newBase} parameter is null + */ + public void setBase(byte[] newBase) throws IllegalArgumentException { + if (newBase == null) { + throw new IllegalArgumentException("newBase cannot be null"); + } + + this.base = newBase; + } + + /** + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String)}). * @param newBase The base object to be set * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ - public void setBase(Object newBase) throws IllegalArgumentException { - this.setBase(newBase, false); + public void setBase(String newBase) throws IllegalArgumentException { + if (newBase == null) { + throw new IllegalArgumentException("newBase cannot be null"); + } + + this.base = convertToByteArray(newBase); } /** - * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(Object)}). + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String)}). * @param newBase The base object to be set - * @param isBase64Encoded Whether the base is base64 encoded * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ - public void setBase(Object newBase, boolean isBase64Encoded) throws IllegalArgumentException { + public void setBase64Base(String newBase) throws IllegalArgumentException { if (newBase == null) { throw new IllegalArgumentException("newBase cannot be null"); } - - this.base = isBase64Encoded ? convertFromBase64String((String)newBase) : convertToByteArray(newBase); + + this.base = convertFromBase64String(newBase); + } + + /** + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String, String, String)}). + * @param newBase The base object to be set + * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking + * when delta is applied using {@link VcdiffDecoder#applyDelta(String, String, String)} + * @throws IllegalArgumentException The provided {@code newBase} parameter is null + */ + public void setBase(String newBase, String newBaseId) { + this.setBase(newBase); + this.baseId = newBaseId; + } + + /** + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(byte[], String, String)}). + * @param newBase The base object to be set + * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking + * when delta is applied using {@link VcdiffDecoder#applyDelta(byte[], String, String)} + * @throws IllegalArgumentException The provided {@code newBase} parameter is null + */ + public void setBase(byte[] newBase, String newBaseId) { + this.setBase(newBase); + this.baseId = newBaseId; } /** - * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(Object, String, String)}). + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String, String, String)}). * @param newBase The base object to be set * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking - * when delta is applied using {@link VcdiffDecoder#applyDelta(Object, String, String)} + * when delta is applied using {@link VcdiffDecoder#applyDelta(String, String, String)} * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ - public void setBase(Object newBase, String newBaseId) { - this.setBase(newBase, false); + public void setBase64Base(String newBase, String newBaseId) { + this.setBase64Base(newBase); this.baseId = newBaseId; } diff --git a/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java b/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java index ab2faa5..9d6aaaf 100644 --- a/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java +++ b/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java @@ -1,8 +1,11 @@ package io.ably.deltacodec; +import org.junit.Rule; import org.junit.Test; import org.junit.After; import org.junit.Before; +import org.junit.rules.ExpectedException; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertArrayEquals; @@ -21,62 +24,53 @@ public void disposeVcdiffDecoderInstance() { decoder = null; } + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test public void setBaseThrowsIllegalArgumentExceptionWhenNewBaseIsNull() { - try { - this.decoder.setBase(null); - } catch (IllegalArgumentException ex) { - assertEquals("newBase cannot be null", ex.getMessage()); - } + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("newBase cannot be null"); + this.decoder.setBase((byte[])null); } @Test public void applyDeltaThrowsIllegalStateExceptionWhenBaseIsNull() throws IOException { - try{ - this.decoder.applyDelta(null); - } catch (IllegalStateException ex) { - assertEquals("Uninitialized decoder - setBase() should be called first", ex.getMessage()); - } + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Uninitialized decoder - setBase() should be called first"); + this.decoder.applyDelta((byte[])null); } @Test - public void applyDeltaThrowsIllegalStateExceptionWhenDeltaIsNotInBinaryFormatAndIsBase64EncodedArgumentIsFalse() throws IOException { - try{ - this.decoder.setBase("baseContent"); - this.decoder.applyDelta("deltaContent"); - } catch (IllegalStateException ex) { - assertEquals("The provided delta does not represent binary data", ex.getMessage()); - } + public void applyDeltaThrowsIllegalStateExceptionWhenDeltaIsNotBase64Encoded() throws IOException { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("The provided delta does not represent binary data"); + this.decoder.setBase("baseContent"); + this.decoder.applyDelta("!deltaContent"); } @Test public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainVcdiffHeaderAndIsBase64EncodedArgumentIsFalse() throws IOException { - try{ - this.decoder.setBase("baseContent"); - this.decoder.applyDelta(new byte[1]); - } catch (IllegalArgumentException ex) { - assertEquals("The provided delta is not a valid VCDIFF delta", ex.getMessage()); - } + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); + this.decoder.setBase("baseContent"); + this.decoder.applyDelta(new byte[1]); } @Test public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64EncodedAndIsBase64EncodedArgumentIsTrue() throws IOException { - try{ - this.decoder.setBase("baseContent"); - this.decoder.applyDelta("nonBase64Content", true); - } catch (IllegalArgumentException ex) { - assertEquals("The provided delta is not a valid VCDIFF delta", ex.getMessage()); - } + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); + this.decoder.setBase("baseContent"); + this.decoder.applyDelta("nonBase64Content"); } @Test public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainsVcdiffHeaderAndIsBase64EncodedArgumentIsTrue() throws IOException { - try{ - this.decoder.setBase("baseContent"); - this.decoder.applyDelta("YmFzZTY0Q29udGVudA==", true); - } catch (IllegalArgumentException ex) { - assertEquals("The provided delta is not a valid VCDIFF delta", ex.getMessage()); - } + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); + this.decoder.setBase("baseContent"); + this.decoder.applyDelta("YmFzZTY0Q29udGVudA=="); } @Test @@ -85,8 +79,8 @@ public void applyDeltaReturnsDeltaResultWhenDeltaIsValidAndIsBase64EncodedArgume String delta = "1sPEAAABGgAoOAAeBAEsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4TGgEeAA=="; String expectedResult = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; - this.decoder.setBase(base, true); - DeltaApplicationResult deltaResult = this.decoder.applyDelta(delta, true); + this.decoder.setBase64Base(base); + DeltaApplicationResult deltaResult = this.decoder.applyDelta(delta); assertEquals(expectedResult, deltaResult.asUtf8String()); } diff --git a/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java b/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java index 4cfc8da..dc165c4 100644 --- a/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java +++ b/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java @@ -18,7 +18,7 @@ public void onConnectionStateChanged(ConnectionStateChange state) { channel.subscribe(new ChannelBase.MessageListener() { @Override public void onMessage(Message message) { - Object data = message.data; + String data = (String)message.data; try { MessageExtras extras = Serialisation.gson.fromJson(message.extras, MessageExtras.class); if (extras != null && extras.delta != null) { From 52d8f584f46543958d47a6480d32877fd0f0bab4 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Thu, 12 Dec 2019 18:39:56 +0200 Subject: [PATCH 04/14] Refactorings --- .../io/ably/deltacodec/VcdiffDecoder.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java b/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java index 34bc324..01dc3c8 100644 --- a/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java +++ b/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java @@ -44,11 +44,15 @@ public DeltaApplicationResult applyDelta(String delta) throws IllegalStateExcept throw new IllegalArgumentException("The provided delta is not a valid VCDIFF delta"); } + return new DeltaApplicationResult(applyDeltaInternal(deltaAsByteArray)); + } + + private byte[] applyDeltaInternal(byte[] deltaAsByteArray) throws IOException { ByteArrayOutputStream decoded = new ByteArrayOutputStream(); this.decoder.decode(this.base, deltaAsByteArray, decoded); this.base = decoded.toByteArray(); // Return a copy to avoid future delta application failures if the returned array is modified - return new DeltaApplicationResult(decoded.toByteArray()); + return decoded.toByteArray(); } /** @@ -69,11 +73,8 @@ public DeltaApplicationResult applyDelta(byte[] delta) throws IllegalStateExcept if (delta == null || !hasVcdiffHeader(delta)) { throw new IllegalArgumentException("The provided delta is not a valid VCDIFF delta"); } - ByteArrayOutputStream decoded = new ByteArrayOutputStream(); - this.decoder.decode(this.base, delta, decoded); - this.base = decoded.toByteArray(); - // Return a copy to avoid future delta application failures if the returned array is modified - return new DeltaApplicationResult(decoded.toByteArray()); + + return new DeltaApplicationResult(applyDeltaInternal(delta)); } /** @@ -118,7 +119,7 @@ public DeltaApplicationResult applyDelta(byte[] delta, String deltaId, String ba /** * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(byte[])}). - * @param newBase The base object to be set + * @param newBase The byte[] to be set as new base * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ public void setBase(byte[] newBase) throws IllegalArgumentException { @@ -131,7 +132,7 @@ public void setBase(byte[] newBase) throws IllegalArgumentException { /** * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String)}). - * @param newBase The base object to be set + * @param newBase The string to be set as new base * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ public void setBase(String newBase) throws IllegalArgumentException { @@ -144,7 +145,7 @@ public void setBase(String newBase) throws IllegalArgumentException { /** * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String)}). - * @param newBase The base object to be set + * @param newBase The base64 string to be set as new base * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ public void setBase64Base(String newBase) throws IllegalArgumentException { @@ -157,7 +158,7 @@ public void setBase64Base(String newBase) throws IllegalArgumentException { /** * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String, String, String)}). - * @param newBase The base object to be set + * @param newBase The string to be set as new base * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking * when delta is applied using {@link VcdiffDecoder#applyDelta(String, String, String)} * @throws IllegalArgumentException The provided {@code newBase} parameter is null @@ -169,7 +170,7 @@ public void setBase(String newBase, String newBaseId) { /** * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(byte[], String, String)}). - * @param newBase The base object to be set + * @param newBase The byte[] to be set as new base * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking * when delta is applied using {@link VcdiffDecoder#applyDelta(byte[], String, String)} * @throws IllegalArgumentException The provided {@code newBase} parameter is null @@ -181,7 +182,7 @@ public void setBase(byte[] newBase, String newBaseId) { /** * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String, String, String)}). - * @param newBase The base object to be set + * @param newBase The base64 string to be set as new base * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking * when delta is applied using {@link VcdiffDecoder#applyDelta(String, String, String)} * @throws IllegalArgumentException The provided {@code newBase} parameter is null From d7c18d1b4ef85ce6d079d64c583b6e69c9950396 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Fri, 17 Jan 2020 20:04:50 +0200 Subject: [PATCH 05/14] Adding CheckedVcdiffDecoder with tests --- .../io/ably/deltacodec/BaseVcdiffDecoder.java | 72 +++++++ .../ably/deltacodec/CheckedVcdiffDecoder.java | 99 +++++++++ .../SequenceContinuityException.java | 2 +- .../io/ably/deltacodec/VcdiffDecoder.java | 200 ++---------------- .../deltacodec/CheckedVcdiffDecoderTest.java | 127 +++++++++++ .../io/ably/deltacodec/VcdiffDecoderTest.java | 14 +- .../java/io/ably/deltasampleapp/Main.java | 4 +- 7 files changed, 331 insertions(+), 187 deletions(-) create mode 100644 delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java create mode 100644 delta-codec/src/main/java/io/ably/deltacodec/CheckedVcdiffDecoder.java create mode 100644 delta-codec/src/test/java/io/ably/deltacodec/CheckedVcdiffDecoderTest.java diff --git a/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java b/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java new file mode 100644 index 0000000..88313ba --- /dev/null +++ b/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java @@ -0,0 +1,72 @@ +package io.ably.deltacodec; + +import com.davidehrmann.vcdiff.VCDiffDecoder; +import com.davidehrmann.vcdiff.VCDiffDecoderBuilder; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +class BaseVcdiffDecoder { + private final VCDiffDecoder decoder = VCDiffDecoderBuilder.builder().buildSimple(); + private byte[] base; + + protected DeltaApplicationResult applyDeltaInternal(byte[] delta) throws IllegalStateException, IllegalArgumentException, IOException { + if (this.base == null) { + throw new IllegalStateException("Uninitialized decoder - setBase() should be called first"); + } + + if (delta == null || !hasVcdiffHeader(delta)) { + throw new IllegalArgumentException("The provided delta is not a valid VCDIFF delta"); + } + + return new DeltaApplicationResult(doApplyDelta(delta)); + } + + protected DeltaApplicationResult applyBase64DeltaInternal(String delta) throws IllegalStateException, IllegalArgumentException, IOException { + return this.applyDeltaInternal(tryConvertFromBase64String(delta)); + } + + protected void setBaseInternal(byte[] newBase) throws IllegalArgumentException { + if (newBase == null) { + throw new IllegalArgumentException("newBase cannot be null"); + } + + this.base = newBase; + } + + protected void setBaseInternal(String newBase) throws IllegalArgumentException { + this.setBaseInternal(convertToByteArray(newBase)); + } + + protected void setBase64BaseInternal(String newBase) throws IllegalArgumentException { + this.setBaseInternal(tryConvertFromBase64String(newBase)); + } + + private byte[] doApplyDelta(byte[] deltaAsByteArray) throws IOException { + ByteArrayOutputStream decoded = new ByteArrayOutputStream(); + this.decoder.decode(this.base, deltaAsByteArray, decoded); + this.base = decoded.toByteArray(); + // Return a copy to avoid future delta application failures if the returned array is modified + return decoded.toByteArray(); + } + + private static boolean hasVcdiffHeader(byte[] delta) { + return delta[0] == (byte)0xd6 && + delta[1] == (byte)0xc3 && + delta[2] == (byte)0xc4 && + delta[3] == (byte)0; + } + + private static byte[] convertToByteArray(String data) { + return data.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] tryConvertFromBase64String(String str) { + try { + return Base64Coder.decode(str); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/delta-codec/src/main/java/io/ably/deltacodec/CheckedVcdiffDecoder.java b/delta-codec/src/main/java/io/ably/deltacodec/CheckedVcdiffDecoder.java new file mode 100644 index 0000000..f4ace39 --- /dev/null +++ b/delta-codec/src/main/java/io/ably/deltacodec/CheckedVcdiffDecoder.java @@ -0,0 +1,99 @@ +package io.ably.deltacodec; + +import java.io.IOException; +import java.util.Objects; + +/** + * VCDIFF decoder capable of processing continuous sequences of consecutively generated VCDIFFs + */ +public class CheckedVcdiffDecoder extends BaseVcdiffDecoder { + private String baseId; + + /** + * Applies the {@code delta} to the result of applying the previous delta or to the base data + * if no previous delta has been applied yet. Base data has to be set by {@link CheckedVcdiffDecoder#setBase(byte[], String)}, + * {@link CheckedVcdiffDecoder#setBase(String, String)} or {@link CheckedVcdiffDecoder#setBase64Base(String, String)} + * before calling this method for the first time. + * @param delta The delta to be applied + * @return {@link DeltaApplicationResult} instance + * @throws IOException Delta application failed + * @throws IllegalStateException The decoder is not initialized by calling {@link CheckedVcdiffDecoder#setBase(byte[], String)}, + * {@link CheckedVcdiffDecoder#setBase(String, String)} or {@link CheckedVcdiffDecoder#setBase64Base(String, String)} + * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF + * @throws SequenceContinuityException The provided {@code baseId} does not match the last preserved sequence ID + */ + public DeltaApplicationResult applyDelta(byte[] delta, String deltaId, String baseId) throws SequenceContinuityException, IllegalStateException, IllegalArgumentException, IOException { + this.checkSequenceContinuity(baseId); + DeltaApplicationResult result = this.applyDeltaInternal(delta); + this.baseId = deltaId; + return result; + } + + /** + * Applies the {@code delta} to the result of applying the previous delta or to the base data + * if no previous delta has been applied yet. Base data has to be set by {@link CheckedVcdiffDecoder#setBase(byte[], String)}, + * {@link CheckedVcdiffDecoder#setBase(String, String)} or {@link CheckedVcdiffDecoder#setBase64Base(String, String)} + * before calling this method for the first time. + * @param delta The delta to be applied as base64 string + * @return {@link DeltaApplicationResult} instance + * @throws IOException Delta application failed + * @throws IllegalStateException The decoder is not initialized by calling {@link CheckedVcdiffDecoder#setBase(byte[], String)}, + * {@link CheckedVcdiffDecoder#setBase(String, String)} or {@link CheckedVcdiffDecoder#setBase64Base(String, String)} + * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF + * @throws SequenceContinuityException The provided {@code baseId} does not match the last preserved sequence ID + */ + public DeltaApplicationResult applyBase64Delta(String delta, String deltaId, String baseId) throws SequenceContinuityException, IllegalStateException, IllegalArgumentException, IOException { + this.checkSequenceContinuity(baseId); + DeltaApplicationResult result = this.applyBase64DeltaInternal(delta); + this.baseId = deltaId; + return result; + } + + /** + * Sets the base object used for the next delta application (see {@link CheckedVcdiffDecoder#applyDelta(byte[], String, String)} + * and {@link CheckedVcdiffDecoder#applyBase64Delta(String, String, String)}). + * @param newBase The byte[] to be set as new base + * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking + * when delta is applied using {@link CheckedVcdiffDecoder#applyDelta(byte[], String, String)} or + * {@link CheckedVcdiffDecoder#applyBase64Delta(String, String, String)} + * @throws IllegalArgumentException The provided {@code newBase} parameter is null + */ + public void setBase(byte[] newBase, String newBaseId) { + this.setBaseInternal(newBase); + this.baseId = newBaseId; + } + + /** + * Sets the base object used for the next delta application (see {@link CheckedVcdiffDecoder#applyDelta(byte[], String, String)} + * and {@link CheckedVcdiffDecoder#applyBase64Delta(String, String, String)}). + * @param newBase The string to be set as new base + * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking + * when delta is applied using {@link CheckedVcdiffDecoder#applyDelta(byte[], String, String)} or + * {@link CheckedVcdiffDecoder#applyBase64Delta(String, String, String)} + * @throws IllegalArgumentException The provided {@code newBase} parameter is null + */ + public void setBase(String newBase, String newBaseId) { + this.setBaseInternal(newBase); + this.baseId = newBaseId; + } + + /** + * Sets the base object used for the next delta application (see {@link CheckedVcdiffDecoder#applyDelta(byte[], String, String)} + * and {@link CheckedVcdiffDecoder#applyBase64Delta(String, String, String)}). + * @param newBase The base64 string to be set as new base + * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking + * when delta is applied using {@link CheckedVcdiffDecoder#applyDelta(byte[], String, String)} or + * {@link CheckedVcdiffDecoder#applyBase64Delta(String, String, String)} + * @throws IllegalArgumentException The provided {@code newBase} parameter is null + */ + public void setBase64Base(String newBase, String newBaseId) { + this.setBase64BaseInternal(newBase); + this.baseId = newBaseId; + } + + private void checkSequenceContinuity(String baseId) throws SequenceContinuityException { + if (!Objects.equals(this.baseId, baseId)) { + throw new SequenceContinuityException(this.baseId, baseId); + } + } +} diff --git a/delta-codec/src/main/java/io/ably/deltacodec/SequenceContinuityException.java b/delta-codec/src/main/java/io/ably/deltacodec/SequenceContinuityException.java index a91b871..3382c3c 100644 --- a/delta-codec/src/main/java/io/ably/deltacodec/SequenceContinuityException.java +++ b/delta-codec/src/main/java/io/ably/deltacodec/SequenceContinuityException.java @@ -5,6 +5,6 @@ */ public class SequenceContinuityException extends Exception { SequenceContinuityException(String expectedId, String actualId) { - super("Sequence continuity check failed - the provided id (" + expectedId + ") does not match the last preserved sequence id (" + actualId + ")"); + super("Sequence continuity check failed - the provided id (" + actualId + ") does not match the last preserved sequence id (" + expectedId + ")"); } } diff --git a/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java b/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java index 01dc3c8..7686fe2 100644 --- a/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java +++ b/delta-codec/src/main/java/io/ably/deltacodec/VcdiffDecoder.java @@ -1,224 +1,70 @@ package io.ably.deltacodec; -import com.davidehrmann.vcdiff.VCDiffDecoder; -import com.davidehrmann.vcdiff.VCDiffDecoderBuilder; - -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; /** * VCDIFF decoder capable of processing continuous sequences of consecutively generated VCDIFFs */ -public class VcdiffDecoder { - private final VCDiffDecoder decoder = VCDiffDecoderBuilder.builder().buildSimple(); - private byte[] base; - private String baseId; - - +public class VcdiffDecoder extends BaseVcdiffDecoder { /** * Applies the {@code delta} to the result of applying the previous delta or to the base data - * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase64Base(String)} - * before calling this method for the first time. - * @param delta The delta to be applied as base64 string - * @return {@link DeltaApplicationResult} instance - * @throws IOException Delta application failed - * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase64Base(String)} - * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF - */ - public DeltaApplicationResult applyDelta(String delta) throws IllegalStateException, IllegalArgumentException, IOException { - if (this.base == null) { - throw new IllegalStateException("Uninitialized decoder - setBase() should be called first"); - } - - byte[] deltaAsByteArray; - - deltaAsByteArray = tryConvertFromBase64String(delta); - - if(deltaAsByteArray == null) { - throw new IllegalStateException("The provided delta does not represent binary data"); - } - - if (!hasVcdiffHeader(deltaAsByteArray)) { - throw new IllegalArgumentException("The provided delta is not a valid VCDIFF delta"); - } - - return new DeltaApplicationResult(applyDeltaInternal(deltaAsByteArray)); - } - - private byte[] applyDeltaInternal(byte[] deltaAsByteArray) throws IOException { - ByteArrayOutputStream decoded = new ByteArrayOutputStream(); - this.decoder.decode(this.base, deltaAsByteArray, decoded); - this.base = decoded.toByteArray(); - // Return a copy to avoid future delta application failures if the returned array is modified - return decoded.toByteArray(); - } - - /** - * Applies the {@code delta} to the result of applying the previous delta or to the base data - * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(byte[])} - * before calling this method for the first time. + * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(byte[])}, + * {@link VcdiffDecoder#setBase(String)} or {@link VcdiffDecoder#setBase64Base(String)} before calling this + * method for the first time. * @param delta The delta to be applied * @return {@link DeltaApplicationResult} instance * @throws IOException Delta application failed - * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(byte[])} + * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(byte[])}, + * {@link VcdiffDecoder#setBase(String)} or {@link VcdiffDecoder#setBase64Base(String)} * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF */ public DeltaApplicationResult applyDelta(byte[] delta) throws IllegalStateException, IllegalArgumentException, IOException { - if (this.base == null) { - throw new IllegalStateException("Uninitialized decoder - setBase() should be called first"); - } - - if (delta == null || !hasVcdiffHeader(delta)) { - throw new IllegalArgumentException("The provided delta is not a valid VCDIFF delta"); - } - - return new DeltaApplicationResult(applyDeltaInternal(delta)); + return this.applyDeltaInternal(delta); } /** * Applies the {@code delta} to the result of applying the previous delta or to the base data - * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(String, String)} - * before calling this method for the first time. + * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(byte[])}, + * {@link VcdiffDecoder#setBase(String)} or {@link VcdiffDecoder#setBase64Base(String)} before calling this + * method for the first time. * @param delta The delta to be applied as base64 string * @return {@link DeltaApplicationResult} instance * @throws IOException Delta application failed - * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(String, String)} - * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF - * @throws SequenceContinuityException The provided {@code baseId} does not match the last preserved sequence ID - */ - public DeltaApplicationResult applyDelta(String delta, String deltaId, String baseId) throws SequenceContinuityException, IllegalStateException, IllegalArgumentException, IOException { - if (!Objects.equals(this.baseId, baseId)) { - throw new SequenceContinuityException(baseId, this.baseId); - } - DeltaApplicationResult result = this.applyDelta(delta); - this.baseId = deltaId; - return result; - } - - /** - * Applies the {@code delta} to the result of applying the previous delta or to the base data - * if no previous delta has been applied yet. Base data has to be set by {@link VcdiffDecoder#setBase(byte[], String)} - * before calling this method for the first time. - * @param delta The delta to be applied - * @return {@link DeltaApplicationResult} instance - * @throws IOException Delta application failed - * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(byte[], String)} + * @throws IllegalStateException The decoder is not initialized by calling {@link VcdiffDecoder#setBase(byte[])}, + * {@link VcdiffDecoder#setBase(String)} or {@link VcdiffDecoder#setBase64Base(String)} * @throws IllegalArgumentException The provided {@code delta} is not a valid VCDIFF - * @throws SequenceContinuityException The provided {@code baseId} does not match the last preserved sequence ID */ - public DeltaApplicationResult applyDelta(byte[] delta, String deltaId, String baseId) throws SequenceContinuityException, IllegalStateException, IllegalArgumentException, IOException { - if (!Objects.equals(this.baseId, baseId)) { - throw new SequenceContinuityException(baseId, this.baseId); - } - DeltaApplicationResult result = this.applyDelta(delta); - this.baseId = deltaId; - return result; + public DeltaApplicationResult applyBase64Delta(String delta) throws IllegalStateException, IllegalArgumentException, IOException { + return this.applyBase64DeltaInternal(delta); } /** - * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(byte[])}). + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(byte[])} and + * {@link VcdiffDecoder#applyBase64Delta(String)}). * @param newBase The byte[] to be set as new base * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ public void setBase(byte[] newBase) throws IllegalArgumentException { - if (newBase == null) { - throw new IllegalArgumentException("newBase cannot be null"); - } - - this.base = newBase; + this.setBaseInternal(newBase); } /** - * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String)}). + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(byte[])} and + * {@link VcdiffDecoder#applyBase64Delta(String)}). * @param newBase The string to be set as new base * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ public void setBase(String newBase) throws IllegalArgumentException { - if (newBase == null) { - throw new IllegalArgumentException("newBase cannot be null"); - } - - this.base = convertToByteArray(newBase); + this.setBaseInternal(newBase); } /** - * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String)}). + * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(byte[])} and + * {@link VcdiffDecoder#applyBase64Delta(String)}). * @param newBase The base64 string to be set as new base * @throws IllegalArgumentException The provided {@code newBase} parameter is null */ public void setBase64Base(String newBase) throws IllegalArgumentException { - if (newBase == null) { - throw new IllegalArgumentException("newBase cannot be null"); - } - - this.base = convertFromBase64String(newBase); - } - - /** - * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String, String, String)}). - * @param newBase The string to be set as new base - * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking - * when delta is applied using {@link VcdiffDecoder#applyDelta(String, String, String)} - * @throws IllegalArgumentException The provided {@code newBase} parameter is null - */ - public void setBase(String newBase, String newBaseId) { - this.setBase(newBase); - this.baseId = newBaseId; - } - - /** - * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(byte[], String, String)}). - * @param newBase The byte[] to be set as new base - * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking - * when delta is applied using {@link VcdiffDecoder#applyDelta(byte[], String, String)} - * @throws IllegalArgumentException The provided {@code newBase} parameter is null - */ - public void setBase(byte[] newBase, String newBaseId) { - this.setBase(newBase); - this.baseId = newBaseId; - } - - /** - * Sets the base object used for the next delta application (see {@link VcdiffDecoder#applyDelta(String, String, String)}). - * @param newBase The base64 string to be set as new base - * @param newBaseId (Optional) The {@code newBase}'s sequence ID, to be used for sequence continuity checking - * when delta is applied using {@link VcdiffDecoder#applyDelta(String, String, String)} - * @throws IllegalArgumentException The provided {@code newBase} parameter is null - */ - public void setBase64Base(String newBase, String newBaseId) { - this.setBase64Base(newBase); - this.baseId = newBaseId; - } - - private static boolean hasVcdiffHeader(byte[] delta) { - return delta[0] == (byte)0xd6 && - delta[1] == (byte)0xc3 && - delta[2] == (byte)0xc4 && - delta[3] == (byte)0; - } - - private static byte[] convertToByteArray(Object data) { - if (data instanceof byte[]) { - return (byte[])data; - } else if (data instanceof String) { - String dataAsString = (String)data; - return dataAsString.getBytes(StandardCharsets.UTF_8); - } else { - throw new IllegalArgumentException("Unsupported data type. Supported types: String, byte[]."); - } - } - - private static byte[] tryConvertFromBase64String(String str) { - try { - return convertFromBase64String(str); - } catch (IllegalArgumentException e) { - return null; - } - } - - private static byte[] convertFromBase64String(String str) { - return Base64Coder.decode(str); + this.setBase64BaseInternal(newBase); } } diff --git a/delta-codec/src/test/java/io/ably/deltacodec/CheckedVcdiffDecoderTest.java b/delta-codec/src/test/java/io/ably/deltacodec/CheckedVcdiffDecoderTest.java new file mode 100644 index 0000000..fcfaba7 --- /dev/null +++ b/delta-codec/src/test/java/io/ably/deltacodec/CheckedVcdiffDecoderTest.java @@ -0,0 +1,127 @@ +package io.ably.deltacodec; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.IOException; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class CheckedVcdiffDecoderTest { + private final String base64Base = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ="; + private final String base64Delta = "1sPEAAABGgAoOAAeBAEsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4TGgEeAA=="; + private final String base64ExpectedResult = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + private final byte[] byteArrayBase = 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 }; + private final byte[] byteArrayDelta = new byte[] { (byte)214, (byte)195, (byte)196, 0, 0, 1, 26, 0, 40, 56, 0, 30, 4, 1, 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, 19, 26, 1, 30, 0 }; + private final byte[] byteArrayExpectedResult = 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 }; + + private CheckedVcdiffDecoder checkedDecoder; + + @Before + public void createVcdiffDecoderInstance() { + checkedDecoder = new CheckedVcdiffDecoder(); + } + + @After + public void disposeVcdiffDecoderInstance() { + checkedDecoder = null; + } + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void setBaseThrowsIllegalArgumentExceptionWhenNewBaseIsNull() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("newBase cannot be null"); + this.checkedDecoder.setBase((byte[])null, null); + } + + @Test + public void applyDeltaThrowsIllegalStateExceptionWhenBaseIsNull() throws IOException, SequenceContinuityException { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Uninitialized decoder - setBase() should be called first"); + this.checkedDecoder.applyDelta((byte[])null, null, null); + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64Encoded() throws IOException, SequenceContinuityException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); + this.checkedDecoder.setBase("baseContent", null); + this.checkedDecoder.applyBase64Delta("!deltaContent", null, null); + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainVcdiffHeaderAndIsBase64EncodedArgumentIsFalse() throws IOException, SequenceContinuityException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); + this.checkedDecoder.setBase("baseContent", null); + this.checkedDecoder.applyDelta(new byte[1], null, null); + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64EncodedAndIsBase64EncodedArgumentIsTrue() throws IOException, SequenceContinuityException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); + this.checkedDecoder.setBase("baseContent", null); + this.checkedDecoder.applyBase64Delta("nonBase64Content", null, null); + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainVcdiffHeaderAndIsBase64EncodedArgumentIsTrue() throws IOException, SequenceContinuityException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); + this.checkedDecoder.setBase("baseContent", null); + this.checkedDecoder.applyBase64Delta("YmFzZTY0Q29udGVudA==", null, null); + } + + @Test + public void applyBase64DeltaReturnsDeltaResultWhenDeltaIsValid() throws IOException, SequenceContinuityException { + this.checkedDecoder.setBase64Base(base64Base, null); + DeltaApplicationResult deltaResult = this.checkedDecoder.applyBase64Delta(base64Delta, null, null); + + assertEquals(base64ExpectedResult, deltaResult.asUtf8String()); + } + + @Test + public void applyDeltaReturnsDeltaResultWhenDeltaIsValid() throws IOException, SequenceContinuityException { + this.checkedDecoder.setBase(byteArrayBase, null); + DeltaApplicationResult deltaResult = this.checkedDecoder.applyDelta(byteArrayDelta, null, null); + + assertArrayEquals(deltaResult.asByteArray(), byteArrayExpectedResult); + } + + @Test + public void applyBase64DeltaThrowsSequenceContinuityExceptionWhenBaseIdsDoesNotMatch() throws IOException, SequenceContinuityException { + thrown.expect(SequenceContinuityException.class); + thrown.expectMessage("Sequence continuity check failed - the provided id (3) does not match the last preserved sequence id (1)"); + this.checkedDecoder.setBase64Base(base64Base, "1"); + this.checkedDecoder.applyBase64Delta(base64Delta, "2", "3"); + } + + @Test + public void applyBase64DeltaDoesNotThrowSequenceContinuityExceptionWhenBaseIdsMatch() throws IOException, SequenceContinuityException { + this.checkedDecoder.setBase64Base(base64Base, "1"); + this.checkedDecoder.applyBase64Delta(base64Delta, "2", "1"); + } + + @Test + public void applyDeltaThrowsSequenceContinuityExceptionWhenBaseIdsDoesNotMatch() throws IOException, SequenceContinuityException { + thrown.expect(SequenceContinuityException.class); + thrown.expectMessage("Sequence continuity check failed - the provided id (3) does not match the last preserved sequence id (1)"); + this.checkedDecoder.setBase(byteArrayBase, "1"); + this.checkedDecoder.applyDelta(byteArrayDelta, "2", "3"); + } + + @Test + public void applyDeltaDoesNotThrowSequenceContinuityExceptionWhenBaseIdsMatch() throws IOException, SequenceContinuityException { + this.checkedDecoder.setBase(byteArrayBase, "1"); + this.checkedDecoder.applyDelta(byteArrayDelta, "2", "1"); + } +} + diff --git a/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java b/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java index 9d6aaaf..51eccd5 100644 --- a/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java +++ b/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java @@ -42,11 +42,11 @@ public void applyDeltaThrowsIllegalStateExceptionWhenBaseIsNull() throws IOExcep } @Test - public void applyDeltaThrowsIllegalStateExceptionWhenDeltaIsNotBase64Encoded() throws IOException { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("The provided delta does not represent binary data"); + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64Encoded() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); this.decoder.setBase("baseContent"); - this.decoder.applyDelta("!deltaContent"); + this.decoder.applyBase64Delta("!deltaContent"); } @Test @@ -62,7 +62,7 @@ public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64EncodedA thrown.expect(IllegalArgumentException.class); thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); this.decoder.setBase("baseContent"); - this.decoder.applyDelta("nonBase64Content"); + this.decoder.applyBase64Delta("nonBase64Content"); } @Test @@ -70,7 +70,7 @@ public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainsVcdi thrown.expect(IllegalArgumentException.class); thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); this.decoder.setBase("baseContent"); - this.decoder.applyDelta("YmFzZTY0Q29udGVudA=="); + this.decoder.applyBase64Delta("YmFzZTY0Q29udGVudA=="); } @Test @@ -80,7 +80,7 @@ public void applyDeltaReturnsDeltaResultWhenDeltaIsValidAndIsBase64EncodedArgume String expectedResult = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; this.decoder.setBase64Base(base); - DeltaApplicationResult deltaResult = this.decoder.applyDelta(delta); + DeltaApplicationResult deltaResult = this.decoder.applyBase64Delta(delta); assertEquals(expectedResult, deltaResult.asUtf8String()); } diff --git a/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java b/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java index dc165c4..728d4ef 100644 --- a/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java +++ b/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java @@ -1,6 +1,6 @@ package io.ably.deltasampleapp; -import io.ably.deltacodec.VcdiffDecoder; +import io.ably.deltacodec.CheckedVcdiffDecoder; import io.ably.lib.realtime.*; import io.ably.lib.types.AblyException; import io.ably.lib.types.Message; @@ -10,7 +10,7 @@ public class Main { public static void main(String[] args) throws AblyException { AblyRealtime ably = new AblyRealtime("HG2KVw.AjZP_A:W7VXUG9yw1-Cza6u"); Channel channel = ably.channels.get("[?delta=vcdiff]delta-sample-app"); - VcdiffDecoder channelDecoder = new VcdiffDecoder(); + CheckedVcdiffDecoder channelDecoder = new CheckedVcdiffDecoder(); ably.connection.on(ConnectionState.connected, new ConnectionStateListener() { @Override public void onConnectionStateChanged(ConnectionStateChange state) { From 1da2fbb2265d0d482dc4b8e2e34e93dd795f86ea Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Fri, 24 Jan 2020 16:30:17 +0200 Subject: [PATCH 06/14] Making BaseVcdiffDecoder abstract --- .../src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java b/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java index 88313ba..1699da2 100644 --- a/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java +++ b/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java @@ -7,7 +7,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -class BaseVcdiffDecoder { +abstract class BaseVcdiffDecoder { private final VCDiffDecoder decoder = VCDiffDecoderBuilder.builder().buildSimple(); private byte[] base; From 5d9fdde67977eb8377248933158dbe654c83f272 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Mon, 27 Jan 2020 21:06:47 +0200 Subject: [PATCH 07/14] Adding tests and minor bug fixes --- .../io/ably/deltacodec/BaseVcdiffDecoder.java | 15 +- .../deltacodec/BaseVcdiffDecoderTests.java | 183 ++++++++++++++++++ .../deltacodec/CheckedVcdiffDecoderTest.java | 137 ++++++------- .../io/ably/deltacodec/VcdiffDecoderTest.java | 90 ++------- 4 files changed, 276 insertions(+), 149 deletions(-) create mode 100644 delta-codec/src/test/java/io/ably/deltacodec/BaseVcdiffDecoderTests.java diff --git a/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java b/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java index 1699da2..b2187b3 100644 --- a/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java +++ b/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java @@ -36,7 +36,7 @@ protected void setBaseInternal(byte[] newBase) throws IllegalArgumentException { } protected void setBaseInternal(String newBase) throws IllegalArgumentException { - this.setBaseInternal(convertToByteArray(newBase)); + this.setBaseInternal(tryConvertToByteArray(newBase)); } protected void setBase64BaseInternal(String newBase) throws IllegalArgumentException { @@ -52,17 +52,26 @@ private byte[] doApplyDelta(byte[] deltaAsByteArray) throws IOException { } private static boolean hasVcdiffHeader(byte[] delta) { + if (delta.length <= 4) { + return false; + } return delta[0] == (byte)0xd6 && delta[1] == (byte)0xc3 && delta[2] == (byte)0xc4 && delta[3] == (byte)0; } - private static byte[] convertToByteArray(String data) { - return data.getBytes(StandardCharsets.UTF_8); + private static byte[] tryConvertToByteArray(String str) { + if (str == null) { + return null; + } + return str.getBytes(StandardCharsets.UTF_8); } private static byte[] tryConvertFromBase64String(String str) { + if (str == null) { + return null; + } try { return Base64Coder.decode(str); } catch (IllegalArgumentException e) { diff --git a/delta-codec/src/test/java/io/ably/deltacodec/BaseVcdiffDecoderTests.java b/delta-codec/src/test/java/io/ably/deltacodec/BaseVcdiffDecoderTests.java new file mode 100644 index 0000000..f532573 --- /dev/null +++ b/delta-codec/src/test/java/io/ably/deltacodec/BaseVcdiffDecoderTests.java @@ -0,0 +1,183 @@ +package io.ably.deltacodec; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.IOException; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; + +public abstract class BaseVcdiffDecoderTests { + protected final String stringBase = "Lorem ipsum dolor sit amet"; + protected final String base64Base = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ="; + protected final String base64Delta = "1sPEAAABGgAoOAAeBAEsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4TGgEeAA=="; + protected final String base64SecondDelta = "1sPEAAABOABFcwA7BAEgRnVzY2UgaWQgbnVsbGEgbGFjaW5pYSwgdm9sdXRwYXQgb2RpbyB1dCwgdWx0cmljZXMgbGlndWxhLhM4ATsA"; + protected final byte[] base = 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 }; + protected final byte[] delta = new byte[] { (byte)214, (byte)195, (byte)196, 0, 0, 1, 26, 0, 40, 56, 0, 30, 4, 1, 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, 19, 26, 1, 30, 0 }; + protected final byte[] expectedResult = 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 }; + protected final byte[] secondDelta = new byte[] { (byte)214, (byte)195, (byte)196, 0, 0, 1, 56, 0, 69, 115, 0, 59, 4, 1, 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, 19, 56, 1, 59, 0 }; + protected final byte[] secondExpectedResult = 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 }; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + protected abstract DeltaApplicationResult applyDelta(byte[] delta) throws IOException, SequenceContinuityException; + protected abstract DeltaApplicationResult applyBase64Delta(String delta) throws IOException, SequenceContinuityException; + protected abstract void setBase(byte[] newBase); + protected abstract void setBase(String newBase); + protected abstract void setBase64Base(String newBase); + + @Test + public void applyDeltaThrowsIllegalStateExceptionWhenBaseIsNull() throws IOException, SequenceContinuityException { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Uninitialized decoder - setBase() should be called first"); + this.applyDelta(null); + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNull() throws IOException, SequenceContinuityException { + this.expectApplyDeltaToThrowIllegalArgumentExceptionForDelta(null); + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotVcdiffDelta1() throws IOException, SequenceContinuityException { + this.expectApplyDeltaToThrowIllegalArgumentExceptionForDelta(new byte[] { 1, 2, 3, 4 }); + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotVcdiffDelta2() throws IOException, SequenceContinuityException { + this.expectApplyDeltaToThrowIllegalArgumentExceptionForDelta(new byte[] { 1 }); + } + + @Test + public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotVcdiffDelta3() throws IOException, SequenceContinuityException { + this.expectApplyDeltaToThrowIllegalArgumentExceptionForDelta(new byte[] { (byte)214 }); + } + + private void expectApplyDeltaToThrowIllegalArgumentExceptionForDelta(byte[] delta) throws IOException, SequenceContinuityException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); + this.setBase("baseContent"); + this.applyDelta(delta); + } + + @Test + public void applyDeltaReturnsDeltaResultWhenDeltaIsValid() throws IOException, SequenceContinuityException { + this.setBase(this.base); + DeltaApplicationResult result = this.applyDelta(this.delta); + assertNotNull(result); + assertArrayEquals(this.expectedResult, result.asByteArray()); + } + + @Test + public void applyDeltaShouldSetBaseProperlyForChaining() throws IOException, SequenceContinuityException { + this.setBase(this.base); + this.applyDelta(this.delta); + DeltaApplicationResult result = this.applyDelta(this.secondDelta); + assertNotNull(result); + assertArrayEquals(this.secondExpectedResult, result.asByteArray()); + } + + @Test + public void applyBase64DeltaThrowsIllegalStateExceptionWhenBaseIsNull() throws IOException, SequenceContinuityException { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Uninitialized decoder - setBase() should be called first"); + this.applyBase64Delta(null); + } + + @Test + public void applyBase64DeltaThrowsIllegalArgumentExceptionWhenDeltaIsNull() throws IOException, SequenceContinuityException { + this.expectApplyBase64DeltaToThrowIllegalArgumentExceptionForDelta(null); + } + + @Test + public void applyBase64DeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64EncodedString() throws IOException, SequenceContinuityException { + this.expectApplyBase64DeltaToThrowIllegalArgumentExceptionForDelta("!base64EncodedDeltaContent"); + } + + @Test + public void applyBase64DeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotVcdiffDelta1() throws IOException, SequenceContinuityException { + this.expectApplyBase64DeltaToThrowIllegalArgumentExceptionForDelta("AQIDBA=="); // 1, 2, 3, 4 + } + + @Test + public void applyBase64DeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotVcdiffDelta2() throws IOException, SequenceContinuityException { + this.expectApplyBase64DeltaToThrowIllegalArgumentExceptionForDelta("AQ=="); // 1 + } + + @Test + public void applyBase64DeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotVcdiffDelta3() throws IOException, SequenceContinuityException { + this.expectApplyBase64DeltaToThrowIllegalArgumentExceptionForDelta("1g=="); // 214 (0xd6) + } + + private void expectApplyBase64DeltaToThrowIllegalArgumentExceptionForDelta(String delta) throws IOException, SequenceContinuityException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); + this.setBase("baseContent"); + this.applyBase64Delta(delta); + } + + @Test + public void applyBase64DeltaReturnsDeltaResultWhenDeltaIsValid() throws IOException, SequenceContinuityException { + this.setBase(this.base); + DeltaApplicationResult result = this.applyBase64Delta(this.base64Delta); + assertNotNull(result); + assertArrayEquals(this.expectedResult, result.asByteArray()); + } + + @Test + public void applyBase64DeltaShouldSetBaseProperlyForChaining() throws IOException, SequenceContinuityException { + this.setBase(this.base); + this.applyBase64Delta(this.base64Delta); + DeltaApplicationResult result = this.applyBase64Delta(this.base64SecondDelta); + assertNotNull(result); + assertArrayEquals(this.secondExpectedResult, result.asByteArray()); + } + + @Test + public void setBaseByteArrayThrowsIllegalArgumentExceptionWhenNewBaseIsNull() { + thrown.expectMessage("newBase cannot be null"); + thrown.expect(IllegalArgumentException.class); + this.setBase((byte[])null); + } + + @Test + public void setBaseStringThrowsIllegalArgumentExceptionWhenNewBaseIsNull() { + thrown.expectMessage("newBase cannot be null"); + thrown.expect(IllegalArgumentException.class); + this.setBase((String)null); + } + + @Test + public void setBase64BaseThrowsIllegalArgumentExceptionWhenNewBaseIsNull() { + thrown.expectMessage("newBase cannot be null"); + thrown.expect(IllegalArgumentException.class); + this.setBase64Base(null); + } + + @Test + public void setBaseByteArrayShouldSetBaseProperly() throws IOException, SequenceContinuityException { + this.setBase(this.base); + DeltaApplicationResult result = this.applyDelta(this.delta); + assertNotNull(result); + assertArrayEquals(this.expectedResult, result.asByteArray()); + } + + @Test + public void setBaseStringShouldSetBaseProperly() throws IOException, SequenceContinuityException { + this.setBase(this.stringBase); + DeltaApplicationResult result = this.applyDelta(this.delta); + assertNotNull(result); + assertArrayEquals(this.expectedResult, result.asByteArray()); + } + + @Test + public void setBase64BaseShouldSetBaseProperly() throws IOException, SequenceContinuityException { + this.setBase64Base(this.base64Base); + DeltaApplicationResult result = this.applyDelta(this.delta); + assertNotNull(result); + assertArrayEquals(this.expectedResult, result.asByteArray()); + } +} diff --git a/delta-codec/src/test/java/io/ably/deltacodec/CheckedVcdiffDecoderTest.java b/delta-codec/src/test/java/io/ably/deltacodec/CheckedVcdiffDecoderTest.java index fcfaba7..45ddb2c 100644 --- a/delta-codec/src/test/java/io/ably/deltacodec/CheckedVcdiffDecoderTest.java +++ b/delta-codec/src/test/java/io/ably/deltacodec/CheckedVcdiffDecoderTest.java @@ -2,126 +2,115 @@ import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import java.io.IOException; import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; -public class CheckedVcdiffDecoderTest { - private final String base64Base = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ="; - private final String base64Delta = "1sPEAAABGgAoOAAeBAEsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4TGgEeAA=="; - private final String base64ExpectedResult = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; - private final byte[] byteArrayBase = 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 }; - private final byte[] byteArrayDelta = new byte[] { (byte)214, (byte)195, (byte)196, 0, 0, 1, 26, 0, 40, 56, 0, 30, 4, 1, 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, 19, 26, 1, 30, 0 }; - private final byte[] byteArrayExpectedResult = 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 }; +public class CheckedVcdiffDecoderTest extends BaseVcdiffDecoderTests { + private final String baseId = "baseId"; + private final String deltaId = "deltaId"; + private final String secondDeltaId = "secondDeltaId"; + private final String invalidBaseId = "invalidBaseId"; private CheckedVcdiffDecoder checkedDecoder; @Before public void createVcdiffDecoderInstance() { - checkedDecoder = new CheckedVcdiffDecoder(); + this.checkedDecoder = new CheckedVcdiffDecoder(); } @After public void disposeVcdiffDecoderInstance() { - checkedDecoder = null; + this.checkedDecoder = null; } - @Rule - public ExpectedException thrown = ExpectedException.none(); + @Override + protected DeltaApplicationResult applyDelta(byte[] delta) throws IOException, SequenceContinuityException { + return this.checkedDecoder.applyDelta(delta, null, null); + } - @Test - public void setBaseThrowsIllegalArgumentExceptionWhenNewBaseIsNull() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("newBase cannot be null"); - this.checkedDecoder.setBase((byte[])null, null); + @Override + protected DeltaApplicationResult applyBase64Delta(String delta) throws IOException, SequenceContinuityException { + return this.checkedDecoder.applyBase64Delta(delta, null, null); } - @Test - public void applyDeltaThrowsIllegalStateExceptionWhenBaseIsNull() throws IOException, SequenceContinuityException { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Uninitialized decoder - setBase() should be called first"); - this.checkedDecoder.applyDelta((byte[])null, null, null); + @Override + protected void setBase(byte[] newBase) { + this.checkedDecoder.setBase(newBase, null); } - @Test - public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64Encoded() throws IOException, SequenceContinuityException { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); - this.checkedDecoder.setBase("baseContent", null); - this.checkedDecoder.applyBase64Delta("!deltaContent", null, null); + @Override + protected void setBase(String newBase) { + this.checkedDecoder.setBase(newBase, null); } - @Test - public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainVcdiffHeaderAndIsBase64EncodedArgumentIsFalse() throws IOException, SequenceContinuityException { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); - this.checkedDecoder.setBase("baseContent", null); - this.checkedDecoder.applyDelta(new byte[1], null, null); + @Override + protected void setBase64Base(String newBase) { + this.checkedDecoder.setBase64Base(newBase, null); } @Test - public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64EncodedAndIsBase64EncodedArgumentIsTrue() throws IOException, SequenceContinuityException { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); - this.checkedDecoder.setBase("baseContent", null); - this.checkedDecoder.applyBase64Delta("nonBase64Content", null, null); + public void applyDeltaThrowsSequenceContinuityExceptionWhenProvidedBaseIdDoesNotMatchTheOneSetBySetBase() throws IOException, SequenceContinuityException { + thrown.expect(SequenceContinuityException.class); + thrown.expectMessage(this.getSequenceContinuityExceptionMessage(this.baseId, this.invalidBaseId)); + this.checkedDecoder.setBase("baseContent", this.baseId); + this.checkedDecoder.applyDelta(null, null, this.invalidBaseId); } @Test - public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainVcdiffHeaderAndIsBase64EncodedArgumentIsTrue() throws IOException, SequenceContinuityException { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); - this.checkedDecoder.setBase("baseContent", null); - this.checkedDecoder.applyBase64Delta("YmFzZTY0Q29udGVudA==", null, null); + public void applyDeltaShouldSetBaseIdProperlyForChaining() throws IOException, SequenceContinuityException { + this.checkedDecoder.setBase(this.base, this.baseId); + this.checkedDecoder.applyDelta(this.delta, this.deltaId, this.baseId); + DeltaApplicationResult result = this.checkedDecoder.applyDelta(this.secondDelta, this.secondDeltaId, this.deltaId); + assertNotNull(result); + assertArrayEquals(this.secondExpectedResult, result.asByteArray()); } @Test - public void applyBase64DeltaReturnsDeltaResultWhenDeltaIsValid() throws IOException, SequenceContinuityException { - this.checkedDecoder.setBase64Base(base64Base, null); - DeltaApplicationResult deltaResult = this.checkedDecoder.applyBase64Delta(base64Delta, null, null); - - assertEquals(base64ExpectedResult, deltaResult.asUtf8String()); + public void applyBase64DeltaThrowsSequenceContinuityExceptionWhenProvidedBaseIdDoesNotMatchTheOneSetBySetBase() throws IOException, SequenceContinuityException { + thrown.expect(SequenceContinuityException.class); + thrown.expectMessage(this.getSequenceContinuityExceptionMessage(this.baseId, this.invalidBaseId)); + this.checkedDecoder.setBase("baseContent", this.baseId); + this.checkedDecoder.applyBase64Delta(null, null, this.invalidBaseId); } @Test - public void applyDeltaReturnsDeltaResultWhenDeltaIsValid() throws IOException, SequenceContinuityException { - this.checkedDecoder.setBase(byteArrayBase, null); - DeltaApplicationResult deltaResult = this.checkedDecoder.applyDelta(byteArrayDelta, null, null); - - assertArrayEquals(deltaResult.asByteArray(), byteArrayExpectedResult); + public void applyBase64DeltaShouldSetBaseIdProperlyForChaining() throws IOException, SequenceContinuityException { + this.checkedDecoder.setBase(this.base, this.baseId); + this.checkedDecoder.applyBase64Delta(this.base64Delta, this.deltaId, this.baseId); + DeltaApplicationResult result = this.checkedDecoder.applyBase64Delta(this.base64SecondDelta, this.secondDeltaId, this.deltaId); + assertNotNull(result); + assertArrayEquals(this.secondExpectedResult, result.asByteArray()); } - @Test - public void applyBase64DeltaThrowsSequenceContinuityExceptionWhenBaseIdsDoesNotMatch() throws IOException, SequenceContinuityException { - thrown.expect(SequenceContinuityException.class); - thrown.expectMessage("Sequence continuity check failed - the provided id (3) does not match the last preserved sequence id (1)"); - this.checkedDecoder.setBase64Base(base64Base, "1"); - this.checkedDecoder.applyBase64Delta(base64Delta, "2", "3"); + private String getSequenceContinuityExceptionMessage(String expectedId, String actualId) { + return "Sequence continuity check failed - the provided id (" + actualId + ") does not match the last preserved sequence id (" + expectedId + ")"; } @Test - public void applyBase64DeltaDoesNotThrowSequenceContinuityExceptionWhenBaseIdsMatch() throws IOException, SequenceContinuityException { - this.checkedDecoder.setBase64Base(base64Base, "1"); - this.checkedDecoder.applyBase64Delta(base64Delta, "2", "1"); + public void setBaseByteArrayShouldSetBaseIdProperly() throws IOException, SequenceContinuityException { + this.checkedDecoder.setBase(this.base, this.baseId); + DeltaApplicationResult result = this.checkedDecoder.applyDelta(this.delta, this.deltaId, this.baseId); + assertNotNull(result); + assertArrayEquals(this.expectedResult, result.asByteArray()); } @Test - public void applyDeltaThrowsSequenceContinuityExceptionWhenBaseIdsDoesNotMatch() throws IOException, SequenceContinuityException { - thrown.expect(SequenceContinuityException.class); - thrown.expectMessage("Sequence continuity check failed - the provided id (3) does not match the last preserved sequence id (1)"); - this.checkedDecoder.setBase(byteArrayBase, "1"); - this.checkedDecoder.applyDelta(byteArrayDelta, "2", "3"); + public void setBaseStringShouldSetBaseIdProperly() throws IOException, SequenceContinuityException { + this.checkedDecoder.setBase(this.stringBase, this.baseId); + DeltaApplicationResult result = this.checkedDecoder.applyDelta(this.delta, this.deltaId, this.baseId); + assertNotNull(result); + assertArrayEquals(this.expectedResult, result.asByteArray()); } @Test - public void applyDeltaDoesNotThrowSequenceContinuityExceptionWhenBaseIdsMatch() throws IOException, SequenceContinuityException { - this.checkedDecoder.setBase(byteArrayBase, "1"); - this.checkedDecoder.applyDelta(byteArrayDelta, "2", "1"); + public void setBase64BaseShouldSetBaseIdProperly() throws IOException, SequenceContinuityException { + this.checkedDecoder.setBase64Base(this.base64Base, this.baseId); + DeltaApplicationResult result = this.checkedDecoder.applyDelta(this.delta, this.deltaId, this.baseId); + assertNotNull(result); + assertArrayEquals(this.expectedResult, result.asByteArray()); } } - diff --git a/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java b/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java index 51eccd5..19fd862 100644 --- a/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java +++ b/delta-codec/src/test/java/io/ably/deltacodec/VcdiffDecoderTest.java @@ -1,99 +1,45 @@ package io.ably.deltacodec; -import org.junit.Rule; -import org.junit.Test; import org.junit.After; import org.junit.Before; -import org.junit.rules.ExpectedException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertArrayEquals; import java.io.IOException; -public class VcdiffDecoderTest { +public class VcdiffDecoderTest extends BaseVcdiffDecoderTests { private VcdiffDecoder decoder; @Before public void createVcdiffDecoderInstance() { - decoder = new VcdiffDecoder(); + this.decoder = new VcdiffDecoder(); } @After public void disposeVcdiffDecoderInstance() { - decoder = null; - } - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void setBaseThrowsIllegalArgumentExceptionWhenNewBaseIsNull() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("newBase cannot be null"); - this.decoder.setBase((byte[])null); - } - - @Test - public void applyDeltaThrowsIllegalStateExceptionWhenBaseIsNull() throws IOException { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Uninitialized decoder - setBase() should be called first"); - this.decoder.applyDelta((byte[])null); + this.decoder = null; } - @Test - public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64Encoded() throws IOException { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); - this.decoder.setBase("baseContent"); - this.decoder.applyBase64Delta("!deltaContent"); + @Override + protected DeltaApplicationResult applyDelta(byte[] delta) throws IOException { + return this.decoder.applyDelta(delta); } - @Test - public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainVcdiffHeaderAndIsBase64EncodedArgumentIsFalse() throws IOException { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); - this.decoder.setBase("baseContent"); - this.decoder.applyDelta(new byte[1]); + @Override + protected DeltaApplicationResult applyBase64Delta(String delta) throws IOException { + return this.decoder.applyBase64Delta(delta); } - @Test - public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaIsNotBase64EncodedAndIsBase64EncodedArgumentIsTrue() throws IOException { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); - this.decoder.setBase("baseContent"); - this.decoder.applyBase64Delta("nonBase64Content"); + @Override + protected void setBase(byte[] newBase) { + this.decoder.setBase(newBase); } - @Test - public void applyDeltaThrowsIllegalArgumentExceptionWhenDeltaDoesNotContainsVcdiffHeaderAndIsBase64EncodedArgumentIsTrue() throws IOException { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("The provided delta is not a valid VCDIFF delta"); - this.decoder.setBase("baseContent"); - this.decoder.applyBase64Delta("YmFzZTY0Q29udGVudA=="); + @Override + protected void setBase(String newBase) { + this.decoder.setBase(newBase); } - @Test - public void applyDeltaReturnsDeltaResultWhenDeltaIsValidAndIsBase64EncodedArgumentIsTrue() throws IOException { - String base = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ="; - String delta = "1sPEAAABGgAoOAAeBAEsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4TGgEeAA=="; - String expectedResult = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; - - this.decoder.setBase64Base(base); - DeltaApplicationResult deltaResult = this.decoder.applyBase64Delta(delta); - - assertEquals(expectedResult, deltaResult.asUtf8String()); - } - - @Test - public void applyDeltaReturnsDeltaResultWhenDeltaIsValidAndIsBase64EncodedArgumentIsFalse() throws IOException { - byte[] base = 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 }; - byte[] delta = new byte[] { (byte)214, (byte)195, (byte)196, 0, 0, 1, 26, 0, 40, 56, 0, 30, 4, 1, 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, 19, 26, 1, 30, 0 }; - byte[] expectedResult = 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 }; - - this.decoder.setBase(base); - DeltaApplicationResult deltaResult = this.decoder.applyDelta(delta); - - assertArrayEquals(deltaResult.asByteArray(), expectedResult); + @Override + protected void setBase64Base(String newBase) { + this.decoder.setBase64Base(newBase); } } From 51fe825105eaa68cc408dd88fa3ffda2d2f78a12 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Wed, 12 Feb 2020 20:23:28 +0200 Subject: [PATCH 08/14] Removing old sample app --- delta-sample-app/build.gradle | 19 ----- .../java/io/ably/deltasampleapp/Main.java | 78 ------------------- settings.gradle | 2 +- 3 files changed, 1 insertion(+), 98 deletions(-) delete mode 100644 delta-sample-app/build.gradle delete mode 100644 delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java diff --git a/delta-sample-app/build.gradle b/delta-sample-app/build.gradle deleted file mode 100644 index 32ce1fa..0000000 --- a/delta-sample-app/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -plugins { - id 'java' -} - -group 'io.ably' -version '1.0.0' - -sourceCompatibility = 1.8 - -repositories { - mavenCentral() - jcenter() -} - -dependencies { - implementation project(':delta-codec') - implementation 'io.ably:ably-java:1.1.3' - implementation 'com.google.code.gson:gson:2.8.5' -} diff --git a/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java b/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java deleted file mode 100644 index 728d4ef..0000000 --- a/delta-sample-app/src/main/java/io/ably/deltasampleapp/Main.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.ably.deltasampleapp; - -import io.ably.deltacodec.CheckedVcdiffDecoder; -import io.ably.lib.realtime.*; -import io.ably.lib.types.AblyException; -import io.ably.lib.types.Message; -import io.ably.lib.util.Serialisation; - -public class Main { - public static void main(String[] args) throws AblyException { - AblyRealtime ably = new AblyRealtime("HG2KVw.AjZP_A:W7VXUG9yw1-Cza6u"); - Channel channel = ably.channels.get("[?delta=vcdiff]delta-sample-app"); - CheckedVcdiffDecoder channelDecoder = new CheckedVcdiffDecoder(); - ably.connection.on(ConnectionState.connected, new ConnectionStateListener() { - @Override - public void onConnectionStateChanged(ConnectionStateChange state) { - try { - channel.subscribe(new ChannelBase.MessageListener() { - @Override - public void onMessage(Message message) { - String data = (String)message.data; - try { - MessageExtras extras = Serialisation.gson.fromJson(message.extras, MessageExtras.class); - if (extras != null && extras.delta != null) { - data = channelDecoder.applyDelta(data, message.id, extras.delta.from).asUtf8String(); - } else { - channelDecoder.setBase(data, message.id); - } - } catch (Exception e) { - /* Delta decoder error */ - } - - /* Process decoded data */ - System.out.println(Serialisation.gson.fromJson((String)data, Data.class).toString()); - } - }); - } catch (AblyException e) { - /* Subscribe error */ - } - - Data data = new Data(); - data.foo = "bar"; - data.count = 1; - data.status = "active"; - - try { - channel.publish("data", Serialisation.gson.toJson(data)); - data.count++; - channel.publish("data", Serialisation.gson.toJson(data)); - data.status = "inactive"; - channel.publish("data", Serialisation.gson.toJson(data)); - } catch (Exception e) { - /* Publish error */ - } - } - }); - } - - private class MessageExtras { - public DeltaExtras delta; - - private class DeltaExtras { - public String format; - public String from; - } - } - - private static class Data { - public String foo; - public int count; - public String status; - - @Override - public String toString() { - return "foo = " + this.foo + "; count = " + this.count + "; status = " + this.status; - } - } -} diff --git a/settings.gradle b/settings.gradle index 403d1e4..e5f5d7d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ rootProject.name = 'delta-codec-java' include 'delta-codec' -include 'delta-sample-app' + From 90cec2a0e447058f164e3e033b622f610f531143 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Wed, 12 Feb 2020 20:33:55 +0200 Subject: [PATCH 09/14] Adding isDelta methods to the API. Adding MQTT sample apps --- .../io/ably/deltacodec/BaseVcdiffDecoder.java | 10 ++- sample-apps/mqtt-binary/build.gradle | 16 ++++ .../main/java/io/ably/mqtt_binary/Main.java | 85 +++++++++++++++++++ sample-apps/mqtt-string/build.gradle | 16 ++++ .../main/java/io/ably/mqtt_string/Main.java | 84 ++++++++++++++++++ settings.gradle | 4 + 6 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 sample-apps/mqtt-binary/build.gradle create mode 100644 sample-apps/mqtt-binary/src/main/java/io/ably/mqtt_binary/Main.java create mode 100644 sample-apps/mqtt-string/build.gradle create mode 100644 sample-apps/mqtt-string/src/main/java/io/ably/mqtt_string/Main.java diff --git a/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java b/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java index b2187b3..727141f 100644 --- a/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java +++ b/delta-codec/src/main/java/io/ably/deltacodec/BaseVcdiffDecoder.java @@ -11,6 +11,14 @@ abstract class BaseVcdiffDecoder { private final VCDiffDecoder decoder = VCDiffDecoderBuilder.builder().buildSimple(); private byte[] base; + public static boolean isDelta(byte[] data) { + return hasVcdiffHeader(data); + } + + public static boolean isBase64Delta(String data) { + return hasVcdiffHeader(tryConvertFromBase64String(data)); + } + protected DeltaApplicationResult applyDeltaInternal(byte[] delta) throws IllegalStateException, IllegalArgumentException, IOException { if (this.base == null) { throw new IllegalStateException("Uninitialized decoder - setBase() should be called first"); @@ -52,7 +60,7 @@ private byte[] doApplyDelta(byte[] deltaAsByteArray) throws IOException { } private static boolean hasVcdiffHeader(byte[] delta) { - if (delta.length <= 4) { + if (delta == null || delta.length <= 4) { return false; } return delta[0] == (byte)0xd6 && 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 index e5f5d7d..dae1888 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +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' From d27ee6ed49fa509e23e358b808cc02cb596e3a11 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Wed, 12 Feb 2020 21:58:03 +0200 Subject: [PATCH 10/14] Adding Travis CI build definition --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6da7c1b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +dist: trusty +language: java +jdk: openjdk7 From c7b463208028f8a302a8218f8bb3545de83d0b06 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Wed, 12 Feb 2020 22:03:31 +0200 Subject: [PATCH 11/14] Fixing build --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6da7c1b..07bc253 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,6 @@ dist: trusty language: java jdk: openjdk7 + +before_install: + - chmod +x gradlew \ No newline at end of file From 76d07867d951ac184cf7e79ade19c749727bc35a Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Wed, 12 Feb 2020 22:09:44 +0200 Subject: [PATCH 12/14] Fixing build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 07bc253..02f9b20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ dist: trusty language: java -jdk: openjdk7 +jdk: oraclejdk8 before_install: - chmod +x gradlew \ No newline at end of file From 6c554e7a66073ad0505ee031fefb7311ac0a9f75 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Wed, 12 Feb 2020 22:26:53 +0200 Subject: [PATCH 13/14] Fixing build --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 02f9b20..759add5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,11 @@ +os: linux dist: trusty language: java jdk: oraclejdk8 before_install: - - chmod +x gradlew \ No newline at end of file + - chmod +x gradlew + +branches: + only: + - master \ No newline at end of file From d7e6470091a8cab7964179512140a61503ac5ef2 Mon Sep 17 00:00:00 2001 From: Tsviatko Yovtchev Date: Mon, 9 Mar 2020 10:08:21 +0200 Subject: [PATCH 14/14] Updating readme. Adding non-Ably usage samples. --- Readme.md | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 141 insertions(+), 4 deletions(-) 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 ##