├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main └── java │ └── jstatebox │ ├── core │ ├── Operation.java │ └── Statebox.java │ └── io │ ├── ClassLoaderAwareObjectInputStream.java │ ├── OperationCodec.java │ ├── OperationCodecMapper.java │ ├── groovy │ ├── GroovyOperationCodec.java │ └── GroovyOperationCodecMapper.java │ └── java │ ├── JavaOperationCodec.java │ └── JavaOperationCodecMapper.java └── test ├── groovy └── jstatebox │ └── core │ ├── SerializableOperation.groovy │ └── StateboxSpec.groovy └── resources └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | *.i* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JStatebox - A statebox implementation for the JVM 2 | 3 | JStatebox is a JVM-based implementation of the Erlang statebox utility, which is likened 4 | to a Monad. It allows asynchronous JVM applications to alter the state of objects without 5 | blocking and by resolving conflicts at read time, rather than write time. 6 | 7 | The original implementation of statebox resides here: 8 | 9 | https://github.com/mochi/statebox 10 | 11 | ### Language Support 12 | 13 | This statebox implementation natively understands Java, Groovy, Scala, and Clojure. 14 | This means you can use any of: 15 | 16 | 1. An anonymous class derived from a special JStatebox interface. 17 | 2. A Groovy closure. 18 | 3. A Scala anonymous function. 19 | 4. A Clojure function. 20 | 5. A Runnable (which returns null). 21 | 6. A Callable (which returns a value). 22 | 23 | What this means for your app is higher overall throughput because there's no synchronization 24 | and, unless you do it on purpose in your handler, no blocking. This is probably only really 25 | useful if you're doing an asynchronous or non-blocking app and you need to safely mutate the 26 | state of objects without killing your performance and scalability by synchronizing access to 27 | shared resources. 28 | 29 | Generally this will involve mutating Lists, Sets, Maps and the like. But in this example, 30 | we'll demonstrate how to safely mutate a string using a statebox. 31 | 32 | ### Usage 33 | 34 | To create a statebox to manage a value, use a factory method: 35 | 36 | def state1 = Statebox.create("Hello") 37 | 38 | To mutate the value stored inside `state1`, you call the `modify` method and pass it an 39 | operation. If the Groovy runtime is available, then you can just pass a closure. In pure Java, 40 | you'd need to implement an anonymous inner class derived from the Operation interface. 41 | 42 | def state2 = state1.modify({ s -> s + " " }) 43 | 44 | In Java, you would pass an Operation: 45 | 46 | Operation op = new Operation() { 47 | public String invoke(String s) { 48 | return s + " World!"; 49 | } 50 | } 51 | Statebox state2 = state1.modify(op); 52 | 53 | And in Scala, you'd use an anonymous function: 54 | 55 | val state2 = state1.modify((s: String) => s + " World!") 56 | 57 | There are no wrappers required for this functionality. There is special code inside JStatebox 58 | that understands what to do with Groovy closures and Scala or Clojure functions (if the respective 59 | runtimes are available in the classpath). 60 | 61 | The value you return from this operation will be the new value of `state2`. If you modified 62 | `state1` again by calling the `modify` method with a new operation, you'll get an entirely new 63 | statebox with an entirely new value. 64 | 65 | def state3 = state1.modify({ s -> s + "World!" }) 66 | 67 | `state3`'s value is "HelloWorld!" (without the space added in `state2`). To merge the various 68 | stateboxes into a single value, call the `merge` method. 69 | 70 | def state4 = state1.merge(state2, state3) 71 | 72 | To get the value of `state4`, call the `value()` method. 73 | 74 | def greeting = state4.value() 75 | 76 | The value of `state4` is now "Hello World!". It is the composition of all the operations performed 77 | on all the stateboxes you merged. They are applied in order based on the timestamp. Operations 78 | added to a statebox within 1ms of another operation are still performed, but the order in which 79 | they are performed is undefined. 80 | 81 | ### Distributed Use 82 | 83 | There is a codec facility within JStatebox that will serialize a statebox and it's operations so you 84 | can transmit that serialized form to another JVM to be further mutated. So far it understands how 85 | to serialize Java and Groovy. When setting a Groovy closure as an operation on a statebox, keep in 86 | mind that, in order to properly serialize the closure, we have to wipe out the `delegate` and `owner`. 87 | Your closure then, even though it's defined within the context of another class, will not be 88 | serialized with that enclosing class. What this means for your use of the statebox on that other node 89 | depends on your application. 90 | 91 | Suffice it to say: the simpler and more self-contained you keep your operations, the better. 92 | 93 | ### License 94 | 95 | JStatebox is Apache 2.0 licensed. 96 | 97 | Copyright 2011 the original author or authors. 98 | 99 | Licensed under the Apache License, Version 2.0 (the "License"); 100 | you may not use this file except in compliance with the License. 101 | You may obtain a copy of the License at 102 | 103 | http://www.apache.org/licenses/LICENSE-2.0 104 | 105 | Unless required by applicable law or agreed to in writing, software 106 | distributed under the License is distributed on an "AS IS" BASIS, 107 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 108 | See the License for the specific language governing permissions and 109 | limitations under the License. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "groovy" 2 | apply plugin: "idea" 3 | apply plugin: "maven" 4 | 5 | def description = "A statebox implementation for Java" 6 | group = "jstatebox" 7 | version = "0.1.0.SNAPSHOT" 8 | 9 | project.sourceCompatibility = 7 10 | 11 | configurations { 12 | all*.exclude group: "commons-logging" 13 | all*.exclude module: "groovy-all", version: "1.8.0-beta-3-SNAPSHOT" 14 | } 15 | 16 | repositories { 17 | mavenLocal() 18 | mavenCentral(artifactUrls: [ 19 | //"http://maven.springframework.org/milestone", 20 | "http://scala-tools.org/repo-releases" 21 | ]) 22 | } 23 | 24 | dependencies { 25 | groovy "org.codehaus.groovy:groovy:$groovyVersion" 26 | 27 | // Logging 28 | compile "org.slf4j:slf4j-api:$slf4jVersion" 29 | runtime "org.slf4j:jcl-over-slf4j:$slf4jVersion" 30 | runtime "ch.qos.logback:logback-classic:$logbackVersion" 31 | 32 | // Scala 33 | compile "org.scala-lang:scala-library:$scalaVersion" 34 | 35 | // Clojure 36 | compile "org.clojure:clojure:$clojureVersion" 37 | 38 | // Testing 39 | testCompile "org.spockframework:spock-core:$spockVersion" 40 | testCompile "org.hamcrest:hamcrest-all:1.1" 41 | 42 | } 43 | 44 | task wrapper(type: Wrapper) { gradleVersion = "1.0-milestone-5" } 45 | 46 | idea.module.jdkName = "OpenJDK 1.7" -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | groovyVersion = 1.8.3 2 | slf4jVersion = 1.6.2 3 | logbackVersion = 0.9.29 4 | scalaVersion = 2.9.1 5 | clojureVersion = 1.3.0 6 | 7 | spockVersion = 0.5-groovy-1.8 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrisbin/jstatebox/29635f9e9277368ae85706196edd2dd1cc8f2e25/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 01 13:14:21 CDT 2011 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://repo.gradle.org/gradle/distributions/gradle-1.0-milestone-5-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add APP_NAME to the JAVA_OPTS as -Xdock:name 109 | if $darwin; then 110 | JAVA_OPTS="$JAVA_OPTS -Xdock:name=$APP_NAME" 111 | # we may also want to set -Xdock:image 112 | fi 113 | 114 | # For Cygwin, switch paths to Windows format before running java 115 | if $cygwin ; then 116 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 117 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 118 | 119 | # We build the pattern for arguments to be converted via cygpath 120 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 121 | SEP="" 122 | for dir in $ROOTDIRSRAW ; do 123 | ROOTDIRS="$ROOTDIRS$SEP$dir" 124 | SEP="|" 125 | done 126 | OURCYGPATTERN="(^($ROOTDIRS))" 127 | # Add a user-defined pattern to the cygpath arguments 128 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 129 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 130 | fi 131 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 132 | i=0 133 | for arg in "$@" ; do 134 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 135 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 136 | 137 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 138 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 139 | else 140 | eval `echo args$i`="\"$arg\"" 141 | fi 142 | i=$((i+1)) 143 | done 144 | case $i in 145 | (0) set -- ;; 146 | (1) set -- "$args0" ;; 147 | (2) set -- "$args0" "$args1" ;; 148 | (3) set -- "$args0" "$args1" "$args2" ;; 149 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 150 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 151 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 152 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 153 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 154 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 155 | esac 156 | fi 157 | 158 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 159 | function splitJvmOpts() { 160 | JVM_OPTS=("$@") 161 | } 162 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 163 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 164 | 165 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 166 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/java/jstatebox/core/Operation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.core; 18 | 19 | import java.io.Serializable; 20 | 21 | /** 22 | * An invokable operation that receives a value and returns the new value. 23 | * 24 | * @author Jon Brisbin 25 | */ 26 | public interface Operation extends Serializable { 27 | 28 | /** 29 | * Perform a mutation of the original object, returning the new, mutated value. 30 | * 31 | * @param obj 32 | * @return 33 | */ 34 | public T invoke(T obj); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/jstatebox/core/Statebox.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.core; 18 | 19 | import java.io.ByteArrayInputStream; 20 | import java.io.ByteArrayOutputStream; 21 | import java.io.IOException; 22 | import java.io.ObjectInputStream; 23 | import java.io.ObjectOutputStream; 24 | import java.io.Serializable; 25 | import java.nio.ByteBuffer; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | import java.util.SortedSet; 29 | import java.util.TreeSet; 30 | import java.util.concurrent.Callable; 31 | import java.util.concurrent.CopyOnWriteArrayList; 32 | 33 | import jstatebox.io.OperationCodec; 34 | import jstatebox.io.OperationCodecMapper; 35 | import jstatebox.io.groovy.GroovyOperationCodecMapper; 36 | import jstatebox.io.java.JavaOperationCodecMapper; 37 | 38 | /** 39 | * A statebox implementation for the JVM. Understands Java, Groovy, and Scala natively. 40 | * It allows asynchronous JVM applications to alter the state of objects without blocking 41 | * and by resolving conflicts at read time, rather than write time. 42 | * 43 | * @author Jon Brisbin 44 | */ 45 | public class Statebox implements Serializable { 46 | 47 | protected static boolean IS_GROOVY_PRESENT = false; 48 | protected static boolean IS_SCALA_PRESENT = false; 49 | protected static boolean IS_CLOJURE_PRESENT = false; 50 | 51 | static { 52 | try { 53 | IS_GROOVY_PRESENT = Class.forName("groovy.lang.Closure") != null; 54 | } catch (ClassNotFoundException e) {} 55 | try { 56 | IS_SCALA_PRESENT = Class.forName("scala.Function1") != null; 57 | } catch (ClassNotFoundException e) {} 58 | try { 59 | IS_CLOJURE_PRESENT = Class.forName("clojure.lang.IFn") != null; 60 | } catch (ClassNotFoundException e) {} 61 | } 62 | 63 | public static final List CODEC_MAPPERS = new CopyOnWriteArrayList() {{ 64 | if (IS_GROOVY_PRESENT) { 65 | add(new GroovyOperationCodecMapper()); 66 | } 67 | add(new JavaOperationCodecMapper()); 68 | }}; 69 | 70 | protected final T origValue; 71 | protected T mutatedValue; 72 | protected final SortedSet ops = new TreeSet<>(); 73 | protected Long lastModified = System.currentTimeMillis(); 74 | 75 | protected Statebox(T value) { 76 | this.origValue = this.mutatedValue = value; 77 | } 78 | 79 | /** 80 | * Create a new statebox, wrapping the given value. 81 | * 82 | * @param value The immutable value of this statebox. 83 | */ 84 | public static Statebox create(T value) { 85 | return new Statebox<>(value); 86 | } 87 | 88 | /** 89 | * Serialize a statebox to a ByteBuffer for saving to a file, sending to a DB, or sending via message. 90 | * 91 | * @param statebox The statebox to serialize. 92 | * @return The ByteBuffer containing the serialized statebox. 93 | * @throws IOException 94 | */ 95 | public static ByteBuffer serialize(Statebox statebox) throws IOException { 96 | ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 97 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 98 | ObjectOutputStream oout = new ObjectOutputStream(bout); 99 | oout.writeObject(statebox.origValue); 100 | oout.writeInt(statebox.ops.size()); 101 | for (StateboxOp op : statebox.ops) { 102 | oout.writeLong(op.timestamp); 103 | OperationCodec codec = findCodecFor(op.operation); 104 | if (null == codec) { 105 | throw new IllegalStateException("No OperationCodec registered for " + op.operation); 106 | } 107 | oout.writeObject(codec.getClass().getName()); 108 | oout.writeObject(codec.encode(op.operation, classLoader).array()); 109 | } 110 | oout.close(); 111 | 112 | return ByteBuffer.wrap(bout.toByteArray()); 113 | } 114 | 115 | /** 116 | * Deserialize a statebox from a file, a DB, or from a message. 117 | * 118 | * @param buffer The buffer containing the serialized statebox. 119 | * @return The deserialized statebox. 120 | * @throws IOException 121 | * @throws ClassNotFoundException Usually thrown when the JVM into which this statebox is deserialized 122 | * doesn't have the classes assigned as operations. 123 | */ 124 | @SuppressWarnings({"unchecked"}) 125 | public static Statebox deserialize(ByteBuffer buffer) throws IOException, ClassNotFoundException { 126 | ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 127 | byte[] bytes = new byte[buffer.remaining()]; 128 | buffer.get(bytes); 129 | ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(bytes)); 130 | 131 | T origValue = (T) oin.readObject(); 132 | Statebox statebox = new Statebox<>(origValue); 133 | int opsSize = oin.readInt(); 134 | for (int i = 0; i < opsSize; i++) { 135 | Long timestamp = oin.readLong(); 136 | String opCodecName = (String) oin.readObject(); 137 | OperationCodec opCodec = findCodecForName(opCodecName); 138 | byte[] opBuff = (byte[]) oin.readObject(); 139 | Object operation = opCodec.decode(ByteBuffer.wrap(opBuff), classLoader); 140 | StateboxOp stOp = new StateboxOp(operation); 141 | stOp.timestamp = timestamp; 142 | statebox.ops.add(stOp); 143 | } 144 | 145 | return statebox; 146 | } 147 | 148 | protected static OperationCodec findCodecForName(String name) { 149 | for (OperationCodecMapper mapper : CODEC_MAPPERS) { 150 | if (name.equals(mapper.getCodecName())) { 151 | return mapper.getCodec(); 152 | } 153 | } 154 | return null; 155 | } 156 | 157 | protected static OperationCodec findCodecFor(Object operation) { 158 | for (OperationCodecMapper mapper : CODEC_MAPPERS) { 159 | if (mapper.isCodecFor(operation)) { 160 | return mapper.getCodec(); 161 | } 162 | } 163 | return null; 164 | } 165 | 166 | /** 167 | * Retrieve the value originally set in this statebox. 168 | * 169 | * @return The immutable value of this statebox. 170 | */ 171 | public T value() { 172 | return mutatedValue; 173 | } 174 | 175 | /** 176 | * When this statebox was last modified or merged. 177 | * 178 | * @return The time (in milliseconds) when this statebox was last mutated or merged. 179 | */ 180 | public Long lastModified() { 181 | return lastModified; 182 | } 183 | 184 | /** 185 | * Remove any operations that are older than the given age. 186 | * 187 | * @param age The number of milliseconds past which to expire operations. 188 | * @return This statebox with old operations removed. 189 | */ 190 | public Statebox expire(Long age) { 191 | List opsToRemove = new ArrayList<>(); 192 | Long expiration = lastModified - age; 193 | for (StateboxOp op : ops) { 194 | if (op.timestamp < expiration) { 195 | opsToRemove.add(op); 196 | } 197 | } 198 | ops.removeAll(opsToRemove); 199 | 200 | return this; 201 | } 202 | 203 | /** 204 | * Truncate the operations to the last N. 205 | * 206 | * @param num 207 | * @return 208 | */ 209 | @SuppressWarnings({"unchecked"}) 210 | public Statebox truncate(int num) { 211 | if (ops.size() > num) { 212 | Statebox st = new Statebox<>(origValue); 213 | int size = ops.size(); 214 | Object[] opsa = ops.toArray(); 215 | // Get a subset of the last N operations 216 | for (int i = (size - num); i < size; i++) { 217 | st.ops.add((StateboxOp) opsa[i]); 218 | } 219 | 220 | return st; 221 | } 222 | 223 | return this; 224 | } 225 | 226 | /** 227 | * Mutate the value of this statebox into a new statebox that is the result of calling 228 | * the operation and passing the current value. 229 | * 230 | * @param operation An operation to perform to mutate the value of this statebox into a new value. 231 | * @return A new statebox containing the result of this operation on the current statebox's value. 232 | */ 233 | public Statebox modify(Op operation) { 234 | Statebox statebox = new Statebox<>(origValue); 235 | statebox.mutatedValue = invoke(operation, origValue); 236 | statebox.ops.add(new StateboxOp(operation)); 237 | lastModified = statebox.lastModified = System.currentTimeMillis(); 238 | 239 | return statebox; 240 | } 241 | 242 | /** 243 | * Merge the operations of the given stateboxes into a single value based on timestamp. 244 | * 245 | * @param stateboxes 246 | * @return 247 | */ 248 | @SuppressWarnings({"unchecked"}) 249 | public Statebox merge(Statebox... stateboxes) { 250 | 251 | SortedSet mergedOps = new TreeSet<>(); 252 | mergedOps.addAll(ops); 253 | 254 | for (Statebox st : stateboxes) { 255 | for (Object op : st.ops) { 256 | mergedOps.add((StateboxOp) op); 257 | } 258 | } 259 | 260 | T val = origValue; 261 | for (StateboxOp op : mergedOps) { 262 | val = invoke(op.operation, val); 263 | } 264 | 265 | lastModified = System.currentTimeMillis(); 266 | 267 | return new Statebox<>(val); 268 | } 269 | 270 | @SuppressWarnings({"unchecked"}) 271 | private T invoke(final Object op, final T value) { 272 | if (op instanceof Operation) { 273 | return ((Operation) op).invoke(value); 274 | } 275 | 276 | if (IS_GROOVY_PRESENT) { 277 | if (op instanceof groovy.lang.Closure) { 278 | return (T) ((groovy.lang.Closure) op).call(value); 279 | } 280 | } 281 | 282 | if (IS_SCALA_PRESENT) { 283 | if (op instanceof scala.Function1) { 284 | return (T) ((scala.Function1) op).apply(value); 285 | } 286 | } 287 | 288 | if (IS_CLOJURE_PRESENT) { 289 | if (op instanceof clojure.lang.IFn) { 290 | return (T) ((clojure.lang.IFn) op).invoke(value); 291 | } 292 | } 293 | 294 | if (op instanceof Runnable) { 295 | ((Runnable) op).run(); 296 | } 297 | 298 | if (op instanceof Callable) { 299 | try { 300 | return ((Callable) op).call(); 301 | } catch (Exception e) { 302 | throw new IllegalStateException(e); 303 | } 304 | } 305 | 306 | return null; 307 | } 308 | 309 | @Override public String toString() { 310 | return "\nStatebox {" + 311 | "\n\t value=" + mutatedValue + 312 | ",\n\t ops=" + ops + 313 | ",\n\t lastModified=" + lastModified + 314 | "\n}"; 315 | } 316 | 317 | private static class StateboxOp implements Comparable, Serializable { 318 | 319 | private Long timestamp = System.currentTimeMillis(); 320 | private Object operation; 321 | 322 | private StateboxOp(Object operation) { 323 | this.operation = operation; 324 | } 325 | 326 | @Override public int compareTo(StateboxOp op) { 327 | return timestamp.compareTo(op.timestamp); 328 | } 329 | 330 | @Override public String toString() { 331 | return "\nStateboxOp {" + 332 | "\n\t timestamp=" + timestamp + 333 | ",\n\t operation=" + operation + 334 | "\n}"; 335 | } 336 | 337 | } 338 | 339 | } 340 | -------------------------------------------------------------------------------- /src/main/java/jstatebox/io/ClassLoaderAwareObjectInputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.io; 18 | 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.ObjectInputStream; 22 | import java.io.ObjectStreamClass; 23 | 24 | /** 25 | * @author Jon Brisbin 26 | */ 27 | public class ClassLoaderAwareObjectInputStream extends ObjectInputStream { 28 | 29 | private ClassLoader classLoader; 30 | 31 | public ClassLoaderAwareObjectInputStream(InputStream in, ClassLoader classLoader) throws IOException { 32 | super(in); 33 | this.classLoader = classLoader; 34 | } 35 | 36 | @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { 37 | try { 38 | return classLoader.loadClass(desc.getName()); 39 | } catch (ClassNotFoundException e) { 40 | return super.resolveClass(desc); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/jstatebox/io/OperationCodec.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.io; 18 | 19 | import java.io.IOException; 20 | import java.nio.ByteBuffer; 21 | 22 | /** 23 | * @author Jon Brisbin 24 | */ 25 | public interface OperationCodec { 26 | 27 | public ByteBuffer encode(Object operation, ClassLoader classLoader) throws IOException; 28 | 29 | public Object decode(ByteBuffer buffer, ClassLoader classLoader) throws IOException, ClassNotFoundException; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/jstatebox/io/OperationCodecMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.io; 18 | 19 | /** 20 | * @author Jon Brisbin 21 | */ 22 | public interface OperationCodecMapper { 23 | 24 | public String getCodecName(); 25 | 26 | public boolean isCodecFor(Object operation); 27 | 28 | public OperationCodec getCodec(); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/jstatebox/io/groovy/GroovyOperationCodec.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.io.groovy; 18 | 19 | import java.io.BufferedInputStream; 20 | import java.io.ByteArrayInputStream; 21 | import java.io.ByteArrayOutputStream; 22 | import java.io.File; 23 | import java.io.FilenameFilter; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.io.ObjectOutputStream; 27 | import java.net.URISyntaxException; 28 | import java.net.URL; 29 | import java.net.URLClassLoader; 30 | import java.nio.ByteBuffer; 31 | import java.nio.file.Files; 32 | import java.nio.file.Paths; 33 | import java.util.ArrayList; 34 | import java.util.Enumeration; 35 | import java.util.List; 36 | import java.util.zip.ZipEntry; 37 | import java.util.zip.ZipFile; 38 | 39 | import groovy.lang.Closure; 40 | import groovy.lang.GroovyClassLoader; 41 | import jstatebox.io.ClassLoaderAwareObjectInputStream; 42 | import jstatebox.io.OperationCodec; 43 | 44 | /** 45 | * @author Jon Brisbin 46 | */ 47 | public class GroovyOperationCodec implements OperationCodec { 48 | 49 | @Override public ByteBuffer encode(Object operation, ClassLoader classLoader) throws IOException { 50 | List buffers = new ArrayList<>(); 51 | int totalSize = 4; 52 | Class clazz = operation.getClass(); 53 | String packagePath = packagePath(clazz); 54 | String innerClassPrefix = innerClassPrefix(clazz); 55 | 56 | // Encode supporting classes 57 | for (URLClassLoader cl : classLoaderHierarchy(classLoader)) { 58 | for (URL url : cl.getURLs()) { 59 | if (!"file".equals(url.getProtocol())) { 60 | continue; 61 | } 62 | 63 | File classes; 64 | try { 65 | classes = new File(url.toURI()); 66 | if (!classes.exists()) { 67 | continue; 68 | } 69 | } catch (URISyntaxException e) { 70 | throw new IllegalStateException(e); 71 | } 72 | 73 | if (classes.isDirectory()) { 74 | File packageDir = null != packagePath ? new File(classes, packagePath) : classes; 75 | if (packageDir.exists()) { 76 | for (String classFile : packageDir.list(new PrefixBasedFilenameFilter(innerClassPrefix))) { 77 | byte[] bytes = Files.readAllBytes(Paths.get(classFile)); 78 | buffers.add(bytes); 79 | totalSize += 4 + bytes.length; 80 | } 81 | } 82 | } else if (classes.getName().endsWith(".jar") || classes.getName().endsWith(".zip")) { 83 | ZipFile zipFile = new ZipFile(classes); 84 | if (null != (null != packagePath ? zipFile.getEntry(packagePath) : classes)) { 85 | Enumeration entries = zipFile.entries(); 86 | while (entries.hasMoreElements()) { 87 | ZipEntry entry = entries.nextElement(); 88 | String name = entry.getName(); 89 | if (name.startsWith(innerClassPrefix) && name.endsWith(".class")) { 90 | byte[] bytes = readFully(zipFile.getInputStream(entry)); 91 | buffers.add(bytes); 92 | totalSize += 4 + bytes.length; 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | // Encode the operation itself 101 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 102 | ObjectOutputStream oout = new ObjectOutputStream(bout); 103 | // Clear certain things that entangle us 104 | if (operation instanceof Closure) { 105 | Closure cl = (Closure) operation; 106 | cl.getMetaClass().setAttribute(operation, "owner", null); 107 | cl.getMetaClass().setAttribute(operation, "thisObject", null); 108 | cl.setDelegate(null); 109 | } 110 | oout.writeObject(operation); 111 | oout.close(); 112 | byte[] opBytes = bout.toByteArray(); 113 | totalSize += 4 + opBytes.length; 114 | 115 | // Encode all this into a ByteBuffer using the following protocol: 116 | // 1. Number of buffers (4 bytes) 117 | // 2. For each buffer, write: 118 | // 1. Buffer length (4 bytes) 119 | // 2. Buffer (variable length) 120 | // 3. Length of encoded operation (4 bytes) 121 | // 4. Encoded operation (variable length) 122 | ByteBuffer compositeBuffer = ByteBuffer.allocate(totalSize); 123 | compositeBuffer.putInt(buffers.size()); 124 | for (byte[] bytes : buffers) { 125 | compositeBuffer.putInt(bytes.length); 126 | compositeBuffer.put(bytes); 127 | } 128 | compositeBuffer.putInt(opBytes.length); 129 | compositeBuffer.put(opBytes); 130 | compositeBuffer.flip(); 131 | 132 | return compositeBuffer; 133 | } 134 | 135 | @Override 136 | public Object decode(ByteBuffer buffer, ClassLoader classLoader) throws IOException, ClassNotFoundException { 137 | GroovyClassLoader gcl = new GroovyClassLoader(classLoader); 138 | int size = buffer.getInt(); 139 | for (int i = 0; i < size; i++) { 140 | int len = buffer.getInt(); 141 | byte[] bytes = new byte[len]; 142 | buffer.get(bytes); 143 | gcl.defineClass(null, bytes); 144 | } 145 | 146 | int oSize = buffer.getInt(); 147 | byte[] oBytes = new byte[oSize]; 148 | buffer.get(oBytes); 149 | ClassLoaderAwareObjectInputStream oin = new ClassLoaderAwareObjectInputStream(new ByteArrayInputStream(oBytes), 150 | gcl); 151 | 152 | return oin.readObject(); 153 | } 154 | 155 | private List classLoaderHierarchy(ClassLoader current) { 156 | List classLoaders = new ArrayList<>(); 157 | while (null != current) { 158 | if (current instanceof URLClassLoader) { 159 | classLoaders.add((URLClassLoader) current); 160 | } 161 | current = current.getParent(); 162 | } 163 | return classLoaders; 164 | } 165 | 166 | private String innerClassPrefix(Class clazz) { 167 | return clazz.getSimpleName().replace("$_$", "$_") + "_"; 168 | } 169 | 170 | private String packagePath(Class clazz) { 171 | if (null != clazz.getPackage()) { 172 | String name = clazz.getPackage().getName(); 173 | if (null != name) { 174 | return name.replace('.', '/'); 175 | } 176 | } 177 | return null; 178 | } 179 | 180 | private byte[] readFully(InputStream in) throws IOException { 181 | BufferedInputStream bin = new BufferedInputStream(in); 182 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 183 | byte[] buff = new byte[16 * 1024]; 184 | int read = bin.read(buff); 185 | while (read > 0) { 186 | bout.write(buff, 0, read); 187 | } 188 | return bout.toByteArray(); 189 | } 190 | 191 | private class PrefixBasedFilenameFilter implements FilenameFilter { 192 | 193 | private String prefix; 194 | 195 | private PrefixBasedFilenameFilter(String prefix) { 196 | this.prefix = prefix; 197 | } 198 | 199 | @Override public boolean accept(File dir, String name) { 200 | return name.startsWith(prefix) && name.endsWith(".class"); 201 | } 202 | 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/jstatebox/io/groovy/GroovyOperationCodecMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.io.groovy; 18 | 19 | import groovy.lang.Closure; 20 | import jstatebox.io.OperationCodec; 21 | import jstatebox.io.OperationCodecMapper; 22 | 23 | /** 24 | * @author Jon Brisbin 25 | */ 26 | public class GroovyOperationCodecMapper implements OperationCodecMapper { 27 | 28 | private final static OperationCodec INSTANCE = new GroovyOperationCodec(); 29 | 30 | @Override public String getCodecName() { 31 | return GroovyOperationCodec.class.getName(); 32 | } 33 | 34 | @Override public boolean isCodecFor(Object operation) { 35 | if (operation instanceof Closure) { 36 | return true; 37 | } else { 38 | return false; 39 | } 40 | } 41 | 42 | @Override public OperationCodec getCodec() { 43 | return INSTANCE; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/jstatebox/io/java/JavaOperationCodec.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.io.java; 18 | 19 | import java.io.ByteArrayInputStream; 20 | import java.io.ByteArrayOutputStream; 21 | import java.io.IOException; 22 | import java.io.ObjectOutputStream; 23 | import java.nio.ByteBuffer; 24 | 25 | import jstatebox.io.ClassLoaderAwareObjectInputStream; 26 | import jstatebox.io.OperationCodec; 27 | 28 | /** 29 | * @author Jon Brisbin 30 | */ 31 | public class JavaOperationCodec implements OperationCodec { 32 | 33 | @Override public ByteBuffer encode(Object operation, ClassLoader classLoader) throws IOException { 34 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 35 | ObjectOutputStream oout = new ObjectOutputStream(bout); 36 | oout.writeObject(operation); 37 | oout.close(); 38 | 39 | return ByteBuffer.wrap(bout.toByteArray()); 40 | } 41 | 42 | @Override 43 | public Object decode(ByteBuffer buffer, ClassLoader classLoader) throws IOException, ClassNotFoundException { 44 | byte[] bytes = new byte[buffer.remaining()]; 45 | buffer.get(bytes); 46 | ClassLoaderAwareObjectInputStream oin = new ClassLoaderAwareObjectInputStream(new ByteArrayInputStream(bytes), 47 | classLoader); 48 | 49 | return oin.readObject(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/jstatebox/io/java/JavaOperationCodecMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.io.java; 18 | 19 | import java.io.Serializable; 20 | 21 | import jstatebox.io.OperationCodec; 22 | import jstatebox.io.OperationCodecMapper; 23 | 24 | /** 25 | * @author Jon Brisbin 26 | */ 27 | public class JavaOperationCodecMapper implements OperationCodecMapper { 28 | 29 | private static final OperationCodec INSTANCE = new JavaOperationCodec(); 30 | 31 | @Override public String getCodecName() { 32 | return JavaOperationCodec.class.getName(); 33 | } 34 | 35 | @Override public boolean isCodecFor(Object operation) { 36 | if (operation instanceof Serializable) { 37 | return true; 38 | } else { 39 | return false; 40 | } 41 | } 42 | 43 | @Override public OperationCodec getCodec() { 44 | return INSTANCE; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/groovy/jstatebox/core/SerializableOperation.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package jstatebox.core 18 | 19 | /** 20 | * @author Jon Brisbin 21 | */ 22 | class SerializableOperation implements Operation { 23 | 24 | @Override String invoke(String s) { 25 | return s + " World!" 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/groovy/jstatebox/core/StateboxSpec.groovy: -------------------------------------------------------------------------------- 1 | package jstatebox.core 2 | 3 | import spock.lang.Specification 4 | 5 | /** 6 | * @author Jon Brisbin 7 | */ 8 | class StateboxSpec extends Specification { 9 | 10 | def "Test Statebox"() { 11 | 12 | given: 13 | def st1 = Statebox.create("Hello") 14 | 15 | when: 16 | def st2 = st1.modify {value -> 17 | value + " " 18 | } 19 | def st3 = st1.modify {value -> 20 | value + "World!" 21 | } 22 | def st4 = st1.merge(st2, st3) 23 | 24 | then: 25 | st4.value() == "Hello World!" 26 | 27 | } 28 | 29 | def "Test that Statebox respects truncation"() { 30 | 31 | given: 32 | def st1 = Statebox.create("Hello") 33 | 34 | when: 35 | def st2 = st1.modify {value -> 36 | value + " " 37 | } 38 | def st3 = st1.modify {value -> 39 | value + "World!" 40 | } 41 | def st4 = st1.merge(st2.truncate(0), st3) 42 | 43 | then: 44 | st4.value() == "HelloWorld!" 45 | 46 | } 47 | 48 | def "Test serialization/deserialization"() { 49 | 50 | given: 51 | def st1 = Statebox.create("Hello") 52 | def st2 = st1.modify {value -> 53 | value + " " 54 | } 55 | def outBuff = Statebox.serialize(st2) 56 | def st3 = Statebox.deserialize(outBuff) 57 | def st4 = st1.modify({value -> 58 | value + "World!" 59 | }) 60 | 61 | when: 62 | def st5 = st1.merge(st3, st4) 63 | 64 | then: 65 | st5.value() == "Hello World!" 66 | 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------